iOS底层分析之类的加载(下)

779 阅读11分钟

和谐学习!不急不躁!!我是你们的老朋友小青龙~

前一篇文章iOS底层分析之类的加载(中)分析了ro、rw的初始化以及分类的本质,本文针对分类的加载展开进一步的探究。 上篇文章,我们分析到:rwe的初始化是在extAllocIfNeeded函数,调用extAllocIfNeeded函数有好几个地方:

  • attachCategories函数(将类别的属性、协议、方法添加到类)

  • demangledName函数

  • class_setVersion函数(类的版本设置)

  • addMethods_finish函数(添加方法)

  • class_addProtocol函数(添加协议)

  • _class_addProperty函数(添加属性)

  • objc_duplicateClass函数

添加属性、方法、协议等会调用extAllocIfNeeded函数,让我不禁想到了文章《iOS类里面的数据为什么要分为ro、rw、rwe?》的一句话:

  • 由于运行时的存在,Methods、Protocols、Properties都是可以通过category手动使用API,动态添加、修改,所以rw里也存了一份这些数据。 而rwe是rw的优化,所以可以理解为:
  • category手动使用API修改/添加Methods、Protocols、Properties等会触发extAllocIfNeeded函数,从而给rwe赋值。

回归正题,我们要探究的是跟category有关的,核心函数是attachCategories,而调用到attachCategories的函数有两个:

  • attachToClass
  • load_categories_nolock
查看哪里调用了attachToClass

objc源码搜索attachToClass,发现只有methodizeClass这个函数内部调用了:

image.png

这里有个if判断,我们需要知道previously值,发现它是由methodizeClass函数第二个参数传进来的。

继续搜索methodizeClass

image.png

跟上面一样,也是函数之间参数的传递,继续搜索realizeClassWithoutSwift

image.png

把上面寻找的过程画了个图:

001.png

回到methodizeClass函数(参数previously为nil):

static void methodizeClass(Class cls, Class previously){
    ...
    // 经过上传的探索,确认previously为nil
    if (previously) {
        if (isMeta) {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_METACLASS);
        } else {
            objc::unattachedCategories.attachToClass(cls, previously,
                                                     ATTACH_CLASS_AND_METACLASS);
        }
    }
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
    ...
}

等于说调用attachToClass的地方只有这一个地方:

static void methodizeClass(Class cls, Class previously){
    ...
    objc::unattachedCategories.attachToClass(cls, cls,
                                             isMeta ? ATTACH_METACLASS : ATTACH_CLASS);
    ...
}
查看哪里调用了load_categories_nolock

objc源码搜索load_categories_nolock,发现这有这两个地方调用了:

  • loadAllCategories
  • _read_images
static void loadAllCategories() {
    ...
    load_categories_nolock(hi);
    ...
}

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){
    ...
    load_categories_nolock(hi);
    ...
}

整理一下调用attachCategories的两条线:

  • realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories

  • load_categories_nolock -> attachCategories

category的load方法实现与否

我们知道,Class根据是否实现load方法,分为懒加载类和非懒加载类,那么category是否也有这样的区分呢? 根据前面分析,调用attachCategories的地方分别是realizeClassWithoutSwiftload_categories_nolock。我们在这几个地方打上断点:

image.png

image.png

image.png

测试一:主类实现load方法,category实现load方法

运行代码:

image.png

001.png

002.png

003.png

004.png

005.png

所以我们得出这样一条走向路线:

graph TD
_read_images --> realizeClassWithoutSwift --> methodizeClass --> attachToClass --> load_categories_nolock --> attachCategories
测试二:主类实现load方法,category不实现load方法

把category里的load方法注视,运行代码,然后会发现走向如下:

  • _read_images
  • realizeClassWithoutSwift
  • methodizeClass
  • attachToClass
  • 结束 它并没有走attachCategories这个函数。
测试三:主类不实现load方法,category实现load方法

相应的处理主类和分类的load方法,运行代码,会发现走向如下:

  • _read_images
  • realizeClassWithoutSwift
  • methodizeClass
  • attachToClass
  • 结束 和第二种测试结果一样
测试四:主类不实现load方法,category不实现load方法

相应的处理主类和分类的load方法,运行代码:

image.png

测试总结:要执行attachCategories,需要「主类」和「分类」都实现load方法

category的数据在什么时候加载到主类

前面的测试是为了让我们知道:load方法实现与否,它的流程走向。我们真正关心的是category的数据是什么时候加载到ro里。

针对测试一分析(主类load、分类load)

我们知道,类的数据读写分配是在realizeClassWithoutSwift函数:

static Class realizeClassWithoutSwift(Class cls, Class previously)
{
    ...
    /******* 辅助代码Direction   START *********/
    const char * className = "Direction";
    if (strcmp(class_getName(cls), className) == 0)
    {
         printf("I'm Direction...");
    }
    /******* 辅助代码Direction   END *********/
    auto ro = (const class_ro_t *)cls->data();
    ...
}

下断点,控制台打印ro数据:

image.png

第二遍进-》 image.png

控制台指令如下:

p ro
p *$0
p $0->baseMethods()
p *$2
p $3.get(0).big()
p $3.get(1).big()
p $3.get(2).big()
p $3.get(3).big()
// 图片上显示$4,是因为我前面有一步输入错误,流程没问题

从上图我们可以看到,「realizeClassWithoutSwift」函数并没有将分类数据加载到主类。

按照前面流程,接下来应该是走methodizeClass函数:

image.png

我们发现「methodizeClass」函数,还是没有加载category数据。

继续进入下一个流程attachToClass函数:

image.png

我们发现「attachToClass」函数,还是没有加载category数据。
放过断点,发现它又走了一遍methodizeClass --> attachToClass

image.png

image.png

继续进入下一个流程load_categories_nolock函数:

通过断点定位到Direction类,通过methodsForMeta函数得到cat里的元类方法列表 image.png


通过methodsForMeta函数得到cat里的实例方法列表

image.png

再来对比下category里的方法:

image.png

由此,我们猜测:

  • category的方法是在load_categories_nolock函数里,装载到类里的。

断点继续往下走,会发现进入一个attachCategories函数。

探究attachCategories做了什么事情

断点继续往下走:

image.png 我们发现会调用attachLists函数,第一个参数是个二维指针。

接下来在attachLists函数打上断点,并通过断点一步步往下走:

image.png

发现进入到这一段代码:

void attachLists(List* const * addedLists, uint32_t addedCount) {
    ...
    else {
        // 1 list -> many lists
        Ptr<List> oldList = list;
        // oldList有可能为nil
        uint32_t oldCount = oldList ? 1 : 0;
        uint32_t newCount = oldCount + addedCount;
        setArray((array_t *)malloc(array_t::byteSize(newCount)));
        array()->count = newCount;
        // 将oldList整体插入到下标为addedCount的位置,addedCount是category的方法个数,也就是当前函数传入来的第二个参数
        if (oldList) array()->lists[addedCount] = oldList;
        for (unsigned i = 0; i < addedCount; i++)
            // 遍历将addedCount的元素,从0下标开始插入
            array()->lists[i] = addedLists[i];
        validate();
    }
    ...
}

控制台打印下list:

image.png

控制台打印下oldList:

image.png

控制台打印下addedLists:

image.png

  • list为空。
  • oldList存放的是主类的方法类表。
  • addedLists前面已经分析过了,它是一个二维指针,最终指向的是cateogry里的方法列表。

断点继续走:

image.png

我们发现,array()->lists里面存放的不是一个个method_list_t,而是一个个method_list_t指针。为了验证这一点,我们继续打印:

image.png

我们再来看看array()->lists[0]存放着什么:

image.png

array存放着category方法列表的指针。

针对测试二分析(主类load、分类无load)

测试结果和针对测试三分析一样。

针对测试三分析(主类无load、分类load)

image.png

image.png

得出结论:

  • 主类未实现load方法,分类实现load方法的情况下,category的数据伴随着image的加载,直接写到data()里;
针对测试四分析(主类无load、分类无load)

发现程序一启动,没有进_read_images函数断点、也没有进realizeClassWithoutSwift函数断点。 image.png

放过断点 进入下一步"

image.png

我们发现,main.m里执行了Direction *dr = [Direction alloc];相当于发送了一个alloc消息,具体详情可以点击左边栏:

image.png

小断点,移动到执行完ro赋值,然后控制台打印ro:

image.png

得出结论:当主类和分类都没有实现load方法,category的数据绑定推迟到第一次发送消息。

综合以上4个测试,对category方法什么时候加载,做一个整理:
  • 主类和分类都实现load方法: attachCategories加载

  • 主类实现load,分类不实现load方法:data()读取的时候加载

  • 主类不实现load,分类实现load方法:data()读取的时候加载

  • 主类和分类都不实现load方法:第一次消息发送的时候加载

拓展:主类实现load,一个分类实现load,另一个分类不实现load

最终还是会走到这里:

image.png

这里的count决定着对多个category进行处理,那么count数值来自哪里呢?我们发现在当前函数下,有着两行代码:

image.png

processCatlist是一个类似block的声明,因为执行了processCatlist(hi->catlist(&count));这行代码,才会触发里面的for循环。

继续进入catlist

category_t * const *header_info::catlist(size_t *outCount) const
{
    ...
    return _getObjc2CategoryList(mhdr(), outCount);
    ...
}

image.png

说明count的数据是从mach-O里面读取的__objc_catlist字段数据。

由此,我们可以get到一个点:

  • load方法乱写,会增加程序启动过程中的负担,延长启动时间。
多个分类

现实过程中,对于一个类,可能存在多个分类,我们在原来一个分类的基础之上,再添加以一个分类Direction+categoryTwo

// Direction+categoryTwo.h
@interface Direction (categoryTwo)
- (void)look_categoryTwo;
- (void)dreamSome_categoryTwo;
+ (void)cleanSome_categoryTwo;
@end

// Direction+categoryTwo.m
#import "Direction+categoryTwo.h"
@implementation Direction (categoryTwo)
- (void)look_categoryTwo{
    NSLog(@"Direction (categoryTwo)---look_categoryTwo");
}

- (void)dreamSome_categoryTwo{
    NSLog(@"Direction (categoryTwo)---dreamSome_categoryTwo");
}

+ (void)cleanSome_categoryTwo{
    NSLog(@"Direction (categoryTwo)---cleanSome_categoryTwo");
}
@end

我们依旧是从load_categories_nolock函数开始分析(前面已经分析过了):

image.png 通过通知台打印,发现i=0的情况下,cat的名字是categoryTwo,再查看 Build Phases:

image.png

可以得出一个结论:

  • category的加载顺序是看Build Phases的先后顺序

言归正传,我们要分析的是多个category的走向,i=0(也就是category只有一个)的情况我们前面分析过了,我们想探究的是当i=1甚至i=2等等的流程走向,(这里取i=1进行分析)。

继续走断点......我们要看的是attachLists函数,看看会不会进入if (hasArray()) {这个语句块:

image.png

发现它进入了,我们还顺便打印了下addedLists参数的内容。
继续查看array~

image.png

image.png

所以可以看到,array依次保存着两个元素:

  • 指向category方法列表的指针
  • 指向本类方法列表的指针

继续分析:

image.png

再结合上图array里保存的数据,我们可以得出一个结论: 当存在多个category的情况,array内部会按照先后顺序排列:categoryX方法列表指针、categoryXX方法列表指针....本类方法类表指针。

经过上图分析,我们得出list的内存结构大概是这样的:

objc源码搜索attachCategories

//将方法列表、属性和协议从类别附加到类。
//假设cats中的类别都已加载并按加载顺序排序,
//首先是最古老的类别。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
                 int flags)
{
    ...
    // ********* 针对性分析,我们研究的是Direction *********
    /// 辅助代码   START *********/
    const char *mangledName = cls->nonlazyMangledName();
    const char *comName = "Direction";
    if (strcmp(comName, mangledName) == 0) {
        //经测试,如果category里面不实现+load方法,不会进入这里
        printf("类的加载处理|| mangledName-->%s\n",mangledName);
    }
    /// 辅助代码   END *********/
    
    
    // 核心代码
    for (uint32_t i = 0; i < cats_count; i++) {
        /**** 方法处理 ***/
        auto& entry = cats_list[i];///遍历一个个category,得到category地址&entry
        // methodsForMeta:如果是元类,返回类方法列表;否则返回实例方法列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            // 判断mcount(category总个数)是否等于64,如果为真就进入语句块
            if (mcount == ATTACH_BUFSIZ) {
                // cls:    类或元类
                // mlists: 从category读取的方法列表,可能是类方法列表,也可能是实例方法类表;
                // prepareMethodLists内部:调用fixupMethodList,实现对mlists的SEL绑定、方法排序
                
                prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__);
                // 将category的方法列表追加到类的方法列表后面
                rwe->methods.attachLists(mlists, mcount);
                mcount = 0;
            }
            
            // 第一个从下标记63的位置,开始插入 -- 也就是倒着插入 (ATTACH_BUFSIZ = 64)
            mlists[ATTACH_BUFSIZ - ++mcount] = mlist;//这一行,mcount会做累加
            // 类的header信息
            fromBundle |= entry.hi->isBundle();
        }
        
        /**** 属性处理 ***/
        // 根据isMeta,返回: 类属性列表 或 实例属性列表
        property_list_t *proplist =
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            if (propcount == ATTACH_BUFSIZ) {
                // 将category的属性列表追加到类的属性列表后面
                rwe->properties.attachLists(proplists, propcount);
                propcount = 0;
            }
            // 跟mlists一样,也是从下表63的位置,向前倒着插入
            proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
        }
        
        /**** 协议处理 ***/
        // 根据isMeta,返回: 空 或 协议列表
        // 注意:元类没有协议
        protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
        if (protolist) {
            if (protocount == ATTACH_BUFSIZ) {
                // 将category的协议列表追加到类的协议列表后面
                rwe->protocols.attachLists(protolists, protocount);
                protocount = 0;
            }
            // 跟mlists一样,也是从下表63的位置,向前倒着插入
            protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
        }
    }
    ...
}

attachLists分析

void attachLists(List* const * addedLists, uint32_t addedCount) {
    ...
    // 第一次hasArray可能为空
    if (hasArray()) {
        ...
        //倒叙拆入,假设lists里已经有3个里,addedLists里有2个,那就
        //  newArray->list[4] = list[2]
        // newArray->list[3] = list[1]
        // newArray->list[2] = list[0]
        // 流出来newArray->list[0]和newArray->list[1]是给addedLists存放的
        for (int i = oldCount - 1; i >= 0; i--)
            newArray->lists[i + addedCount] = array()->lists[i];
        // addedLists里的从0开始插入到newArray->lists里
        for (unsigned i = 0; i < addedCount; i++)
            newArray->lists[i] = addedLists[i];
        ...
    }
    else if (!list  &&  addedCount == 1) {
    // 如果list为空,且addedLists只有一个
            list = addedLists[0];
            validate();
    }
    else {
            // 1 list -> many lists
            Ptr<List> oldList = list;
            // oldList有可能为nil
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            // 将oldList整体插入到下表为addedCount的位置,addedCount是category的方法个数,也就是当前函数传入来的第二个参数
            if (oldList) array()->lists[addedCount] = oldList;
            for (unsigned i = 0; i < addedCount; i++)
                // 遍历将addedCount的元素,从0下标开始插入
                array()->lists[i] = addedLists[i];
            validate();
        }
       
}

image.png

我们可以发现,attachLists函数的目的是:把category的List插入到原来List的最前面。

attachCategories代码的静态分析已经结束,接下来开始实操、下断点、控制台打印验证:

  • 下断点

image.png

运行工程,等进入第一个断点,说明当前cls是Direction类,然后把第二个断点打开:

002.png 这边可以看一下category里的内容:

image.png

image.png

放过断点,会再一次进入attachCategories函数,按照前面一次的样子打印mlist:

003.png

如此看来:

  • isMeta为false,methodsForMeta(isMeta)返回的是分类-实例方法列表

  • isMeta为true,methodsForMeta(isMeta)返回的是分类-类方法列表

属性列表也同样的操作

image.png

第二次进断点,元类打印为nil,因为当前没有类属性。

本文总结

  1. 针对主类和分类是否实现load方法,衍生出了4种(多个分类这种没算上)情况。
  2. 针对不同的情况,通过断点大概有了一个走位了解。
  3. 通过断点,控制台ro的打印,定位category方法是什么时候加载。
  4. 分析多个category,具体的流程走向,及attachCategories和attachLists的源码分析

代码:

链接: pan.baidu.com/s/1cXT7f_Na…
密码: mu1m
--来自百度网盘超级会员V2的分享