OC运行时机制

Posted by wanglilong on April 20, 2021

一、内存结构

objc_class

OC对象都会转为结构体。OC对象对应的结构体就是objc_class

1
2
3
4
5
6
struct objc_class {
    Class isa;
    Class superclass;
    cache_t cache;             // 方法缓存
    class_data_bits_t bits;    // 具体的类信息(指针)
};

class_ro_t

class_ro_t存储了当前类在编译期就已经确定的属性方法以及遵循的协议,里面是没有分类的方法的。那些运行时添加的方法将会存储在运行时生成的class_rw_t中。ro即表示read only,是无法进行修改的。

1
2
3
4
5
6
7
8
9
10
11
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; //对象占用内存空间
    const char * name; //类名
    method_list_t *baseMethodList; //方法
    protocol_list_t * baseProtocols; //接口
    property_list_t *baseProperties; //属性
    ivar_list_t * ivars; //成员变量
    const uint8_t * weakIvarLayout; // 弱指针
};

class_rw_t

ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro; //指向ro_t指针
    /*
     这三个都是二位数组,是可读可写的,包含了类的初始内容、分类的内容。
     methods中,存储 method_list_t ----> method_t
     二维数组,method_list_t --> method_t
     这三个二位数组中的数据有一部分是从class_ro_t中合并过来的。
    */
    method_array_t methods; //方法列表
    property_array_t properties; //属性列表
    protocol_array_t protocols; //协议列表
    Class firstSubclass;
    Class nextSiblingClass;
};

method_t

与类和对象一样,方法在内存中也是一个结构体。

1
2
3
4
5
struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

其它参考

深入解析 ObjC 中方法的结构

OC语言(一)对象内存分析

OC语言(二)对象的本质及分类

二、继承的实现

isa指针是为了查找,方法和类方法。superclass是为了实现继承。方法查找可看成是树上的查找(更准确的说是有向图)。

通过源码可以知道isaisa_t类型的内部结构

1
2
3
4
5
6
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
};

下面这张图介绍了对象,类与元类之间的关系。

消息发送机制

一、消息发送

1、objc_class的cache中查找(二分查找)

​ cache是个散列表,散列函数为f(@selector()) = @selector() & _mask,散列处理方式是–i;

2、class_rw_t的方法列表中查找,如果方法没排序遍历查找,如果排序则二分查找

3、从superclass的的cache中查找

4、从superclass的rw_t中查找

(查找结束后,将方法添加到缓存中)

二、动态解析

1、是否已动态解析—->下个阶段

2、resolveInstanceMethod:

​ resolveClassMethod:

3、标注已动态解析

4、动态解析后,重走消息发送流程

三、消息转发

1、forwardingTargetForSelector. ———>消息转发另个对象

2、methodSignatureForSelector. ———–>完备消息转发

3、doesNotRecognizeSelecter(抛出异常)

addMethod直接在二维方法数组末尾添加一个数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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(false);
        } else {
            result = _method_setImplementation(cls, m, imp);
        }
    } else {
        // fixme optimize
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(method_list_t::byteSize(method_t::bigSize, 1), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(struct method_t::big) | fixed_up_method_list;
        newlist->count = 1;
        auto &first = newlist->begin()->big();
        first.name = name;
        first.types = strdupIfMutable(types);
        first.imp = imp;
        addMethods_finish(cls, newlist);
        result = nil;
    }
    return result;
}

###运行时

Runtime(一)Runtime简介

Runtime(二)isa指针 从 NSObject 的初始化了解 isa isa 和 Class

Runtime(三)方法缓存

Runtime(四)objc_msgSend

Runtime(五)类的判定

三、分类

分类的内存结构大体如下

1
2
3
4
5
6
7
8
9
truct category_t {
    const char *name; //分类名
    classref_t cls; //类
    struct method_list_t *instanceMethods;//实例方法列表
    struct method_list_t *classMethods; //类方法列表
    struct protocol_list_t *protocols; //协议列表
    struct property_list_t *instanceProperties; //属性列表
    struct property_list_t *_classProperties;
};

1、在运行时加载阶段,会依次加载类相应的分类。

2、分类的方法和实例变量合并到class_rw_t相应的二维数组中。

3、后合并的分类数据,放到原来数据的前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    /*
     * Only a few classes have more than 64 categories during launch.
     * This uses a little stack, and avoids malloc.
     *
     * Categories must be added in the proper order, which is back
     * to front. To do that with the chunking, we iterate cats_list
     * from front to back, build up the local buffers backwards,
     * and call attachLists on the chunks. attachLists prepends the
     * lists, so the final result is in the expected order.
     */
    constexpr uint32_t ATTACH_BUFSIZ = 64;
    method_list_t   *mlists[ATTACH_BUFSIZ];
    property_list_t *proplists[ATTACH_BUFSIZ];
    protocol_list_t *protolists[ATTACH_BUFSIZ];

    uint32_t mcount = 0;
    uint32_t propcount = 0;
    uint32_t protocount = 0;
    bool fromBundle = NO;
    bool isMeta = (flags & ATTACH_METACLASS);
    auto rwe = cls->data()->extAllocIfNeeded();

    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }

    if (mcount > 0) {
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {
            flushCaches(cls, __func__, [](Class c){
                // constant caches have been dealt with in prepareMethodLists
                // if the class still is constant here, it's fine to keep
                return !c->cache.isConstantOptimizedCache();
            });
        }
    }
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}

OC语言(三)Category

四、关联对象

通过单例存储一个两层的Map。

第一层Map,类(实例)为Key,map为value。

第二层Map,关联对象为Key,关联的对象为value。

OC语言(四)关联对象

五、KVC和KVO

KVO通过动态子类的方式实现观察者模式。

当为类添加KVO时,动态为类对象创建一个子类。

覆盖kvo对象的set方法。

1
2
3
4
5
-(void)_setValueAndNotify{
  willChangeValueForKey();
  setAge();
  didChangeValueForKey();
}

OC语言(五)KVC与KVO

六、load和initialize

OC语言(六)load和initialize

七、NSClassFromString实现原理

从汇编代码探究 NSClassFromString 实现分析NSClassFromString实现大体如下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Class _Nullable NSClassFromString(NSString *aClassName) {
    if (!aClassName) { return Nil; }
    
    NSUInteger classNameLength = [aClassName length];
    char buffer[1000];
    
    // @"Big\0Dog" 以及 @"🐶" 都会使得
    // classNameLength == strlen(buffer) 不成立
    if ([aClassName getCString:buffer maxLength:1000 encoding:NSUTF8StringEncoding]
        && classNameLength == strlen(buffer)) {
        return objc_lookUpClass(buffer);
    } else if (classNameLength == 0) {
        // 检查是否空字符串 @"",这个分支要处理的情况不太理解
        return objc_lookUpClass([aClassName UTF8String]);
    }
    
    for (int i = 0; i < classNameLength; i++) {
        // 如果 aClassName 中含有 \0 字符,向外返回 Nil
        // 比如 @"Big\0Dog" 的情况
        if ([aClassName characterAtIndex:i] == 0) {
            return Nil;
        }
    }
    
    return objc_lookUpClass([aClassName UTF8String]);
}

也就是说NSClassFromString最终会调用objc_lookUpClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/***********************************************************************
* look_up_class
* Map a class name to a class using various methods.
* This is the common implementation of objc_lookUpClass and objc_getClass, 
* and is also used internally to get additional search options.
* Sequence:
* 1. class_hash
* 2. unconnected_class_hash (optional)
* 3. classLoader callback
* 4. classHandler callback (optional)
**********************************************************************/
Class look_up_class(const char *aClassName, bool includeUnconnected, 
                    bool includeClassHandler)
{
    bool includeClassLoader = YES; // class loader cannot be skipped
    Class result = nil;
    struct objc_class query;

    query.name = aClassName;

 retry:

    if (!result  &&  class_hash) {
        // Check ordinary classes
        mutex_locker_t lock(classLock);
        result = (Class)NXHashGet(class_hash, &query);
    }

    if (!result  &&  includeUnconnected  &&  unconnected_class_hash) {
        // Check not-yet-connected classes
        mutex_locker_t lock(classLock);
        result = (Class)NXHashGet(unconnected_class_hash, &query);
    }

    if (!result  &&  includeClassLoader  &&  _objc_classLoader) {
        // Try class loader callback
        if ((*_objc_classLoader)(aClassName)) {
            // Re-try lookup without class loader
            includeClassLoader = NO;
            goto retry;
        }
    }

    if (!result  &&  includeClassHandler  &&  objc_classHandler) {
        // Try class handler callback
        if ((*objc_classHandler)(aClassName)) {
            // Re-try lookup without class handler or class loader
            includeClassLoader = NO;
            includeClassHandler = NO;
            goto retry;
        }
    }

    return result;
}

objc_lookUpClass通过一个通过NXHashGet方法,查找一个方法名的hash表.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/***********************************************************************
* getClassExceptSomeSwift
* Looks up a class by name. The class MIGHT NOT be realized.
* Demangled Swift names are recognized.
* Classes known to the Swift runtime but not yet used are NOT recognized.
*   (such as subclasses of un-instantiated generics)
* Use look_up_class() to find them as well.
* Locking: runtimeLock must be read- or write-locked by the caller.
**********************************************************************/

// This is a misnomer: gdb_objc_realized_classes is actually a list of 
// named classes not in the dyld shared cache, whether realized or not.
// This list excludes lazily named classes, which have to be looked up
// using a getClass hook.
NXMapTable *gdb_objc_realized_classes;  // exported for debuggers in objc-gdb.h

static Class getClass_impl(const char *name)
{
    runtimeLock.assertLocked();

    // allocated in _read_images
    ASSERT(gdb_objc_realized_classes);

    // Try runtime-allocated table
    Class result = (Class)NXMapGet(gdb_objc_realized_classes, name);
    if (result) return result;

    // Try table from dyld shared cache.
    // Note we do this last to handle the case where we dlopen'ed a shared cache
    // dylib with duplicates of classes already present in the main executable.
    // In that case, we put the class from the main executable in
    // gdb_objc_realized_classes and want to check that before considering any
    // newly loaded shared cache binaries.
    return getPreoptimizedClass(name);
}