無埋點核心技術:iOS Hook在字節的實踐經驗

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"衆所周知,精確的推薦離不開大量埋點,常見的埋點採集方案是在響應用戶行爲操作的路徑上進行埋點。但是由於 App 通常會有比較多界面和操作路徑,主動埋點的維護成本就會非常大。所以行業的做法是無埋點,而無埋點實現需要 AOP 編程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個常見的場景,比如想在"},{"type":"codeinline","content":[{"type":"text","text":"UIViewController"}]},{"type":"text","text":"出現和消失的時刻分別記錄時間戳用於統計頁面展現的時長。要達到這個目標有很多種方法,但是 AOP 無疑是最簡單有效的方法。Objective-C 的 Hook 其實也有很多種方式,這裏以 Method Swizzle 給個示例。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@interface UIViewController (MyHook)\n\n@end\n\n@implementation UIViewController (MyHook)\n\n+ (void)load {\n static dispatch_once_t onceToken;\n dispatch_once(&onceToken, ^{\n \/\/\/ 常規的 Method Swizzle封裝\n swizzleMethods(self, @selector(viewDidAppear:), @selector(my_viewDidAppear:));\n \/\/\/ 更多Hook\n });\n}\n\n- (void)my_viewDidAppear:(BOOL)animated {\n \/\/\/ 一些Hook需要的邏輯\n\n \/\/\/ 這裏調用Hook後的方法,其實現其實已經是原方法了。\n [self my_viewDidAppear: animated];\n}\n\n@end"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們探討一個具體場景:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"UICollectionView"}]},{"type":"text","text":"或者"},{"type":"codeinline","content":[{"type":"text","text":"UITableView"}]},{"type":"text","text":"是 iOS 中非常常用的列表 UI 組件,其中列表元素的點擊事件回調是通過"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]},{"type":"text","text":"完成的。這裏以"},{"type":"codeinline","content":[{"type":"text","text":"UICollectionView"}]},{"type":"text","text":"爲例,"},{"type":"codeinline","content":[{"type":"text","text":"UICollectionView"}]},{"type":"text","text":"的"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]},{"type":"text","text":",有個方法聲明,"},{"type":"codeinline","content":[{"type":"text","text":"collectionView:didSelectItemAtIndexPath:"}]},{"type":"text","text":",實現這個方法我們就可以給列表元素添加點擊事件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"我們的目標是 Hook 這個 delegate 的方法,在點擊回調的時候進行額外的埋點操作。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"方案迭代"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"方案 1 Method Swizzle"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常情況下,Method Swizzle 可以滿足絕大部分的 AOP 編程需求。因此首次迭代,我們直接使用 Method Swizzle 來進行 Hook。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@interface UICollectionView (MyHook)\n\n@end\n\n@implementation UICollectionView (MyHook)\n\n\/\/ Hook, setMyDelegate:和setDelegate:交換過\n- (void)setMyDelegate:(id)delegate {\n if (delegate != nil) {\n \/\/\/ 常規Method Swizzle\n swizzleMethodsXXX(delegate, @selector(collectionView:didSelectItemAtIndexPath:), self, @selector(my_collectionView:didSelectItemAtIndexPath:));\n\n }\n\n [self setMyDelegate:nil];\n}\n\n- (void)my_collectionView:(UICollectionView *)ccollectionView didSelectItemAtIndexPath:(NSIndexPath *)index {\n \/\/\/ 一些Hook需要的邏輯\n\n \/\/\/ 這裏調用Hook後的方法,其實現其實已經是原方法了。\n [self my_collectionView:ccollectionView didSelectItemAtIndexPath:index];\n}\n\n@end"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們把這個方案集成到今日頭條 App 裏面進行測試驗證,發現沒法辦法驗證通過。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要原因今日頭條 App 是一個龐大的項目,其中引入了非常多的三方庫,比如 IGListKit 等,這些三方庫通常對"},{"type":"codeinline","content":[{"type":"text","text":"UICollectionView"}]},{"type":"text","text":"的使用都進行了封裝,而這些封裝,恰恰導致我們不能使用常規的 Method Swizzle 來 Hook 這個 delegate。直接的原因總結有以下兩點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"setDelegate"}]},{"type":"text","text":"傳入的對象不是實現"},{"type":"codeinline","content":[{"type":"text","text":"UICollectionViewDelegate"}]},{"type":"text","text":"協議的那個對象"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/64\/6460545a1108207a658db20fe88dccff.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如圖示,"},{"type":"codeinline","content":[{"type":"text","text":"setDelegate"}]},{"type":"text","text":"傳入的是一個代理對象 proxy,proxy 引用了實際的實現"},{"type":"codeinline","content":[{"type":"text","text":"UICollectionViewDelegate"}]},{"type":"text","text":"協議的"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]},{"type":"text","text":",proxy 實際上並沒有實現"},{"type":"codeinline","content":[{"type":"text","text":"UICollectionViewDelegate"}]},{"type":"text","text":"的任何一個方法,它把所有方法都轉發給實際的"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]},{"type":"text","text":"。這種情況下,我們不能直接對 proxy 進行 Method Swizzle"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"多次"},{"type":"codeinline","content":[{"type":"text","text":"setDelegate"}]}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/64\/6460545a1108207a658db20fe88dccff.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上述圖例中,使用方存在連續調用兩次"},{"type":"codeinline","content":[{"type":"text","text":"setDelegate"}]},{"type":"text","text":"的情況,第一次是真實"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]},{"type":"text","text":",第二次是"},{"type":"codeinline","content":[{"type":"text","text":"proxy"}]},{"type":"text","text":",我們需要區別對待。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"代理模式和 NSProxy 介紹"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 proxy 對原對象進行代理,在處理完額外操作之後再調用原對象,這種模式稱爲代理模式。而 Objective-C 中要實現代理模式,使用 NSProxy 會比較高效。詳細內容參考下列文章。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代理模式"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"NSProxy 使用"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏面"},{"type":"codeinline","content":[{"type":"text","text":"UICollectionView"}]},{"type":"text","text":"的"},{"type":"codeinline","content":[{"type":"text","text":"setDelegate"}]},{"type":"text","text":"傳入的是一個"},{"type":"codeinline","content":[{"type":"text","text":"proxy"}]},{"type":"text","text":"是非常常見的操作,比如 IGListKit,同時 App 基於自身需求,也有可能會做這一層封裝。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"codeinline","content":[{"type":"text","text":"UICollectionView"}]},{"type":"text","text":"的"},{"type":"codeinline","content":[{"type":"text","text":"setDelegate"}]},{"type":"text","text":"的時候,把"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]},{"type":"text","text":"包裹在"},{"type":"codeinline","content":[{"type":"text","text":"proxy"}]},{"type":"text","text":"中,然後把 proxy 設置給"},{"type":"codeinline","content":[{"type":"text","text":"UICollectionView"}]},{"type":"text","text":",使用"},{"type":"codeinline","content":[{"type":"text","text":"proxy"}]},{"type":"text","text":"對"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]},{"type":"text","text":"進行消息轉發。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/64\/6460545a1108207a658db20fe88dccff.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"方案 2 使用代理模式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"方案 1 已經無法滿足我們的需求了,我們考慮到既然對"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]},{"type":"text","text":"進行代理是一種常規操作,我們何不也使用代理模式,對"},{"type":"codeinline","content":[{"type":"text","text":"proxy"}]},{"type":"text","text":"再次代理。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"代碼實現"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先 Hook "},{"type":"codeinline","content":[{"type":"text","text":"UICollectionView"}]},{"type":"text","text":"的"},{"type":"codeinline","content":[{"type":"text","text":"setDelegate"}]},{"type":"text","text":"方法"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代理"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡單的代碼示意如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\/\/\/ 完整封裝了一些常規的消息轉發方法\n@interface DelegateProxy : NSProxy\n\n@property (nonatomic, weak, readonly) id target;\n\n@end\n\n\/\/\/ 爲 CollectionView delegate轉發消息的proxy\n@interface BDCollectionViewDelegateProxy : DelegateProxy\n\n@end\n\n@implementation BDCollectionViewDelegateProxy \n\n- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {\n \/\/track event here\n if ([self.target respondsToSelector:@selector(collectionView:didSelectItemAtIndexPath:)]) {\n [self.target collectionView:collectionView didSelectItemAtIndexPath:indexPath];\n\n }\n}\n\n- (BOOL)bd_isCollectionViewTrackerDecorator {\n return YES;\n}\n\n\/\/ 還有其他的消息轉發的代碼 先忽略\n- (BOOL)respondsToSelector:(SEL)aSelector {\n if (aSelector == @selector(bd_isCollectionViewTrackerDecorator)) {\n return YES;\n }\n\n return [self.target respondsToSelector:aSelector];\n}\n\n\n@end\n\n@interface UICollectionView (MyHook)\n\n@end\n\n@implementation UICollectionView (MyHook)\n\n- (void) setDd_TrackerProxy:(BDCollectionViewDelegateProxy *)object {\n objc_setAssociatedObject(self, @selector(bd_TrackerProxy), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);\n}\n\n- (BDCollectionViewDelegateProxy *) bd_TrackerProxy {\n BDCollectionViewDelegateProxy *bridge = objc_getAssociatedObject(self, @selector(bd_TrackerProxy));\n\n return bridge;\n}\n\n\/\/ Hook, setMyDelegate:和setDelegate:交換過了\n- (void)setMyDelegate:(id)delegate {\n if (delegate == nil) {\n [self setMyDelegate:delegate];\n return\n }\n\n \/\/ 不會釋放,不重複設置\n if ([delegate respondsToSelector:@selector(bd_isCollectionViewTrackerDecorator)]) {\n [self setMyDelegate:delegate];\n return;\n }\n\n BDCollectionViewDelegateProxy *proxy = [[BDCollectionViewDelegateProxy alloc] initWithTarget:delegate];\n [self setMyDelegate:proxy];\n self.bd_TrackerProxy = proxy;\n\n}\n\n@end"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"模型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下圖實線表示強引用,虛線表示弱引用。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"情況一"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果使用方沒有對"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]},{"type":"text","text":"進行代理,而我們使用代理模式"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"UICollectionView"}]},{"type":"text","text":",其"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]},{"type":"text","text":"指針指向 DelegateProxy"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"DelegateProxy,被 UICollectionView 用 runtime 的方式強引用,其 target 弱引用真實 Delegate"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/64\/6460545a1108207a658db20fe88dccff.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"情況二"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果使用方也對"},{"type":"codeinline","content":[{"type":"text","text":"delegate"}]},{"type":"text","text":"進行代理,我們使用代理模式"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們只需要保證我們的 DelegateProxy 處於代理鏈中的一環即可"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/57\/57a55e92caf5c31ac6b7f5646370d8f5.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從這裏我們可以看出,代理模式有很好的擴展性,它允許代理鏈不斷嵌套,只要我們都遵循代理模式的原則即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"到這裏,我們的方案已經在今日頭條 App 上測試通過了。但是事情遠還沒有結束。"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"踩坑之旅"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前的還算比較可以,但是也不能完全避免問題。這裏其實不僅僅是"},{"type":"text","marks":[{"type":"strong"}],"text":"UICollectionView 的 delegate,包括:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"UIWebView"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"WKWebView"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"UITableView"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"UICollectionView"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"UIScrollView"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"UIActionSheet"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"UIAlertView"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們都採用相同的方法來進行 Hook。"},{"type":"text","marks":[{"type":"strong"}],"text":"同時我們將方案封裝一個 SDK 對外提供,以下統稱爲 MySDK。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"第一次踩坑"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"某客戶接入我們的方案之後,在集成過程中反饋有必現 Crash,下面詳細介紹一下這一次踩坑的經歷。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"堆棧信息"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重點信息是"},{"type":"codeinline","content":[{"type":"text","text":"[UIWebView webView:decidePolicyForNavigationAction:request:frame:decisionListener:]"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"Thread 0 Crashed:\n\n0 libobjc.A.dylib 0x000000018198443c objc_msgSend + 28\n\n1 UIKit 0x000000018be05b4c -[UIWebView webView:decidePolicyForNavigationAction:request:frame:decisionListener:] + 200\n\n2 CoreFoundation 0x0000000182731cd0 __invoking___ + 144\n\n3 CoreFoundation 0x000000018261056c -[NSInvocation invoke] + 292\n\n4 CoreFoundation 0x000000018261501c -[NSInvocation invokeWithTarget:] + 60\n\n5 WebKitLegacy 0x000000018b86d654 -[_WebSafeForwarder forwardInvocation:] + 156"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從堆棧信息不難判斷出 crash 原因是 UIWebView 的 delegate 野指針,那爲啥出現野指針呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏先說明一下 crash 的直接原因,然後再來具體分析爲什麼就出現了問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"MySDK 對 setDelegate 進行了 Hook"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"客戶也對 setDelegate 進行了 Hook"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"先執行 MySDK 的 Hook 邏輯調用,然後執行客戶的 Hook 邏輯調用"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"客戶 Hook 的代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@interface UIWebView (JSBridge)\n\n@end\n\n@implementation UIWebView (JSBridge)\n\n- (void)setJsBridge:(id)object {\n objc_setAssociatedObject(self, @selector(jsBridge), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);\n}\n\n- (WebViewJavascriptBridge *)jsBridge {\n WebViewJavascriptBridge *bridge = objc_getAssociatedObject(self, @selector(jsBridge));\n return bridge;\n}\n\n+ (void)load {\n static dispatch_once_t onceToken;\n dispatch_once(&onceToken, ^{\n swizzleMethods(self, @selector(setDelegate:), @selector(setJSBridgeDelegate:));\n swizzleMethods(self, @selector(initWithFrame:), @selector(initJSWithFrame:));\n });\n\n}\n\n- (instancetype)initJSWithFrame:(CGRect)frame {\n self = [self initJSWithFrame:frame];\n if (self) {\n WebViewJavascriptBridge *bridge = [WebViewJavascriptBridge bridgeForWebView:self];\n [self setJsBridge:bridge];\n }\n return self;\n}\n\n\/\/\/ webview.delegate = xxx 會被調用多次且傳入的對象不一樣\n- (void)setJSBridgeDelegate:(id)delegate {\n WebViewJavascriptBridge *bridge = self.jsBridge;\n if (delegate == nil || bridge == nil) {\n [self setJSBridgeDelegate:delegate];\n } else if (bridge == delegate) {\n [self setJSBridgeDelegate:delegate];\n } else {\n \/\/\/ 第一次進入這裏傳入 bridge\n \/\/\/ 第二次進入這裏傳入一個delegate\n if (![delegate isKindOfClass:[WebViewJavascriptBridge class]]) {\n [bridge setWebViewDelegate:delegate];\n \/\/\/ 下面這一行代碼是客戶缺少的\n \/\/\/ fix with this\n [self setJSBridgeDelegate:bridge];\n } else {\n [self setJSBridgeDelegate:delegate];\n }\n }\n}\n\n@end"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"MySDK Hook 代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@interface UIWebView (MyHook)\n\n@end\n\n@implementation UIWebView (MyHook)\n\n\/\/ Hook, setWebViewDelegate:和setDelegate:交換過\n- (void)setWebViewDelegate:(id)delegate {\n if (delegate == nil) {\n [self setWebViewDelegate:delegate];\n }\n BDWebViewDelegateProxy *proxy = [[BDWebViewDelegateProxy alloc] initWithTarget:delegate];\n self.bd_TrackerDecorator = proxy;\n [self setWebViewDelegate:proxy];\n}\n\n@end"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"野指針原因"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"UIWebView 有兩次調用 setDelegate 方法,第一次是傳的 WebViewJavascriptBridge,第二次傳的另一個實際的 WebViewDelegate。暫且稱第一次傳了 bridge 第二次傳了實際上的 delegate。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"第一次調用,MySDK Hook 的時候會用 DelegateProxy 包裝住 bridge,所有方法通過 DelegateProxy 轉發到 bridge,這裏傳給 "},{"type":"codeinline","content":[{"type":"text","text":"setJSBridgeDelegate:(id)delegate"}]},{"type":"text","text":"的 delegate 實際上是 DelegateProxy"},{"type":"text","marks":[{"type":"strong"}],"text":"而非 bridge"},{"type":"text","text":"。"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/07\/076c25fe31e1c0065b11d31a358ad6ff.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏需要注意,UIWebView 的 delegate 指向 DelegateProxy 是客戶給設置上的,且這個屬性"},{"type":"text","marks":[{"type":"strong"}],"text":"assign 而非 weak,這個 assign 很關鍵,assigin 在對象釋放之後不會自動變爲 nil。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"第二次調用,MySDK Hook 的時候會用新的 DelegateProxy 包裝住 delegate 也就是 WebViewDelegate,這個時候 MySDK 的邏輯是把新的 DelegateProxy 給強引用中,老的 DelegateProxy 就失去了強引用因此釋放了。"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/27\/271914de5fb6ed8e7228ccfafa7ceab6.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此時的狀態如果不做任何處理,當前狀態就如圖示:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"delegate 指向已經釋放的 DelegateProxy,野指針"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"UIWebview 觸發回調就導致 crash"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"修復方法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果補上那一句,"},{"type":"codeinline","content":[{"type":"text","text":"setJSBridgeDelegate:(id)delegate"}]},{"type":"text","text":"在判斷了 delegate 不是 bridge 之後,把 UIWebView 的 delegate 設置爲 bridge 就可以完成了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"註釋中 fix with this 下一行代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"修復後模型如下圖"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/46\/46aca0f5cf89eb7abc17b2c2819b8de2.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 Proxy 的方式雖然也可以解決一定的問題,但是也需要使用方遵循一定的規範,要意識到第三方 SDK 也可能"},{"type":"codeinline","content":[{"type":"text","text":"setDelegate"}]},{"type":"text","text":"進行 Hook,也可能使用 Proxy"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"第二次踩坑"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先補充一些參考資料"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RxCocoa 源碼參考 https:\/\/github.com\/ReactiveX\/RxSwift"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"rxcocoa 學習-DelegateProxy"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RxCocoa 也使用了代理模式,對 delegate 進行了代理,按道理應該沒有問題。但是 RxCocoa 的實現有點出入。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/6a\/6adecde829e551ce8643bdac7bf5ef6a.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果單獨只使用了"},{"type":"text","marks":[{"type":"strong"}],"text":"RxCocoa"},{"type":"text","text":"的方案,和方案是一致,也就不會有任何問題。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"RxCocoa+MySDK"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/5e\/5ebd347129f97b743e4f15f709fe19b6.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RxCocoa+MySDK 之後,變成這樣子。UICollectionView 的 delegate 直接指向誰在於誰調用的"},{"type":"codeinline","content":[{"type":"text","text":"setDelegate"}]},{"type":"text","text":"方法後調。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"理論也應該沒有問題,就是引用鏈多一個 poxy 包裝而已。但是實際上有兩個問題。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"問題 1"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RxCocoa 的 delegate 的 get 方法命中 assert"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\/\/ UIScrollView+Rx.swift\nextension Reactive where Base: UIScrollView {\n public var delegate: DelegateProxy {\n return RxScrollViewDelegateProxy.proxy(for: base)\n \/\/ base可以理解爲一個UIScrollView 實例\n }\n}\n\nopen class RxScrollViewDelegateProxy {\n public static func proxy(for object: ParentObject) -> Self {\n let maybeProxy = self.assignedProxy(for: object)\n let proxy: AnyObject\n if let existingProxy = maybeProxy {\n proxy = existingProxy\n } else {\n proxy = castOrFatalError(self.createProxy(for: object))\n self.assignProxy(proxy, toObject: object)\n assert(self.assignedProxy(for: object) === proxy)\n }\n let currentDelegate = self._currentDelegate(for: object)\n let delegateProxy: Self = castOrFatalError(proxy)\n if currentDelegate !== delegateProxy {\n delegateProxy._setForwardToDelegate(currentDelegate, retainDelegate: false)\n assert(delegateProxy._forwardToDelegate() === currentDelegate)\n self._setCurrentDelegate(proxy, to: object)\n \/\/\/ 命中下面這一行assert\n assert(self._currentDelegate(for: object) === proxy)\n assert(delegateProxy._forwardToDelegate() === currentDelegate)\n }\n return delegateProxy\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"重點邏輯"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"delegateProxy 即使 RxDelegateProxy"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"currentDelegate 爲 RxDelegateProxy 指向的對象"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RxDelegateProxy._setForwardToDelegate 把 RxDelegateProxy 指向真實的 Delegate"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"標紅的前面一句執行的時候,是調用 setDelegate 方法,把 RxDelegateProxy 的 proxy 設置給 UIScrollView(其實是一個 UICollectionView 實例)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後進入了 MySDK 的 Hook 方法,把 RxDelegateProxy 給包了一層"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最終結果如下圖"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後導致 self._currentDelegate(for: object) 是 DelegateProxy 而非 RxDelegateProxy,"},{"type":"text","marks":[{"type":"strong"}],"text":"觸發標紅斷言"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/55\/55ff35af7558540f75ed9f163298dfc2.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"這個斷言就很霸道"},{"type":"text","text":",相當於 RxCocoa 認爲就只有它能夠去使用 Proxy 包裝 delegate,其他人不能這樣做,只要做了,就斷言。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"進一步分析"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當前狀態"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/55\/55ff35af7558540f75ed9f163298dfc2.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再次進入 Rx 的方法"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"currentDelegate 是 UICollectionView 指向的 DelegateProxy(MySDK 的包裝)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"delegateProxy 指向還是 RxDelegateProxy"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"觸發 Rx 的 if 判斷,Rx 會把其指向真實的 delegate 改向 UICollectionView 指向的 DelegateProxy"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"導致循環指向,引用鏈中真實的 Delegate 丟失了"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/af\/af2db4fe1a1a955949529b0ed53170c0.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"問題 2"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"上面提到多次調用導致了循環指向,而循環指向導致了在實際的方法轉發的時候變成了死循環。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ca\/ca12231b269b21c442525c71acbd5687.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"responds 代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"open class RxScrollViewDelegateProxy {\n override open func responds(to aSelector: Selector!) -> Bool {\n return super.responds(to: aSelector)\n || (self._forwardToDelegate?.responds(to: aSelector) ?? false)\n || (self.voidDelegateMethodsContain(aSelector) && self.hasObservers(selector: aSelector))\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"@implementation BDCollectionViewDelegateProxy\n\n- (BOOL)respondsToSelector:(SEL)aSelector {\n if (aSelector == @selector(bd_isCollectionViewTrackerDecorator)) {\n return YES;\n }\n return [super respondsToSelector:aSelector];\n}\n\n@end"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"似乎只要不多次調用就沒有問題了?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"關鍵在於 Rx 的 setDelegate 方法也調用了 get 方法,導致一次 get 就觸發第二次調用。也就是多次調用是無法避免。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"解決方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"問題的原因比較明顯,如果改造 RxCocoa 的代碼,把第三方可能的 Hook 考慮進來,完全可以解決問題。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"解決方案 1"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"參考 MySDK 的 proxy 方案,在 proxy 中加入一個特殊方法,來判斷 RxDelegateProxy 是否已經在引用鏈中,而不去主動改變這個引用鏈。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/bc\/bcd5cf5e258fe58b81974855214cf1b4.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"open class RxScrollViewDelegateProxy {\n public static func proxy(for object: ParentObject) -> Self {\n ...\n let currentDelegate = self._currentDelegate(for: object)\n let delegateProxy: Self = castOrFatalError(proxy)\n \/\/if currentDelegate !== delegateProxy\n if !currentDelegate.responds(to: xxxMethod) {\n delegateProxy._setForwardToDelegate(currentDelegate, retainDelegate: false)\n assert(delegateProxy._forwardToDelegate() === currentDelegate)\n self._setCurrentDelegate(proxy, to: object)\n assert(self._currentDelegate(for: object) === proxy)\n assert(delegateProxy._forwardToDelegate() === currentDelegate)\n } else {\n return currentDelegate\n }\n\n return delegateProxy\n }\n\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"類似這樣的改造,就可以解決問題。我們與 Rx 團隊進行了溝通,也提了 PR,可惜最終被拒絕合入了。Rx 給出的說明是,Hook 是不優雅的方式,不推薦 Hook 系統的任何方法,也不想兼容任何第三方的 Hook。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"解決方案 2"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有沒有可能,RxCocoa 不改代碼,MySDK 來兼容?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"剛纔提到,有可能是兩種狀態。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"狀態 1"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"setDelegate 的時候,先進 Rx 的方法,後進 MySDK 的 Hook 方法,"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"傳給 Rx 的就是 delegate"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"傳給 MySDK 的是 RxDelegateProxy"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Delegate 的 get 調用就觸發 bug"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/5a\/5a78e89bfb906d0e45aa0971f703d68f.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"狀態 2"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"setDelegate 的時候,先進 MySDK 的 Hook 方法,後進 Rx 的方法?"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"傳給 Rx 的就是 DelegateProxy"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ea\/eaad6beab7ba858cd0e051063ab522a9.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實如果是狀態 2,似乎 Rxcocoa 的 bug 是不會復現的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"但是仔細查看 Rxcocoa 的 setDelegate 代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"extension Reactive where Base: UIScrollView {\n public func setDelegate(_ delegate: UIScrollViewDelegate)\n\n -> Disposable {\n return RxScrollViewDelegateProxy.installForwardDelegate(delegate, retainDelegate: false, onProxyForObject: self.base)\n }\n}\n\nopen class RxScrollViewDelegateProxy {\n public static func installForwardDelegate(_ forwardDelegate: Delegate, retainDelegate: Bool, onProxyForObject object: ParentObject) -> Disposable {\n weak var weakForwardDelegate: AnyObject? = forwardDelegate as AnyObject\n let proxy = self.proxy(for: object)\n assert(proxy._forwardToDelegate() === nil, \"\")\n proxy.setForwardToDelegate(forwardDelegate, retainDelegate: retainDelegate)\n return Disposables.create {\n ...\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"emmm?Rx 裏面,UICollectionView 的 setDelegate 和 Delegate 的 get 方法"},{"type":"text","marks":[{"type":"strong"}],"text":"不是 Hook..."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"collectionView.rx.setDelegate(delegate)\n\nlet delegate = collectionView.rx.delegate"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"最終流程就只能是"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"setDelegate 的時候,先進 Rx 的方法,傳給 Rx 真實的 delegate"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"後進 MySDK 的 Hook 方法"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"傳給 MySDK 的是 RxDelegateProxy"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rx 裏面獲取 CollectionView 的 delegate 觸發判斷"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Delegate 的 get 調用就觸發 bug"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果 MySDK 還是採用當前的 Hook 方案,就沒法在 MySDK 解決了。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"解決方案 3"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"仔細看了一下,發現 Rx 裏面是通過重寫 RxDelegateProxy 的 forwardInvocation 來達到方法轉發的目的,即"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RxDelegateProxy 沒有實現"},{"type":"codeinline","content":[{"type":"text","text":"UICollectionViewDelegate"}]},{"type":"text","text":"的任何方法"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"forwardInvocation 中處理"},{"type":"codeinline","content":[{"type":"text","text":"UICollectionViewDelegate"}]},{"type":"text","text":"相關回調"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回顧消息轉發機制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/a3\/d2\/a389945e99a7811f6430930b2c7d08d2.jpg","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以在 forwardingTargetForSelector 這一步進行處理,這樣可以避開與 Rx 相關的衝突,處理完再直接跳過。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"forwardingTargetForSelector 中針對 delegate 的回調,target 返回一個 SDK 處理的類,比 DelegateProxy"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"DelegateProxy 上報完成之後,直接調用跳到 RxDelegateProxy 的 forwardInvocation 方法"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個解決方案其實也不完美,只能暫時規避與 Rx 的衝突。如果後續有其他 SDK 也來在這個階段處理 Hook 衝突,也容易出現問題。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"確實如 Rx 團隊描述的那樣,Hook 不是很優雅的方式,任何 Hook 都有可能存在兼容性問題。"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"謹慎使用 Hook"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"Hook 系統接口一定要遵循一定的規範,不能假想只有你在 Hook 這個接口"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"不要假想其他人會怎麼處理,直接把多種方案集成到一起,構建多種場景,測試兼容性"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文章列舉的方案可能不全或者不完善,如果有更好的方案,歡迎討論。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"參考文檔"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"NSProxy 使用"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代理模式"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"rxcocoa 學習-DelegateProxy"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/github.com\/ReactiveX\/RxSwift"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:字節跳動技術團隊(ID:toutiaotechblog)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/8nq6q1p5-RJPiDTEJquyNQ","title":"xxx","type":null},"content":[{"type":"text","text":"無埋點核心技術:iOS Hook在字節的實踐經驗"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章