探秘Runtime - Runtime源码分析

栏目: Objective-C · 发布时间: 5年前

内容简介:本文基于重点来了~,可以到我的

<简书 — 刘小壮> https://www.jianshu.com/p/3019605a4fc9

探秘Runtime - Runtime源码分析

本文基于 objc-723 版本,在 Apple Github 和Apple OpenSource上有源码,但是需要自己编译。

重点来了~,可以到我的 Github 上下载编译好的源码,源码中已经写了大量的注释,方便读者研究。(如果觉得还不错,各位大佬麻烦点个 Star :grin:) Runtime Analyze

对象的初始化流程

在对象初始化的时候,一般都会调用 alloc+init 方法实例化,或者通过 new 方法进行实例化。下面将会分析通过 alloc+init 的方式实例化的过程,以下代码都是关键代码。

前面两步很简单,都是直接进行函数调用。

+ (id)alloc {
    return _objc_rootAlloc(self);
}

id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
复制代码

在创建对象的地方有两种方式,一种是通过 calloc 开辟内存,然后通过 initInstanceIsa 函数初始化这块内存。第二种是直接调用 class_createInstance 函数,由内部实现初始化逻辑。

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (fastpath(cls->canAllocFast())) {
        bool dtor = cls->hasCxxDtor();
        id obj = (id)calloc(1, cls->bits.fastInstanceSize());
        if (slowpath(!obj)) return callBadAllocHandler(cls);
        obj->initInstanceIsa(cls, dtor);
        return obj;
    }
    else {
        id obj = class_createInstance(cls, 0);
        if (slowpath(!obj)) return callBadAllocHandler(cls);
        return obj;
    }
}
复制代码

但是在最新版的 objc-723 中,调用 canAllocFast 函数直接返回 false ,所以只会执行上面第二个 else 代码块。

bool canAllocFast() {
    return false;
}
复制代码

初始化代码最终会调用到 _class_createInstanceFromZone 函数,这个函数是初始化的关键代码。下面代码中会进入 if 语句内,根据 instanceSize 函数返回的 size ,通过 calloc 函数分配内存,并初始化 isa_t 指针。

id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

static __attribute__((always_inline))
id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                                 bool cxxConstruct = true, 
                                 size_t *outAllocatedSize = nil)
{
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size = cls->instanceSize(extraBytes);

    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;
        obj->initIsa(cls);
    }

    return obj;
}
复制代码

instanceSize() 函数中,会通过 alignedInstanceSize 函数获取对象原始大小,在 class_ro_t 结构体中的 instanceSize 变量中定义。这个变量中存储了对象实例化时,所有变量所占的内存大小,这个大小是在编译器就已经决定的,不能在运行时进行动态改变。

获取到 instanceSize 后,对获取到的 size 进行地址对其。需要注意的是, CF 框架要求所有对象大小最少是16字节,如果不够则直接定义为16字节。

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}
复制代码

这也是很关键的一步,由于调用 initIsa 函数时, nonpointer 字段传入 true ,所以直接执行 if 语句,设置 isacls 为传入的 Classisaobjc_object 的结构体成员变量,也就是 isa_t 的类型。

inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    initIsa(cls, true, hasCxxDtor);
}

inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        isa_t newisa(0);
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
        isa = newisa;
    }
}
复制代码

通过 new 函数创建对象其实是一样的,内部通过 callAlloc 函数执行创建操作,如果调用 alloc 方法的话也是调用的 callAlloc 函数。所以调用 new 函数初始化对象时,可以等同于 alloc+init 的调用。

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
复制代码

在runtime源码中,执行init操作本质上就是直接把self返回。

- (id)init {
    return _objc_rootInit(self);
}

id _objc_rootInit(id obj)
{
    return obj;
}
复制代码

dealloc

在对象销毁时,运行时环境会调用 NSObjectdealloc 方法执行销毁代码,并不需要我们手动去调用。接着会调用到 Runtime 内部的 objc_object::rootDealloc (C++命名空间)函数。

rootDealloc 函数中会执行一些释放前的操作,例如将对象所有的引用指向 nil ,并且调用 free 函数释放内存空间等。

探秘Runtime - Runtime源码分析

下面的 if-else 语句中有判断条件,如果是 ARC 环境,并且当前对象定义了实例变量,才会进入 else 中执行 object_dispose 函数,否则进入上面的 if 语句。上面的 if 语句表示当前对象没有实例变量,则直接将当前对象 free

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
复制代码

object_dispose 函数中,主要是通过 objc_destructInstance 函数实现的。在函数内部主要做了三件事:

  1. 对当前对象进行析构,会调用析构函数 .cxx_destruct 函数,在函数内部还会进行对应的 release 操作。
  2. 移除当前对象的所有关联关系。
  3. 进行最后的 clear 操作。
// dealloc方法的核心实现,内部会做判断和析构操作
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // 判断是否有OC或C++的析构函数
        bool cxx = obj->hasCxxDtor();
        // 对象是否有相关联的引用
        bool assoc = obj->hasAssociatedObjects();

        // 对当前对象进行析构
        if (cxx) object_cxxDestruct(obj);
        // 移除所有对象的关联,例如把weak指针置nil
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}
复制代码

上面的函数中会调用 object_cxxDestruct 函数进行析构,而函数内部是通过 object_cxxDestructFromClass 函数实现的。

函数内部会从当前对象所属的类开始遍历,一直遍历到根类位置。在遍历的过程中,会不断执行 .cxx_destruct 函数,对传入的对象进行析构。

因为在继承者链中,每个类都会有自己的析构代码,所以需要将当前对象传入,并逐个执行析构操作,将对象的所有析构操作都执行完成才可以。

// 调用C++的析构函数
static void object_cxxDestructFromClass(id obj, Class cls)
{
    void (*dtor)(id);

    // 从当前类开始遍历,直到遍历到根类
    for ( ; cls; cls = cls->superclass) {
        if (!cls->hasCxxDtor()) return;
        // SEL_cxx_destruct就是.cxx_destruct的selector
        dtor = (void(*)(id))
            lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
        if (dtor != (void(*)(id))_objc_msgForward_impcache) {
            // 获取到.cxx_destruct的函数指针并调用
            (*dtor)(obj);
        }
    }
}
复制代码

在对象被执行 .cxx_destruct 析构函数后,析构函数内部还会调用一次 release 函数,完成最后的释放操作。

探秘Runtime - Runtime源码分析

addMethod实现

在项目中经常会动态对方法列表进行操作,例如动态添加或替换一个方法,这时候会用到下面两个 Runtime 函数。在下面两个函数中,本质上都是通过 addMethod 函数实现的,在 class_addMethod 中对返回值进行了一个取反,所以如果此函数返回 NO 则表示方法已存在,不要重复添加。

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return NO;

    rwlock_writer_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return nil;

    rwlock_writer_t lock(runtimeLock);
    return addMethod(cls, name, imp, types ?: "", YES);
}
复制代码

下面我们就分析一下 addMethod 函数的实现,依然只保留核心源码。

addMethod 函数中会先判断需要添加的方法是否存在,如果已经存在则直接返回对应的 IMP ,否则就动态添加一个方法。在 class_addMethod 函数中有一个 replace 字段,表示区别是否 class_replaceMethod 函数调用过来的。如果 replaceNO 则直接返回 IMP ,如果是 YES 则替换方法原有实现。

如果添加的方法不存在,则创建一个 method_list_t 结构体指针,并设置三个基本参数 nametypesimp ,然后通过 attachLists 函数将新创建的 method_list_t 结构体添加到方法列表中。

static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;
    method_t *m;
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // already exists
        if (!replace) {
            result = m->imp;
        } else {
            result = _method_setImplementation(cls, m, imp);
        }
    } else {
        // fixme optimize
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(method_t) | fixed_up_method_list;
        newlist->count = 1;
        newlist->first.name = name;
        newlist->first.types = strdupIfMutable(types);
        newlist->first.imp = imp;

        prepareMethodLists(cls, &newlist, 1, NO, NO);
        cls->data()->methods.attachLists(&newlist, 1);
        flushCaches(cls);

        result = nil;
    }

    return result;
}
复制代码

attachLists 函数中实现比较简单,通过对原有地址做位移,并将新创建的 method_list_t 结构体 copy 到方法列表中。

void attachLists(List* const * addedLists, uint32_t addedCount) {
    // ...
    memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0]));
    memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));
    // ...
}
复制代码

添加Ivar

Runtime 中可以通过 class_addIvar 函数,向一个类添加实例对象。但是需要注意的是,这个函数不能向一个已经存在的类添加实例变量,只能想通过 Runtime API 创建的类动态添加实例变量。

函数应该在调用 objc_allocateClassPair 函数创建类之后,以及调用 objc_registerClassPair 函数注册的类之间添加实例变量,否则就会失败。也不能向一个元类添加实例变量,只能想类添加实例变量。

下面是动态创建一个类,并向新创建的类添加实例变量的代码。

Class testClass = objc_allocateClassPair([NSObject class], "TestObject", 0);
BOOL isAdded = class_addIvar(testClass, "password", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
objc_registerClassPair(testClass);

if (isAdded) {
    id object = [[testClass alloc] init];
    [object setValue:@"lxz" forKey:@"password"];
}
复制代码

那么,为什么需要把动态添加实例变量的代码放在这两个函数中间呢?让我们一起来探究一下吧。

首先通过 objc_allocateClassPair 函数来创建类,创建时通过 getClass 函数判断类名是否已用,然后通过 verifySuperclass 函数判断 superclass 是否合适,如果任意条件不符合则创建类失败。

下面通过 alloc_class_for_subclass 函数创建类和元类,在 alloc 函数内部本质上是通过 calloc 函数分配内存空间,没有做其他操作。然后就执行 objc_initializeClassPair_internal 函数, initialize 函数内部都是初始化操作,用来初始化刚刚创建的 ClassmetaClass

Class objc_allocateClassPair(Class superclass, const char *name, 
                             size_t extraBytes)
{
    Class cls, meta;
    if (getClass(name)  ||  !verifySuperclass(superclass, true/*rootOK*/)) {
        return nil;
    }

    cls  = alloc_class_for_subclass(superclass, extraBytes);
    meta = alloc_class_for_subclass(superclass, extraBytes);

    objc_initializeClassPair_internal(superclass, name, cls, meta);
    return cls;
}
复制代码

这就是 initialize 函数内部的实现,都是各种初始化代码,没有做其他逻辑操作。至此,类的初始化完成,可以在外面通过 class_addIvar 函数添加实例变量了。

static void objc_initializeClassPair_internal(Class superclass, const char *name, Class cls, Class meta)
{
    class_ro_t *cls_ro_w, *meta_ro_w;
    
    cls->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1));
    meta->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1));
    cls_ro_w   = (class_ro_t *)calloc(sizeof(class_ro_t), 1);
    meta_ro_w  = (class_ro_t *)calloc(sizeof(class_ro_t), 1);
    cls->data()->ro = cls_ro_w;
    meta->data()->ro = meta_ro_w;

    // Set basic info
    cls->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
    meta->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING;
    cls->data()->version = 0;
    meta->data()->version = 7;

    // ....
}
复制代码

在创建类之后,会通过 objc_registerClassPair 函数注册新类。和创建新类一样,注册新类也分为注册类和注册元类。通过下面的 addNonMetaClass 函数注册元类,通过直接调用 NXMapInsert 函数注册类。

void objc_registerClassPair(Class cls)
{
    cls->ISA()->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);
    cls->changeInfo(RW_CONSTRUCTED, RW_CONSTRUCTING | RW_REALIZING);

    addNamedClass(cls, cls->data()->ro->name);
}

static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
    Class old;
    if ((old = getClass(name))  &&  old != replacing) {
        inform_duplicate(name, old, cls);
        addNonMetaClass(cls);
    } else {
        NXMapInsert(gdb_objc_realized_classes, name, cls);
    }
}
复制代码

无论是注册类还是注册元类,内部都是通过 NXMapInsert 函数实现的。在 Runtime 中,所有类都是存在一个哈希表中的,在 tablebuckets 中存储。每次新创建类之后,都需要把该类加入到哈希表中,下面是向哈希表插入的逻辑。

void *NXMapInsert(NXMapTable *table, const void *key, const void *value) {
    MapPair	*pairs = (MapPair *)table->buckets;
    // 计算key在当前hash表中的下标,hash下标不一定是最后
    unsigned	index = bucketOf(table, key);
    // 找到buckets的首地址,并通过index下标计算对应位置,获取到index对应的MapPair
    MapPair	*pair = pairs + index;
    // 如果key为空,则返回
    if (key == NX_MAPNOTAKEY) {
        _objc_inform("*** NXMapInsert: invalid key: -1\n");
        return NULL;
    }

    unsigned numBuckets = table->nbBucketsMinusOne + 1;
    // 如果当前地址未冲突,则直接对pair赋值
    if (pair->key == NX_MAPNOTAKEY) {
        pair->key = key; pair->value = value;
        table->count++;
        if (table->count * 4 > numBuckets * 3) _NXMapRehash(table);
            return NULL;
    }
    
    /* 到这一步,则表示hash表冲突了 */
    
    // 如果同名,则将旧类换为新类
    if (isEqual(table, pair->key, key)) {
        const void	*old = pair->value;
        if (old != value) pair->value = value;
            return (void *)old;
    
    // hash表满了,对hash表做重哈希,然后再次执行这个函数
    } else if (table->count == numBuckets) {
        /* no room: rehash and retry */
        _NXMapRehash(table);
        return NXMapInsert(table, key, value);

    // hash表冲突了
    } else {
        unsigned	index2 = index;
        // 解决hash表冲突,这里采用的是线性探测法,解决哈希表冲突
        while ((index2 = nextIndex(table, index2)) != index) {
            pair = pairs + index2;
            if (pair->key == NX_MAPNOTAKEY) {
                pair->key = key; pair->value = value;
                table->count++;
                // 在查找过程中,发现哈希表不够用了,则进行重哈希
                if (table->count * 4 > numBuckets * 3) _NXMapRehash(table);
                    return NULL;
            }
            // 找到同名类,则用新类替换旧类,并返回
            if (isEqual(table, pair->key, key)) {
                const void	*old = pair->value;
                if (old != value) pair->value = value;
                    return (void *)old;
            }
        }
    return NULL;
    }
}
复制代码

思考

那为什么只能向运行时动态创建的类添加 ivars ,不能向已经存在的类添加 ivars 呢?

这是因为在编译时只读结构体 class_ro_t 就会被确定,在运行时是不可更改的。 ro 结构体中有一个字段是 instanceSize ,表示当前类在创建对象时需要多少空间,后面的创建都根据这个 size 分配类的内存。

如果对一个已经存在的类增加一个参数,改变了 ivars 的结构,这样在访问改变之前创建的对象时,就会出现问题。

探秘Runtime - Runtime源码分析

以上图为例,在项目中创建 TestObject 类,并且添加三个成员变量,其 ivars 的内存结构占用20字节。如果在运行时动态添加一个 bool 型参数,之后创建的对象 ivars 都占用21字节。

在通过 ivars 结构体访问之前创建的对象时,因为之前创建的对象没有 sex ,所以还是按照20字节分配的内存空间,这时候访问 sex 就会导致地址越界。

数据访问

定义对象时都会给其设置类型,类型本质上并不是一个对象,而是用来标示当前对象所占空间的。以 C语言 为例,访问对象都是通过地址做访问的,而类型就是从首地址开始读取多少位是当前对象。

int number = 18;
char text = 'i';
复制代码

以上面代码为例,定义了一个 int 类型的 number ,占用四字节,定义一个 char 类型的 text 变量,占用一字节。在内存中访问对象时,就是根据指针地址找到对应的内存区,然后按照指针类型取多少范围的内存,就完成对象的读取操作。

探秘Runtime - Runtime源码分析

而在面向对象语言中,函数或方法的命名规则还需要保留在运行期。以 C++ 为例, C++ 中有一个概念叫做“函数重载”,函数重载指的是允许有一组相同函数名,但参数列表类型不同的函数。

原函数:void print(char c)
重载结果:_ZN4test5printEc
复制代码

C++ 函数重载是有一定规则的,例如上面就是对 print 函数重载后的结果,重载结果才是运行时真正执行的函数。函数重载发生在编译期,会包含 namespaceclass namefunction name 、返回值、参数等部分,根据这些部分重新生成函数名。

在OC中其实也存在函数重载的概念,只不过OC并不是直接对原有方法名做修改,而是增加对返回值和参数按照一定规则进行编码,然后放在 method_t 结构体中。

method_t 结构体存储着方法的信息,其中 types 字段就是返回值和参数的编码。编码后的字符串类似于 "iv@:d" ,完整的编码规则可以查看官方文档。

下面就是 Method 的定义,主要包含了三个关键信息。

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};
复制代码

Protocol

我们在项目中经常使用协议,那协议又是怎么实现的呢?

根据 Runtime 源码可以看出,协议都是 protocol_t 结构体的对象,而 protocol_t 结构体是继承自 objc_object 的,所以具备对象的特征。

除了 objc_object 中定义的一些结构体参数外, protocol_t 中还定义了一些独有的参数,例如常用的 namemethod listproperty listsize 等。所以可以看出,一个协议中可以声明对象方法、类方法,以及对象属性和类属性。

struct protocol_t : objc_object {
    const char *mangledName;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    // Fields below this point are not always present on disk.
    const char **_extendedMethodTypes;
    const char *_demangledName;
    property_list_t *_classProperties;
};
复制代码

既然具备了对象的特征,那也是有isa指针的。在Protocol中所有的isa都指向同一个类Protocol。在 Protocol 类中没有做太复杂的处理,只是实现了一些基础的方法。

@implementation Protocol 

+ (void) load {

}

- (BOOL) conformsTo: (Protocol *)aProtocolObj {
    return protocol_conformsToProtocol(self, aProtocolObj);
}

- (struct objc_method_description *) descriptionForInstanceMethod:(SEL)aSel {
    return method_getDescription(protocol_getMethod((struct protocol_t *)self, 
                                                     aSel, YES, YES, YES));
}

- (struct objc_method_description *) descriptionForClassMethod:(SEL)aSel {
    return method_getDescription(protocol_getMethod((struct protocol_t *)self, 
                                                    aSel, YES, NO, YES));
}

- (const char *)name {
    return protocol_getName(self);
}

// Protocol重写了isEqual方法,内部不断查找其父类,判断是否Protocol的子类。
- (BOOL)isEqual:other {
    Class cls;
    Class protoClass = objc_getClass("Protocol");
    for (cls = object_getClass(other); cls; cls = cls->superclass) {
        if (cls == protoClass) break;
    }
    if (!cls) return NO;
    // check equality
    return protocol_isEqual(self, other);
}

- (NSUInteger)hash {
    return 23;
}

@end
复制代码

协议的初始化也是在 _read_images 函数中完成的,初始化过程主要是一个遍历。逻辑就是获取 Protocol list ,然后遍历这个数组,并调用 readProtocol 函数进行初始化操作。

// 遍历所有协议列表,并且将协议列表加载到Protocol的哈希表中
for (EACH_HEADER) {
    extern objc_class OBJC_CLASS_$_Protocol;
    // cls = Protocol类,所有协议和对象的结构体都类似,isa都对应Protocol类
    Class cls = (Class)&OBJC_CLASS_$_Protocol;
    assert(cls);
    // 获取protocol哈希表
    NXMapTable *protocol_map = protocols();
    bool isPreoptimized = hi->isPreoptimized();
    bool isBundle = hi->isBundle();

    // 从编译器中读取并初始化Protocol
    protocol_t **protolist = _getObjc2ProtocolList(hi, &count);
    for (i = 0; i < count; i++) {
        readProtocol(protolist[i], cls, protocol_map, 
                     isPreoptimized, isBundle);
    }
}
复制代码

readProtocol 函数中,会根据传入的协议进行初始化操作。在传入参数中, protocol_class 就是 Protocol 类,所有的协议类的 isa 都指向这个类。

根据 Protocol 的源码可以看出,其对象模型是比较简单的,和 Class 的对象模型还不太一样。 Protocol 的对象模型只有从 Protocol list 中加载的对象和 isa 指向的 Protocol 类构成,没有其他的实例化过程, Protocol 类并没有元类。

// 初始化传入的所有Protocol,如果哈希表中已经存在初始化的Protocol,则不做任何处理
static void
readProtocol(protocol_t *newproto, Class protocol_class,
             NXMapTable *protocol_map, 
             bool headerIsPreoptimized, bool headerIsBundle)
{
    auto insertFn = headerIsBundle ? NXMapKeyCopyingInsert : NXMapInsert;
    // 根据名字获得对应的Protocol对象
    protocol_t *oldproto = (protocol_t *)getProtocol(newproto->mangledName);

    // 如果Protocol不为NULL,表示已经存在相同的Protocol,则不做任何处理,进入下面if语句。
    if (oldproto) {
        // nothing
    }
    // 如果Protocol为NULL,则对其进行简单的初始化,并将Protocol的isa设置为Protocol类
    else if (headerIsPreoptimized) {
        protocol_t *cacheproto = (protocol_t *)
            getPreoptimizedProtocol(newproto->mangledName);
        protocol_t *installedproto;
        if (cacheproto  &&  cacheproto != newproto) {
            installedproto = cacheproto;
        }
        else {
            installedproto = newproto;
        }
        // 哈希表插入函数的指针
        insertFn(protocol_map, installedproto->mangledName, 
                 installedproto);
    }
    // 下面两个else都是初始化protocol_t的过程
    else if (newproto->size >= sizeof(protocol_t)) {
        newproto->initIsa(protocol_class);
        insertFn(protocol_map, newproto->mangledName, newproto);
    }
    else {
        size_t size = max(sizeof(protocol_t), (size_t)newproto->size);
        protocol_t *installedproto = (protocol_t *)calloc(size, 1);
        memcpy(installedproto, newproto, newproto->size);
        installedproto->size = (__typeof__(installedproto->size))size;
        
        installedproto->initIsa(protocol_class);
        insertFn(protocol_map, installedproto->mangledName, installedproto);
    }
}
复制代码

Protocol是可以在运行时动态创建添加的,和创建Class的过程类似,分为创建和注册两部分。创建 Protocol 之后, Protocol 处于一个未完成的状态,只有注册后才是可以使用的 Protocol

// 创建新的Protocol,创建后还需要调用下面的register方法
Protocol *
objc_allocateProtocol(const char *name)
{
    if (getProtocol(name)) {
        return nil;
    }

    protocol_t *result = (protocol_t *)calloc(sizeof(protocol_t), 1);

    // 下面的cls是__IncompleteProtocol类,表示是未完成的Protocol
    extern objc_class OBJC_CLASS_$___IncompleteProtocol;
    Class cls = (Class)&OBJC_CLASS_$___IncompleteProtocol;
    result->initProtocolIsa(cls);
    result->size = sizeof(protocol_t);
    result->mangledName = strdupIfMutable(name);
    
    return (Protocol *)result;
}
复制代码

注册 Protocol

// 向protocol的哈希表中,注册新创建的Protocol对象
void objc_registerProtocol(Protocol *proto_gen) 
{
    protocol_t *proto = newprotocol(proto_gen);

    extern objc_class OBJC_CLASS_$___IncompleteProtocol;
    Class oldcls = (Class)&OBJC_CLASS_$___IncompleteProtocol;
    extern objc_class OBJC_CLASS_$_Protocol;
    Class cls = (Class)&OBJC_CLASS_$_Protocol;

    // 如果已经被注册到哈希表中,则直接返回
    if (proto->ISA() == cls) {
        return;
    }
    // 如果当前protocol的isa不是__IncompleteProtocol,表示这个protocol是有问题的,则返回
    if (proto->ISA() != oldcls) {
        return;
    }
    proto->changeIsa(cls);
    NXMapKeyCopyingInsert(protocols(), proto->mangledName, proto);
}
复制代码

SEL

之前 SEL 是由 objc_selector 结构体实现的,但是从现在的源码来看, SEL 是一个 const char* 的常量字符串,只是代表一个名字而已。

typedef struct objc_selector *SEL;
复制代码

为什么说 SEL 只是一个常量字符串呢?我们在 Runtime 源码中探究一下。

这是在 _read_images 函数中 SEL list 的实现,主要逻辑是加载 SEL list 到内存中,然后通过 sel_registerNameNoLock 函数,将所有 SEL 都注册到属于 SEL 的哈希表中。

但是我们从这段代码中可以看出,大部分的 SELconst char* 的转换,都是直接进行强制类型转换的,所以二者是同一块内存。

// 将所有SEL都注册到哈希表中,是另外一张哈希表
static size_t UnfixedSelectors;
sel_lock();
for (EACH_HEADER) {
    if (hi->isPreoptimized()) continue;

    bool isBundle = hi->isBundle();
    // 取出的是字符串数组,例如首地址是"class"
    SEL *sels = _getObjc2SelectorRefs(hi, &count);
    UnfixedSelectors += count;
    for (i = 0; i < count; i++) {
        // sel_cname函数内部就是将SEL强转为常量字符串
        const char *name = sel_cname(sels[i]);
        // 注册SEL的操作
        sels[i] = sel_registerNameNoLock(name, isBundle);
    }
}
复制代码

再进入 sel_registerNameNoLock 函数中可以看出, SEL 的哈希表也是将字符串注册到哈希表中,并不是之前的 objc_selector 结构体,所以可以看出现在 SEL 就是单纯的 const char* 常量字符串。

static SEL sel_alloc(const char *name, bool copy)
{
    return (SEL)(copy ? strdupIfMutable(name) : name);    
}
复制代码

对等交换协议

研究 Apple 的源码时,还可以通过 GNUStep 研究, GNUStep 是苹果的一套对等交换源码,将OC代码以重新实现了一遍,内部实现大致和苹果的类似。GNUStep

简书由于排版的问题,阅读体验并不好,布局、图片显示、代码等很多问题。所以建议到我 Github 上,下载 Runtime PDF 合集。把所有 Runtime 文章总计九篇,都写在这个 PDF 中,而且左侧有目录,方便阅读。

探秘Runtime - Runtime源码分析

下载地址: Runtime PDF 麻烦各位大佬点个赞,谢谢!:grin:


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

PHP Hacks

PHP Hacks

Jack Herrington D. / O'Reilly Media / 2005-12-19 / USD 29.95

Programmers love its flexibility and speed; designers love its accessibility and convenience. When it comes to creating web sites, the PHP scripting language is truly a red-hot property. In fact, PH......一起来看看 《PHP Hacks》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

MD5 加密
MD5 加密

MD5 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具