众所周知,字节跳动的推荐在业内处于领先水平,而精确的推荐离不开大量埋点,常见的埋点采集方案是在响应用户行为操作的路径上进行埋点。但是由于 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:
,实现这个方法我们就可以给列表元素添加点击事件。@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
UICollectionView
的使用都进行了封装,而这些封装,恰恰导致我们不能使用常规的 Method Swizzle 来 Hook 这个 delegate。直接的原因总结有以下两点:setDelegate
传入的对象不是实现 UICollectionViewDelegate
协议的那个对象setDelegate
传入的是一个代理对象 proxy,proxy 引用了实际的实现 UICollectionViewDelegate
协议的 delegate
,proxy 实际上并没有实现 UICollectionViewDelegate
的任何一个方法,它把所有方法都转发给实际的 delegate
。这种情况下,我们不能直接对 proxy 进行 Method SwizzlesetDelegate
setDelegate
的情况,第一次是真实 delegate
,第二次是 proxy
,我们需要区别对待。代理模式:https://juejin.cn/post/6844903544965857294 NSProxy 使用:https://juejin.cn/post/6962720860700409886
UICollectionView
的 setDelegate
传入的是一个 proxy
是非常常见的操作,比如 IGListKit,同时 App 基于自身需求,也有可能会做这一层封装。UICollectionView
的 setDelegate
的时候,把 delegate
包裹在 proxy
中,然后把 proxy 设置给 UICollectionView
,使用 proxy
对 delegate
进行消息转发。delegate
进行代理是一种常规操作,我们何不也使用代理模式,对 proxy
再次次代理。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;
}
@end
下图实线表示强引用,虚线表示弱引用
如果使用方没有对 delegate
进行代理,而我们使用代理模式:
UICollectionView
,其 delegate
指针指向 DelegateProxy
DelegateProxy,被 UICollectionView 用 runtime 的方式强引用,其 target 弱引用真实 Delegate
如果使用方也对 delegate
进行代理,我们使用代理模式:
我们只需要保证我们的 DelegateProxy 处于代理链中的一环即可
UIWebView
WKWebView
UITableView
UICollectionView
UIScrollView
UIActionSheet
UIAlertView
Thread 0 Crashed:
0 libobjc.A.dylib 0x000000018198443c objc_msgSend + 28
1 UIKit 0x000000018be05b4c -[UIWebView webView:decidePolicyForNavigationAction:request:frame:decisionListener:] + 200
2 CoreFoundation 0x0000000182731cd0 __invoking___ + 144
3 CoreFoundation 0x000000018261056c -[NSInvocation invoke] + 292
4 CoreFoundation 0x000000018261501c -[NSInvocation invokeWithTarget:] + 60
5 WebKitLegacy 0x000000018b86d654 -[_WebSafeForwarder forwardInvocation:] + 156
从堆栈信息不难判断出 crash 原因是 UIWebView 的 delegate 野指针,那为啥出现野指针呢?
@interface UIWebView (JSBridge)
@end
@implementation UIWebView (JSBridge)
- (void)setJsBridge:(id)object {
objc_setAssociatedObject(self, @selector(jsBridge), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (WebViewJavascriptBridge *)jsBridge {
WebViewJavascriptBridge *bridge = objc_getAssociatedObject(self, @selector(jsBridge));
return bridge;
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
swizzleMethods(self, @selector(setDelegate:), @selector(setJSBridgeDelegate:));
swizzleMethods(self, @selector(initWithFrame:), @selector(initJSWithFrame:));
});
}
- (instancetype)initJSWithFrame:(CGRect)frame {
self = [self initJSWithFrame:frame];
if (self) {
WebViewJavascriptBridge *bridge = [WebViewJavascriptBridge bridgeForWebView:self];
[self setJsBridge:bridge];
}
return self;
}
/// webview.delegate = xxx 会被调用多次且传入的对象不一样
- (void)setJSBridgeDelegate:(id)delegate {
WebViewJavascriptBridge *bridge = self.jsBridge;
if (delegate == nil || bridge == nil) {
[self setJSBridgeDelegate:delegate];
} else if (bridge == delegate) {
[self setJSBridgeDelegate:delegate];
} else {
/// 第一次进入这里传入 bridge
/// 第二次进入这里传入一个delegate
if (![delegate isKindOfClass:[WebViewJavascriptBridge class]]) {
[bridge setWebViewDelegate:delegate];
/// 下面这一行代码是客户缺少的
/// fix with this
[self setJSBridgeDelegate:bridge];
} else {
[self setJSBridgeDelegate:delegate];
}
}
}
@end
@interface UIWebView (MyHook)
@end
@implementation UIWebView (MyHook)
// Hook, setWebViewDelegate:和setDelegate:交换过
- (void)setWebViewDelegate:(id)delegate {
if (delegate == nil) {
[self setWebViewDelegate:delegate];
}
BDWebViewDelegateProxy *proxy = [[BDWebViewDelegateProxy alloc] initWithTarget:delegate];
self.bd_TrackerDecorator = proxy;
[self setWebViewDelegate:proxy];
}
@end
setJSBridgeDelegate:(id)delegate
的 delegate 实际上是 DelegateProxy 而非 bridge。setJSBridgeDelegate:(id)delegate
在判断了 delegate 不是 bridge 之后,把 UIWebView 的 delegate 设置为 bridge 就可以完成了。修复后模型
setDelegate
进行 Hook,也可能使用 Proxy。
先补充一些参考资料
RxCocoa 源码参考 https://github.com/ReactiveX/RxSwift rxcocoa 学习-DelegateProxy
如果单独只使用了 RxCocoa 的方案,和方案是一致,也就不会有任何问题。
RxCocoa+MySDK 之后,变成这样子。UICollectionView 的 delegate 直接指向谁在于谁调用的 setDelegate
方法后调。
理论也应该没有问题,就是引用链多一个 poxy 包装而已。但是实际上有两个问题。
// UIScrollView+Rx.swift
extension Reactive where Base: UIScrollView {
public var delegate: DelegateProxy<UIScrollView, UIScrollViewDelegate> {
return RxScrollViewDelegateProxy.proxy(for: base)
// base可以理解为一个UIScrollView 实例
}
}
open class RxScrollViewDelegateProxy {
public static func proxy(for object: ParentObject) -> Self {
let maybeProxy = self.assignedProxy(for: object)
let proxy: AnyObject
if let existingProxy = maybeProxy {
proxy = existingProxy
} else {
proxy = castOrFatalError(self.createProxy(for: object))
self.assignProxy(proxy, toObject: object)
assert(self.assignedProxy(for: object) === proxy)
}
let currentDelegate = self._currentDelegate(for: object)
let delegateProxy: Self = castOrFatalError(proxy)
if currentDelegate !== delegateProxy {
delegateProxy._setForwardToDelegate(currentDelegate, retainDelegate: false)
assert(delegateProxy._forwardToDelegate() === currentDelegate)
self._setCurrentDelegate(proxy, to: object)
/// 命中下面这一行assert
assert(self._currentDelegate(for: object) === proxy)
assert(delegateProxy._forwardToDelegate() === currentDelegate)
}
return delegateProxy
}
}
重点逻辑
delegateProxy 即使 RxDelegateProxy
currentDelegate 为 RxDelegateProxy 指向的对象
RxDelegateProxy._setForwardToDelegate 把 RxDelegateProxy 指向真实的 Delegate
标红的前面一句执行的时候,是调用 setDelegate 方法,把 RxDelegateProxy 的 proxy 设置给 UIScrollView (其实是一个 UICollectionView 实例)
然后进入了 MySDK 的 Hook 方法,把 RxDelegateProxy 给包了一层
最终结果如下图
然后导致 self._currentDelegate(for: object) 是 DelegateProxy 而非 RxDelegateProxy,触发标红断言
open class RxScrollViewDelegateProxy {
override open func responds(to aSelector: Selector!) -> Bool {
return super.responds(to: aSelector)
|| (self._forwardToDelegate?.responds(to: aSelector) ?? false)
|| (self.voidDelegateMethodsContain(aSelector) && self.hasObservers(selector: aSelector))
}
}
@implementation BDCollectionViewDelegateProxy
- (BOOL)respondsToSelector:(SEL)aSelector {
if (aSelector == @selector(bd_isCollectionViewTrackerDecorator)) {
return YES;
}
return [super respondsToSelector:aSelector];
}
@end
open class RxScrollViewDelegateProxy {
public static func proxy(for object: ParentObject) -> Self {
...
let currentDelegate = self._currentDelegate(for: object)
let delegateProxy: Self = castOrFatalError(proxy)
//if currentDelegate !== delegateProxy
if !currentDelegate.responds(to: xxxMethod) {
delegateProxy._setForwardToDelegate(currentDelegate, retainDelegate: false)
assert(delegateProxy._forwardToDelegate() === currentDelegate)
self._setCurrentDelegate(proxy, to: object)
assert(self._currentDelegate(for: object) === proxy)
assert(delegateProxy._forwardToDelegate() === currentDelegate)
} else {
return currentDelegate
}
return delegateProxy
}
}
状态1
setDelegate 的时候,先进 Rx 的方法,后进 MySDK 的 Hook 方法,
传给 Rx 的就是 delegate
传给 MySDK 的是 RxDelegateProxy
Delegate 的 get 调用就触发 bug
状态2
setDelegate 的时候,先进 MySDK 的 Hook 方法,后进 Rx 的方法?
传给 Rx 的就是 DelegateProxy
extension Reactive where Base: UIScrollView {
public func setDelegate(_ delegate: UIScrollViewDelegate)
-> Disposable {
return RxScrollViewDelegateProxy.installForwardDelegate(delegate, retainDelegate: false, onProxyForObject: self.base)
}
}
open class RxScrollViewDelegateProxy {
public static func installForwardDelegate(_ forwardDelegate: Delegate, retainDelegate: Bool, onProxyForObject object: ParentObject) -> Disposable {
weak var weakForwardDelegate: AnyObject? = forwardDelegate as AnyObject
let proxy = self.proxy(for: object)
assert(proxy._forwardToDelegate() === nil, "")
proxy.setForwardToDelegate(forwardDelegate, retainDelegate: retainDelegate)
return Disposables.create {
...
}
}
}
collectionView.rx.setDelegate(delegate)
let delegate = collectionView.rx.delegate
setDelegate 的时候,先进 Rx 的方法,传给 Rx 真实的 delegate
后进 MySDK 的 Hook 方法
传给 MySDK 的是 RxDelegateProxy
Rx 里面获取 CollectionView 的 delegate 触发判断
Delegate 的 get 调用就触发 bug
RxDelegateProxy 没有实现 UICollectionViewDelegate
的任何方法
forwardInvocation 中处理 UICollectionViewDelegate
相关回调
forwardingTargetForSelector 中针对 delegate 的回调,target 返回一个 SDK 处理的类,比 DelegateProxy
DelegateProxy 上报完成之后,直接调用跳到 RxDelegateProxy 的 forwardInvocation 方法
谨慎使用 Hook
Hook 系统接口一定要遵循一定的规范,不能假想只有你在 Hook 这个接口
不要假想其他人会怎么处理,直接把多种方案集成到一起,构建多种场景,测试兼容性
NSProxy使用:
https://juejin.cn/post/6962720860700409886/
代理模式:
https://juejin.cn/post/6844903544965857294
rxcocoa 学习-DelegateProxy:
https://xing-ou.github.io/2017/06/13/rxcocoa学习-DelegateProxy/