OC底层原理十八:类的加载(中) SEL & 分类的加载

OC底层原理 学习大纲

上一节,我们了解了map_images的整体结构 & 非懒加载类,了解了APP启动时,所有都已记录哈希表中(仅类名字地址)。

  • 实现类+load方法非懒加载类,会在启动时,实现类的加载,从macho中读取原始数据存放到rw
  • 懒加载类则是在被第一次调用时,通过消息机制触发类的实现
    两种类的加载方式,最终都是调用realizeClassWithoutSwift完成实现。

上节回顾:

上节回顾

我们上一节留下了2个问题:rwe何时加载?分类如何加载?

  • 现在不急着回答,本节结束后,我相信你就完全懂了。

本节尽可能讲得详细一些:

  1. sel注册
  2. 分类的本质
  3. 分类的数据加载
  4. attachCategories详解
  5. attachCategories的调用

准备工作:

1. sel注册

我们在前面学习msgSend消息机制时,慢速查找阶段中,在类的函数列表查找方法时,是使用二分查找(👉流程图)。

Q: 二分查找必须是有序的,那排序依据是什么,如何排序

  • 上一节我们分析map_images流程时,在第2步 修复预编译阶段的SEL的混乱问题时,就需要将SEL插入到nameSelectors哈希表中。
image.png
  • 其中_getObjc2SelectorRefsmacho__objc_selrefs,存储的内容是SEL:
    image.png
  • 遍历从macho__objc_selrefs读取SEL,其中的sels包含的是带地址sel(后面证明)。
  • 循环注册sel,检查sel地址,如果不同,就重新赋值sel地址

进入sel_registerNameNoLock:

image.png
  • 进入__sel_registerName:
image.png
  • 一般是可以通过name搜索到result,直接返回result
  • 但如果特殊情况name搜索不到,就重新创建,再返回sel

我们进入search_builtins来了解查询路径:

image.png
  • 发现_dyld_get_objc_selectorextern申明在dyld中:
// Called only by objc to see if dyld has uniqued this selector.
// Returns the value if dyld has uniqued it, or nullptr if it has not.
// Note, this function must be called after _dyld_objc_notify_register.
//
// Exists in Mac OS X 10.15 and later
// Exists in iOS 13.0 and later
extern const char* _dyld_get_objc_selector(const char* selName);
  • 打开dyld源码,搜索_dyld_get_objc_selector(const
image.png
  • 进入getObjCSelector
image.png
  • 发现是调用getString方法在读取内容,所以我们反向搜索getString(const,检查函数的实现:
image.png
  • 通过这里,我们就明确知道了:

sel虽然是函数名(字符串),但同时它是有地址值的。

拓展:

  1. 函数地址完全随机,是由它所在的段基础地址偏移值确定的。程序每次运行,函数地址可能变化
  2. 判断两个函数是否相等,是通过地址值进行判断
    两个不同类相同名称函数,但函数地址不同,是两个独立的函数
  3. 函数列表排序,是依据SEL地址进行排序。所以排序后,可使用二分查找。

2.分类的本质

  • main.m文件加入测试代码
// 本类
@interface HTPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;

- (void)func1;
- (void)func3;
- (void)func2;

+ (void)classFunc;

@end

@implementation HTPerson

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

- (void)func1 { NSLog(@"%s",__func__); };
- (void)func3 { NSLog(@"%s",__func__); };
- (void)func2 { NSLog(@"%s",__func__); };

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

@end

// 分类 CatA
@interface HTPerson (CatA)

@property (nonatomic, copy) NSString *catA_name;
@property (nonatomic, assign) int catA_age;

- (void)func1;
- (void)func3;
- (void)func2;

+ (void)classFunc;

@end

@implementation HTPerson (CatA)

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

- (void)func1 { NSLog(@"%s",__func__); };
- (void)func3 { NSLog(@"%s",__func__); };
- (void)func2 { NSLog(@"%s",__func__); };

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

@end

int main(int argc, const char * argv[]) {
    return 0;
}

检查格式的方式:1. clang 2. 官方帮助文档

  • 2.1 clang
    cdmain.m所在文件夹,输入clang -rewrite-objc main.m -o main.cpp,打开main.cpp文件,搜索分类_CatA

  • 分类的实例方法类方法

    image.png

  • 分类的属性:

    image.png

  • 分类的结构

    image.png

  • 我们搜索struct _category_t,可看到分类的完整格式
    image.png

发现编译期HTPerson(CatA)nameHTPersoncls也是HTPerosn类

  • 分类的实现:
    image.png

本类属性分类属性的区别:

  • 本类属性:在clang编译环节,会自动生成并实现对应的set和get方法

  • 分类属性:会存在set、get方法,但是没有实现需要runtime设置关联属性)。

    易混淆点: 分类属性存在setget方法,但没有实现
    检验方式: 使用person对象可以快捷访问到catA_age,并可以赋值。但是程序运行时crash。 这是因为方法存在,但找不到对应的imp实现

    image.png

    Q: 1. 分类属性为何存在setget方法? 2. 如何让它不crash(关联属性的动态实现)

  • 第1个问题在本节后续探索中,会得到很清晰的答案。 第2个问题,我们下一节专门讲解关联属性

  • 2.2 官方帮助文档

打开官方文档 (快捷键:shift + command + 0),搜索Categor:

image.png
  • 切换语言为Objective-C:

    image.png

  • 发现类型是objc_category,在objc4源码中搜索:

image.png
  • 💣 格式不一样?name呢?cls呢?
  • 😂 注意看后面的声明:OBJC2_UNAVAILABLE, objc2不可用。文档已过期的。这个时候,我们要以真实运行的代码为准

了解了分类数据格式,那分类的数据如何加到HTPerson的呢?

3. 分类的加载

如何研究呢?

  • 已知的信息出发先找一条抵达目的地的路径,找到核心方法,再反向搜索核心方法被调用的地方,进行全面推理

我们上一节分析_read_images结构时,第9步 实现非懒加载类->methodizeClass内部有对分类的处理

  • methodizeClass中加入测试代码:
// >>>> 测试代码
    const char *mangledName = cls->mangledName();
    const char * HTPersonName = "HTPerson";
    if (strcmp(HTPersonName, mangledName) == 0 ) {
        if (!isMeta) {
            printf("%s - 精准定位: %s\n", __func__, mangledName);
        }
    }
    // <<<< 测试代码
  • printf打印处加入断点,运行程序
image.png
  • 发现进入了HTPerosn类,查看ro信息,发现其中baseMethods只有8个,分别打印查看,都是HTPerosn本类实例函数。 从信息栏可以看rwe此时为Null

ro的读取:

image.png
  • 单步往下运行,发现最终会到达attachToClass处:
image.png

methodizeClass的内容是:

  • 读取函数(已排序)存到list -> 读取属性存到proplist -> 读取协议存到protolist -> 分类添加到类attachToClass

有个细节,我们发现initialize在这里被添加到根元类函数列表了。根元类拥有initialize方法,所有继承NSObject的类,都将拥有initialize方法。

我们知道+load方法会将懒加载类转变为非懒加载类,在app启动前完成了所有非懒加载类加载。但是app启动环节加载过多内容,会影响app的启动时长

  • Q:有些准备必须在类初始化之前就完成,如果不写+load方法内,怎么做到提前准备呢?
  • A:写在initialize内,因为每个类都继承自NSObject,所以都自带了initialize函数,而initialize函数是在类第一次发送消息时,就触发。 所以可以做到提前准备
  • 进入attachToClass,加入测试代码:
image.png

看到了关键的attachCategories函数:绑定分类

  • 如果是元类,需要分别绑定对象类方法。否则,只需要绑定对象方法。

注意,此时测试代码中HTPersonHTPerson(CatA)都必须实现+load方法,才会进入attachCategories代码区域) 具体原因,后面第5部分 本类与分类的+load区别 会详细讲解。

下面,我们详细分析一下attachCategories

4. attachCategories详解

进入attachCategories,加入定位测试代码

image.png

开辟了64个空间大小的mlistsproplistsprotolists容器,分别用于存储函数属性协议

image.png

attachCategories流程:

  • 首先,开辟空间,对rwe进行初始化
  • 然后,遍历所有的分类
    entry记录当前分类,entry.cat是category_t结构,存储了分类所有数据。
    从分类中读取函数属性协议信息,存放指定容器内。
  • 最后,将容器内数据,分别添加rwe指定属性中。

此处分为3小部分讲解:

  1. rwe的初始化
  2. 数据读取
  3. prepareMethodLists函数排序
  4. attachLists 绑定数据

4.1 rwe的初始化

哈哈哈 😃 走过千山万水,终于找到你,我的rwe

  • 进入 extAllocIfNeeded
image.png
  • 进入extAlloc:
image.png

此时,rwe才完成了初始化工作。各项属性完备。(关于attachLists赋值操作,在4.3小部分进行讲解)

关于rwe何时加载的问题:
我们现在知道分类加载会进行rwe初始化加载数据。那还有其他地方触发rwe的加载吗?

  • rwe的加载,是执行了extAlloc方法,所以我们反向搜索,查看谁调用extAlloc方法:
image.png

只有extAllocIfNeededdeepCopy调用了。

  • deepCopy深拷贝: 搜索deepCopy(,发现只被objc_duplicateClass调用,而是objc_duplicateClass开放使用的API接口,并没自动调用的地方。 所以此处不做考虑。

  • extAllocIfNeeded: 搜索extAllocIfNeeded(,发现有以下7处调用了它:

    image.png

  • 发现都是动态添加(函数、属性、协议、分类等)时,才会创建rwe

还记得上面ro的读取吗?

  • rwe存在时:表示这个类有数据被修改了,所以需要从rwe返回数据
  • 而如果rwe不存在,表明这个类的数据没有动态修改过,所以可以直接从macho拷贝一份ro返回即可。

附上WWDC2020视频Advancements in the Objective-C runtime,回顾官方对于rwe的解释,会理解得更深刻。

4.2 数据读取和prepareMethodLists函数排序

初始化rwe后,我们读取分类数据

image.png

  • 查看entry.cat结构:
image.png
  • 查看category_t结构,发现存储了分类所有数据。
    image.png

所以分类的数据都是从entry.cat进行读取。

  • 我们在上面定位测试代码打印处加上断点,运行代码,到达断点后,往下进入循环内:
    image.png
  • 发现此时name已从编译时的HTPerson变成了CatA,而我们的cls仍旧是HTPerson
    (类地址在内存中是唯一的,地址相同表示是一个类)
    image.png
  • 下面以函数读取为例,(属性、协议的读取和赋值方式一样):
    image.png

将分类的methods函数列表读取到mlist,如果存在:

  • 如果数组是否已满(64),将mlist内部排序后,调用attachLists存到rwemethods中,并将mcount归零。
  • mlist倒序插入到mlists

属性协议也是相同的操作方式,只是读取的内容和存入的容器不同而已。

image.png
  • 至此,已遍历分类,将分类的函数、属性、协议都分别存储到mlistsproplistsprotolists中了。

接下来,是将他们赋值给rwe对应属性:

image.png

4.3 prepareMethodLists函数排序

函数在插入前,都会预先进行一轮排序,进入prepareMethodLists

image.png
  • 进入fixupMethodList:
image.png
  • 执行完prepareMethodLists函数后,我们p mlists打印容器,p $7[63]取出刚才存放在最后的mlistp $8->get(index)打印数据:
image.png

发现排序后的顺序为: [ func1, func3 , func2 ] ,确实不是根据sel字符串进行的排序。

  • 我们使用p/x $8->get(0),打印SEL地址:
image.png
  • 0x0000000100003e12 < 0x0000000100003e18 < 0x0000000100003e1e,发现我们SEL地址确实是从小到大排列的。

所以验证了:
函数的排序:不是根据SEL字符串排序,也不是通过imp进行排序,而通过SEL地址进行排序

  • 排序后,我们通过attachLists完成数据的绑定

4.4 attachLists 绑定数据

  • 进入attachLists:
image.png

拓展函数:

  • memcpy(开始位置,放置内容,占用大小)内存拷贝
  • memmove(开始位置,移动内容,占用大小)内存平移

LRU算法:

  • Least Recently Used的缩写,最近最少使用算法,越容易被调用(访问)的放前面

  • 回想一下,不管我们是动态插入函数,还是添加分类,一定是有需求时才这么操作。而新加入的数据,明显访问频率高于默认模板内容。所以我们addedLists使用LRU算法,将旧数据放在最后面新数据永远插入最前面。 这样可以提高查询效率减少运行时资源的占用

这里有3种情况:

- 0->1: 首次加入,直接将addedLists[0]赋值给list,是一维数组
(首次加载是本类数据在extAllocIfNeeded时,从macho读取ro中的对应数据加入)

image.png

- 1->多: 此时扩容为二维数组旧数据插入后面新数据插入前面:
将数组扩容newCount大小
-> array()count记录个数
-> 如果有旧数据插入lists容器尾部
-> 调用memcpy内存拷贝,从array()首地址开始,将addedLists插入,占用addedCount个元素大小。

image.png

- 多 -> 更多: 类似于1->多的操作,也是旧数据移到后面新数据插入前面
将数组扩容newCount大小
-> array()count记录个数
-> 调用memmove内存评议,从array()首地址偏移addedCount个元素位置开始,移动array()旧数据,占用oldCount个元素大小
-> 调用memcpy内存拷贝,从array()首地址开始,将新数据addedLists插入,占用addedCount个元素大小。

image.png

所以这里rwe函数、属性、协议都是attachLists进行处理后完成的赋值。

image.png

5. attachCategories的调用

此时,我们通过一条线,完整熟悉了attachCategories分类数据添加到rwe中的整个流程和细节。

  • 我们可以反过来搜索attachCategories被哪些地方调用:
image.png

我们发现,除了我们已分析的attachToClass函数,就只有load_categories_nolock函数调用了attachCategories

  • 进入load_categories_nolock,加入测试代码:
const char *mangledName = cls->mangledName();
const char * HTPersonName = "HTPerson";
if (strcmp(HTPersonName, mangledName) == 0 ) {
    auto ht_ro = (const class_ro_t *)cls->data();
    auto ht_isMeta = ht_ro->flags & RO_META;
    if (!ht_isMeta) {
        printf("%s - 精准定位: %s\n", __func__, mangledName);
    }
}
  • 再检查load_categories_nolock在哪里被调用:

第一处被调用:loadAllCategories

image.png

继续搜索loadAllCategories,发现在load_images被调用:

image.png

第二处被调用:_read_images第8步 分类的加载

image.png
  • _read_images的加载,是从map_images过来的。

总结:
分类的加载,总得来说有2个大的调用路径

    1. map_images -> map_images_nolock -> _read_images 有2个可能路径:
      路径一: 第8步 分类的处理 -> load_categories_nolock -> attachCategories
      路径二: 第9步 实现非懒加载类 -> realizeClassWithoutSwift -> methodizeClass -> attachToClass -> attachCategories
    1. load_images -> loadAllCategories -> load_categories_nolock -> attachCategories

至此,文初的2个问题,rwe何时加载?分类如何加载? 相信大家都十分清楚了


本节,我们已经熟悉了分类加载方式

  • 但是我们一切研究都是在本类分类都实现+Load方法的前提,那其他组合的情况是怎样呢?
  • attachCategories这些调用路径在什么情况下进入哪条路径呢?

下一节OC底层原理十九:类的加载(下) 本类与分类load区别 & 关联属性,我们将所有情况都一一分析。

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