日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

iOS之深入解析消息转发objc_msgSend的应用场景

發布時間:2024/5/28 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 iOS之深入解析消息转发objc_msgSend的应用场景 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、消息轉發

  • 現有如下示例:
id o = [NSObject new]; [o lastObject];
  • 執行上面代碼,程序會崩潰并拋出以下異常:
[NSObject lastObject]: unrecognized selector sent to instance 0x100200160
  • 錯誤顯而易見,實例對象 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 versionif (!imp) {return _objc_msgForward;}return imp; }

① Lazy method resolution

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

② Fast forwarding path

  • 在這一步,_objc_msgForward 直接或間接調用了以下方法:
- (id)forwardingTargetForSelector:(SEL)aSelector;
  • 這個方法還是只附帶了無法找到對應 IMP 的 SEL,可以根據這個 SEL,判斷是否有其它對象可以響應它,然后選擇將消息轉發給這個對象。如果返回除 nil / self 之外的對象,那么會重啟一次消息發送動作給返回的對象,否則進入下個流程 Normal forwarding path。

③ Normal forwarding path

  • 在這一步,_objc_msgForward 直接或間接調用了以下方法:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; - (void)forwardInvocation:(NSInvocation *)anInvocation;
  • 這是消息轉發的最后一步,首先會調用的是 -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));
  • 上面程序輸出如下:
old type: @24@0:8Q16, imp: 0x7fffb5fc31e0 new type: @24@0:8Q16, imp: 0x7fffcada5cc0
  • 可以看到,更改的只有方法實現 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 existsif (!replace) {// 不替換返回已存在方法實現IMPresult = _method_getImplementation(m);} else {// 直接替換類cls的m函數指針為impresult = _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; }
  • 消息轉發流程大體如此,如果想了解具體的轉發原理、_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; // 強引用需要,弱引用不需要 }
  • 這種分散的管理方式,總會讓使用者在某些場景下忘記了停止 _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 directlyreturn _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
    • 通過上面代碼,可以看出 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
    • 其對象間的引用關系可以用下圖表示:
---> 強引用 ~~~> 弱引用 FLAnimatedImageView(object) ---> displayLink ---> weakProxy ~~~> FLAnimatedImageView(object)
    • 這樣一來, 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 <UICollectionViewDelegate> collectionViewDelegate;/**The object that receives `UIScrollViewDelegate` events.*/ @property (nonatomic, nullable, weak) id <UIScrollViewDelegate> scrollViewDelegate;...@end
  • 使用者可以成為 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; }...
  • 當代理方法較少的時候,這種寫法是可以接受的。不過隨著代理方法的增多,編寫這種膠水代碼就有些煩人了,侵入性的修改方式也不符合開放閉合原則。來看下 IGListKit 是如何利用 IGListAdapterProxy 解決這個問題的:
@interface IGListAdapterProxy : NSProxy - (instancetype)initWithCollectionViewTarget:(nullable id<UICollectionViewDelegate>)collectionViewTargetscrollViewTarget:(nullable id<UIScrollViewDelegate>)scrollViewTargetinterceptor:(IGListAdapter *)interceptor; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE;@endstatic BOOL isInterceptedSelector(SEL sel) {return (// UICollectionViewDelegatesel == @selector(collectionView:didSelectItemAtIndexPath:) ||sel == @selector(collectionView:willDisplayCell:forItemAtIndexPath:) ||sel == @selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:) ||// UICollectionViewDelegateFlowLayoutsel == @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:) ||// UIScrollViewDelegatesel == @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<UICollectionViewDelegate>)collectionViewTargetscrollViewTarget:(nullable id<UIScrollViewDelegate>)scrollViewTargetinterceptor:(IGListAdapter *)interceptor {IGParameterAssert(interceptor != nil);// -[NSProxy init] is undefinedif (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
  • 這個類總共有三個自定義屬性,分別是用來支持外界代理方法回調的 _collectionViewTarget、 _scrollViewTarget,以及用以支持 AOP 的攔截者 _interceptor(IGListAdapter 在調用外界實現的代理方法前,插入了自己的實現,所以可視為攔截者)。isInterceptedSelector 函數表明攔截者使用到了哪些代理方法,而 -respondsToSelector: 和 -forwardingTargetForSelector: 則根據這個函數的返回值決定是否能響應方法,以及應該把消息轉發給攔截者還是外部代理。事實上,外部代理就是本小節開頭所說的使用者可以訪問的屬性:
@implementation IGListAdapter ... self.delegateProxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:_collectionViewDelegatescrollViewTarget:_scrollViewDelegateinterceptor:self]; ... @end
  • 通過這種轉發機制,即使后續有新的代理方法,也不用手動添加“膠水代碼”了,一些流行的開源庫中也可以看到這種做法的身影,比如 AsyncDisplayKit 就有對應的 _ASCollectionViewProxy 來轉發未實現的代理方法。

四、Multicast Delegate

  • 通知和代理是解耦對象間消息傳遞的兩種重要方式,其中通知主要針對一對多的單向通信,而代理則主要提供一對一的雙向通信。
  • 通常來說, IM 應用在底層模塊接受到新消息后,都會進行一次廣播處理,讓各模塊能根據新消息來更新狀態。當接收模塊不需要向發送模塊反饋任何信息時,使用 NSNotificationCenter 就可以實現上述需求。但是一旦發送模塊需要根據接收模塊返回的信息做一些額外處理,也就是實現一對多的雙向通信, NSNotificationCenter 就不滿足要求了。
  • 最直接的解決方案是,針對這個業務場景自定義一個消息轉發中心,讓遵守特定協議的外圍模塊主動注冊成為消息接收者。不過既然涉及到了特定協議,就注定了這個消息轉發中心缺少通用性,這時候就可以參考下業界現成的方案了。
  • 來看看 XMPPFramework 是如何解決這個問題的:
    • 從文檔中可以看出,作者希望 XMPPFramework 具備以下幾個特性:
      • 將事件廣播給多個監聽者;
      • 易于擴展;
      • 選擇的機制要支持返回值;
      • 選擇的機制要易于編寫線程安全代碼。
    • 但是代理或者通知機制都不能很好地滿足上述需求,所以 GCDMulticastDelegate 類應運而生。 使用這個類時,廣播類需要初始化 GCDMulticastDelegate 對象:
GCDMulticastDelegate <MyPluginDelegate> *multicastDelegate; multicastDelegate = (GCDMulticastDelegate <MyPluginDelegate> *)[[GCDMulticastDelegate alloc] init];
    • 并且添加增刪代理的方法:
- (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]; }
    • 當廣播對象需要向所有注冊的代理發送消息時,可以用以下方式調用:
[multicastDelegate worker:self didFinishSubTask:subtask inDuration:elapsed];
    • 只要注冊的代理實現了這個方法,就可以接收到發送的信息。
  • 再來看下 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_IPHONEif (nodeDelegate == [NSNull null])nodeDelegate = node.unsafeDelegate;#endifNSMethodSignature *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_IPHONEif (nodeDelegate == [NSNull null])nodeDelegate = node.unsafeDelegate;#endifif ([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_IPHONEif (nodeDelegate == [NSNull null])nodeDelegate = node.unsafeDelegate;#endifif (nodeDelegate == nil) {[indexSet addIndex:i];}i++;}[delegateNodes removeObjectsAtIndexes:indexSet];} }- (void)doesNotRecognizeSelector:(SEL)aSelector {// Prevent NSInvalidArgumentException }- (void)doNothing {}
    • 可以看到, -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)];} }
  • 通過調用代碼塊中 NSUndoManager 對象的 undo,可以“撤銷”以上方法對 myObject 相關屬性的設置,其中需要關注的是,NSUndoManager 是如何記錄目標對象接收發生改變的信息:
[[undoManager prepareWithInvocationTarget:self] setMyObjectWidth:currentWidth height:currentHeight]
  • 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
  • 結合 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
  • 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<AspectToken>)aspect_hookSelector:(SEL)selectorwithOptions:(AspectOptions)optionsusingBlock:(id)blockerror:(NSError **)error;// Adds a block of code before/instead/after the current `selector` for a specific instance. - (id<AspectToken>)aspect_hookSelector:(SEL)selectorwithOptions:(AspectOptions)optionsusingBlock:(id)blockerror:(NSError **)error;
  • 開發者可以用以下方式 hook 所有 UIViewController 實例對象的 -viewWillAppear: 方法:
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated); } error:NULL];
  • 因為不知道使用者會 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);... }
  • 忽略掉 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之深入解析消息转发objc_msgSend的应用场景的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。