Post

构建 iOS 埋点监控 SDK

构建 iOS 埋点监控 SDK

设计目标

埋点监控 SDK 的核心目标:

  1. 事件采集:支持手动埋点和自动埋点。
  2. 数据缓存:本地存储未上传的数据,防止网络异常时丢失。
  3. 上传机制:批量上传,减少网络请求。
  4. 可扩展性:支持后续添加新功能,如性能监控。

技术实现

1. 项目初始化

使用 Objective-C 创建一个静态库。创建一个新项目,并设置基础文件结构:

1
2
3
4
5
6
7
8
9
TrackingSDK/
├── TrackingManager.h
├── TrackingManager.m
├── EventModel.h
├── EventModel.m
├── StorageManager.h
├── StorageManager.m
├── NetworkManager.h
└── NetworkManager.m

2. 事件模型

定义一个 EventModel 类来表示埋点事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// EventModel.h
@interface EventModel : NSObject
@property (nonatomic, copy) NSString *eventName;
@property (nonatomic, strong) NSDictionary *parameters;
@property (nonatomic, strong) NSDate *timestamp;
@end

// EventModel.m
@implementation EventModel
- (instancetype)initWithEventName:(NSString *)name parameters:(NSDictionary *)params {
    self = [super init];
    if (self) {
        _eventName = [name copy];
        _parameters = [params copy];
        _timestamp = [NSDate date];
    }
    return self;
}
@end

3. 核心管理类

TrackingManager 是 SDK 的入口,提供事件记录和配置方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// TrackingManager.h
@interface TrackingManager : NSObject
+ (instancetype)sharedManager;
- (void)startWithApiKey:(NSString *)apiKey serverURL:(NSString *)url;
- (void)trackEvent:(NSString *)eventName withParameters:(NSDictionary *)parameters;
@end

// TrackingManager.m
@implementation TrackingManager {
    NSString *_apiKey;
    NSString *_serverURL;
}

+ (instancetype)sharedManager {
    static TrackingManager *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[TrackingManager alloc] init];
    });
    return instance;
}

- (void)startWithApiKey:(NSString *)apiKey serverURL:(NSString *)url {
    _apiKey = apiKey;
    _serverURL = url;
    NSLog(@"Tracking SDK initialized with API Key: %@", apiKey);
}

- (void)trackEvent:(NSString *)eventName withParameters:(NSDictionary *)parameters {
    EventModel *event = [[EventModel alloc] initWithEventName:eventName parameters:parameters];
    [self saveEvent:event];
    [self uploadEventsIfNeeded];
}

- (void)saveEvent:(EventModel *)event {
    // 交给 StorageManager 保存
    [[StorageManager sharedManager] saveEvent:event];
}

- (void)uploadEventsIfNeeded {
    // 交给 NetworkManager 上传
    [[NetworkManager sharedManager] uploadEventsToServer:_serverURL withApiKey:_apiKey];
}
@end

4. 本地缓存

使用 NSUserDefaults 或文件存储来缓存事件数据。这里以文件为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// StorageManager.h
@interface StorageManager : NSObject
+ (instancetype)sharedManager;
- (void)saveEvent:(EventModel *)event;
- (NSArray<EventModel *> *)loadEvents;
- (void)clearEvents;
@end

// StorageManager.m
@implementation StorageManager

+ (instancetype)sharedManager { /* 单例实现 */ }

- (NSString *)eventsFilePath {
    NSString *documents = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    return [documents stringByAppendingPathComponent:@"tracking_events.plist"];
}

- (void)saveEvent:(EventModel *)event {
    NSMutableArray *events = [[self loadEvents] mutableCopy] ?: [NSMutableArray array];
    [events addObject:[NSKeyedArchiver archivedDataWithRootObject:event]];
    [events writeToFile:[self eventsFilePath] atomically:YES];
}

- (NSArray<EventModel *> *)loadEvents {
    NSArray *dataArray = [NSArray arrayWithContentsOfFile:[self eventsFilePath]];
    NSMutableArray *events = [NSMutableArray array];
    for (NSData *data in dataArray) {
        EventModel *event = [NSKeyedUnarchiver unarchiveObjectWithData:data];
        [events addObject:event];
    }
    return [events copy];
}

- (void)clearEvents {
    [[NSFileManager defaultManager] removeItemAtPath:[self eventsFilePath] error:nil];
}
@end

5. 数据上传

使用 NSURLSession 实现批量上传:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// NetworkManager.m
@implementation NetworkManager

+ (instancetype)sharedManager { /* 单例实现 */ }

- (void)uploadEventsToServer:(NSString *)serverURL withApiKey:(NSString *)apiKey {
    NSArray *events = [[StorageManager sharedManager] loadEvents];
    if (events.count == 0) return;

    NSMutableArray *jsonEvents = [NSMutableArray array];
    for (EventModel *event in events) {
        NSDictionary *dict = @{
            @"event_name": event.eventName,
            @"parameters": event.parameters ?: @{},
            @"timestamp": @([event.timestamp timeIntervalSince1970])
        };
        [jsonEvents addObject:dict];
    }

    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonEvents options:0 error:nil];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:serverURL]];
    [request setHTTPMethod:@"POST"];
    [request setValue:apiKey forHTTPHeaderField:@"X-API-Key"];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setHTTPBody:jsonData];

    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (!error && [(NSHTTPURLResponse *)response statusCode] == 200) {
            [[StorageManager sharedManager] clearEvents];
            NSLog(@"Events uploaded successfully");
        } else {
            NSLog(@"Upload failed: %@", error.localizedDescription);
        }
    }];
    [task resume];
}
@end

自动埋点:利用 Method Swizzling

手动埋点虽然灵活,但对于页面浏览或按钮点击这类常见事件,手动添加代码会增加开发负担。可以通过 Method Swizzling 实现自动埋点,拦截 UIViewController 的生命周期方法和 UIControl 的事件。

实现步骤

  1. 创建一个 Category: 在 TrackingManager 中添加自动埋点的逻辑,通过 Category 扩展 UIViewControllerUIControl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// UIViewController+Tracking.h
#import <UIKit/UIKit.h>
@interface UIViewController (Tracking)
@end

// UIViewController+Tracking.m
#import <objc/runtime.h>
@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));
        Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzled_viewDidAppear:));
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (void)swizzled_viewDidAppear:(BOOL)animated {
    [self swizzled_viewDidAppear:animated]; // 调用原始方法
    NSString *pageName = NSStringFromClass([self class]);
    [[TrackingManager sharedManager] trackEvent:@"PageView" withParameters:@{@"page": pageName}];
}

@end
  1. 拦截 UIControl 事件: 类似地,扩展 UIControl 来捕获按钮点击:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// UIControl+Tracking.m
@implementation UIControl (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
        Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzled_sendAction:to:forEvent:));
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    [self swizzled_sendAction:action to:target forEvent:event]; // 调用原始方法
    if ([event type] == UIEventTypeTouches) {
        NSString *actionName = NSStringFromSelector(action);
        NSString *targetName = NSStringFromClass([target class]);
        [[TrackingManager sharedManager] trackEvent:@"ButtonClick" withParameters:@{@"action": actionName, @"target": targetName}];
    }
}

@end

使用示例

在 App 中集成 SDK:

1
2
3
4
5
6
// AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [[TrackingManager sharedManager] startWithApiKey:@"your-api-key" serverURL:@"https://your-server.com/track"];
    [[TrackingManager sharedManager] trackEvent:@"AppLaunch" withParameters:@{@"version": @"1.0.0"}];
    return YES;
}
This post is licensed under CC BY 4.0 by the author.