上一节 ,我们已完整的分析分类的加载过程,知识量较大,需要慢慢消化。
本节进行拓展和补充以下内容:
-
本类与分类的+load区别 - Category分类与Extension拓展的区别
- 关联对象
准备工作:
- 可编译的
objc4-781源码: //www.greatytc.com/p/45dc31d91000
1. 本类与分类的+load区别
上一节我们的研究都是在本类和分类都实现+Load方法的前提下完成的。 而且attachCategories有多种被调用的路径,具体什么情况走哪条路径,我们不清楚。
现在,我们开始覆盖性测试和探究:(ps: 下面以+load和无区分是否实现+load方法)
- 本类
+load,分类无- 本类
+load,分类+load- 本类
无,分类无- 本类
无,分类+load- 本类
无,分类A无,分类B+load
准备阶段
-
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
// 分类 CatB
@interface HTPerson (CatB)
@property (nonatomic, copy) NSString *catB_name;
@property (nonatomic, assign) int catB_age;
- (void)func1;
- (void)func3;
- (void)func2;
+ (void)classFunc;
@end
@implementation HTPerson (CatB)
+ (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[]) {
@autoreleasepool {
HTPerson * person = [HTPerson alloc];
[person func1];
}
return 0;
}
我们在readClass 和 attachCategories两个函数内部加入定位的测试代码,并在printf一行加入断点(确保当前观察的使我们的HTPerson类):
// >>>> 测试代码
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);
}
}
// <<<< 测试代码
-
readClass加入测试代码和断点

-
attachCategories加入测试代码和断点

- 在
HTPerosn首次调用处加上断点:

准备工作完成后,我们可以开始探索了:
1.1 本类+load,分类无
测试配置: 保留
HPPerson类的+load,注释掉CatA和CatB分类的+load方法
- 运行代码,进入了
readClass处:

提取信息如下:
- 路径: 是
map_images调用的- ro函数列表:此时
ro读取的是macho中的值,ro中已包含本类和所有函数信息(14个)。- 函数排序:
分类的函数不会覆盖本类的同名函数,而是后加载的分类函数排序在先加载的分类和本类前面。
- 放开断点,继续运行,发现没有进入
attachCategories内部。
结论:【本类+load,分类无】的情况:数据在编译层就已经加入到data中。
1.2. 本类+load,分类+load
测试配置: 保留
HPPerson类、CatA和CatB分类的+load方法
- 运行代码,进入了
readClass处:

提取信息如下:
- 路径: 是
map_images调用的- ro函数列表:此时
ro读取的是macho中的值,ro中仅有HTPerosn本类函数信息(8个)。
继续运行代码,进入attachCategories处:

attachLists拓展:此处可观察到
attachLists的加载顺序,验证上一节对attachLists的分析
我们在
attachLists加入三个断点,检查排序。运行代码,发现第一次是从
extAllocIfNeeded初始化rwe时进入,从macho中只存储了本类信息,由于当前是首次创建,所以attachLists走的是0->1的流程,是直接将addLists[0]赋值给了list
image.png继续运行代码,发现是
本类的属性进入attachLists的0->1:
image.png继续运行代码,发现
CatA函数进入attachLists的1->多:
(可以看到oldList是HTPerson本类的8个函数,addedLists是CatA分类的3个函数)
image.png继续运行代码,发现
CatA属性进入attachLists的1->多:image.png
- 继续运行代码,发现
本类的元类函数(类方法)进入attachLists的0->1:image.png
- 继续运行代码,发现
CatA的元类函数(类方法)进入attachLists的1->多:image.png
- 继续运行代码,又回到了
attachCategories处,我们继续运行代码,进入CatB函数进入attachLists的多->更多:image.png
继续运行代码,发现
CatB属性进入attachLists的多->更多:
image.png继续运行代码,发现
CatB的元类函数(类方法)进入attachLists的多->更多:
image.png总结:
image.png
1.3. 本类无,分类无
测试配置: 注释
HPPerson类、CatA和CatB分类的+load方法
- 运行代码,进入了
readClass处:

此时在map_images阶段,macho中记录了本类和所有分类的数据。
- 继续运行代码,没有进入
attachCategories中。
1.4. 本类无,分类+load
测试配置: 注释
HPPerson类的+load方法、保留CatA和CatB分类的+load方法
- 运行代码,进入了
readClass处:

- 继续运行代码,进入了
attachCategories处,在attachLists加入三个断点,继续运行,发现attachLists中0->1加载了HTPerosn本类函数

- 继续运行代码,发现
attachLists中0->1加载了HTPerosn本类属性

- 继续运行代码,发现进入了
attachLists中`1->多:

💣 注意: 此时addedCount为2,表示当前需要添加的列表有2个元素。并不是只有CatB分类。我们打印 addedLists[0] 和addedLists[1],就找到了CatA和CatB两个分类
Q: 为什么
本类没有+load方法,只实现分类+load方法,也在app启动前加载出来了呢?A: 我们查看左边堆栈,
load_images调用了prepare_load_methods:
image.png
- 而
prepare_load_methods中会检查有没有非懒加载的分类,如果有就执行下面的循环。
循环中在add_category_to_loadable_list加载分类前,会执行realizeClassWithoutSwift先检查本类是否实现。image.png
1.5 本类无,分类A无 ,分类B+load
测试配置: 注释
HPPerson类和CatA分类的+load方法,保留CatB分类的+load方法
- 运行代码,进入了
readClass处:

- 发现
ro中加载好了本类和2个分类的所有数据(14个函数),没有再进入attachCategories了。
本类
无,分类A+load,分类B无的结果与这个一样
总结:本类和分类的+load区别:

2. Category分类与Extension拓展的区别
2.1 Category:类别,分类
- 专门用来给类
添加新的方法 -
不能给类添加成员属性,添加了也取不到。 - 分类中用
@property定义的变量,只会生成变量的getter和setter方法,不能生成方法实现和带下划线的成员变量。
成员属性不可添加:@interface HTPerson(CatA) { NSString * catA_name; // 不可这样添加 }
@property属性可添加:@interface HTPerson(CatA) @property (nonatomic, copy) NSString *prop_name; @end编译器
可读取到名称。表示有getter和setter方法的声明。
- 运行后会
crash。是因为没有实现和带下划线的成员变量。
image.png
2.2 Extension:类拓展
- 可以说成是
特殊的分类,已称作匿名分类 -
可以给类添加成员属性、属性、方法,但都是私有的
拓展必须添加在
@interface声明和@implementation实现之间:
image.png
Extension拓展与@interface声明是一样的作用,但是Extension拓展中的成员变量、属性、方法都是私有的。- 可以通过
clang,查看编译结果进行验证。Extension类拓展的下划线成员变量、函数等,都直接加入了本类的相关位置,完成相应实现。
Q: Category中的属性如何用runtime实现?
- A: 在属性的
get和set方法实现内,动态添加关联对象:
// CatA分类
#import <objc/runtime.h>
// 本类
@interface HTPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation HTPerson
@end
// CatA分类
@interface HTPerson (CatA)
@property (nonatomic, copy) NSString *catA_name; // 属性
@end
@implementation HTPerson(CatA)
- (void)setCatA_name:(NSString *)catA_name { // 给属性`catA_name`,动态添加set方法
objc_setAssociatedObject(self, "catA_name", catA_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)catA_name { // 给属性`catA_name`,动态添加get方法
return objc_getAssociatedObject(self, "catA_name");
}
@end
参数解读:
- 动态
设置关联属性:objc_setAssociatedObject(关联对象,关联属性key,关联属性value,策略)- 动态
读取关联属性:objc_getAssociatedObject(关联对象,关联属性key)
3. 关联对象
- 点击进入
objc_setAssociatedObject:

- 点击进入
get():

- 看不懂是什么...,
get()看不懂,那我们往看看它的调用者:SetAssocHook

- 我们
查看结构,发现它就是嵌套了一层objc_hook_setAssociatedObject的方法。调用get(),就是读取内容。所以:
SetAssocHook.get()(object, key, value, policy);
可以直接写成
_base_objc_setAssociatedObject(object, key, value, policy);
- 我们进入
_base_objc_setAssociatedObject:

加入
断点,验证一下,确实是给HTPerson属性完成了赋值image.png
-
进入
_object_set_associative_reference:
image.png 在分析
关联对象的写入操作前,我们先回顾一下本类的正常属性的写入操作:
3.1 回顾本类正常属性写入操作:
-
cd到main.m文件夹,clang -rewrite-objc main.mm -o main.cpp编译一份cpp文件,打开main.cpp文件,搜索HTPerson,找到属性name的set方法:
image.png 发现常规是调用
objc_setProperty完成set方法,我们在源码中检查objc_setProperty的实现:

- 进入
reallySetProperty:

- 主要流程:1. 通过地址
读取属性-> 2.新值retain-> 3.属性赋值-> 4.旧值release
熟悉了常规属性的写入流程。 现在我们来对比关联对象的写入操作:
3.2 关联对象写入操作:
我们回到_object_set_associative_reference流程:
3.2.1 记录数据
-
DisguisedPtr和ObjcAssociation分别对入参object、policy和value进行了包装。
- 查看
DisguisedPtr结构,只有一个value。 所以实际是将入参object对象给到DisguisedPtr对象的value,包装记录一下。image.png
- 查看
ObjcAssociation结构,只有_policy和_value。 所以实际是将入参policy策略和value新值给到ObjcAssociation对象,包装记录一下。image.png
3.2.2 新值retain
- 接下来查看
acquireValue(),发现是完成了新值的retain:

3.2.3 赋值或释放
- 接下来到了核心执行环节
1. 创建管理对象 & hashMap
AssociationsManager manager;

Q: 这样真的创建了对象吗?
- 我们创建
HTObjc进行测试,打印结果显示,确实是构造和析构函数:
image.png
-
AssociationsManager结构中,manager只是对外代言人,并不是唯一的,AssociationsHashMap才是唯一的。
1. 运行验证:
移除锁,这样可以同时存在2个manager了。
image.png
- 加入测试代码,创建2个
manager,都调用get(),发现2个读取的associations是相同地址。- 证明
AssociationsHashMap在内存中是独一份的,而manager只是外层包装,可以创建多个。
image.png2. 代码结构分析:
进入
get(),发现是调用的_storage:
image.png返回查看
_storage,发现是static静态声明。所以AssociationsHashMap确实是内存中独一份。
image.png
2 关联值value是否存在
2.1 value存在(赋值)
-
返回结构如下:
image.png try_emplace创建空ObjectAssociationMap去取查询的键值对-
进入
try_emplace查看源码:(不管是否存在,都会返回true)
image.png
运行代码。断点查询,发现
没有这个key就插入一个空的BucketT进去并返回true
-
进入
LookupBucketFor,发现有两个同名方法,是重载方法,唯一区别是第二个入参的是否有const
image.png -
我们观察外部
try_emplace源码,入参TheBucket是没有const声明的,所以进入的是第二个LookupBucketFor:
image.png -
回到第一个
LookupBucketFor,循环查找key对应的buckets:
image.png 通过
setHasAssociatedObjects标记对象存在关联对象

- 查看
setHasAssociatedObjects:

Q:请问
关联对象是否需要手动释放?
A:指针优化的isa中的has_assoc记录了是否有关联属性,在析构函数触发时,会检查是否有关联属性并主动释放。image.png
- 查看
hasAssociatedObjects:
image.png
继续往下执行,我们在第二次
try_emplace前后检查refs:-
第二次
try_emplace前:插入的Bucktes是空桶,所以还没值:
image.png -
第二次
try_emplace后:插入的Bucktes已经有值了:
image.png 往下走,到达
association.swap(result.first->second)时,我们用当前policy策略和value值组成了一个ObjcAssociation替换原来BucketT中的空:

- 观察内容,此时赋值操作已完成。
2.2 value不存在(移除):
-
首先,
寻找类对:
image.png 查看
find内部:找到了返回buckets,没找到返回end()。

- 先找到
类对,再找到当前类的关联属性对,将当前关联属性对质空,buckets计数更新
image.png
3.2.4 旧值release
- 接下来查看
releaseHeldValue(),发现是完成了旧值的retain:

小总结:
AssociationsHashMap内有多个类对key-value结构,而每个类对应的value,又包含多个关联属性对key-value结构。- 所以我们不管
插入还是移除,都是先通过类信息找到相应的类对,再从类对的value中,通过关联属性key找到对应的关联属性,进行相应操作。其中复杂的
DisguisedPtr和ObjcAssociation结构,都只是类和关联属性信息的一层包装,负责记录信息并统计计数而已。
至此,我们对类的加载,分类和拓展、关联属性,都已经非常熟悉了。



































