iOS之深入解析消息轉發objc_msgSend的應用場景

      網友投稿 949 2022-05-30

      一、消息轉發

      現有如下示例:

      id o = [NSObject new]; [o lastObject];

      1

      2

      執行上面代碼,程序會崩潰并拋出以下異常:

      [NSObject lastObject]: unrecognized selector sent to instance 0x100200160

      1

      錯誤顯而易見,實例對象 o 無法響應 lastObject 方法。那么問題來了, Objetive-C 作為一門動態語言,更有強大的 runtime 在背后撐腰,它會讓程序沒有任何預警地直接奔潰么?當然不會,Objetive-C 的 runtime 不但提供了挽救機制,而且還是三部曲:

      Lazy method resolution

      Fast forwarding path

      Normal forwarding path

      上述程序崩潰的根本原因在于沒有找到方法的實現,也就是通常所說的 IMP 不存在。結合以下源碼,可以知道消息轉發三部曲是由 _objc_msgForward 函數發起的:

      IMP class_getMethodImplementation(Class cls, SEL sel) { IMP imp; if (!cls || !sel) return nil; imp = lookUpImpOrNil(cls, sel, nil, YES/*initialize*/, YES/*cache*/, YES/*resolver*/); // Translate forwarding function to C-callable external version if (!imp) { return _objc_msgForward; } return imp; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      ① Lazy method resolution

      在這一步,_objc_msgForward 直接或間接調用了以下方法:

      // 針對類方法 + (BOOL)resolveClassMethod:(SEL)sel; // 針對對象方法 + (BOOL)resolveInstanceMethod:(SEL)sel;

      1

      2

      3

      4

      由于形參中傳入了無法找到對應 IMP 的 SEL,就可以在這個方法中動態添加 SEL 的實現,并返回 YES 重新啟動一次消息發送動作;如果方法返回 NO ,那么就進行消息轉發的下個流程 Fast forwarding path。這種方式能夠方便地實現 @dynamic 屬性, CoreData 中模型定義中就廣泛使用到了 @dynamic 屬性。

      ② Fast forwarding path

      在這一步,_objc_msgForward 直接或間接調用了以下方法:

      - (id)forwardingTargetForSelector:(SEL)aSelector;

      1

      這個方法還是只附帶了無法找到對應 IMP 的 SEL,可以根據這個 SEL,判斷是否有其它對象可以響應它,然后選擇將消息轉發給這個對象。如果返回除 nil / self 之外的對象,那么會重啟一次消息發送動作給返回的對象,否則進入下個流程 Normal forwarding path。

      ③ Normal forwarding path

      在這一步,_objc_msgForward 直接或間接調用了以下方法:

      - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; - (void)forwardInvocation:(NSInvocation *)anInvocation;

      1

      2

      這是消息轉發的最后一步,首先會調用的是 -methodSignatureForSelector: 方法,這個方法返回一個方法簽名,用以構造 NSInvocation 并作為實參傳入 -forwardInvocation: 方法中。如果 -methodSignatureForSelector: 返回 nil,將會拋出 unrecognized selector 異常。

      由于在 -forwardInvocation: 方法中可以獲取到 NSInvocation,而 NSInvocation 包含了參數、發送目標以及 SEL 等信息,尤其是參數信息,因此這一步也是可操作性最強的一步。我們可以選擇直接執行傳入的 NSInvocation 對象,也可以通過 -invokeWithTarget: 指定新的發送目標。

      一般來說,既然走到這一步,這個對象都是沒有 SEL 對應的 IMP 的,所以通常來說都必須要重寫 -methodSignatureForSelector: 方法以返回有效的方法簽名,否則就會拋出異常。不過有種例外,當對象實現了相應的方法,但還是走到了 Normal forwarding path 這一步時,就可以不重寫 -methodSignatureForSelector: 方法。

      理解這種操作需要知曉 method swizzling 技術中的一個知識點,替換 IMP 是不會影響到 SEL 和 參數信息的。因此當把某個方法的實現替換成 _objc_msgForward / _objc_msgForward_stret 以啟動消息轉發時,即使不重寫 -methodSignatureForSelector:,這個方法依舊能返回有效的方法簽名信息。如下所示:

      NSArray *arr = [NSArray new]; Method old = class_getInstanceMethod([arr class], @selector(objectAtIndex:)); printf("old type: %s, imp: %p\n", method_getTypeEncoding(old), method_getImplementation(old)); class_replaceMethod([arr class], @selector(objectAtIndex:), _objc_msgForward, NULL); Method new = class_getInstanceMethod([arr class], @selector(objectAtIndex:)); printf("new type: %s, imp: %p\n", method_getTypeEncoding(new), method_getImplementation(new));

      1

      2

      3

      4

      5

      6

      7

      8

      9

      上面程序輸出如下:

      old type: @24@0:8Q16, imp: 0x7fffb5fc31e0 new type: @24@0:8Q16, imp: 0x7fffcada5cc0

      1

      2

      可以看到,更改的只有方法實現 IMP,并且從源碼層面看,method swizzling 在方法已存在的情況下,只是設置了對應的 Method 的 IMP,當方法不存在時,才會設置額外的一些屬性:

      IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) { if (!cls) return nil; rwlock_write(&runtimeLock); IMP old = addMethod(cls, name, imp, types ?: "", YES); rwlock_unlock_write(&runtimeLock); return old; } static IMP addMethod(Class cls, SEL name, IMP imp, const char *types, BOOL replace) { IMP result = nil; rwlock_assert_writing(&runtimeLock); assert(types); assert(cls->isRealized()); method_t *m; // 方法是否存在 if ((m = getMethodNoSuper_nolock(cls, name))) { // already exists if (!replace) { // 不替換返回已存在方法實現IMP result = _method_getImplementation(m); } else { // 直接替換類cls的m函數指針為imp result = _method_setImplementation(cls, m, imp); } } else { // fixme optimize // 申請方法列表內存 method_list_t *newlist; newlist = (method_list_t *)_calloc_internal(sizeof(*newlist), 1); newlist->entsize_NEVER_USE = (uint32_t)sizeof(method_t) | fixed_up_method_list; newlist->count = 1; // 賦值名字,類型,方法實現(函數指針) newlist->first.name = name; newlist->first.types = strdup(types); if (!ignoreSelector(name)) { newlist->first.imp = imp; } else { newlist->first.imp = (IMP)&_objc_ignored_method; } // 向類添加方法列表 attachMethodLists(cls, &newlist, 1, NO, NO, YES); result = nil; } return result; }

      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

      56

      消息轉發流程大體如此,如果想了解具體的轉發原理、_objc_msgForward 內部是如何實現的,可以參考:

      iOS之深入解析Runtime的objc_msgSend“快速查找”底層原理;

      iOS之深入解析Runtime的objc_msgSend“慢速查找”底層原理;

      iOS之深入解析objc_msgSend消息轉發機制的底層原理。

      二、Week Proxy

      NSTimer、CADisplayLink 是實際項目中常用的計時器類,它們都使用 target - action 機制設置目標對象以及回調方法,相信很多人都遇到過 NSTimer 或者 CADisplayLink 對象造成的循環引用問題。實際上,這兩個對象是強引用 target 的,如果使用者管理不當,輕則造成 target 對象的延遲釋放,重則導致與 target 對象的循環引用。

      假如有個 UIViewController 引用了一個 repeat 的 NSTimer 對象 (先不論強弱引用) ,正確的管理方式是在控制器退出回調中手動 invalidate 并釋放對 NSTimer 對象的引用:

      - (void)popViewController { [_timer invalidate]; _timer = nil; // 強引用需要,弱引用不需要 }

      1

      2

      3

      4

      這種分散的管理方式,總會讓使用者在某些場景下忘記了停止 _timer ,特別是使用者希望在 UIViewController 對象的 dealloc 方法中停止定時器時,很容易掉進這個坑里。有沒有更加優雅的管理機制呢?

      來看看 FLAnimatedImage 是如何管理 CADisplayLink 對象的:

      FLAnimatedImage 創建了以下弱引用代理:

      @interface FLWeakProxy : NSProxy + (instancetype)weakProxyForObject:(id)targetObject; @end @interface FLWeakProxy () @property (nonatomic, weak) id target; @end @implementation FLWeakProxy #pragma mark Life Cycle // This is the designated creation method of an `FLWeakProxy` and // as a subclass of `NSProxy` it doesn't respond to or need `-init`. + (instancetype)weakProxyForObject:(id)targetObject { FLWeakProxy *weakProxy = [FLWeakProxy alloc]; weakProxy.target = targetObject; return weakProxy; } #pragma mark Forwarding Messages - (id)forwardingTargetForSelector:(SEL)selector { // Keep it lightweight: access the ivar directly return _target; } #pragma mark - NSWeakProxy Method Overrides #pragma mark Handling Unimplemented Methods - (void)forwardInvocation:(NSInvocation *)invocation { // Fallback for when target is nil. Don't do anything, just return 0/NULL/nil. // The method signature we've received to get here is just a dummy to keep `doesNotRecognizeSelector:` from firing. // We can't really handle struct return types here because we don't know the length. void *nullPointer = NULL; [invocation setReturnValue:&nullPointer]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { // We only get here if `forwardingTargetForSelector:` returns nil. // In that case, our weak target has been reclaimed. Return a dummy method signature to keep `doesNotRecognizeSelector:` from firing. // We'll emulate the Obj-c messaging nil behavior by setting the return value to nil in `forwardInvocation:`, but we'll assume that the return value is `sizeof(void *)`. // Other libraries handle this situation by making use of a global method signature cache, but that seems heavier than necessary and has issues as well. // See https://www.mikeash.com/pyblog/friday-qa-2010-02-26-futures.html and https://github.com/steipete/PSTDelegateProxy/issues/1 for examples of using a method signature cache. return [NSObject instanceMethodSignatureForSelector:@selector(init)]; } @end

      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

      通過上面代碼,可以看出 FLWeakProxy 是弱引用 target 的,而且它在消息轉發的第二步,將所有的消息都轉發給了 target 對象,如下是調用方使用此弱引用代理的代碼:

      @interface FLAnimatedImageView () @property (nonatomic, strong) CADisplayLink *displayLink; @end @implementation FLAnimatedImageView ... - (void)startAnimating { ... FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self]; self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)]; [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode]; ... } ... @end

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      其對象間的引用關系可以用下圖表示:

      ---> 強引用 ~~~> 弱引用 FLAnimatedImageView(object) ---> displayLink ---> weakProxy ~~~> FLAnimatedImageView(object)

      1

      2

      這樣一來, displayLink 間接弱引用 FLAnimatedImageView 對象,使得 FLAnimatedImageView 對象得以正常釋放。而且由于 weakProxy 將消息全部轉發給了 FLAnimatedImageView 對象,-displayDidRefresh: 也得以正確地回調。

      事實上,以上問題也可以通過 block 回調的方式解決,具體實現就是讓創建的定時器對象持有 NSTimer 類對象,并且在類回調方法中,執行經 userInfo 傳過來的 block 回調。此外,蘋果私有庫 MIME.framework 中就有這種機制的應用 MFWeakProxy;YYKit 的 YYAnimatedImageView 也使用了相同的機制管理 CADisplayLink,其對應類為 YYWeakProxy。

      三、Delegate Proxy

      Delegate Proxy 主要實現部分代理方法的轉發,顧名思義,就是封裝者使用了被封裝對象代理的一部分方法,然后將剩余的方法通過新的代理轉發給調用者,這種機制在二次封裝第三方框架或者原生控件時,能減少不少“膠水”代碼。

      接下來,以 IGListKit 中的 IGListAdapterProxy 為例,來描述如何利用這種機制來簡化代碼。在開始之前先了解下與 IGListAdapterProxy 直接相關的 IGListAdapter,IGListAdapter 是 UICollectionView 的數據源和代理實現者,以下是它與本主題相關聯的兩個屬性:

      @interface IGListAdapter : NSObject ... /** The object that receives `UICollectionViewDelegate` events. @note This object *will not* receive `UIScrollViewDelegate` events. Instead use scrollViewDelegate. */ @property (nonatomic, nullable, weak) id collectionViewDelegate; /** The object that receives `UIScrollViewDelegate` events. */ @property (nonatomic, nullable, weak) id scrollViewDelegate; ... @end

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      使用者可以成為 IGListAdapter 的代理,獲得和 UICollectionView 原生代理一致的編寫體驗。實際上, IGListAdapter 只是使用并實現了部分代理方法,那么它又是如何編寫有關這兩個屬性的代碼,讓使用者實現的代理方法能正確地執行呢?可能有些人會這樣寫:

      #pragma mark - UICollectionViewDelegateFlowLayout ... - (BOOL)collectionView:(UICollectionView *)collectionView canFocusItemAtIndexPath:(NSIndexPath *)indexPath { if ([self.collectionViewDelegate respondsToSelector:@selector(collectionView:canFocusItemAtIndexPath:)]) { return [self.collectionViewDelegate collectionView:collectionView canFocusItemAtIndexPath:indexPath]; } return YES; } - (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath { if ([self.collectionViewDelegate respondsToSelector:@selector(collectionView:shouldShowMenuForItemAtIndexPath:)]) { [self.collectionViewDelegate collectionView:collectionView shouldShowMenuForItemAtIndexPath:indexPath]; } return YES; } ...

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      當代理方法較少的時候,這種寫法是可以接受的。不過隨著代理方法的增多,編寫這種膠水代碼就有些煩人了,侵入性的修改方式也不符合開放閉合原則。來看下 IGListKit 是如何利用 IGListAdapterProxy 解決這個問題的:

      @interface IGListAdapterProxy : NSProxy - (instancetype)initWithCollectionViewTarget:(nullable id)collectionViewTarget scrollViewTarget:(nullable id)scrollViewTarget interceptor:(IGListAdapter *)interceptor; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; @end static BOOL isInterceptedSelector(SEL sel) { return ( // UICollectionViewDelegate sel == @selector(collectionView:didSelectItemAtIndexPath:) || sel == @selector(collectionView:willDisplayCell:forItemAtIndexPath:) || sel == @selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:) || // UICollectionViewDelegateFlowLayout sel == @selector(collectionView:layout:sizeForItemAtIndexPath:) || sel == @selector(collectionView:layout:insetForSectionAtIndex:) || sel == @selector(collectionView:layout:minimumInteritemSpacingForSectionAtIndex:) || sel == @selector(collectionView:layout:minimumLineSpacingForSectionAtIndex:) || sel == @selector(collectionView:layout:referenceSizeForFooterInSection:) || sel == @selector(collectionView:layout:referenceSizeForHeaderInSection:) || // UIScrollViewDelegate sel == @selector(scrollViewDidScroll:) || sel == @selector(scrollViewWillBeginDragging:) || sel == @selector(scrollViewDidEndDragging:willDecelerate:) ); } @interface IGListAdapterProxy () { __weak id _collectionViewTarget; __weak id _scrollViewTarget; __weak IGListAdapter *_interceptor; } @end @implementation IGListAdapterProxy - (instancetype)initWithCollectionViewTarget:(nullable id)collectionViewTarget scrollViewTarget:(nullable id)scrollViewTarget interceptor:(IGListAdapter *)interceptor { IGParameterAssert(interceptor != nil); // -[NSProxy init] is undefined if (self) { _collectionViewTarget = collectionViewTarget; _scrollViewTarget = scrollViewTarget; _interceptor = interceptor; } return self; } - (BOOL)respondsToSelector:(SEL)aSelector { return isInterceptedSelector(aSelector) || [_collectionViewTarget respondsToSelector:aSelector] || [_scrollViewTarget respondsToSelector:aSelector]; } - (id)forwardingTargetForSelector:(SEL)aSelector { if (isInterceptedSelector(aSelector)) { return _interceptor; } return [_scrollViewTarget respondsToSelector:aSelector] ? _scrollViewTarget : _collectionViewTarget; } - (void)forwardInvocation:(NSInvocation *)invocation { void *nullPointer = NULL; [invocation setReturnValue:&nullPointer]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { return [NSObject instanceMethodSignatureForSelector:@selector(init)]; } @end

      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

      iOS之深入解析消息轉發objc_msgSend的應用場景

      48

      49

      50

      51

      52

      53

      54

      55

      56

      57

      58

      59

      60

      61

      62

      63

      64

      65

      66

      67

      68

      69

      70

      71

      72

      73

      74

      75

      76

      這個類總共有三個自定義屬性,分別是用來支持外界代理方法回調的 _collectionViewTarget、 _scrollViewTarget,以及用以支持 AOP 的攔截者 _interceptor(IGListAdapter 在調用外界實現的代理方法前,插入了自己的實現,所以可視為攔截者)。isInterceptedSelector 函數表明攔截者使用到了哪些代理方法,而 -respondsToSelector: 和 -forwardingTargetForSelector: 則根據這個函數的返回值決定是否能響應方法,以及應該把消息轉發給攔截者還是外部代理。事實上,外部代理就是本小節開頭所說的使用者可以訪問的屬性:

      @implementation IGListAdapter ... self.delegateProxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:_collectionViewDelegate scrollViewTarget:_scrollViewDelegate interceptor:self]; ... @end

      1

      2

      3

      4

      5

      6

      7

      通過這種轉發機制,即使后續有新的代理方法,也不用手動添加“膠水代碼”了,一些流行的開源庫中也可以看到這種做法的身影,比如 AsyncDisplayKit 就有對應的 _ASCollectionViewProxy 來轉發未實現的代理方法。

      四、Multicast Delegate

      通知和代理是解耦對象間消息傳遞的兩種重要方式,其中通知主要針對一對多的單向通信,而代理則主要提供一對一的雙向通信。

      通常來說, IM 應用在底層模塊接受到新消息后,都會進行一次廣播處理,讓各模塊能根據新消息來更新狀態。當接收模塊不需要向發送模塊反饋任何信息時,使用 NSNotificationCenter 就可以實現上述需求。但是一旦發送模塊需要根據接收模塊返回的信息做一些額外處理,也就是實現一對多的雙向通信, NSNotificationCenter 就不滿足要求了。

      最直接的解決方案是,針對這個業務場景自定義一個消息轉發中心,讓遵守特定協議的外圍模塊主動注冊成為消息接收者。不過既然涉及到了特定協議,就注定了這個消息轉發中心缺少通用性,這時候就可以參考下業界現成的方案了。

      來看看 XMPPFramework 是如何解決這個問題的:

      將事件廣播給多個監聽者;

      易于擴展;

      選擇的機制要支持返回值;

      選擇的機制要易于編寫線程安全代碼。

      但是代理或者通知機制都不能很好地滿足上述需求,所以 GCDMulticastDelegate 類應運而生。 使用這個類時,廣播類需要初始化 GCDMulticastDelegate 對象:

      GCDMulticastDelegate *multicastDelegate; multicastDelegate = (GCDMulticastDelegate *)[[GCDMulticastDelegate alloc] init];

      1

      2

      并且添加增刪代理的方法:

      - (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue { [multicastDelegate addDelegate:delegate delegateQueue:delegateQueue]; } - (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue { [multicastDelegate removeDelegate:delegate delegateQueue:delegateQueue]; }

      1

      2

      3

      4

      5

      6

      7

      當廣播對象需要向所有注冊的代理發送消息時,可以用以下方式調用:

      [multicastDelegate worker:self didFinishSubTask:subtask inDuration:elapsed];

      1

      只要注冊的代理實現了這個方法,就可以接收到發送的信息。

      再來看下 GCDMulticastDelegate 的實現原理:

      首先, GCDMulticastDelegate 會在外界添加代理時,創建 GCDMulticastDelegateNode 對象封裝傳入的代理以及回調執行隊列,然后保存在 delegateNodes 數組中,當外界向 GCDMulticastDelegate 對象發送無法響應的消息時,它會針對此消息啟動轉發機制,并在 Normal forwarding path 這一步轉發給所有能響應此消息的注冊代理,以下是消息轉發相關的源碼:

      - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif NSMethodSignature *result = [nodeDelegate methodSignatureForSelector:aSelector]; if (result != nil) { return result; } } // This causes a crash... // return [super methodSignatureForSelector:aSelector]; // This also causes a crash... // return nil; return [[self class] instanceMethodSignatureForSelector:@selector(doNothing)]; } - (void)forwardInvocation:(NSInvocation *)origInvocation { SEL selector = [origInvocation selector]; BOOL foundNilDelegate = NO; for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if ([nodeDelegate respondsToSelector:selector]) { // All delegates MUST be invoked ASYNCHRONOUSLY. NSInvocation *dupInvocation = [self duplicateInvocation:origInvocation]; dispatch_async(node.delegateQueue, ^{ @autoreleasepool { [dupInvocation invokeWithTarget:nodeDelegate]; }}); } else if (nodeDelegate == nil) { foundNilDelegate = YES; } } if (foundNilDelegate) { // At lease one weak delegate reference disappeared. // Remove nil delegate nodes from the list. // // This is expected to happen very infrequently. // This is why we handle it separately (as it requires allocating an indexSet). NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; NSUInteger i = 0; for (GCDMulticastDelegateNode *node in delegateNodes) { id nodeDelegate = node.delegate; #if __has_feature(objc_arc_weak) && !TARGET_OS_IPHONE if (nodeDelegate == [NSNull null]) nodeDelegate = node.unsafeDelegate; #endif if (nodeDelegate == nil) { [indexSet addIndex:i]; } i++; } [delegateNodes removeObjectsAtIndexes:indexSet]; } } - (void)doesNotRecognizeSelector:(SEL)aSelector { // Prevent NSInvalidArgumentException } - (void)doNothing {}

      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

      56

      57

      58

      59

      60

      61

      62

      63

      64

      65

      66

      67

      68

      69

      70

      71

      72

      73

      74

      75

      76

      77

      78

      79

      80

      81

      可以看到, -methodSignatureForSelector: 方法遍歷了 delegateNodes ,并返回首個有效的方法簽名。當沒有找到有效的方法簽名時,會返回 -doNothing 方法的簽名,以規避未知方法導致的崩潰。在得到方法簽名并構造 NSInvocation 對象后, -forwardInvocation: 同樣遍歷了 delegateNodes ,并在特定的任務隊列中執行代理回調。如果發現已被銷毀的代理,則刪除它對應的 GCDMulticastDelegateNode 對象。

      五、Record Message Call

      NSUndoManager 是 Foundation 框架中,一個基于命令模式設計的撤消棧管理類。通過這個類可以很方便地實現撤消、重做功能,比如以下蘋果官方 Demo:

      - (void)setMyObjectWidth:(CGFloat)newWidth height:(CGFloat)newHeight{ float currentWidth = [myObject size].width; float currentHeight = [myObject size].height; if ((newWidth != currentWidth) || (newHeight != currentHeight)) { [[undoManager prepareWithInvocationTarget:self] setMyObjectWidth:currentWidth height:currentHeight]; [undoManager setActionName:NSLocalizedString(@"Size Change", @"size undo")]; [myObject setSize:NSMakeSize(newWidth, newHeight)]; } }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      通過調用代碼塊中 NSUndoManager 對象的 undo,可以“撤銷”以上方法對 myObject 相關屬性的設置,其中需要關注的是,NSUndoManager 是如何記錄目標對象接收發生改變的信息:

      [[undoManager prepareWithInvocationTarget:self] setMyObjectWidth:currentWidth height:currentHeight]

      1

      NSUndoManager 是如何通過這種方式存儲調用 -setMyObjectWidth:height: 這一動作呢?背后的關鍵在于 -prepareWithInvocationTarget: 所返回的對象,也就是 NSUndoManagerProxy。NSUndoManagerProxy 是 NSProxy 的子類,而 NSProxy 除了重載消息轉發機制外,基本上就沒有其他用法了。結合蘋果官方文檔, NSUndoManagerProxy 重載了 -forwardInvocation: 來幫助 NSUndoManager 獲取目標的方法調用信息。到目前為止,這個應用場景并不難理解,不過為了能切合 NSUndoManagerProxy 的實際實現,這里還是結合 Foundation 框架反匯編出的代碼,簡單地實現這個功能。

      首先創建 YDWUndoProxy, 重寫它的消息轉發機制:

      @interface YDWUndoProxy : NSProxy @property (weak, nonatomic) YDWUndoManager *manager; @end @implementation YDWUndoProxy - (void)forwardInvocation:(NSInvocation *)invocation { [_manager _forwardTargetInvocation:invocation]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [_manager _methodSignatureForTargetSelector:sel]; } @end

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      結合 LLDB 中的調試信息, YDWUndoProxy 只是簡單地把信息傳送給了 YDWUndoManager,再來看下將原生邏輯簡化后的 YDWUndoManager 的實現:

      @interface YDWUndoManager : NSObject { NSMutableArray *_invocations; YDWUndoProxy *_proxy; __weak id _target; } - (id)prepareWithInvocationTarget:(id)target; - (void)undo; @end @interface YDWUndoManager (Private) - (void)_forwardTargetInvocation:(NSInvocation *)invocation; - (NSMethodSignature *)_methodSignatureForTargetSelector:(SEL)sel; @end @implementation YDWUndoManager - (instancetype)init { self = [super init]; if (self) { _invocations = [NSMutableArray array]; } return self; } - (id)prepareWithInvocationTarget:(id)target { _target = target; _proxy = [YDWUndoProxy alloc]; _proxy.manager = self; return _proxy; } - (void)undo { [_invocations.lastObject invoke]; [_invocations removeObject:_invocations.lastObject]; } - (void)_forwardTargetInvocation:(NSInvocation *)invocation { [invocation setTarget:_target]; [_invocations addObject:invocation]; } - (NSMethodSignature *)_methodSignatureForTargetSelector:(SEL)sel { NSMethodSignature *signature = [super methodSignatureForSelector:sel]; if (!signature && _target) { signature = [_target methodSignatureForSelector:sel]; } return signature; } @end

      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

      YDWUndoManager 通過 -prepareWithInvocationTarget: 方法將發送消息對象保存為 _target 成員變量,然后創建了代理類 YDWUndoProxy 并返回給方法調用者。當外部調用者用這個返回值作為消息發送對象時, YDWUndoProxy 并沒有對應的方法實現,于是就觸發了消息轉發機制, YDWUndoManager 則利用保存的 _target 返回有效的方法簽名,并且保存重組了 YDWUndoProxy 回傳的 NSInvocation。最終,當外界調用 undo 時,執行的就是保有 _target 和 -prepareWithInvocationTarget: 信息的 NSInvocation(原生代碼將 NSInvocation 包裝成 _NSUndoInvocation 、 _NSUndoObject 壓入 _NSUndoStack 棧中)。

      六、Intercept Any Message Call

      Aspects 是一個提供面向切片編程的庫,它可以讓開發者以無侵入的方式添加額外的功能,它提供了兩個簡單易用的入口,用于 hook 特定類或者特定對象的方法:

      // Adds a block of code before/instead/after the current `selector` for a specific class. + (id)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; // Adds a block of code before/instead/after the current `selector` for a specific instance. - (id)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error;

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      開發者可以用以下方式 hook 所有 UIViewController 實例對象的 -viewWillAppear: 方法:

      [UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id aspectInfo, BOOL animated) { NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated); } error:NULL];

      1

      2

      3

      因為不知道使用者會 hook 什么方法,所以就無法像傳統的 swizzling method 一樣,預先編寫對應的 IMP 去替換傳入的方法,這時就需要內部實現一個統一調用機制,這個機制需要滿足以下兩點:

      為了能進行切片操作,需要讓所有被 hook 方法的調用都通過一個統一的入口完成;

      為了給原始實現和切片操作提供參數/返回值信息,這個入口要能獲取被 hook 方法完整的簽名信息。

      綜合上述兩點以及 Normal forwarding path 的執行過程,可以比較輕松地聯想到 -forwardInvocation: 方法非常適合作為這個入口。結合 Aspects 源碼,來看下其實現中,和消息轉發相關的兩個步驟:

      static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) { NSCParameterAssert(selector); Class klass = aspect_hookClass(self, error); Method targetMethod = class_getInstanceMethod(klass, selector); IMP targetMethodIMP = method_getImplementation(targetMethod); if (!aspect_isMsgForwardIMP(targetMethodIMP)) { // Make a method alias for the existing method implementation, it not already copied. const char *typeEncoding = method_getTypeEncoding(targetMethod); SEL aliasSelector = aspect_aliasForSelector(selector); if (![klass instancesRespondToSelector:aliasSelector]) { __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding); NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); } // We use forwardInvocation to hook in. class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding); AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); } } static Class aspect_hookClass(NSObject *self, NSError **error) { NSCParameterAssert(self); ... aspect_swizzleForwardInvocation(subclass); ... } static void aspect_swizzleForwardInvocation(Class klass) { NSCParameterAssert(klass); // If there is no method, replace will act like class_addMethod. IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@"); if (originalImplementation) { class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@"); } AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass)); } static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) { NSCParameterAssert(self); NSCParameterAssert(invocation); ... // Before hooks. aspect_invoke(classContainer.beforeAspects, info); aspect_invoke(objectContainer.beforeAspects, info); // Instead hooks. BOOL respondsToAlias = YES; if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) { aspect_invoke(classContainer.insteadAspects, info); aspect_invoke(objectContainer.insteadAspects, info); } else { Class klass = object_getClass(invocation.target); do { if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) { [invocation invoke]; break; } }while (!respondsToAlias && (klass = class_getSuperclass(klass))); } // After hooks. aspect_invoke(classContainer.afterAspects, info); aspect_invoke(objectContainer.afterAspects, info); ... }

      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

      56

      57

      58

      59

      60

      61

      62

      63

      64

      65

      忽略掉 Aspects 創建子類等操作后,可以看出以上代碼總共做了兩件事:

      對原始 -forwardInvocation: 方法執行 swizzling method,將實現替換成 ASPECTS_ARE_BEING_CALLED,以便在 ASPECTS_ARE_BEING_CALLED 函數中執行了額外的切片操作;

      對被 hook 的方法執行 swizzling method,將實現替換成 _objc_msgForward / _objc_msgForward_stret,以便觸發被 hook 方法的消息轉發機制,然后在上面步驟的 ASPECTS_ARE_BEING_CALLED 函數中,進行切片操作。

      值得一提的是, JSPatch 也是利用相似的機制,實現用 defineClass 接口任意替換一個類的方法的功能,不同的是 JSPatch 在它的 ASPECTS_ARE_BEING_CALLED 函數中,直接把參數傳給了 JavaScript 的實現。

      七、總結

      消息轉發有三步,分別是 Lazy method resolution(動態添加方法)、 Fast forwarding path(轉發至可響應對象)、 Normal forwarding path(獲取 NSInvocation 信息)。關于消息轉發的應用,本文主要摘錄了以下幾個例子:

      Week Proxy

      Delegate Proxy

      Multicast Delegate

      Record Message Call

      Intercept Any Message Call

      可以看出,在這些例子中,都創建了一個代理類,并且這個代理類幾乎沒有實現自定義方法,或者直接是 NSProxy 的子類。這樣,基本上所有的發送給代理類對象的消息,都會觸發消息轉發機制,而這個代理類就可以對攔截的消息做額外處理。

      其中大部分應用場景都涉及到消息轉發的第二三步,即 Fast forwarding path、Normal forwarding path,特別是 Normal forwarding path,配合 _objc_msgForward / _objc_msgForward_stret 函數強行進行消息轉發,可以獲取攜帶完整調用信息的 NSInvocation。

      iOS

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:一篇文章快速理解微服務架構
      下一篇:技術綜述十六:自然場景文字檢測與識別--相關工作
      相關文章
      亚洲性色成人av天堂| 久久久久亚洲AV片无码下载蜜桃| 911精品国产亚洲日本美国韩国 | 在线日韩日本国产亚洲| 亚洲 无码 在线 专区| 亚洲精品乱码久久久久久V| 亚洲乱亚洲乱妇无码| 色噜噜亚洲男人的天堂| 亚洲成A人片在线播放器| 亚洲AV日韩综合一区尤物| 国产色在线|亚洲| 亚洲日韩AV一区二区三区四区 | 亚洲免费闲人蜜桃| 亚洲激情视频网站| 亚洲狠狠狠一区二区三区| 亚洲国产成人91精品| 亚洲AV无码久久久久网站蜜桃| 亚洲a级片在线观看| 亚洲kkk4444在线观看| 最新亚洲春色Av无码专区| 亚洲中文字幕无码av| 亚洲色成人WWW永久在线观看| 亚洲欧美日韩国产精品一区| 国产成人精品日本亚洲语音| 亚洲av无码成人影院一区| 亚洲国产精品成人网址天堂| 亚洲国产精品日韩| 亚洲色爱图小说专区| 亚洲爱情岛论坛永久| 亚洲欧洲日韩综合| 四虎必出精品亚洲高清| 亚洲a无码综合a国产av中文| 亚洲色欲久久久久综合网| 亚洲理论电影在线观看| 亚洲国产精品国自产电影| 亚洲国产精品美女| 亚洲精品无码成人| 国产成人精品日本亚洲专区| 亚洲AV无码精品色午夜果冻不卡 | 亚洲?V乱码久久精品蜜桃 | 国产亚洲av人片在线观看|