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

2,271 阅读29分钟

前言

前文iOS底层原理之OC类的加载原理(中)已经分析了类的加载,并探索了懒加载类和非懒加载类的不同流程,同时还初步确定了分类加载的两条流程,本文就来详细分析下分类加载的流程,以及分类加载和主类加载之间的联系与区别。

准备工作

一: 分类的加载

前文iOS底层原理之OC类的加载原理(中)分析确定了分类的加载有两条流程:

  1. map_images --> map_images_nolock --> _read_images --> realizeClassWithoutSwift --> methodizeClass --> attachToClass --> attachCategories --> attachLists

  2. load_images --> loadAllCategories --> load_categories_nolock --> attachCategories --> attachLists

但是这两条流程只是我们根据代码推测出来的,今天就来实际分析验证一下到底对不对。

1.1: attachCategories

两条流程最终都会调用到attachCategories函数,加载分类的方法列表、属性列表和协议列表到rwe

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.
     */
    
    // 阈值,如果分类数量超过64个,就先附加到类,然后再存剩下的
    // 从后往前存,63、62、61...2、1、0
    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);
    // 获取或创建rwe,创建完就会将主类的方法、属性、协议添加进rwe
    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) {
            // 如果分类个数超过64个,就把这64个分类的方法列表先排序再附加到主类
            // count设为0,重新存剩下的
            if (mcount == ATTACH_BUFSIZ) {
                // 方法列表修正且排序(按sel地址排序)
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            // 如果 mcount 为 0,那么 ATTACH_BUFSIZ - ++mcount 为 63
            // 存放下标为63 ~ 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;
        }
    }
    
    // 分类总数或剩下的没有超过64个,下面一起附加到类
    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中
        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中
    rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
    
    // 将分类协议添加到rwe的protocols中
    rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
  • 获取或创建rwe,创建完就会将主类的方法、属性、协议添加进rwe
  • 遍历所有分类,获取方法列表,属性列表,协议列表,存储到对应的总列表中,总列表容量64,从后往前存。
  • 如果分类超过64个,就调用attachLists函数先将已获取的方法列表,属性列表,协议列表添加到rwe中,再存剩下的。
  • 遍历完所有分类后, 分类总数或剩下的没有超过64个,调用attachLists函数将获取的方法列表,属性列表,协议列表添加到rwe中。

1.2: attachLists

attachCategories函数获取分类的数据,然后调用attachLists函数将数据附加到主类中,所以核心逻辑就在attachLists函数中。

    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;
        // 如果array()存在进入判断
        if (hasArray()) {
            // many lists -> many lists
            // oldCount = array()->lists的个数
            uint32_t oldCount = array()->count;
            // 新的newCount = 原有的count + 新增的count
            uint32_t newCount = oldCount + addedCount;
            // 根据`newCount`开辟内存,类型是 array_t, array()->lists是一个二维数组
            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();
        }
        // 如果主类中有方法,第一次进来是主类的方法列表
        // 如果主类中没有方法,则进来的就是分类的方法列表
        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,反之为 0
            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`中的list将其存放在array()->lists中  
            // 且是从开始的位置开始存放
            // 因为 attachCategories 函数中已经是倒序插入的,现在顺序插入就可以了
            // 越后面加载的分类,附加到主类时越在前面
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }

attachLists函数分为三种情况:

0 lists -> 1 list

  • addedLists[0]赋值给list

如果主类中有方法,第一次进来是主类的方法列表;如果主类中没有方法,则进来的就是分类的方法列表。属性,协议也是如此。

1 list -> many lists

  • 计算原有的总数,原有list只有一个,所以不是1就是0
  • 新的总数等于原有总数加上新增总数,newCount = oldCount + addedCount
  • 根据新的总数开辟相应的内存,数据类型为array_t,并设置数组setArray
  • 设置数组的总数等于newCount
  • 将原有的list放入数组的末尾,之前只有一个,所以不需要遍历。
  • 遍历获得新增数组中的list,从0号位开始存入数组的lists中。

many lists -> many listsarray()存在才会进入)

  • 计算原有数组的总数。
  • 新的总数等于原有总数加上新增总数,newCount = oldCount + addedCount
  • 根据新的总数开辟相应的内存,数据类型为array_t
  • 设置新数组的总数等于newCount
  • 设置原有数据的总数等于newCount
  • 遍历获得原有数组中的list,存入新数组的lists中,且是从最后开始取和存。
  • 遍历获得新增数组中的list,从0号位开始存入新数组的lists中。
  • 释放原有数组。
  • 设置新的数组。

注意:List* const * addedLists二维指针。就像XJPerson *p = [XJPerson alloc]p一维指针&p就是二维指针。

array_t结构体相关探索:

class list_array_tt {
    struct array_t {
        uint32_t count;
        Ptr<List> lists[0];

        static size_t byteSize(uint32_t count) {
            return sizeof(array_t) + count*sizeof(lists[0]);
        }
        size_t byteSize() {
            return byteSize(count);
        }
    };
    
protected:
    ...
    
private:
    union {
        Ptr<List> list;
        uintptr_t arrayAndFlag;
    };

    bool hasArray() const {
        return arrayAndFlag & 1;
    }

    array_t *array() const {
        return (array_t *)(arrayAndFlag & ~1);
    }

    void setArray(array_t *array) {
        arrayAndFlag = (uintptr_t)array | 1;
    }

    void validate() {
        for (auto cursor = beginLists(), end = endLists(); cursor != end; cursor++)
            cursor->validate();
    }
    
    ...
}
  • setArray函数arrayAndFlag = (uintptr_t)array | 1arrayAndFlag的第0位一定是1
  • hasArray函数arrayAndFlag & 1,如果arrayAndFlag的第0位是1,就返回YES,反之返回NO
  • 所以只要调用了setArray函数之后,释放之前,hasArray函数返回的就是YES
  • 在联合体里面listarrayAndFlag互斥,也就是说有list就无array,有array就无list

1.2.1: attachLists流程图:

attachLists流程图.jpg

1.3: 实例验证attachCategoriesattachLists

前面分析了attachCategoriesattachLists的流程,下面通过实例来进行相关验证(以方法为例)。

创建主类XJPerson和分类XJPerson+XJAXJPerson+XJB

@implementation XJPerson

+ (void)load
{
    NSLog(@"__%s__", __func__);
}

- (void)instanceMethod1
{
    NSLog(@"%s", __func__);
}

- (void)instanceMethod2
{
    NSLog(@"%s", __func__);
}

@end

@implementation XJPerson (XJA)

+ (void)load
{
    NSLog(@"%s", __func__);
}

- (void)xja_instanceMethod1
{
    NSLog(@"%s", __func__);
}

- (void)xja_instanceMethod2
{
    NSLog(@"%s", __func__);
}

@end

@implementation XJPerson (XJB)

+ (void)load
{
    NSLog(@"%s", __func__);
}

- (void)xjb_instanceMethod1
{
    NSLog(@"%s", __func__);
}

- (void)xjb_instanceMethod2
{
    NSLog(@"%s", __func__);
}

@end

1.3.1: attachCategories验证

添加调试代码,运行源码,定位到attachCategories函数进行调试。

image.png

  • 加载的是XJA分类。

接着输出变量mlists查看数据。

image.png

  • mlists最后一个存储的就是XJA分类的方法列表的地址。

mlistsmethod_list_t *[]类型(指针数组),里面存储的是method_list_t *类型数据。

image.png

  • mlists首地址是一个二维指针。
  • mlists + ATTACH_BUFSIZ - mcount就是地址平移,mlists是首地址,ATTACH_BUFSIZ - mcount是具体的第几个位置,获取到的就是一个二维指针。

1.3.2: attachLists验证

验证方式采用动态源码调试macho文件(可执行文件)进行相互验证,所以先介绍下源码是怎么读取macho文件的,源码中遇到了_getObjc2NonlazyClassList_getObjc2NonlazyCategoryList等相关方法,进入查看具体实现。

#define GETSECT(name, type, sectname)                                   \
    type *name(const headerType *mhdr, size_t *outCount) {              
        return getDataSection<type>(mhdr, sectname, nil, outCount);     \ 
    }                                                                   \
    type *name(const header_info *hi, size_t *outCount) {               \
        return getDataSection<type>(hi->mhdr(), sectname, nil, outCount); \
    }

//      function name                 content type     section name
//refs结尾的都是需要修复的类和方法等
GETSECT(_getObjc2SelectorRefs,        **SEL**,          "__objc_selrefs"); 

GETSECT(_getObjc2MessageRefs,         message_ref_t,   "__objc_msgrefs"); 

GETSECT(_getObjc2ClassRefs,           Class,           "__objc_classrefs");

GETSECT(_getObjc2SuperRefs,           Class,           "__objc_superrefs");
//macho section 等于__objc_classlist   所有类的列表(不包括分类)
GETSECT(_getObjc2ClassList,           classref_t **const**,      "__objc_classlist");
//macho section 等于__objc_nlclslist   非懒加载类的列表
GETSECT(_getObjc2NonlazyClassList,    classref_t **const**,      "__objc_nlclslist");
//macho section 等于__objc_catlist     分类的列表
GETSECT(_getObjc2CategoryList,        category_t * **const**,    "__objc_catlist");
//macho section 等于__objc_catlist2    分类的列表
GETSECT(_getObjc2CategoryList2,       category_t * **const**,    "__objc_catlist2");
//macho section 等于__objc_nlcatlist   非懒加载分类
GETSECT(_getObjc2NonlazyCategoryList, category_t * **const**,    "__objc_nlcatlist");
//macho section 等于__objc_protolist   协议列表
GETSECT(_getObjc2ProtocolList,        protocol_t * **const**,    "__objc_protolist");
//macho section 等于__objc_protorefs   协议修复列表
GETSECT(_getObjc2ProtocolRefs,        protocol_t *,    "__objc_protorefs");
//macho section 等于__objc_init_func   __objc_init初始化方法列表
GETSECT(getLibobjcInitializers,       UnsignedInitializer, "__objc_init_func");

__objc_nlclslist等对应macho文件中的section名字,右边就是对应section的数据。

image.png

1.3.2.1: 0 lists -> 1 list

前文分析过extAlloc函数在创建完rwe之后,如果主类有方法,就会调用attachLists函数将其添加到rwe对应的列表中(属性和协议也是如此,本文以方法为例),如果没有就什么也不做。那么此时就会出现主类有方法主类无方法的两种情况(默认分类都有方法)。

1.3.2.1.1: 主类有方法

运行源码定位到attachCategories函数获取rwe的相关位置。

image.png

进入extAllocIfNeeded函数获取或创建rwe

image.png

因为此时还没有rwe,就会进入extAlloc函数创建rwe

image.png

  • 此时主类有方法,list有值,所以调用attachLists函数将主类方法添加到rwe相应的列表中。

image.png

  • 此时rwe刚刚创建,listarray都为空,所以进入了0 lists -> 1 list的分支。
  • lldb调试显示此时添加的是主类的方法列表。
1.3.2.1.2: 主类无方法

注释掉主类的方法,重复上面的调试步骤,进入extAlloc函数。

image.png

  • 此时主类无方法,listNULL,所以不会调用attachLists函数。

继续调试,回到attachCategories函数,lldb查看cats_list相关信息。

image.png

  • 此时加载的是被包装成locstamped_category_t *类型的分类XJA

继续往下调试。

image.png

  • 分类数据读取完之后调用attachLists函数添加到rwe中(此文只分析方法,属性、协议类似)。

image.png

  • 主类无方法,此时listarray都为空,所以分类加载进入了0 lists -> 1 list的分支。
  • lldb调试显示此时添加的是分类XJA的方法列表。

现在有XJAXJB两个分类,为什么先加载的是XJA而不是XJB呢?

这是由分类文件的编译顺序决定的,按编译的先后顺序进行加载。

image.png

image.png

如果将分类XJB放到分类XJA的前面,那么XJB就会先编译先加载。

1.3.2.1.3: 0 lists -> 1 list总结

0 lists -> 1 list分支分为两种情况(默认分类有方法):

  • 主类有方法:创建完rwe之后就将主类方法添加进去。
  • 主类无方法:第一个添加进rwe的是第一个加载的分类。

1.3.2.2: 1 list -> many lists

重新将主类方法添加回去,并将分类XJB放到XJA前面(验证加载顺序),运行源码。

image.png

  • 将分类XJB放到XJA前面,就会先加载XJB
  • 读取完分类XJB数据之后调用attachLists函数添加到rwe中(此文只分析方法,属性、协议类似)。

image.png

  • list是主类方法列表指针,addedLists中存放的是分类XJB方法列表指针。

合并完之后继续查看。

image.png

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

1.3.2.3: many lists -> many lists

继续调试加载分类XJA,进入many lists -> many lists分支。

image.png

  • 读取完分类XJA数据之后调用attachLists函数添加到rwe中(此文只分析方法,属性、协议类似)。

image.png

  • addedLists中存放的是分类XJA方法列表指针。

合并完之后继续查看。

image.png

  • 合并完之后newArray->lists中存放了3个方法列表地址,从前往后依次是分类XJA分类XJB主类。主类在lists最后面,最后编译的分类在lists最前面。

二: 类与分类搭配加载情况

前面分析过类的加载根据是否实现+load方法分为懒加载类和非懒加载类,那么分类的加载是否与+load方法有关呢,同时类与分类搭配加载又会有那些情况呢?下面就通过几种搭配方式来探索一下:

  1. 类和分类都实现+load方法。
  2. 类实现+load方法,分类不实现。
  3. 类和分类都不实现+load方法。
  4. 类不实现,分类实现+load方法。
  5. 多分类

为了方便跟踪,在前面分析出的两条路线的关键函数中添加调试代码和断点。

image.png

2.1: 类和分类都实现+load方法

非懒加载类的数据加载是通过_getObjc2NonlazyClassList函数从macho文件获取,非懒加载分类的数据加载是通过_getObjc2NonlazyCategoryList函数从macho文件获取。

非懒加载类读取数据示意图:

image.png image.png

非懒加载分类读取数据示意图:

image.png image.png

  • 非懒加载类加载流程:map_images -> map_images_nolock -> _read_images -> realizeClassWithoutSwift -> methodizeClass -> attachToClass
  • 非懒加载分类加载流程:load_images -> loadAllCategories -> load_categories_nolock -> attachCategories -> attachLists

日志打印流程:

image.png

此时类为非懒加载类,在realizeClassWithoutSwift函数中查看ro数据。

image.png

  • 此时只有主类的方法,没有分类的方法。

主类加载完之后,调用load_images函数。

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // didInitialAttachCategories 控制只来一次。  
    // didCallDyldNotifyRegister 在 _objc_init 中注册完回调之后赋值为true
    if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
        didInitialAttachCategories = true;
        // 加载所有分类
        loadAllCategories();
    }
    ……
}
  • didInitialAttachCategories控制只执行一次loadAllCategories(因为load_images会执行多次),didCallDyldNotifyRegister_objc_init函数中注册完回调之后赋值为true

2.1.1: load_categories_nolock

loadAllCategories函数中根据header_info循环调用load_categories_nolock函数,核心代码如下:

static void load_categories_nolock(header_info *hi) {
    bool hasClassProperties = hi->info()->hasCategoryClassProperties();

    size_t count;
    // 声明并实现函数 processCatlist 
    auto processCatlist = [&](category_t * const *catlist) {
        for (unsigned i = 0; i < count; i++) { // 分类数量
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
            // 将cat和hi包装成 locstamped_category_t
            locstamped_category_t lc{cat, hi};
   ……
            // Process this category.
            if (cls->isStubClass()) {
         ……
            } else {
                // 实例方法/协议/实例属性
                if (cat->instanceMethods ||  cat->protocols
                    ||  cat->instanceProperties)
                {
                    if (cls->isRealized()) { // 非懒加载类
                        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
                    } else { // 懒加载类
                        objc::unattachedCategories.addForClass(lc, cls);
                    }
                }
                
                // 类方法/协议/类属性
                if (cat->classMethods  ||  cat->protocols
                    ||  (hasClassProperties && cat->_classProperties))
                {
                    if (cls->ISA()->isRealized()) { // 非懒加载类
                        attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);
                    } else { // 懒加载类
                        objc::unattachedCategories.addForClass(lc, cls->ISA());
                    }
                }
            }
        }
    };
    // 调用 processCatlist
    // 加载分类 __objc_catlist,count从macho中读取。
    processCatlist(hi->catlist(&count));
    //__objc_catlist2
    processCatlist(hi->catlist2(&count));
}
  • 声明并实现processCatlist函数。
  • 调用processCatlist函数,读取macho文件名为__objc_catlist__objc_catlist2section,获取分类信息(有共享缓存的情况下才会生成__objc_catlist2)。
  • processCatlist函数根据读取的分类信息循环处理分类。
  • 此时非懒加载分类是一个一个加载的,所以attachCategories函数传值的count都是1

lldb查看分类信息:

image.png

attachCategories函数和attachLists函数在1.11.2小节已经分析过了,此处就不再赘述了。

更改实现+load方法的分类数量,会发现只要有一个分类实现了+load方法,所有分类加载都走非懒加载分类的加载流程,而不会合并到主类ro中。

结论:类实现了+load方法,分类只要有一个实现+load方法,所有分类都不合并到主类的ro中,而是走非懒加载分类流程合并到rwe中。

2.2: 类实现+load方法,分类不实现

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

日志打印流程:

image.png

  • 非懒加载类还是走的map_images流程。
  • 懒加载分类没有走attachCategories流程。

那么分类数据是什么时候加载的呢?

image.png

  • macho文件中分类列表是没有数据的,说明分类数据不是动态时加载的。

不是动态时加载,最有可能就是在编译器,那么就在realizeClassWithoutSwift函数加载类的时候,获取ro数据,看看有没有分类相关的数据。

image.png

  • ro中不仅有主类的方法,还有分类的方法。

ro是在编译期就确定的,也就是说懒加载分类中的数据在编译期就已经合并到了主类中。而且分类的方法也是放在主类的方法前面。

2.3: 类和分类都不实现+load方法

日志打印流程:

image.png

打印信息显示类加载流程和分类加载流程都没有走,查看macho文件。

image.png

  • macho文件中没有非懒加载类和非懒加载分类的section,分类列表也没有数据。

main函数里实例化XJPerson并调用实例方法,调试进入加载类的realizeClassWithoutSwift函数中(消息慢速查找流程进入),查看函数调用栈。

image.png

  • 函数调用栈显示是消息慢速查找流程调用了realizeClassWithoutSwift函数,这里就与前面分析过的消息慢速查找流程关联起来了。

lldb查看ro数据:

image.png

  • 根据lldb输出ro数据,可以发现分类的数据也是在编译时就合并到ro中了。

懒加载类的加载流程是推迟到第一次消息发送的时候了,而懒加载分类的数据是在编译时就合并到ro中了。

2.4: 类不实现,分类实现+load方法

这种情况比较复杂,因为非懒加载类的个数会对整个加载流程有影响。

2.4.1: 类不实现,一个分类实现+load方法

运行源码,查看日志打印流程:

image.png

  • 这种情况的流程和类实现+load方法,分类不实现是一样的。

非懒加载分类强制把懒加载类变成非懒加载类了(类被迫营业了),而且非懒加载分类的数据也合并到了主类中。

image.png

  • XJPerson类被强制变成非懒加载类了。

image.png

  • 分类列表没有数据,已经在编译时合并到ro中了。

运行源码,断点停在realizeClassWithoutSwift函数后,lldb查看ro数据:

image.png

  • 懒加载类被强制变成非懒加载类了,而且分类的数据也在编译期间就合并到ro中了。

2.4.2: 类不实现,多个分类实现+load方法

重新添加分类XJB,运行源码,查看日志打印流程:

image.png

  • 主类加载没有走map_images流程。
  • 调用两次load_categories_nolock函数加载两个分类后,没有调用attachCategories函数,而是调用了realizeClassWithoutSwift函数加载主类,然后再调用attachCategories函数。

那么现在就有了两个问题:

  1. 两个分类加载完之后为什么没有走attachCategories函数?
  2. 怎么调用到realizeClassWithoutSwift函数的?

image.png image.png

  • macho文件里分类列表和非懒加载分类列表有分类XJAXJB的数据,但是没有非懒加载类列表。

打开load_categories_nolock函数里调试代码处的断点,运行源码,查看调用两次后的逻辑。

image.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);
    }
}
  • 首先到哈希表中根据clskey去查找lc,没有就插入。
  • 判断lc.second是否有数据,如果没有则赋值。

此时分类数据只是以clskey存储在哈希表中,还没有加载到rwe中。

接下来探索类是怎么加载的,打开realizeClassWithoutSwift函数里调试代码处的断点,继续往下调试。

image.png

  • 函数调用栈显示在调用流程为:load_images -> prepare_load_methods -> realizeClassWithoutSwift

在函数调用栈选中load_images看看prepare_load_methods是怎么调用的。

image.png

  • loadAllCategories函数走完之后,调用hasLoadMethods函数判断是否有+load方法,没有就返回,有就调用prepare_load_methods函数。
bool hasLoadMethods(const headerType *mhdr)
{
    size_t count;
    if (_getObjc2NonlazyClassList(mhdr, &count)  &&  count > 0) return true;
    if (_getObjc2NonlazyCategoryList(mhdr, &count)  &&  count > 0) return true;
    return false;
}
  • 调用_getObjc2NonlazyClassList函数读取macho文件名为__objc_nlclslistsection的数据,如果有,返回true
  • 调用_getObjc2NonlazyCategoryList函数读取macho文件名为__objc_nlcatlistsection的数据,如果有,返回true
  • 都没有就返回false

此时分类XJAXJB都实现了+load方法,所以返回true,接着调用了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++) {
    // 将重新映射的类添加到 loadable_classes 列表中
    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());
    // 将分类添加到分类的loadable_categories列表中
    add_category_to_loadable_list(cat);
  }
}
  • macho中获取非懒加载类列表。
  • 调用schedule_class_load函数,将重新映射的类添加到loadable_classes列表中。
  • macho中获取非懒加载分类列表。
  • 重新映射分类的类。
  • 调用realizeClassWithoutSwift函数加载类。
  • 调用add_category_to_loadable_list函数,将分类添加到分类的loadable_categories列表中。

查看下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_class_to_loadable_list函数。

add_class_to_loadable_list函数和add_category_to_loadable_list函数逻辑差不多。

static struct loadable_class *loadable_classes = nil;
static int loadable_classes_used = 0;
static int loadable_classes_allocated = 0;

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

void add_class_to_loadable_list(Class cls)
{
    IMP method;
    
    loadMethodLock.assertLocked();
    
    // 获取load方法
    method = cls->getLoadMethod();
    if (!method) return// Don't bother if cls has no +load method
    ...
    // 扩容,第一次为16
    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));
    }
    存储
    loadable_classes[loadable_classes_used].cls = cls;
    loadable_classes[loadable_classes_used].method = method;
    loadable_classes_used++;
}

static struct loadable_category *loadable_categories = nil;
static int loadable_categories_used = 0;
static int loadable_categories_allocated = 0;

struct loadable_category {
    Category cat;  // may be nil
    IMP method;
};

void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    ...
    // 扩容,第一次为16
    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));
    }
    // 存储
    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}
  • 将类或者分类和他们的+load方法的IMP一起存入列表中。

继续调试,realizeClassWithoutSwift后面的流程为realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories -> attachLists

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();
    // 以previously(cls)为key去哈希表中查找在addForClass函数中插入的分类数据
    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);
    }
}
  • previously(cls)key去哈希表中查找在addForClass函数中插入的分类数据。
  • 获取到的数据和表中end标记不相等,就调用attachCategories函数(此时传入的分类数量cats_count为所有分类的数量),然后擦除哈希表中cls对应的分类数据。

lldb调试查看分类数据:

image.png

  • it != map.end()条件成立,调用attachCategories函数。

调用流程:load_images -> loadAllCategories -> load_categories_nolock -> addForClass -> prepare_load_methods -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories -> attachLists

结论:主类不实现+load方法,至少2个分类实现+load方法,则在load_images函数的loadAllCategories流程走完之后,接着走load_images函数里的prepare_load_methods流程强制让主类加载,然后将分类加入到rwe

2.5: 小结

  • 类实现+load方法(非懒加载类):

    • 至少一个分类实现+load方法,会在load_images过程中通过loadAllCategories将所有分类数据加入rwe中,不会合并进主类ro

    • 所有分类不实现load会将分类的方法合并到类的ro中。

  • 类不实现+load方法:

    • 只有一个分类实现+load方法,分类方法会被合并到类的ro中(由于合并了ro,类被强制变成非懒加载类了)。

    • 至少两个分类实现+load方法。分类方法不会被合并进主类ro中,在load_images函数的loadAllCategories流程走完之后,接着走load_images函数里的prepare_load_methods流程强制让主类加载,然后将分类加入到rwe中。(由于没有合并ro,类本身是懒加载类。分类导致它被加载)。

  • 类和分类都不实现+load方法,所有分类方法会被合并进类的ro中,在第一次消息发送的时候加载(类是懒加载类)。

  • 类本身是非懒加载类或者子类是非懒加载类是在map_images过程中实例化的。

  • 类本身是懒加载类,由于自身或者子类的非懒加载分类导致的类被实例化是在load_images函数里的prepare_load_methods流程中的。(ro合并的情况如果分类有+load方法会导致类变为非懒加载类)。

  • 本质上只有类自身实现+load方法才是非懒加载类。其它情况都是被迫,本质上不属于非懒加载类。

  • 类与分类方法列表会不会合并,取决于+load方法的总个数。只有一个或者没有则会合并,否则不合并。

  • 空的分类不会被加载。

⚠️为什么类和分类实现+load方法加起来大于等于2个之后,分类就不会在编译时合并到ro中了呢?

因为所有+load方法在load_images函数中都是要被调用的,每个类或分类的+load方法可能都做了自己相应的操作,不能暴力的将它们合并到一起。所以实际开发中,尽量少实现+load方法。实现过多+load方法需要单独调用,会导致分类不会合并到ro,而且类和分类的加载流程也提前到了程序启动阶段,会导致程序启动变慢。

三: 类中同名方法的查找

3.1: 方法查找流程分析

消息慢速查找流程lookUpImpOrForward函数中调用getMethodNoSuper_nolock函数查找方法。

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    ...
    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
            ...
        } else {
            // curClass method list.
            // 在类的方法列表中查找方法(采用二分查找算法),如果找到,则返回,将方法缓存到cache中
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            ...
        }
        ...
    }
    ...
}

3.1.1: getMethodNoSuper_nolock

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    ……    
    // 获取methods
    auto const methods = cls->data()->methods();
    // 循环,这个时候找的是methodlist存的是method_list_t,有可能是二维数据。动态加载方法和类导致的
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         
         // 没到最后一个,就地址平移 1
         mlists != end;
         ++mlists)
    {
        //查找方法
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}
  • 获取clsmethods(二维指针)。
  • 遍历methods,获取里面存储的method_list_t *类型的方法列表mlists,调用search_method_list_inline函数。
  • beginLists开始遍历,相当于从数组最开始遍历。也就是后加载的分类会被先查找。
const Ptr<List>* beginLists() const {
    if (hasArray()) {
        // 这里不是返回的array,直接返回的lists数组首地址。
        return array()->lists;
    } else {
        // 这里进行了&地址操作,相当于包装了一层,* -> **。
        return &list;
    }
}

const Ptr<List>* endLists() const {
    if (hasArray()) {
        return array()->lists + array()->count;
    } else if (list) {
        return &list + 1;
    } else {
        return &list;
    }
}
  • 直接判断是否存在hasArray从而返回array()->lists&list
  • &list相当于直接包装了一层(* -> **),所以在for循环中就是同一个数据类型的调用了。

3.1.2: 方法列表结构验证:

3.1.2.1: ro合并(此处采用2.2小节)的情况:

image.png

  • 没有array&list包装一层(* -> **),以便在getMethodNoSuper_nolock函数里的for循环中数据类型统一。
  • ro合并了,所以只有list,没有array

image.png

  • ro合并了,所有方法都在list中。

3.1.2.2: ro不合并(此处采用2.1小节)的情况:

image.png

  • ro不合并,所以有array,里面存放类、分类的方法列表指针。

image.png

  • 分类XJB后加载,放在分类XJA前面,先查找。最后加载的分类放在array最前面,最先查找,主类如果有数据,放在最后面。

3.1.3: findMethodInSortedMethodList

getMethodNoSuper_nolock之后的调用流程: getMethodNoSuper_nolock -> search_method_list_inline -> findMethodInSortedMethodList -> findMethodInSortedMethodList

findMethodInSortedMethodList函数的核心是使用二分查找法查找方法。

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    auto first = list->begin(); // 第一个method的位置
    auto base = first;
    decltype(first) probe;
    
    // 将key直接转换成uintptr_t值,因为修复过后的method_list_t中的元素是排过序的
    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    // count = 数组的个数,count >> 1 相当于 count / 2 取整
    // count >>= 1 = (count = count >> 1) = (count = count / 2)
    /*
    案例一:例如 count = 8  需要查找的sel的index为2
     1(第一次).count = 8
     2(第二次).count = (8 >>= 1) = 4
    */
    /*
    案例二:例如 count = 8  需要查找的sel的index为7
     1(第一次).count = 8
     2(第二次).count = (7 >>= 1) = 3(下面count--了)
     3(第三次).count = (2 >>= 1) = 1(下面count--了)
    */
    for (count = list->count; count != 0; count >>= 1) {
        // 内存平移,获取探查值(中间值)
        /*
        案例一:
         1.probe = 首地址(0) + (count / 2) = 0 + 4
         2.prebe = 0 + (4 / 2) = 2
        */
        /*
        案例二:
         1.probe = 首地址(0) + (count / 2) = 0 + 4
         2.probe = 5 + (3 / 2) = 6
         3.probe = 7 + (1 / 2) = 7
        */
        probe = base + (count >> 1);
        
        // 获取探查的sel的uintptr_t值
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        /*
        案例一:
         1.key = 2,probe = 4,不相等
         2.key = 2,prebe = 2,相等,返回method_t *
        */
        /*
        案例二:
         1.key = 7,probe = 4,不相等
         2.key = 7,prebe = 6,不相等
         3.key = 7,probe = 7,相等,返回method_t *
        */
        if (keyValue == probeValue) { // 如果目标sel的uintptr_t值和探查sel的uintptr_t值匹配成功
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            
            // 探查值不是第一个 && 上一个sel的uintptr_t值也等于keyValue
            // 说明有分类同名方法覆盖,获取分类的方法,多个分类最后编译的在最前面
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            // 返回
            return &*probe;
        }
        
        // 如果keyValue > 探查值
        /*
         案例一:
         1. 2 不大于 4,不进入,继续循环
        */
        /*
         案例二:
         1. 7 大于 4,进入
         2. 7 大于 6,进入
        */
        if (keyValue > probeValue) { 
            /*
             案例二:
             1.base = 4 + 1 = 5,count-- = 8-- = 7
             2.base = 6 + 1 = 7,count-- = 3-- = 2
            */
            base = probe + 1;
            count--;
        }
    }
    
    return nil; // 查询完没找到就返回nil
}
  • while (probe > first && keyValue == (uintptr_t)getName((probe - 1)))逻辑就是为了分类和主类有同名方法,在编译阶段合并到ro中,获取的时候需要获取到最后编译的分类而做的处理。

这就是分类会覆盖主类同名方法的原因。

3.1.4: 二分查找验证

在主类XJPerson和分类XJAXJB中都添加并实现方法- (void)sameMethod

3.1.4.1: ro合并的情况

此处依然采用2.2小节的情况,仅主类实现+load方法,并且在main函数中调用sameMethod方法。

运行源码,首先在类加载过程的realizeClassWithoutSwift函数验证ro数据:

image.png

  • 主类和分类的方法都合并到ro中了。

但是在方法查找的时候同名方法应该是放在一起的,目前还没有在一起,在查看下修正、排序前后的对比:

sel修正前后: image.png

方法排序前后:

image.png

  • 经过修正排序后,同名方法确实放到了一起。

这也就是为什么在二分查找函数findMethodInSortedMethodList内部会在找到方法后继续往前找的原因了。

那么按照前面分析的,方法查找的时候最后面加载的分类XJBsameMethod方法就应该在最前面。

image.png

  • 在合并ro的情况下,类和分类的同名方法,类的放在最后面,越后加载的分类放在越前面。

3.1.4.2: ro不合并的情况

此处依然采用2.1小节的情况,主类和任一分类实现+load方法,并且在main函数中调用sameMethod方法。

image.png

  • 由于不合并ro,现在的方法列表就有array,是一个二维指针,所以首先查找的是分类XJB的方法列表。

继续运行到二分查找函数findMethodInSortedMethodList验证。

image.png

  • 找到对应的sameMethod方法就返回了,没有走while流程。

四:initialize流程分析

经过类的加载原理这三篇文章的分析,我们知道了+load方法是在load_images流程调用的,那么+initialize方法是在什么时候调用的呢?下面就一起来探索下。

我们都知道+initialize方法是在第一次调用方法之前调用的,那么就新建一个非源码工程,并添加XJPerson类,同时给XJPerson类添加+initialize方法,然后在main函数实例化XJPerson,并调用方法。

image.png

  • +initialize方法是被消息慢速查找lookUpImpOrForward流程调用的。

+initialize方法被调用前一步的汇编里发现了与objc源码相关的内容。

image.png

源码里全局搜索CALLING_SOME_+initialize_METHOD字段,定位到了callInitialize函数。

image.png

接着全局搜索callInitialize函数,发现只有initializeNonMetaClass函数调用了。

image.png

继续全局搜索initializeNonMetaClass函数,找到调用者initializeAndMaybeRelock函数。

image.png

继续搜索initializeAndMaybeRelock函数,找到调用者initializeAndLeaveLocked函数。

image.png

继续搜索initializeAndLeaveLocked函数,找到调用者realizeAndInitializeIfNeeded_locked函数。

image.png

继续搜索realizeAndInitializeIfNeeded_locked函数,找到调用者lookUpImpOrForward函数。

image.png

这样就一直找到了消息慢速查找流程了。说明了+initialize方法,确实是在第一次消息发送的时候调用的。

+initialize方法调用流程:

  • lookUpImpOrForward -> realizeAndInitializeIfNeeded_locked -> initializeAndLeaveLocked -> initializeAndMaybeRelock -> initializeNonMetaClass -> callInitialize -> initialize

+initialize方法在第一次消息发送的时候才调用,所以并不会影响类和分类的加载情况。

五: 总结

  • 类与分类的合并:取决于+load方法的实现总个数是否存在多个(+initialize不影响)(因为所有+load方法在load_images函数中都是要被调用的,每个类或分类的+load方法可能都做了自己相应的操作,不能暴力的将它们合并到一起)。

    • 合并:0/1+load方法实现,分类方法列表会被合并进主类ro中,后编译的分类同名方法在前。

      • 0+load方法实现,类为懒加载类。
      • 1+load方法实现,类为非懒加载类(由于合并,谁实现load已经无所谓了)。
    • 不合并:2个及以上+load。分类的方法列表会被加载到rwe中。

      • 主类实现+loadload_images过程中通过loadAllCategories将分类数据加载到rwe中。
      • 主类没有实现+loadload_images过程中通过prepare_load_methods流程最终实例化类和加载分类方法到rwe中。
  • 类的实例化(加载)

    • 分类或者子类的分类(+load)导致类被实例化是在load_images过程中。类本身是懒加载类,被迫实例化。
    • 子类或者类的+load方法导致类被实例化是在map_images中。
    • 其它情况类为懒加载类,在消息慢速查找lookUpImpOrForward流程中实例化。
  • 类的懒加载&非懒加载

    • 懒加载:类、子类、分类、子类分类没有实现+load方法的情况,类为懒加载类。

    • 非懒加载

      • 完全非懒加载:类实现+load方法,此时类为非懒加载类

      • 依赖非懒加载:类本身没有实现+load方法。

        • 子类实现+load方法:由于递归实例化导致父类被实例化,父类本质上还是懒加载类,在这里相当于非懒加载类。
        • 分类/子类分类实现+load方法:在prepare_load_methods中由于分类是非懒加载分类导致类被实例化,也相当于类变成了非懒加载类。

类和分类加载流程:

类和分类加载流程.jpg