Runtime

153 阅读3分钟

消息发送流程

消息发送流程

isa

arm64 架构开始,变成了一个共用体结构,还使用位域来存储更多的信息

union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
        uintptr_t nonpointer: 1;
        uintptr_t has_assoc: 1;
        uintptr_t has_cxx_dtor: 1;
        uintptr_t shiftcls: 33;
        uintptr_t magic: 6;
        uintptr_t weakly_referenced: 1;
        uintptr_t deallocating: 1;
        uintptr_t has_sidetable_rc: 1;
        uintptr_t extra_rc: 19;
    }
}
  • nonpointer: 0: 代表普通指针,存储着 Class、Meta-Class 对象的内存地址,1: 代表优化过,使用位域存储更多的信息

  • has_assoc: 是否有设置关联对象,如果没有,释放时会更快

  • has_cxx_dtor: 是否有 c++ 的析构函数,如果没有,释放会更快

  • shiftcls: 存储着 Class、Meta-Class对象的内存地址信息

  • magic: 用于在调试时分辨对象是否未完成初始化

  • weakly_referenced: 是否有被弱引用指向过,如果没有,释放时会更快

  • deallocating: 是否正在释放

  • extra_rc: 里面存储的值是引用计数器减1

  • has_sidetable_rc: 引用计数器是否过大无法存储在 isa 中,如果为1,那么引用计数会存储在一个叫 SideTable 的类的属性中

所以 isa 需要进行一次位运算,才能取出对象的真实地址值

#define ISA_MASK 0x0000000ffffffff8ULL
isa.bits & ISA_MASK

所以所有对象的地址后三位都是0

数据结构

typedef struct objc_class *Class;
struct objc_object {
    Class isa;
}
struct objc_class: objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits; // typedef unsigned long uintptr_t;这个就是让系统分配64个字节的内存给 isa 用而已
}
struct cache_t {
    struct bucket_t *_buckets; // 缓存方法的散列表
    mask_t _mask; // typedef uint32_t mask_t; 散列表长度 - 1
    mask_t _occupied; // 已经占用的数量
}
struct bucket_t {
    MethodCacheIMP _imp; // using MethodCacheIMP = IMP;
    cache_key_t _key; // typedef uintptr_t cache_key_t;
}
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
    struct {
        ISA_BITFIELD;
    }
}
struct class_rw_t {
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
}
class class_ro_t {
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
}
struct method_t {
    SEL name;
    const char *type; // types包含了函数返回值,参数编码的字符串
    MethodListIMP imp;
    struct SortBySELAddress
}

Cache 的算法

把 SEL 利用 hash 算法算出一个 cache_key,然后将 cache_key % _mask,算出在 _buckets 中存储的位置,将 SEL 作为 key 和 IMP 作为 value 存入。遇到哈希冲突怎么办,将算出的位置 -1 存入,以此循环,直至不冲突存入。 取的时候也一样,根据 SEL 算出位置,比较 SEL 是否相等,如果不相等,就 -1 继续比较,否则返回 imp 执行。存入的时候 _occupied + 1,取出的时候 _occupied - 1,如果 _occupied > _mask * 3 / 4(占用的空间大于总容量的3/4时),就会扩容,扩容为原 _mask 的 2 倍。同时原来的缓存也就被清空了。

方法查找的算法

在运行时,首先会将 class_ro_t 的方法合并进 class_rw_t 中。

无序的直接遍历,有序的二分查找。

为什么分类会覆盖原始实现?

系统是在运行时将分类中对应的实例方法、类方法等插入到了原来类或元类的方法列表中,且是在列表的前边!所以,方法调用时通过isa去对应的类或元类的列表中查找对应的方法时先查到的是分类中的方法!查到后就直接调用不在继续查找。这即是’覆盖’的本质!

memmove:将原来类中的信息列表在内存中向后移动,移动的大小就是分类中的信息所占大小。

memcopy:将分类中的信息复制到上一步移动出来的空间。

注意点

  1. 动态方法解析,会标记已经解析过了,然后重新走消息发送流程。
  2. 不管是从哪个父类中查找到的方法,都会缓存到当前 receive class 中的方法缓存中。

相关代码

注意一点就是 Person1.h 里面不用声明方法,Person1.m 里面直接实现就可以了。

person.m
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        class_addMethod([self class], @selector(test), (IMP)t, "v@:");
    }
    return true;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return [[Person1 alloc] init];
    }
    return nil;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    Person1 *person1 = [[Person1 alloc] init];
    [anInvocation invokeWithTarget:person1];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
    return sign;
}

void t(id self, SEL _cmd) {
    NSLog(@"%s", _cmd);
    [self h];
}

- (void)h {
    NSLog(@"h");
}