美文网首页
iOS-runtime的理解

iOS-runtime的理解

作者: Arthur澪 | 来源:发表于2020-03-11 17:15 被阅读0次

    什么是runtime

    官方描述:
    The Objective-C language defers as many decisions as it can from compile time and link time to runtime.
    “尽量将决定放到运行的时候,而不是在编译和链接过程”

    runtime是一个C语言库,包含了很多底层的纯C语言API。 平时编写的OC代码中,程序运行,其实最终都是转成了runtime的C语言代码,runtime算是OC的幕后工作者 。

    • 特点
      OC与其他语言不同的一点就是,函数调用采用了消息转发的机制,但直到程序运行之前,消息都没有与任何方法绑定起来。只有在真正运行的时候,才会根据函数的名字来,确定该调用的函数。

    runtime 是有个两个版本的:
    Objective-C 1.0使用的是legacy,在2.0使用的是modern。
    现在一般来说runtime都是指modern。

    1. isa指针

    首先要了解它底层的一些常用数据结构,比如isa指针。

    当创建一个新对象时,会为它分配一段内存,该对象的实例变量也会被初始化。第一个变量就是一个指向它的类的指针(isa)。
    通过isa指针,一个对象可以访问它的类,并通过它的类来访问所有父类。

    • 一个实例对象,在runtime中用结构体表示
    // 描述类中的一个方法
    typedef struct objc_method *Method;
    
    // 实例变量
    typedef struct objc_ivar *Ivar;
    
    // 类别Category
    typedef struct objc_category *Category;
    
    // 类中声明的属性
    typedef struct objc_property *objc_property_t;
    

    查看runtime源码可以看到关于isa结构。

    union isa_t {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
        struct {
            ISA_BITFIELD;  // defined in isa.h
        };
    };
    

    下面的代码对isa_t中的结构体进行了位域声明,地址从nonpointer起到extra_rc结束,从低到高进行排列。位域也是对结构体内存布局进行了一个声明,通过下面的结构体成员变量可以直接操作某个地址。位域总共占8字节,所有的位域加在一起正好是64位。

    小提示:unionbits可以操作整个内存区,而位域只能操作对应的位。

    define ISA_BITFIELD                                                      \
          uintptr_t nonpointer        : 1;   //指针是否优化过                                   \
          uintptr_t has_assoc         : 1;   //是否有设置过关联对象,如果没有,释放时会更快                                   \
          uintptr_t has_cxx_dtor      : 1;   //是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快                                     \
          uintptr_t shiftcls          : 33; //存储着Class、Meta-Class对象的内存地址信息 \
          uintptr_t magic             : 6;  //用于在调试时分辨对象是否未完成初始化                                     \
          uintptr_t weakly_referenced : 1;  //是否有被弱引用指向过,如果没有,释放时会更快                                     \
          uintptr_t deallocating      : 1;  //对象是否正在释放                                     \
          uintptr_t has_sidetable_rc  : 1;  //引用计数器是否过大无法存储在isa中                                     \
          uintptr_t extra_rc          : 19 //里面存储的值是引用计数器减1
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
    
    • nonpointer
      0:代表普通的指针,存储着Class、Meta-Class对象的内存地址。
      1:代表优化过,使用位域存储更多的信息。
    • has_assoc
      是否有设置过关联对象。如果没有,释放时会更快。
    • has_cxx_dtor
      是否有C++的析构函数.cxx_destruct如果没有,释放时会更快。
    • shiftcls
      存储着Class、Meta-Class对象的内存地址信息
    • magic
      用于在调试时,分辨对象是否未完成初始化
    • weakly_referenced
      是否有被弱引用指向过。如果没有,释放时会更快
    • deallocating
      对象是否正在释放
    • extra_rc
      里面存储的值是引用计数器减1
    • has_sidetable_rc
      引用计数器是否过大无法存储在isa中
      如果为1,那么引用计数会存储在一个叫SideTable的类的属性中

    2. class结构

    结构体

    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    
    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;   // 父类
        cache_t cache;    //方法缓存
        class_data_bits_t bits;    // 用于获取具体的类的信息
    }
    

    查看源码(只保留了主要代码)

    • class_rw_t
    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint32_t version;
    
        const class_ro_t *ro;
    
        method_array_t methods; //方法列表
        property_array_t properties; //属性列表
        protocol_array_t protocols; // 协议列表
    
        Class firstSubclass;
        Class nextSiblingClass;
    
        char *demangledName;
    }
    

    其中的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容。

    • method_array_t
    class method_array_t : 
        public list_array_tt<method_t, method_list_t> 
    {
        typedef list_array_tt<method_t, method_list_t> Super;
    
     public:
        method_list_t **beginCategoryMethodLists() {
            return beginLists();
        }
        
        method_list_t **endCategoryMethodLists(Class cls);
    
        method_array_t duplicate() {
            return Super::duplicate<method_array_t>();
        }
    };
    

    方法列表 中存放着很多一维数组method_list_t,而每一个method_list_t中存放着method_t。method_t中是对应方法的imp指针、名字、类型等方法信息。

    • method_t
    struct method_t {
        SEL name; //函数名
        const char *types; //编码(返回值类型,参数类型)
        MethodListIMP imp; //指向函数的指针(函数地址)
    
        struct SortBySELAddress :
            public std::binary_function<const method_t&,
                                        const method_t&, bool>
        {
            bool operator() (const method_t& lhs,
                             const method_t& rhs)
            { return lhs.name < rhs.name; }
        };
    };
    

    IMP:代表函数的具体实现
    SEL:代表方法、函数名,一般叫做选择器。
    types:包含了函数返回值、参数编码的字符串

    关于SEL:
    可以通过@selector()sel_registerName()获得
    可以通过sel_getName()NSStringFromSelector()转成字符串
    不同类中相同名字的方法,所对应的方法选择器是相同的。即,不同类的相同SEL是同一个对象。

    • class_ro_t
    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;//instance对象占用的内存空间
    #ifdef __LP64__
        uint32_t reserved;
    #endif
    
        const uint8_t * ivarLayout;
        
        const char * name; //类名
        method_list_t * baseMethodList; //方法列表
        protocol_list_t * baseProtocols; //协议列表
        const ivar_list_t * ivars; //成员变量列表
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
    };
    

    class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容

    3. Type Encoding

    iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码。比如:

    +(int)testWithNum:(int)num{
        return num;
    }
    

    上面的方法可以用 i20@0:8i16来表示:

    i表示返回值是int类型,20是参数总共20字节
    @表示第一个参数是id类型,0表示第一个参数从第0个字节开始
    :表示第二个参数是SEL类型。8表示第二个参数从第8个字节开始。
    i表示第三个参数是int类型,16表示第三个参数从第16个字节开始
    第三个参数从第16个字节开始,是Int类型,占用4字节。总共20字节

    4. 方法缓存

    用散列表来缓存曾经调用过的方法,可以提高方法的查找速度。
    结构体 cache_t

    struct cache_t {
        struct bucket_t *_buckets; // 散列表
        mask_t _mask; //散列表的长度 -1
        mask_t _occupied; //已经缓存的方法数量
    }
    
    // 其中的 散列表
    struct bucket_t {
        MethodCacheIMP _imp; //函数的内存地址
        cache_key_t _key;   //SEL作为Key
    }
    
    • cache_t中如何查找方法
    // 散列表中查找方法缓存
    bucket_t * cache_t::find(cache_key_t k, id receiver)
    {
        assert(k != 0);
    
        bucket_t *b = buckets();
        mask_t m = mask();
        mask_t begin = cache_hash(k, m);
        mask_t i = begin;
        do {
            if (b[i].key() == 0  ||  b[i].key() == k) {
                return &b[i];
            }
        } while ((i = cache_next(i, m)) != begin);
    
        // hack
        Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
        cache_t::bad_cache(receiver, (SEL)k, cls);
    }
    

    其中,根据key和散列表长度减1 mask 计算出下标 key & mask,取出的值如果key和当初传进来的Key相同,就说明找到了。否则,就不是自己要找的方法,就有了hash冲突,把i的值加1,继续计算。如下代码:

    // 计算下标
    static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
    {
        return (mask_t)(key & mask);
    }
    
    
    //hash冲突的时候
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return (i+1) & mask;
    }
    
    • cache_t的扩容
      当方法缓存太多的时候,超过了容量的3/4s时候,就需要扩容了。扩容是,把原来的容量增加为2倍。
    static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
    {
                ...
        if (cache->isConstantEmptyCache()) {
            // Cache is read-only. Replace it.
            cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
        }
        else if (newOccupied <= capacity / 4 * 3) {
            // Cache is less than 3/4 full. Use it as-is.
        }
        else {
            // 来到这里说明,超过了3/4,需要扩容
            cache->expand();
        }
    
             ...
    }
    
    // 扩容
    enum {
        INIT_CACHE_SIZE_LOG2 = 2,
        INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
    };
    
    // cache_t的扩容
    void cache_t::expand()
    {
        cacheUpdateLock.assertLocked();
        
        uint32_t oldCapacity = capacity();
        // 扩容为原来的2倍
        uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    
        if ((uint32_t)(mask_t)newCapacity != newCapacity) {
            // mask overflow - can't grow further
            // fixme this wastes one bit of mask
            newCapacity = oldCapacity;
        }
    
        reallocate(oldCapacity, newCapacity);
    }
    

    5. 消息转发机制

    OC方法调用的本质是,消息转发机制。比如:
    对象instance 调用dotest方法[instance1 dotest];
    底层会转化为:objc_msgSend(instance1, sel_registerName("dotest"));
    OC中方法的调用,其实都是转换为objc_msgSend函数的调用。

    实例对象中存放着 isa 指针以及实例变量。由 isa 指针找到实例对象所属的类对象 (类也是对象)。类中存放着实例方法列表。在这个列表中,方法的保存形式是SEL 作 key,IMP作value。

    这是在编译时根据方法名,生成唯一标识SELIMP其实就是函数指针 ,指向最终的函数实现。

    整个 Runtime 的核心就是 objc_msgSend(receiver, @selector (message)) 函数,通过给类发送 SEL以传递消息,找到匹配的IMP 再获取最终的实现。

    执行流程可以分为3大阶段:消息发送->动态方法解析->消息转发

    • 消息发送阶段:
      首先判断receiver是否为空
      如果不为空,从receiverClass的缓存中,查找方法。(找到了就调用)
      如果没找到,就从receiverClass的class_rw_t中查找方法。(找到就调用,并缓存)
      如果没找到,就去receiverClassd的父类的缓存中查找。
      如果没找到,就从父类的class_rw_t中查找方法。
      如果没找到,就看是否还有父类,有就继续查父类的缓存,方法列表。

    由上述知道,去查缓存、方法列表、查父类等这些操作之后,都没有找到这个方法的实现,这时如果后面不做处理,必然抛出异常:
    ...due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[xxx xxxx]: unrecognized selector sent to instance 0x100f436c0’

    如果没有父类,说明消息发送阶段结束,那么就进入第二阶段,动态方法解析阶段。

    • 动态方法解析:
      在此,可以给未找到的方法,动态绑定方法实现。或者给某个方法重定向。

    源码:

    // 动态方法解析
    void _class_resolveMethod(Class cls, SEL sel, id inst)
    {
        if (! cls->isMetaClass()) { //如果不是元类对象
            // try [cls resolveInstanceMethod:sel]
            _class_resolveInstanceMethod(cls, sel, inst);
        } 
        else { // 是元类对象
            // try [nonMetaClass resolveClassMethod:sel]
            // and [cls resolveInstanceMethod:sel]
            _class_resolveClassMethod(cls, sel, inst);
            if (!lookUpImpOrNil(cls, sel, inst, 
                                NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
            {
                _class_resolveInstanceMethod(cls, sel, inst);
            }
        }
    }
    

    其中的resolveClassMethodresolveInstanceMethod默认是返回NO

    + (BOOL)resolveClassMethod:(SEL)sel {
        return NO;
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        return NO;
    }
    
    • 在动态解析阶段,可以重写resolveInstanceMethod并添加方法的实现。
      假如,没有找到run这个方法:
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
         if (sel == @selector( run )) {
            // 获取其他方法 实例方法 或类方法,作为run的实现
            Method method = class_getInstanceMethod(self, @selector(test));
    
            // 动态添加test方法的实现
            class_addMethod(self, sel,
                            method_getImplementation(method),
                            method_getTypeEncoding(method));
                    
           // 返回YES代表有动态添加方法  其实这里返回NO,也是可以的,返回YES只是增加了一些打印
            return NO;
         }
         return [super resolveInstanceMethod:sel];
    }
    

    上面的代码,就相当于,调用run的时候,实际上调用的是test。

    如果前面消息发送 和动态解析阶段,都没有对方法进行处理,我们还有最后一个阶段。如下

    • 消息转发

    ____forwarding___这个函数中,交代了消息转发的逻辑。但是不开源。

    先判断forwardingTargetForSelector的返回值。有,就向这个返回值发送消息,让它调用方法。
    如果返回nil,就调用methodSignatureForSelector方法,有就调用forwardInvocation

    其中的参数是一个 NSInvocation 对象,并将消息全部属性记录下来。 NSInvocation对象包括了Selector、target以及其他参数。其中的实现仅仅是改变了 target指向,使消息保证能够调用。

    倘若发现本类无法处理,则继续查找父类,直至 NSObject 。如果methodSignatureForSelector方法返回nil,就调用doesNotRecognizeSelector:方法。

    应用举例:

    场景1:

    类Person只定义了方法run但没有实现,另外有类Car实现了方法run。

    现在Person中,重写forwardingTargetForSelector返回Car对象

    // 消息转发
    - (id)forwardingTargetForSelector:(SEL)aSelector{
        if (aSelector == @selector(run)) {
            return [[Car alloc] init];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    

    这时,当person实例调用run方法时,会变成car实例调用run方法。

    证明forwardingTargetForSelector返回值不为空的话,就向这个返回值发送消息,也就是objc_msgSend(返回值, SEL)

    场景2:

    如果前面的forwardingTargetForSelector返回为空。底层就会调用 methodSignatureForSelector获取方法签名后,再调用 forwardInvocation

    因此:可以重写这两个方法:

    // 方法签名:返回值类型、参数类型
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
        if (aSelector == @selector(run)) {
           return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
        }
         return [super methodSignatureForSelector:aSelector];
    }
    
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation{
    
       [anInvocation invokeWithTarget:[[Car alloc] init]];
    }
    

    这样,依然可以调用到car的run方法。

    NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
    anInvocation.target 方法调用者
    anInvocation.selector 方法名
    [anInvocation getArgument:NULL atIndex:0]

    补充:
    1、消息转发的forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation不仅支持实例方法,还支持类方法。不过系统没有提示,需要写成实例方法,然后把前面的-改成+即可。

    +(IMP)instanceMethodForSelector:(SEL)aSelector{
        
    }
    
    -(IMP)methodForSelector:(SEL)aSelector{
        
    }
    

    2、只能向运行时动态创建的类添加ivars,不能向已经存在的类添加ivars。 这是因为在编译时,只读结构体class_ro_t就被确定,在运行时不可更改。

    相关文章

      网友评论

          本文标题:iOS-runtime的理解

          本文链接:https://www.haomeiwen.com/subject/zmkhjhtx.html