OC底层原理(14)类的加载原理(下)

123 阅读15分钟

前面已经探究了类的加载,类的加载分为懒加载类和非懒加载类,他们有不同加载流程,下面来探究下分类的加载,以及分类和主类之间加载不同的情况

一. 分类的加载

分类的底层结构是结构体categor_t

struct category_t {
    const char *name; // 分类名 比如 `YJPerson(YJ)`这里的 name 就是 `YJ`
    classref_t cls; // 分类所属类 `YJPerson`
    WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods; // 分类中的 实例方法
    WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods; // 分类中的 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 属性

    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

分类一般是加载到主类中的,什么时候加载以及加载的过程,这是下面要探究的

1.1 分类加载引入

2020WWDC 类的优化中苹果为分类和动态添加专门分配的了一块内存rwe,因为rwe属于dirty memory所以肯定是需要动态开辟内存。下面从class_rw_t中去查找相关rwe的源码

struct class_rw_t {
    ... // 省略部分代码
    class_rw_ext_t *ext() const {
        return get_ro_or_rwe().dyn_cast<class_rw_ext_t *>(&ro_or_rw_ext);
    }

    class_rw_ext_t *extAllocIfNeeded() {
        auto v = get_ro_or_rwe();
        // 判断rwe是否存在
        if (fastpath(v.is<class_rw_ext_t *>())) {
            // 如果已经有rwe直接返回地址指针
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext);
        } else {
            // 为rwe开辟内存并且返回地址指针
            return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
        }
    }
    class_rw_ext_t *deepCopy(const class_ro_t *ro)
        return extAlloc(ro, true);
    }
    ... // 省略部分代码
}

源码中显示如果你主要用到rwe,肯定会调用extAllocIfNeeded()。全局搜索extAllocIfNeeded(),发现调用的地方很多有attachCategoriesdemangledNameclass_setVersionaddMethods_finishclass_addProtocol_class_addPropertyobjc_duplicateClass方法。从这些方法中发现一些是动态添加的方法,关于分类的方法,还有一些关于类名、设置类的版本,以及修复类。今天探究分类的加载流程,所以定位到attachCategories

1.2 反推 attachCategories 

static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t 
cats_count, int flags)
{   
    ... // 省略部分代码
    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;
    // 获取rwe
    auto rwe = cls->data()->extAllocIfNeeded();
    ...
    // 遍历所有的分类,但是分类中的方法一般不要超过64个分类,不然苹果会认为你这个人有问题
    for (uint32_t i = 0; i < cats_count; i++) {
        auto& entry = cats_list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            // 如果你的分类的个数超过64个,那么把这64个分类的方法列表加载到主类中
            // 把后面的数据继续放到 mlists[]中
            if (mcount == ATTACH_BUFSIZ) {
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0; //mcount 设置为0 
            }
            // 如果 mcount = 0,mlist存放的位置在63个位置,总共是0 ~ 63
            // ATTACH_BUFSIZ = 64, ++mcount = 1, 64-1=63
            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) {
        // 在将分类方法添加到主类之前将方法进行排序
        // 地址偏移还记得d+n嘛,这里:mlists = d,n = ATTACH_BUFSIZ - mcount
        // mlists + ATTACH_BUFSIZ - mcount =  d + n
        // 此时的mlists + ATTACH_BUFSIZ - mcount 是一个二维指针,里面存放的是方法列表的首地址
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        // 在将分类方法添加到主类方法中
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
        if (flags & ATTACH_EXISTING) {...}
    } 
    
    // 在将分类属性添加到主类属性中
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    // 在将分类协议添加到主类协议中
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
     ... //省略部分代码
} 

attachCategories准备分类的数据,然后调用attachLists将数据添加到主类中,那么到底哪些地方调用attachCategories方法,全局搜索attachCategories

Xnip2022-07-19_12-29-21.png

源码显示 attachToClass 中调用了 attachCategories 方法

Xnip2022-07-19_12-31-02.png

源码显示 load_categories_nolock 中调用了 attachCategories 方法

1.2.1 attachToClass 流程

全局搜索 attachToClass,只有methodizeClass调用了attachToClass

static void methodizeClass(Class cls, Class previously)
{   ... //省略部分代码
    // Attach categories.
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            // When a class relocates, categories with class methods
            // may be registered on the class itself rather than on
            // the metaclass. Tell attachToClass to look for those.
            objc::unattachedCategories.attachToClass(cls, previously,
                                                   ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
    ... //省略部分代码
}
  • methodizeClass这个方法很熟悉realizeClassWithoutSwift调用,而previously条件决定是否调用条件里面的attachToClass方法,而previously是作为参数传进来的。最终找到是realizeClassWithoutSwift传进来的,全局搜索realizeClassWithoutSwift,调用的地方传入的previously = nil,所以attachToClass里面的previously条件不会走,只能最后的attachToClass方法
  • previously作为备用参数,这种设计可能是苹果内部调试用的
  • attachToClass流程:
    • _read_images -->
    • realizeClassWithoutSwift -->
    • methodizeClass -->
    • attachToClass -->
    • attachCategories -->
    • attachLists

1.2.2 load_categories_nolock 流程

全局搜索 load_categories_nolock

static void loadAllCategories() {
    mutex_locker_t lock(runtimeLock);
    for (auto *hi = FirstHeader; hi != NULL; hi = hi->getNext()) {
        load_categories_nolock(hi);
    }
}

loadAllCategories方法中调用load_categories_nolock。全局搜索loadAllCategories

static bool  didInitialAttachCategories = false;

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        loadAllCategories();
    }
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;
    recursive_mutex_locker_t lock(loadMethodLock);
    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }
    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

load_images 调用loadAllCategoriesload_imagesdyld中调用

  • didInitialAttachCategories默认是false,当执行完loadAllCategories()后自动将didInitialAttachCategories设为true,其实就是只调用一次loadAllCategories()
  • objcdyld完成注册回调后didCallDyldNotifyRegister = true
  • load_categories_nolock流程:
    • load_images -->
    • loadAllCategories -->
    • load_categories_nolock -->
    • attachLists

1.2.3 attachLists

attachLists 是核心方法,attachLists作用是将分类数据加载到主类中

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;
    // hasArray()如果array()存在进入判断
    if (hasArray()) {
        // many lists -> many lists
        // oldCount = 获取array()->lists的个数
        uint32_t oldCount = array()->count;
        // 新的个数 =  oldCount + 新添加的
        uint32_t newCount = oldCount + addedCount;
        // 根据`newCount`开辟内存,类型是 array_t
        array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
        // 设置新数组的个数等于`newCount`
        newArray->count = newCount;
        // 设置原有数组的个数等于`newCount`
        array()->count = newCount;
        // 遍历原有数组中list将其存放在newArray->lists中 且是放在数组的末尾
        for (int i = oldCount - 1; i >= 0; i--)
            newArray->lists[i + addedCount] = array()->lists[i];
        // 遍历二维指针`addedLists`中的list将其存放在newArray->lists中 且是从开始 
        // 的位置开始存放 
        for (unsigned i = 0; i < addedCount; i++)
            newArray->lists[i] = addedLists[i];
        // 释放原有的array()
        free(array());
        // 设置新的 newArray
        setArray(newArray);
        validate();
    }
    // 如果主类中有方法,第一次list主类中的方法
    // 如果主类中没有方法,则进来的就是分类方法
    else if (!list  &&  addedCount == 1) {
        // 0 lists -> 1 list
        list = addedLists[0];
        validate();
    } 
    // 当list是一维数组时,此时进入下面的判断创建`array_t`类型的结构体类型
    // `array_t`的lists存放的是各个分类数组的地址指针
    else {
        // 1 list -> many lists
        // 将list数组赋值给oldList 
        Ptr<List> oldList = list; 
        // oldList 存在 oldCount = 1
        uint32_t oldCount = oldList ? 1 : 0; 
        // 新的newCount = 原有的count + 新增的count
        uint32_t newCount = oldCount + addedCount;
        // 根据`newCount`开辟内存,类型是 array_t, array()->lists是一个二维数组
        setArray((array_t *)malloc(array_t::byteSize(newCount)));
        // 设置数组的个数
        array()->count = newCount;
        // 将原来的list放在数组的末尾
        if (oldList) array()->lists[addedCount] = oldList;
        // 遍历addedLists将遍历的数据从数组的开始位置存储
        for (unsigned i = 0; i < addedCount; i++)
            array()->lists[i] = addedLists[i];
        validate();
    }
}

源码分析,attachLists总共分三步流程

  • 0 list --> 1 list

    • addedLists[0]的指针赋值给list
  • 1 list --> many lists

    • 计算旧的list的个数
    • 计算新的list个数 ,新的list个数 = 原有的list个数 + 新增的list个数
    • 根据newCount开辟相应的内存,类型是array_t类型,并设置数组setArray
    • 将原有的list放在数组的末尾,因为最多只有一个不需要遍历存储
    • 遍历addedLists将遍历的数据从数组的开始位置存储
  • many lists --> many lists

    • 判断array()是否存在
    • 计算原有的数组中的list个数array()->lists的个数
    • 新的newCount = 原有的count + 新增的count
    • 根据newCount开辟相应的内存,类型是array_t类型
    • 设置新数组的个数等于newCount
    • 设置原有数组的个数等于newCount
    • 遍历原有数组中list将其存放在newArray->lists中 且是放在数组的末尾
    • 遍历addedLists将遍历的数据从数组的开始位置存储
    • 释放原有的array()
    • 设置新的newArray

注意List* const * addedLists是 二级指针

setArrayhasArray的探究

union {
    Ptr<List> list;
    uintptr_t arrayAndFlag;
};

bool hasArray() const {
    // arrayAndFlag 的第 0 位是 1,则返回 ture
    return arrayAndFlag & 1;
}

array_t *array() const {
    // 将arrayAndFlag 的第 0 位置 0,返回
    return (array_t *)(arrayAndFlag & ~1);
}

void setArray(array_t *array) {
    // 将 arrayAndFlag 的第 0 位置 1
    arrayAndFlag = (uintptr_t)array | 1;
}

attachLists 流程分析图

attachLists流程图.png

1.3 验证 attachLists

验证attachLists绕不过attachCategories,前面只是简单介绍了attachCategories具体的细节没有探究,下面断点调试验证下

1.3.1 attachCategories 验证

创建 YJPerson 的分类 YJ

@implementation YJPerson
+ (void)load {
    NSLog(@"调用了 : %s", __func__);
}
- (void)say1 {
    NSLog(@"调用了 : %s", __func__);
}
@end

@implementation YJPerson (YJ)
+ (void)load {
    NSLog(@"调用了 : %s", __func__);
}
- (void)sayCate1 {
    NSLog(@"调用了 : %s", __func__);
}

运行源码,进行调试:

Xnip2022-07-19_16-08-59.png

图中 mlist 是分类 YJ 中的方法列表。此时打印下 mlists 中数据:

Xnip2022-07-19_16-14-23.png

跳过 mlists[ATTACH_BUFSIZ - ++mcount] = mlist; 再次打印 mlists

Xnip2022-07-19_16-24-05.png

再次打印 发现最后一个地址变成了 0x00000001000080f8,正好是前面的 mlist 地址;即 mlists 中最后一位存方的是YJ分类的方法列表的地址

mlists是一个二维指针类型

Xnip2022-07-19_16-34-52.png

mlists + ATTACH_BUFSIZ - mcount就是地址偏移和前面探究的d+n是一样的。mlists是首地址,ATTACH_BUFSIZ - mcount具体的第几个位置

1.3.2 attachLists验证

1.3.2.1 0 对 1

前面我们知道在 extAllocIfNeeded 中返回 rwe,有值直接返回,没有就去创建

class_rw_ext_t *extAllocIfNeeded() {  
    auto v = get_ro_or_rwe(); // 判断rwe是否存在      
    if (fastpath(v.is<class_rw_ext_t *>())) { // 如果已经有rwe直接返回地址指针             
        return v.get<class_rw_ext_t *>(&ro_or_rw_ext);   
    } else { // 为rwe开辟内存并且返回地址指针           
        return extAlloc(v.get<const class_ro_t *>(&ro_or_rw_ext));
    } 
}

rwe不存在就会去开辟内存调用extAlloc方法

class_rw_ext_t *
class_rw_t::extAlloc(const class_ro_t *ro, bool deepCopy)
{
    runtimeLock.assertLocked();
    // 为rwe开辟内存
    auto rwe = objc::zalloc<class_rw_ext_t>();
    rwe->version = (ro->flags & RO_META) ? 7 : 0;
    // 如果主类有方法,那么就会将主类的方法列表进行`attachLists`此时是 0 对 多
    method_list_t *list = ro->baseMethods();
    if (list) {
        if (deepCopy) list = list->duplicate();
        rwe->methods.attachLists(&list, 1);
    }
    //如果主类有属性,那么就会将主类的属性列表进行`attachLists`此时是0 对 多
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rwe->properties.attachLists(&proplist, 1);
    }
     //如果主类有协议,那么就会将主类的协议列表进行`attachLists`此时是 0 对 多
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rwe->protocols.attachLists(&protolist, 1);
    }
    // 设置rwe
    set_ro_or_rwe(rwe, ro);
    // 返回rwe
    return rwe;
}

源码显示如果主类中存在方法,那么就将方法进行 attachLists,如果主类中没有方法就此时什么都不做。此时就会出现两种情况:主类有方法主类无方法

主类有方法/分类有方法

创建主类 YJPerson,分类 YJPerson+YJA 和 YJPerson+YJB 代码如下:

@implementation YJPerson
+ (void)load {
    NSLog(@"调用了 : %s", __func__);
}
- (void)say1 {
    NSLog(@"调用了 : %s", __func__);
}
@end

@implementation YJPerson (YJA)
+ (void)load {
    NSLog(@"调用了 : %s", __func__);
}
- (void)sayCateA {
    NSLog(@"调用了 : %s", __func__);
}
@end

@implementation YJPerson (YJB)
+ (void)load {
    NSLog(@"调用了 : %s", __func__);
}
- (void)sayCateB {
    NSLog(@"调用了 : %s", __func__);
}
@end

运行objc源码定位到 attachCategories 方法,因为现在研究的是分类

Xnip2022-07-19_21-04-32.png

图中的第一个断点先断住,然后再进入第二个断点,目的是确定我们调试的是自定义的 YJPerson类。然后进入extAllocIfNeeded方法

Xnip2022-07-19_21-08-17.png

此时rwe还没有值所以需要开辟内存走extAlloc方法

Xnip2022-07-19_21-11-40.png

此时list有值,并且是主类的,所以调用attachLists方法,进入attachLists方法

Xnip2022-07-19_21-16-01.png

  • 断点进入 0对1的分支中
  • 通过lldb调试,确定是主类的方法列表
主类没有方法/分类有方法

将主类 YJPerson 中的 say1 移除,重复上面的步骤:

@implementation YJPerson 
+ (void)load {    
    NSLog(@"调用了 : %s", __func__); }
}
@end

Xnip2022-07-19_21-20-56.png

按照上面的调试流程,进入extAlloc方法,此时的list = NULL,所以不会调用attachLists方法,但是要想动态添加到主类0对1这个流程是必须要走的的,接着调试

Xnip2022-07-19_21-28-52.png

attachCategories 的第二个参数是对category_t进行了一层包装,包装成locstamped_category_t类型,通过lldb调试里面分类是YJA分类,接着往下调试

Xnip2022-07-19_21-40-09.png

此时会调用attachLists方法,进入attachLists方法

Xnip2022-07-19_21-47-03.png

进入0对1流程的是YJA中的方法列表

总结

01流程有两种情况

  • 主类有方法/分类有方法:以主类的方法为基础将分类的方法加载到主类中
  • 主类没有方法/分类有方法:以第一个编译的分类为基础将其它分类一起合并,最后加载到主类中

1.3.2.2 1 对 

现在把还原 YJPerson 中的 say1方法,运行源码:

Xnip2022-07-19_22-03-16.png

oldlist是主类中的方法列表,addedLists中存放着分类方法列表的指针,从addedLists取出分类中数据,现在看合并后的数据

Xnip2022-07-19_22-20-31.png

array()->lists 中存放着两个方法列表的地址,分类的方法列表是是放在前面的

1.3.2.3  对 

按照上面的逻辑继续执行,进入的流程,继续进行断点调试

Xnip2022-07-19_22-33-50.png

再次调用 addedLists,进去:

Xnip2022-07-19_22-37-18.png

hasArray()=ture进入到多对多流程,addedLists二级指针中只有一个方法列表,是YJB分类的

Xnip2022-07-19_22-44-05.png

array()lists中存放3个方法列表,分别是分类YJB、YJA 和主类类 YJPerson的方法列表。最后编译的分类是在整个lists的最前面

二. 类和分类的组合

2.1 非懒类+非懒分类

主类 YJPerson 和分类 YJA 都实现load 方法:

Xnip2022-07-20_10-55-22.png

前面我们知道分类的加载必然会调用 attachCategories(准备分类数据,然后调用 attachLists 添加到主类列表)方法,我们在 attachCategories 中加入断点,bt 查看调用栈,即可知。运行源码,输出日志:

Xnip2022-07-20_10-59-02.png

总结:

  • 非懒类加载流程

    • map_images --> 
    • map_images_nolock --> 
    • _read_images --> 
    • realizeClassWithoutSwift --> 
    • methodizeClass --> 
    • attachToClass
  • 非懒分类加载流程

    • load_images --> 
    • loadAllCategories --> 
    • load_categories_nolock --> 
    • attachCategories --> 
    • attachLists

2.2 非懒类+懒分类

主类 YJPerson 实现 load 方法,分类 YJA 不实现load 方法:

Xnip2022-07-20_11-00-55.png

运行源码:

Xnip2022-07-20_11-03-04.png

没有调用 attachCategories 方法,直接就结束了。。。 既然没有动态加载,那么是不是在编译期呢,在加载类的时候,去获取ro中的方法列表,看看有没有分类的数据

Xnip2022-07-20_11-23-34.png

ro中不仅有主类的方法,同时还有分类的方法。ro是在编译期就确定的

总结:

  • 非懒类: 还是走的 map_images 流程
  • 懒分类 懒分类的数据在编译期就已经合并到了主类中,且分类的数据也是放在主类的方法前面

2.3 懒类+懒分类

主类 YJPerson 和分类 YJA 都不实现load 方法:

Xnip2022-07-20_11-50-38.png

运行源码,输出:

Xnip2022-07-20_11-54-30.png

直接走到了 mian 函数,继续下一步:

Xnip2022-07-20_11-58-05.png

断在了 realizeClassWithoutSwift,查看调用栈信息

Xnip2022-07-20_12-01-15.png

确实是我们调用的 alloc 方法

堆栈信息显示是通过 消息慢速查找流程 调用了realizeClassWithoutSwift方法。。。 有些信息明明之间关联起来了。。。

那么分类的数据是在什么时候加载的呢?很明显是在编译时。验证下

Xnip2022-07-20_12-05-49.png

总结:

  • 懒类的流程是第一次发消息的时会进行类的加载,
  • 懒分类的数据是在编译时就合并到ro

2.4 懒类+非懒分类

2.4.1 懒类 + 一个非懒分类

主类不实现 load 方法,分类实现 load 方法

Xnip2022-07-20_11-37-30.png

运行源码,输出:

Xnip2022-07-20_11-38-24.png

这个不是和 2.2 非懒类+懒分类 一样么?断住类的加载,打印 ro 看一下:

Xnip2022-07-20_11-41-33.png

果然和 2.2 非懒类+懒分类 一样,非懒分类 强制把 懒类非懒类来加载,然后将 非懒分类的数据合并到了主类中

总结: 参考2.2 非懒类+懒分类

2.4.2 懒类+多个非懒分类

主类不实现 load 方法,分类 YJAYJB 实现 load 方法

Xnip2022-07-20_13-18-29.png

运行源码,输出:

Xnip2022-07-20_13-36-06.png

打印的信息分析:主类的加载没有走map_images流程,调用两次load_categories_nolock说明是有两个分类,但是最后没有走attachCategories方法,而是走realizeClassWithoutSwift加载主类,然后调用attachCategories流程。在整个流程中需要解决两个问题

  • 分类加载过程中没有走attachCategories方法,那么它的流程是什么?
  • 怎么调用到realizeClassWithoutSwift流程的

load_categories_nolock添加断点,运行源码

Xnip2022-07-20_13-39-35.png

cls如果初始化则走attachCategories方法,如果没有则走unattachedCategories.addForClass方法。进入addForClass方法

void addForClass(locstamped_category_t lc, Class cls)
{
    runtimeLock.assertLocked();
    if (slowpath(PrintConnecting)) {
        _objc_inform("CLASS: found category %c%s(%s)",
                     cls->isMetaClassMaybeUnrealized() ? '+' : '-',
                     cls->nameForLogging(), lc.cat->name);
    }
    // 先到哈希表中的去查找又没有lc
    auto result = get().try_emplace(cls, lc);
    // 如果有 判断result.second 是否有数据,没有将lc存入result.second
    if (!result.second) {
        result.first->second.append(lc);
    }
}
  • 首先到哈希表中根据keycls)是查找lclc是系统底层统一封装的数据类型
  • 如果表中有lc,判断lc.second是否有数据,如果没有则赋值
  • 注意现在的分类数据直接存在哈希表中和类现在没有关系。这点很重要

再探究下怎么去加载类的在realizeClassWithoutSwift添加断点,运行源码

Xnip2022-07-20_13-48-57.png

堆栈信息显示是load_images --> prepare_load_methods -->realizeClassWithoutSwift 探究下prepare_load_methods的具体实现

void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;

    runtimeLock.assertLocked();
    // 从macho中获取类的非懒加载列表
    classref_t const *classlist = 
    _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        // 将重新映射的类添加到load列表中
        schedule_class_load(remapClass(classlist[i]));
    }
    // 从macho中获取非懒加载类的列表
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        if (!cls) continue;  // category for ignored weak-linked class
        if (cls->isSwiftStable()) {
            _objc_fatal("Swift class extensions and categories on Swift "
                        "classes are not allowed to have +load methods");
        }
        // 类的加载
        realizeClassWithoutSwift(cls, nil);
        ASSERT(cls->ISA()->isRealized());
        // 将分类添加到分类的load列表中
        add_category_to_loadable_list(cat);
    }
}
  • macho中获取非懒加载类的列表
  • 将重新映射的类添加到类的load列表中
  • macho中获取非懒加载分类列表
  • 类的初始化加载
  • 将分类添加到分类的load列表中

探究下schedule_class_load

static void schedule_class_load(Class cls)
{
    if (!cls) return;
    ASSERT(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;
    // 递归`cls`的父类
    // Ensure superclass-first ordering
    schedule_class_load(cls->getSuperclass());
    // 将类还有父类都添加到load表中
    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); `IMP`
}

add_category_to_loadable_list和 add_class_to_loadable_list基本一样

{
    ...// 省略部分代码
    // 获取类中load方法的IMP
    method = cls->getLoadMethod();
    // 类的load表
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
    ... 

    ... 
    // 获取分类中load方法的IMP
    method = _category_getLoadMethod(cat);
    // 分类的load表
    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
    ... 
} 

add_category_to_loadable_listadd_class_to_loadable_list保存的是封装的类型,这个类型有两个变量一个是cls保存类,一个是method保存的是load方法的IMP

探究realizeClassWithoutSwift后续的流程,根据打印的消息是流程是realizeClassWithoutSwift --> attachToClass --> attachCategories

void attachToClass(Class cls, Class previously, int flags)
{
runtimeLock.assertLocked();
ASSERT((flags & ATTACH_CLASS) ||
       (flags & ATTACH_METACLASS) ||
       (flags & ATTACH_CLASS_AND_METACLASS));
auto &map = get();

// 在哈希表中key = previously(cls)对应的一张表
// 这张表中存放者分类的数据
auto it = map.find(previously);
// 如果it和表中的最后一个数据不相等进入判断,最后一个算是标识位
if (it != map.end()) {
    // 获取分类的数据list的地址
    category_list &list = it->second;
    if (flags & ATTACH_CLASS_AND_METACLASS) {
        // 给元类类加载分类数据
        int otherFlags = flags & ~ATTACH_CLASS_AND_METACLASS;
        attachCategories(cls, list.array(), list.count(), otherFlags | ATTACH_CLASS);
        attachCategories(cls->ISA(), list.array(), list.count(), otherFlags | ATTACH_METACLASS);
    } else {
        // 给类加载分类数据
        attachCategories(cls, list.array(), list.count(), flags);
    }
    // 释放表中cls对应表的数据
    map.erase(it);
}

attachToClass中断点调试

Xnip2022-07-20_13-55-57.png

it != map.end()成立进入判断流程,只要哈希表中存分类的数据条件就成立,下面探究下it中到底在哪里存了分类的数据

Xnip2022-07-20_14-07-21.png

分类的数据存在了底层的表中,当需要把分类的数据加载到主类的时候,就从表中获取,加载完以后清空对应表中分类的数据