iOS-runtime的理解

2020-03-11  本文已影响0人  Arthur澪

什么是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的幕后工作者 。

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

1. isa指针

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

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

// 描述类中的一个方法
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;    // 用于获取具体的类的信息
}

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

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是二维数组,是可读可写的,包含了类的初始内容、分类的内容。

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指针、名字、类型等方法信息。

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是同一个对象。

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
}
// 散列表中查找方法缓存
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;
}
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大阶段:消息发送->动态方法解析->消息转发

由上述知道,去查缓存、方法列表、查父类等这些操作之后,都没有找到这个方法的实现,这时如果后面不做处理,必然抛出异常:
...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;
}
+ (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就被确定,在运行时不可更改。

上一篇下一篇

猜你喜欢

热点阅读