为了研发一个无侵入的 iOS 页面停留时长采集 SDK,您可以采用AOP(面向切面编程)的方法,结合Runtime 动态替换 UIViewController 的生命周期方法,实现无侵入的埋点方案。

下面是一个全面的 SDK 实现步骤,主要包括 Runtime 方法交换和自动埋点功能,确保不需要开发者在每个 UIViewController 中手动添加代码。

1. SDK 实现思路

  • Runtime 交换方法:使用 Objective-C Runtime 替换系统的 viewDidAppear:viewWillDisappear: 方法,以插入埋点逻辑。
  • 自动采集页面信息:通过 UIViewController 的类名自动识别页面,不需要开发者传入页面标识。
  • 数据上报:封装网络请求,将采集的页面停留时长发送到服务器。
  • 无侵入:开发者无需手动集成代码,SDK 自动管理所有页面的时长统计。

2. SDK 代码实现

2.1 PageTrackingManager.h

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface PageTrackingManager : NSObject

+ (instancetype)sharedInstance;
- (void)startTracking; // 启动页面时长埋点监控

@end

2.2 PageTrackingManager.m

 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
45
46
47
48
49
50
51
52
53
54
55
#import "PageTrackingManager.h"
#import <objc/runtime.h>

@implementation PageTrackingManager

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

- (void)startTracking {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleUIViewControllerLifecycle];
    });
}

- (void)swizzleUIViewControllerLifecycle {
    Class class = [UIViewController class];

    // 替换 viewDidAppear
    SEL originalSelector = @selector(viewDidAppear:);
    SEL swizzledSelector = @selector(tracked_viewDidAppear:);
    [self swizzleMethodInClass:class originalSelector:originalSelector swizzledSelector:swizzledSelector];

    // 替换 viewWillDisappear
    SEL originalDisappearSelector = @selector(viewWillDisappear:);
    SEL swizzledDisappearSelector = @selector(tracked_viewWillDisappear:);
    [self swizzleMethodInClass:class originalSelector:originalDisappearSelector swizzledSelector:swizzledDisappearSelector];
}

- (void)swizzleMethodInClass:(Class)class originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector {
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    BOOL didAddMethod = class_addMethod(class,
                                        originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));

    if (didAddMethod) {
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

@end

2.3 UIViewController 类别(Category)实现替换逻辑

 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
#import "PageTrackingManager.h"
#import <objc/runtime.h>

@implementation UIViewController (Tracking)

- (void)tracked_viewDidAppear:(BOOL)animated {
    [self tracked_viewDidAppear:animated];  // 调用原方法 (由于方法交换,此处是原始方法)
    
    // 记录页面进入时间
    NSString *pageName = NSStringFromClass([self class]);
    NSLog(@"Enter page: %@", pageName);
    objc_setAssociatedObject(self, @selector(tracked_viewDidAppear:), [NSDate date], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)tracked_viewWillDisappear:(BOOL)animated {
    [self tracked_viewWillDisappear:animated];  // 调用原方法

    // 获取页面进入时间并计算停留时长
    NSDate *enterTime = objc_getAssociatedObject(self, @selector(tracked_viewDidAppear:));
    if (enterTime) {
        NSTimeInterval stayDuration = [[NSDate date] timeIntervalSinceDate:enterTime];
        NSString *pageName = NSStringFromClass([self class]);
        NSLog(@"Exit page: %@, Stay Duration: %.2f seconds", pageName, stayDuration);

        // 上报数据
        [self reportPageStayDuration:pageName duration:stayDuration];
    }
}

- (void)reportPageStayDuration:(NSString *)pageName duration:(NSTimeInterval)duration {
    // 模拟网络请求,将数据发送到后台
    NSLog(@"Reporting: %@ stayed for %.2f seconds", pageName, duration);
    
    // 此处可以封装实际的网络请求,将 pageName 和 duration 发送到服务器
    // 使用 NSURLSession 或者其他第三方网络库,比如 AFNetworking
}

@end

2.4 启动 SDK

在应用启动时调用 SDK 的 startTracking 方法,开始埋点监控。

1
2
3
4
5
6
7
#import "PageTrackingManager.h"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 启动页面停留时长埋点 SDK
    [[PageTrackingManager sharedInstance] startTracking];
    return YES;
}

3. 考虑因素

  • App 切换前后台的处理:可以通过监听 UIApplicationWillResignActiveNotificationUIApplicationDidBecomeActiveNotification 来判断 App 进入后台或返回前台,调整停留时长的统计逻辑。
  • 崩溃处理:当 App 意外崩溃时,页面停留时长可能无法被正确上报,可以结合崩溃日志工具来确保数据的完整性。
  • 性能优化:确保方法交换不会影响应用性能,通过异步网络请求上报数据来减少 UI 线程的压力。

4. 总结

通过 Runtime 方法交换的无侵入方式,SDK 可以自动采集 iOS 应用的页面停留时长,开发者无需在每个页面中手动调用代码。SDK 具备很好的扩展性,可以方便地加入更多的埋点采集功能,如按钮点击、页面滑动等。