消息发送

查找方法的本质都是消息发送,objc_msgSend是由汇编代码实现的,目的是更快更高效。之后的慢速查找函数lookUpImpOrForwardC实现的。

消息查找流程总结

  1. objc_msgSend之方法快速查找流程(CacheLoopUp查找cache_t)是查找本类的缓存 ,此行为在汇编里,涉及到内存操作的都是汇编,更高效
  2. 缓存里没找的话,就会来到objc_msgSend的后半段慢速查找流程(lookUpImpOrForward)是查找当前cls或其元类继承链的class_data_bits_t里方法列表里使用二分查找``findMethodInSortedMethodList看有没有,这里还会查找父类的缓存。如果这里没有找到,则imp就会换成 imp_forwardlookUpImpOrForward的后段会进行动态方法决议。//这里二分查找方法列表的时候,是已经排好序的(map_images流程里初始化rwe时attachLists,这里分类方法是放在方法数组最前面的,应该是运用了LRU的算法思维,即分类方法使用概率要高于本类,优先调用,这也是分类存在的意义),分类方法在前,类方法在后,但查找是从后向前,并且找到第一个之后不立马返回,而是执行减减操作,即找到列表中最靠前的符合的分类方法才会返回。因为类和分类如果有同名方法,会优先执行分类的方法,所以这里不是所谓的网上说的先查找到分类方法就返回(如我理解有误,欢迎指出)。
  3. 动态方法决议LOOKUP_RESOLVEbool resolved = msg(cls, resolve_sel, sel); 如果没有实现动态决议的方法,resolveInstance/Class方法会执行两次后崩溃,因为第一次是系统先查找(查找执行的方法名是lookUpImpOrForward)是否实现了resolveInstance/Class方法(容错),如果实现了,第二次则是再去下发selresolveInstance/Class方法,否则就没有第二次下发resolve方法。注意这里指的是动态决议的方法,而不是未实现的自定义的方法。接下来通过反汇编会知道要进入消息转发流程.
  4. 消息转发快速转发 forwardingTargetForSelector ,这里只需要返回任意一个实现了要查找的方法的类的对象即可,可以利用runtime动态创建类并添加要查找的方法来防止崩溃。注意消息转发为什么会调用这个方法及下面的慢速转发呢,可以通过反汇编CoreFoundation查看。
  5. 慢速转发 methodSignatureForSelectorforwardInvocation(更灵活),如果methodSignatureForSelector返回nil就会崩溃报错unrecognized selector sent to instance。返回方法签名就会执行到forwardInvocation,这里不处理也不会崩溃,自由度很高。

全局避免unrecognized selector sent to instance

可以在NSObject分类中的resolveInstanceMethod里处理类方法实例方法。因为类方法的查找,在其继承链,查找的也是实例方法。根本原因还是类方法在元类中的实例方法,而元类方法查找最终会找到根NSObject


快速查找流程是CacheLoopUp,即objc_msgSend的时候在汇编这里通过mask等查找还原出类的cache_tbuckets里存储的selimp的过程,也就是查找缓存。这也是消息发送的第一步。如果缓存没找到,则进入lookUpImpOrForward慢速查找流程。

cache_t

缓存,类的首地址移动16位(isa+superclass)得到的是cache_t结构体,里面存放的是调用过的方法的缓存(SEL和IMP),对象的init方法也会缓存到cache_t. 那么在进行消息发送的时候也会优先来cache_t去查找方法缓存,这一过程成为快速查找,即CacheLoopUp
移动32位 得到的是class_data_bits_t的类信息结构体

从源码的分析中,我们知道sel-imp是在cache_t的_buckets属性中(目前处于macOS环境),而在cache_t结构体中提供了获取_buckets属性的方法buckets()

获取了_buckets属性,就可以获取sel-imp了,这两个的获取在bucket_t结构体中同样提供了相应的获取方法sel() 以及 imp(pClass)

因为在cache初始化时,分配的空间是4个,随着方法调用的增多,当存储的sel-imp个数,即newOccupied + CACHE_END_MARKER(等于1)的和 超过 总容量的3/4,例如有4个时,当occupied等于2时,就需要对cache的内存进行两倍扩容,扩容时,是将原有的内存全部清除了,再重新申请了内存.所以_occupied的值总是0,1,2

从数据结构角度及使用方法来看 cache_t 的话,它是一个 SEL 作为 Key ,SEL + IMP(bucket_t) 作为 Value 的散列表。执行了一次方法调用,cache中就有了一个缓存,即调用一次方法就会缓存一次方法.


NSCoding的手动与自动

//手动  一旦属性躲起来  麻烦
- (void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:self.name forKey:@"name"];
}

- (id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
    }
    return self;
}

//自动
- (void)encodeWithCoder:(NSCoder *)aCoder{
    unsigned int outCount = 0;
    Ivar *vars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar var = vars[i];
        const char *name = ivar_getName(var);
        NSString *key = [NSString stringWithUTF8String:name];

        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
}

- (nullable __kindof)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        unsigned int outCount = 0;
        Ivar *vars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar var = vars[i];
            const char *name = ivar_getName(var);
            NSString *key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
    }
    return self;
}

伪多继承

  • 在OC程序中可以借用消息转发机制来实现多继承的功能。通过forwardingTargetForSelector:方法就能知道,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟多继承的效果。

  • 即使我们利用转发消息来实现了“假”继承,但是NSObject类还是会将两者区分开。像respondsToSelector:和 isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。如果非要制造假象,就得重写转发链,制造假象。


method-swizzling注意事项

  • +load方法系统只会调用一次,并且主类优先,分类次之。但可以主动触发多次,且用户主动触发时分类覆盖主类。还有一种是在继承关系中,比如同时对NSArray和NSMutableArray中的objectAtIndex:方法都进行了Swizzling,这样可能会导致NSArray中的Swizzling失效的。所以mehod-swizzling写在load方法中,防止方法的重复交换,可以使用单例设计,使方法只交换一次。

  • +initialize方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize方法是永远不会被调用的。所以Swizzling要是写在+initialize方法中,是有可能永远都不被执行

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [RuntimeManager methodSwizzlingWithClass:self oriSEL:@selector(viewDidLoad) swizzledSEL:@selector(my_viewDidLoad)];
    });
}
  • 父类实现了方法A,但子类只是继承,没有实现方法A。然后在子类的分类里mehod-swizzling了方法A,并指向分类的方法category_A。这时,如果父类调用方法A,就会崩溃。因为父类找不到方法category_A。 解决:mehod-swizzling前先尝试class_addMethod
BOOL didAddMethod = class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
if (didAddMethod)
{
    class_replaceMethod(class, swizzledSelector,
    method_getImplementation(originalMethod),
    method_getTypeEncoding(originalMethod));
}
else
{    //已有此方法,直接交换
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
  • 如果父类子类都没有实现方法,则需要在class_addMethod前执行
 if (!oriMethod) {
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ //空实现 }));
    }

本文是本人笔记整理而成,可能不太面向小白,是一个总结性质的,也可能有点乱。若有不对的,欢迎指出哦。

你可能感兴趣的:(消息发送)