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

Posted ╰つ栺尖篴夢ゞ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 version
    if (!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 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;

二、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 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
    • 通过上面代码,可以看出 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>)collectionViewTarget
                            scrollViewTarget:(nullable id<UIScrollViewDelegate>)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<UICollectionViewDelegate>)collectionViewTarget
                            scrollViewTarget:(nullable id<UIScrollViewDelegate>)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
  • 这个类总共有三个自定义属性,分别是用来支持外界代理方法回调的 _collectionViewTarget、 _scrollViewTarget,以及用以支持 AOP 的拦截者 _interceptor(IGListAdapter 在调用外界实现的代理方法前,插入了自己的实现,所以可视为拦截者)。isInterceptedSelector 函数表明拦截者使用到了哪些代理方法,而 -respondsToSelector: 和 -forwardingTargetForSelector: 则根据这个函数的返回值决定是否能响应方法,以及应该把消息转发给拦截者还是外部代理。事实上,外部代理就是本小节开头所说的使用者可以访问的属性:
@implementation IGListAdapter
...
self.delegateProxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:_collectionViewDelegate
                                                                 scrollViewTarget:_scrollViewDelegate
                                                                      interceptor: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 以上是关于iOS之深入解析消息转发objc_msgSend的应用场景的主要内容,如果未能解决你的问题,请参考以下文章

iOS-『Runtime』详解基础知识

iOS底层探索之Runtime: 消息转发

iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析

[OC学习笔记]objc_msgSend:动态方法决议和消息转发

runtime总结二之消息机制(包括消息转发,消息交换的黑魔法)

Runtime objc4-779.1 一图看懂iOS Runtime消息转发