iOS之深入解析消息轉發objc_msgSend的應用場景
一、消息轉發
現有如下示例:
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
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
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
這個類總共有三個自定義屬性,分別是用來支持外界代理方法回調的 _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
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
1
2
3
4
5
6
7
8
9
10
11
開發者可以用以下方式 hook 所有 UIViewController 實例對象的 -viewWillAppear: 方法:
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id
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小時內刪除侵權內容。