iOS 底层原理-类的加载(中)

在上一篇 iOS 底层原理-类的加载(上) 分析了 map_images 的流程,那么问题来了,一个应用程序有很多的类,如果都按照这个流程走,太耗费性能了,这是苹果不允许的,那么它真实的流程是什么样子呢?下面我们一起来探索下

如何定位当前研究的类

我们知道一个应用程序运行需要加载很多的类文件,那么我们如何去定位到我们要研究的指定的类呢?这里我们需要对源码进行一些处理,让它只针对我们需要研究的类。上一篇中讲到 objc 源码通过 cls->mangledName() 来获取类名,那么我们判断自定义研究的类名与 mangledName 是否一致,如果一致,则就是我们需要研究的,反之,则不需要研究。如下

const char *mangledName  = cls->mangledName();
const char *LCPersonName = "LCPerson";

if (strcmp(mangledName, LCPersonName) == 0) {
    bool lc_isMeta = cls->isMetaClass(); // 用来判断是否是元类,排除干扰(如果需要)
    if (!lc_isMeta) {
        printf("%s: 这个是我要研究的 %s \n", __func__, LCPersonName);
    }
}

这样我们就可以避免了其他类的干扰,只关注我们自定义的类。核心的方法在上一篇中提到了,只需要将自定义的代码添加上去就可以了

懒加载与非懒加载类

ObjC 中,判断一个类是否是懒加载类,就是看它是否实现了 +load 方法

  • 实现了 +load 方法,它就是非懒加载类

  • 反之,就是懒加载类

+load 方法会提前加载(+load 会在 load_images 调用,前提是类存在,这个会在后面进行验证)。如果没实现 +load 方法,会在第一次调用方法时加载

实现 load 方法的类加载

创建一个 LCPerson 类,声明并实现一个实例方法以及重写 +load 方法

@interface LCPerson : NSObject

- (void)lc_instanceMethod1;

@end

@implementation LCPerson

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

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

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello!");
    }
    return 0;
}

运行 objc-781 源码,查看打印结果,如下

非懒加载的流程图如下

未实现 load 方法的类加载

删除 +load 方法,此时我们运行 objc-781 源码,发现只会打印 readClass 方法,那么它是什么时候加载的呢?现在我们在 main 函数中添加如下代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LCPerson *person = [LCPerson alloc];
        [person lc_instanceMethod1];
        NSLog(@"Hello!");
    }
    return 0;
}
1. 断点调试

此时在 main 函数处打个断点,我们运行 objc-781 源码,调用了 readClass 方法后直接来到断点处,再次执行下一步,可以看到调用流程如下

2.堆栈信息

我们在 realizeClassWithoutSwift 处打个断点,运行 objc-781 源码,查看堆栈信息

方法调用流程为什么能来 realizeClassWithoutSwift?,本质上调用 allocalloc 的本质是消息的发送。因为是第一次调用,会走消息的慢速查找 lookUpImpOrForward,类没有初始化,会调用 realizeClassMaybeSwiftAndLeaveLocked,后续调用 realizeClassMaybeSwiftMaybeRelock,最后调用 realizeClassWithoutSwift,后面是实现类。

懒加载类的流程图如下

分类

分类是什么?要研究分类,首先我们需要知道分类是什么,怎么研究呢?可以从下面三种方法探索,首先在 main 文件中定义个分类,如下

@interface LCPerson (LC)

@property (nonatomic, copy) NSString *lc_name;
@property (nonatomic, assign) int lc_age;

- (void)lc_instanceMethod3;
- (void)lc_instanceMethod1;
- (void)lc_instanceMethod2;

@end

@implementation LCPerson (LC)

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

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

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

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

@end

1. 通过 clang 探索

cdmain.m 所在的文件夹,执行 clang -rewrite-objc main.m -o main.cpp,文件夹中会多出一个 main.cpp 类文件,打开 main.cpp,就可以看到底层编译,分类的类型是 _category_t 结构体

搜索 struct _category_t,看它的结构如下

struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

从上面对应关系可以看出,由于 LCPerson (LC) 没有协议,所以对应的为 0,结构体中有两个 _method_list_t,分别表示的是 实例方法列表类方法列表。搜索 _CATEGORY_INSTANCE_METHODS_LCPerson_ 查看它的实例方法列表底层实现

对应分类中我们添加的三个实例方法,其格式为 sel + 签名 + 地址,看着很熟悉是不是?就是 method_t 结构体的属性

struct method_t {
    SEL name; // 方法名
    const char *types; // 方法签名
    MethodListIMP imp; // 函数地址

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

我们再来看下分类的属性列表

可以看到,我们在分类里定义了属性,但是在底层编译中并没有看到它的成员变量,而且在实例方法列表中也没有看到属性的 setter 以及 getter 方法。这是因为分类中定义的属性不会生成成员变量,只是有 settergetter 的声明,并没有实现它的 settergetter 方法。我们可以通过关联对象来设置(objc_setAssociatedObjectobjc_getAssociatedObject)。

2. 通过 Xcode 官方文档探索

如果不会 clang,可以通过 Xcode 文档搜索 Category 查看

3. 通过 objc 源码探索

打开 objc 源码,搜索 category_t

分类的本质是一个 _category_t 的结构体类型,它有两个属性: name(类名)和 cls(本类对象);两个方法列表:实例方法列表和类方法列表;一个协议列表;一个属性列表。另外分类中的属性是没有成员变量的,只有 settergetter 的声明,并没有实现 settergetter 方法。

分类数据的加载时机

在上一篇 iOS 底层原理-类的加载(上) 中分析了分类数据的加载是在 attachCategories 方法中实现的,且分类的加载顺序是根据编译器编译的先后顺序加载到类中,越晚加进来,越在前面。

但是它在什么时机调用的,我们还不得而知,下面就让我们就一起探索下吧

什么时机调用

下面我们通过反推法和堆栈信息两种方法去探索

1. 反推法
  • objc 源码中全局搜索 attachCategories(,发现只有两处调用,分别是 attachToClassload_categories_nolock

通过调试发现不会走 attachToClass 中的 attachCategories(这里我们设置的主类和分类都实现了 +load 方法,如果主类未实现 +load 方法,分类有实现 +load 方法,则会调用 attachToClass 中的 attachCategories,后面会分析到)

  • 全局搜索 load_categories_nolock 的调用,发现有两处调用 _read_imagesloadAllCategories

_read_images 中的调用如下

通过调试,不会走 _read_imagesif 流程,走的是 loadAllCategories 的流程

  • 再次全局搜索 loadAllCategories 的调用,发现只有一次调用,是在 load_images 时调用的
2. 堆栈信息

现在我们在 attachCategories 中加上自定义的断点,bt 查看它的堆栈

这里也验证了我们刚刚反推的流程,反推流程和正常流程图如下

分类与类的搭配使用(+load 方法实现与否)

上面我们分析了主类的懒加载与非懒加载,下面我们看下它们搭配使用(+load 实现与否)的加载情况。大致可以分为四种情况

分类实现 +load 分类未实现 +load
主类实现 +load 非懒加载类 + 非懒加载分类 非懒加载类 + 懒加载分类
主类未实现 +load 懒加载类 + 非懒加载分类 懒加载类 + 懒加载分类

主类源码

/*------ .h ------**/
@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *kc_name;
@property (nonatomic, assign) int kc_age;

- (void)kc_instanceMethod1;
- (void)kc_instanceMethod2;
- (void)kc_instanceMethod3;

+ (void)kc_sayClassMethod;

@end

/*------ .m ------**/
#import "LGPerson.h"

@implementation LGPerson

+ (void)load {

}

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

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

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

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

@end

分类源码

/*------分类LGA.h ------**/
@interface LGPerson (LGA)

- (void)cateA_1;
- (void)cateA_2;
- (void)cateA_3;

@end

/*------分类LGA.m ------**/
#import "LGPerson+LGA.h"

@implementation LGPerson (LGA)

+ (void)load{

}

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

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

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

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

@end

/*------分类LGB.h ------**/
@interface LGPerson (LGB)

- (void)cateB_1;
- (void)cateB_2;
- (void)cateB_3;

@end

/*------分类LGB.m ------**/
#import "LGPerson+LGB.h"

@implementation LGPerson (LGB)

+ (void)load{

}

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

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

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

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

@end

非懒加载类与非懒加载分类

这种情况是 主类实现了 +load 方法,分类也实现了 +load 方法,前面我们分析的就是这种情况,我们可以得出如下结论

  • 类的数据加载是在 _read_images 中调用 _getObjc2NonlazyClassList 加载,插入表操作,ro、rw 的操作。调用路径:

map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass ,后面会走 load_images 方法

  • 分类的数据加载是通过 load_images 加载到类中的,调用路径为

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

通过我们自定义的打印数据,运行程序,打印日志如下

非懒加载类与懒加载分类

这种情况是 主类实现了 +load 方法,分类没有实现 +load 方法

  • 首先,我们在 realizeClassWithoutSwift 的自定义代码中下个断点,查看 ro 情况

从上面可以看到,方法列表中有 16 个方法,但是在主类中没有这么多,那剩余的方法是从哪里来的呢?我们通过 lldb 命令一一打印出方法列表中的方法

从上面的打印信息可以看出,除了主类的方法外,分类的方法也被加载进来了,依次是 LGA->LGB->LGPerson。方法还没有排序,说明分类的数据没有进行非懒加载时,通过 cls->data() 读取到 mach-o 可执行文件时,数据就已经进来了,不需要在运行时添加进去了

  • 下面我们进入 methodizeClass 方法中查看排序后的方法列表数据

通过打印发现,方法排序只对 同名方法进行了排序,而类中的其他方法则是按照 imp地址有序排列,排序的源码如下(核心代码)

static void 
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
                   bool baseMethods, bool methodsFromBundle)
{
    for (int i = 0; i < addedCount; i++) {
        method_list_t *mlist = addedLists[I];
        ASSERT(mlist);

        // Fixup selectors if necessary
        if (!mlist->isFixedUp()) {
            fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
        }
    }
}
👇
static void 
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
    // sel - imp
    // Sort by selector address.
    if (sort) {
        method_t::SortBySELAddress sorter;
        std::stable_sort(mlist->begin(), mlist->end(), sorter);
    }
    
    // Mark method list as uniqued and sorted
    mlist->setFixedUp();
}

通过我们自定义的打印数据,运行程序,打印日志如下

懒加载类与懒加载分类

这种情况是 主类和分类都没有实现 +load 方法,这里我们需要在 main 函数中调用类的实例方法来辅助,添加代码如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        [person kc_instanceMethod1];
    }
    return 0;
}
  • 我们先通过添加我们自定义的打印,不加任何断点,直接运行程序,看下打印日志

其中的 realizeClassMaybeSwiftMaybeRelock 方法调用是消息发送流程的慢速查找的函数,在第一次发送消息时才走的函数

  • 我们在 readClass 处下个断点,读取此时的 ro 情况

此时的 baseMethodList 的个数是 16 个,说明也是从 data 中读取出来的

懒加载类 与 非懒加载分类

这种情况是 主类没有实现 +load 方法,分类实现了 +load 方法

  • 我们先运行程序,获取打印日志
  • 我们在 readClass 处打个断点,查看 ro 情况

可以看到,baseMethodList 的 count 是 8 个,我们打印出每个方法如下

可以看到方法列表里是 LGPerson 的三个实例方法和属性的 settergetter 方法以及 1 个 cxx 方法,说明 ro 中只有主类的数据。那么怎么查看分类的数据呢?为了调试分类的数据加载,继续往下执行:load_images -> loadAllCategories -> load_categories_nolock。打印此时的堆栈信息

继续执行,在 attachToClass 方法打个断点,继续点击下一步,走到 attachCategories

主类未实现 +load,分类实现了 +load,会迫使主类提前加载,即主类强行转换为非懒加载类样式

总结

类和分类搭配使用,其数据的加载时机总结如下:

  • 非懒加载类 + 非懒加载分类:类的数据加载是在 _read_images 中调用 _getObjc2NonlazyClassList 加载;分类的数据加载是通过 load_images 加载到类中的

  • 懒加载类 + 非懒加载分类:分类实现了 +load,会迫使主类提前加载,即在 _read_images 中不会对类做实现操作,在 load_images 方法中触发类的数据加载,同时加载分类数据。

  • 非懒加载类 + 懒加载分类:数据加载在read_image就加载数据,数据来自data,data在编译时期就已经完成,即data中除了类的数据,还有分类的数据,与类绑定在一起。

  • 懒加载类 + 懒加载分类:其数据加载推迟到 第一次消息时,数据同样来自data,data在编译时期就已经完成。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容