在上一篇OC底层探索17 - 类的加载(上)中对类的名称、data、方法、属性、协议的注入完成了分析。还留下了一个问题就是类中分类的加载
二、 分类的加载
书接上文,在methodizeClass中发现了attachToClass这个方法中对分类方法进行了处理。
前提:
@implementation HRTest
@property(nonatomic, copy)NSString *name;
-(void)sayHappy;
+(void)load{ }
@end
@implementation HRTest (cate1)
-(void)sayHappy;
-(void)sayHappyCate1;
+(void)load{ }
@end
- 类和分类中都实现了
+load方法,后续会用到。 
1、分类的加载时机
static void methodizeClass(Class cls, Class previously)
{
    ...
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
}
void attachToClass(Class cls, Class previously, int flags)
{
    auto &map = get();
    auto it = map.find(previously);
    if (it != map.end()) {
        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);
        }
        map.erase(it);
    }
}
- 
根据断点调试发现,就发现该方法没有被调用过。可是根据观察这个方法attachCategories就是完成分类的加载的,所以在attachCategories增加断点查看。 

- 通过堆栈可以看到执行流程:
load_images-loadAllCategories-load_categories_nolock-attachCategories。 
所有分类的加载是在map_images之后的load_images里被调起的,真的是这样吗?记得在文章的开始有提过一个前提,是类、分类中都实现了+load方法,如果没有实现这个方法呢?
1.1 类、分类都不实现+load
我们知道如果类中不实现load方法,则该类是一个懒加载类,类的加载时机推迟到第一次消息调用。那个分类的加载时机是什么时候呢?

- 断点设置在
methodizeClass,因为attachCategories不会被调用; - 堆栈信息看到起点是在
类第一次消息发送时; - 在类从mach-o中读出
ro时,类、分类的方法都已经保存在ro里了; 
1.2 类、分类的4类情况
| 类 | 分类 | 类加载情况 | 分类加载情况 | 
|---|---|---|---|
| load | load | 类在map_image加载 | 分类在load_image加载 | 
| load | 类在map_image加载 | 分类方法已经通过mach-o读取到ro里 | |
| load | 类被标记为非懒加载类,在map_image加载 | 分类方法已经通过mach-o读取到ro里 | |
| 类在第一次消息转发时加载 | 分类方法已经通过mach-o读取到ro里 | 
2、分类的加载
static void load_categories_nolock(header_info *hi) {
    size_t count;
    //所有分类进行循环
    auto processCatlist = [&](category_t * const *catlist) {
        for (unsigned i = 0; i < count; i++) {
            if (cls->isRealized()) {
                attachCategories(cls, &lc, 1, ATTACH_EXISTING);
            }
        }
    ...
}
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count...) {
    // 脏内存是wwdc2020提出的一种类的内存优化
    // extAllocIfNeeded 调用该方法之后才生成rwe。
    // 分类、addmethod、addprotocol、addproperty四种情况下才会产生rwe脏内存
    auto rwe = cls->data()->extAllocIfNeeded();
    
    //根据调试cats_count = 1,该循环只执行一次,该类的其他分类是通过
    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号位置,猜测方便查询
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        // 属性、协议的方法注入就省略了
        ....
    }
    if (mcount > 0) {
        //依旧进行排序
        prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
                           NO, fromBundle, __func__);
        //插入方法列表
        rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
    }
}
- 通过该方法完成分类方法、属性、协议的
获取、排序、插入; - 分类中属性是不自动生成
set、get方法; 
3、分类方法的插入
在OC底层探索17 - 类的加载(上)已经提到过该方法的一种情况,事实上该方法有3种情况.
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            //数组进行扩容
            array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;
            //旧数组元素从后往前插
            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i];
            //新数组元素从前往后插
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i];
            free(array());
            setArray(newArray);
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            Ptr<List> oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            //数组进行扩容
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            // 把旧数组当做一个元素放到lists最后一位
            if (oldList) array()->lists[addedCount] = oldList;
            // 把新数组从头依次放入
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
        }
    }
- 当分类方法首次注入时会走到
1 list -> many lists这里,这就是导致类中methodsList有时会是一个二维数组的原因。 - 会把分类的新方法放入到新数组的最开头,
所有重写类中的方法并没有被替换,而是插入到了最前方。这就是为什么在方法查找(lookupImp)时从后往前进行查询的。 - 当第二个分类方法进行注入时,将
数组进行扩容,然后把新的方法从头依次插入。 
三、 load_images(非懒加载类)
map_images完成后,还记得在_objc_init - _dyld_objc_notify_register(&map_images, load_images, unmap_image);中的load_images吗?
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        //非懒加载类的分类中实现load方法后,通过该方法完成分类的加载.
        loadAllCategories();
    }
    // Discover load methods
    {
        //准备所有load方法
        prepare_load_methods((const headerType *)mh);
    }
    // Call +load methods (without runtimeLock - re-entrant)
    //调用所有load方法
    call_load_methods();
}
1、prepare_load_methods 准备所有load方法
void prepare_load_methods(const headerType *mhdr)
{
    size_t count, i;
    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }
    //获取所有实现load方法的分类
    
    category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
    //所有分类的load方法都会被加载
    for (i = 0; i < count; i++) {
        category_t *cat = categorylist[i];
        Class cls = remapClass(cat->cls);
        //非懒加载分类迫使主类完成加载
        realizeClassWithoutSwift(cls, nil);
        add_category_to_loadable_list(cat);
    }
}
- 完成了
类的load获取,同时也完成了分类load方法的获取; - 即使类是一个懒加载类,在获取
非懒加载分类的load方法时迫使主类完成加载; - 多个分类的
load方法都会被添加 
1.1 add_class_to_loadable_list(类)
static void schedule_class_load(Class cls)
{
    // Ensure superclass-first ordering
    schedule_class_load(cls->getSuperclass());
}
void add_class_to_loadable_list(Class cls)
{
    IMP method;
    method = cls->getLoadMethod();
    //在数组到达上限时,完成数组的扩容
    if (loadable_classes_used == loadable_classes_allocated) {
        loadable_classes_allocated = loadable_classes_allocated*2 + 16;
        loadable_classes = (struct loadable_class *)
            //数组的扩容
            realloc(loadable_classes,
                              loadable_classes_allocated *
                              sizeof(struct loadable_class));
    }
    //将load方法和对应的类放入loadable_classes
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}
- 将
类和load方法添加到数组loadable_classes中; - 在数组达到上限后再进行扩容操作,尽可能的节省内存;
 
1.2 add_category_to_loadable_list(分类)
void add_category_to_loadable_list(Category cat)
{
    IMP method;
    method = _category_getLoadMethod(cat);
    //扩容
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }
    //将分类load方法加入loadable_categories
    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}
- 方法原理都是一样的;
 - 将分类的
load方法加入loadable_categories 
2、prepare_load_methods 调用所有load方法
void call_load_methods(void)
{
    bool more_categories;
    //当前循环值执行一次
    do {
        // 1. Repeatedly call class +loads until there aren't any more
        //通过完成类所有load方法的调用
        //当前循环值执行一次
        while (loadable_classes_used > 0) {
            call_class_loads();
        }
        // 2. Call category +loads ONCE
        //完成所有分类所有load方法的调用
        more_categories = call_category_loads();
        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);
}
- 
类的load方法和分类的load方法都会被调用,而且是类的load方法先被调用。 
2.1 call_class_loads
static void call_class_loads(void)
{
    int i;
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    //直接设置为0,外层就不会继续循环
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 
        //通过函数指针调用
        (*load_method)(cls, @selector(load));
    }
    //释放class
    if (classes) free(classes);
}
- 
call_category_loads是类似的就是不赘述了。 通过函数指针完成load方法的调用
面试题
题:如果类和分类有同名方法,调用会调用哪个方法?
答:两种情况:
- 如果是
普通方法,则会调用分类中的重名方法 - 如果是
+load方法,则先调用类中的+load,在依次调用分类的load.

 
总结
类的加载-分类的加载-load方法调用后,加载一个类所有的工作都已经完成了,等待后续使用。










