无埋点核心技术:iOS Hook在字节的实践经验
发布时间:2023-04-01 13:44:39 所属栏目:教程 来源:
导读:众所周知,字节跳动的推荐在业内处于领先水平,而精确的推荐离不开大量埋点,常见的埋点采集方案是在响应用户行为操作的路径上进行埋点。但是由于App通常会有比较多界面和操作路径,主动埋点的维护成本就会非常大。所
|
众所周知,字节跳动的推荐在业内处于领先水平,而精确的推荐离不开大量埋点,常见的埋点采集方案是在响应用户行为操作的路径上进行埋点。但是由于App通常会有比较多界面和操作路径,主动埋点的维护成本就会非常大。所以行业的做法是无埋点,而无埋点实现需要AOP编程。 一个常见的场景,比如想在UIViewController出现和消失的时刻分别记录时间戳用于统计页面展现的时长。要达到这个目标有很多种方法,但是AOP无疑是最简单有效的方法。Objective-C的Hook其实也有很多种方式,这里以Method Swizzle给个示例。 @interface UIViewController (MyHook) @end @implementation UIViewController (MyHook) + (void)load { static dispatch_once_t oncetoken; dispatch_once(&oncetoken, ^{ /// 常规的 Method Swizzle封装 swizzleMethods(self, @selector(viewDidAppear:), @selector(my_viewDidAppear:)); /// 更多Hook }); } - (void)my_viewDidAppear:(BOOL)animated { /// 一些Hook需要的逻辑 /// 这里调用Hook后的方法,其实现其实已经是原方法了。 [self my_viewDidAppear: animated]; } @end 接下来我们探讨一个具体场景: UICollectionView或者UITableView是iOS中非常常用的列表UI组件,其中列表元素的点击事件回调是通过delegate完成的。这里以UICollectionView为例,UICollectionView的delegate,有个方法声明,collectionView:didSelectItemAtIndexPath:,实现这个方法我们就可以给列表元素添加点击事件。 我们的目标是Hook这个delegate的方法,在点击回调的时候进行额外的埋点操作。 方案迭代 方案1 Method Swizzle 通常情况下,Method Swizzle可以满足绝大部分的AOP编程需求。因此首次迭代,我们直接使用Method Swizzle来进行Hook。 @interface UICollectionView (MyHook) @end @implementation UICollectionView (MyHook) // Hook, setMyDelegate:和setDelegate:交换过 - (void)setMyDelegate:(id)delegate { if (delegate != nil) { /// 常规Method Swizzle swizzleMethodsXXX(delegate, @selector(collectionView:didSelectItemAtIndexPath:), self, @selector(my_collectionView:didSelectItemAtIndexPath:)); } [self setMyDelegate:nil]; } - (void)my_collectionView:(UICollectionView *)ccollectionView didSelectItemAtIndexPath:(NSIndexPath *)index { /// 一些Hook需要的逻辑 /// 这里调用Hook后的方法,其实现其实已经是原方法了。 [self my_collectionView:ccollectionView didSelectItemAtIndexPath:index]; } @end 我们把这个方案集成到今日头条App里面进行测试验证,发现没法办法验证通过。 主要原因今日头条App是一个庞大的项目,其中引入了非常多的三方库,比如IGListKit等,这些三方库通常对UICollectionView的使用都进行了封装,而这些封装,恰恰导致我们不能使用常规的Method Swizzle来Hook这个delegate。直接的原因总结有以下两点: setDelegate传入的对象不是实现UICollectionViewDelegate协议的那个对象 在上述图例中,使用方存在连续调用两次setDelegate的情况,第一次是真实delegate,第二次是proxy,我们需要区别对待。 代理模式和nsproxy介绍 使用proxy对原对象进行代理,在处理完额外操作之后再调用原对象,这种模式称为代理模式。而Objective-C中要实现代理模式,使用nsproxy会比较高效。详细内容参考下列文章。 代理模式 NSProxy使用 这里面UICollectionView的setDelegate传入的是一个proxy是非常常见的操作,比如IGListKit,同时App基于自身需求,也有可能会做这一层封装。 在UICollectionView的setDelegate的时候,把delegate包裹在proxy中,然后把proxy设置给UICollectionView,使用proxy对delegate进行消息转发。 方案2 使用代理模式 方案1已经无法满足我们的需求了,我们考虑到既然对delegate进行代理是一种常规操作,我们何不也使用代理模式,对proxy再次代理。 代码实现 先Hook UICollectionView的setDelegate方法 代理delegate 简单的代码示意如下 /// 完整封装了一些常规的消息转发方法 @interface DelegateProxy : nsproxy @property (nonatomic, weak, readonly) id target; @end /// 为 CollectionView delegate转发消息的proxy @interface BDCollectionViewDelegateProxy : DelegateProxy @end @implementation BDCollectionViewDelegateProxy <UICollectionViewDelegate> - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { //track event here if ([self.target respondsToSelector:@selector(collectionView:didSelectItemAtIndexPath:)]) { [self.target collectionView:collectionView didSelectItemAtIndexPath:indexPath]; } } - (BOOL)bd_isCollectionViewTrackerDecorator { return YES; } // 还有其他的消息转发的代码 先忽略 - (BOOL)respondsToSelector:(SEL)aSelector { if (aSelector == @selector(bd_isCollectionViewTrackerDecorator)) { return YES; } return [self.target respondsToSelector:aSelector]; } @end @interface UICollectionView (MyHook) @end @implementation UICollectionView (MyHook) - (void) setDd_TrackerProxy:(BDCollectionViewDelegateProxy *)object { objc_setAssociatedobject(self, @selector(bd_TrackerProxy), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BDCollectionViewDelegateProxy *) bd_TrackerProxy { BDCollectionViewDelegateProxy *bridge = objc_getAssociatedobject(self, @selector(bd_TrackerProxy)); return bridge; } // Hook, setMyDelegate:和setDelegate:交换过了 - (void)setMyDelegate:(id)delegate { if (delegate == nil) { [self setMyDelegate:delegate]; return } // 不会释放,不重复设置 if ([delegate respondsToSelector:@selector(bd_isCollectionViewTrackerDecorator)]) { [self setMyDelegate:delegate]; return; } BDCollectionViewDelegateProxy *proxy = [[BDCollectionViewDelegateProxy alloc] initWithTarget:delegate]; [self setMyDelegate:proxy]; self.bd_TrackerProxy = proxy; } (编辑:汽车网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
推荐文章
站长推荐
