上一节 ,我们已完整的分析分类的加载过程,知识量较大,需要慢慢消化。
本节进行拓展和补充以下内容:
-
本类
与分类
的+load
区别 - Category分类与Extension拓展的区别
- 关联对象
准备工作:
- 可编译的
objc4-781
源码: https://www.jianshu.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
继续运行代码,发现是
本类的属性
进入attachLists
的0->1
:
继续运行代码,发现
CatA函数
进入attachLists
的1->多
:
(可以看到oldList
是HTPerson本类
的8个
函数,addedLists
是CatA分类
的3个
函数)
继续运行代码,发现
CatA属性
进入attachLists
的1->多
:
- 继续运行代码,发现
本类的元类函数(类方法)
进入attachLists
的0->1
:
- 继续运行代码,发现
CatA的元类函数(类方法)
进入attachLists
的1->多
:
- 继续运行代码,又回到了
attachCategories
处,我们继续运行代码,进入CatB函数
进入attachLists
的多
->更多
:
继续运行代码,发现
CatB属性
进入attachLists
的多->更多
:
继续运行代码,发现
CatB的元类函数(类方法)
进入attachLists
的多->更多
:
总结:
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
:
- 而
prepare_load_methods
中会检查有没有非懒加载的分类
,如果有就执行下面的循环。
循环中在add_category_to_loadable_list
加载分类前,会执行realizeClassWithoutSwift
先检查本类是否实现。
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
。是因为没有实现
和带下划线
的成员变量
。
2.2 Extension:类拓展
- 可以说成是
特殊的分类
,已称作匿名分类
-
可以
给类添加成员属性
、属性
、方法
,但都是私有
的
拓展必须添加在
@interface声明
和@implementation实现
之间:
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
属性完成了赋值
-
进入
_object_set_associative_reference
:
在分析
关联对象
的写入
操作前,我们先回顾一下本类
的正常属性的写入
操作:
3.1 回顾本类
正常属性写入
操作:
-
cd
到main.m
文件夹,clang -rewrite-objc main.mm -o main.cpp
编译一份cpp文件,打开main.cpp
文件,搜索HTPerson
,找到属性name
的set
方法:
发现常规是调用
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
,包装记录一下。
- 查看
ObjcAssociation
结构,只有_policy
和_value
。 所以实际是将入参policy
策略和value
新值给到ObjcAssociation
对象,包装记录一下。
3.2.2 新值retain
- 接下来查看
acquireValue()
,发现是完成了新值
的retain
:
3.2.3 赋值或释放
- 接下来到了核心执行环节
1. 创建管理对象 & hashMap
AssociationsManager manager;
Q: 这样真的创建了对象吗?
- 我们创建
HTObjc
进行测试,打印结果显示,确实是构造
和析构
函数:
-
AssociationsManager
结构中,manager
只是对外代言人,并不是唯一的,AssociationsHashMap
才是唯一的。
1. 运行验证:
移除锁
,这样可以同时存在2个manager
了。
- 加入测试代码,创建2个
manager
,都调用get()
,发现2个读取的associations
是相同地址
。- 证明
AssociationsHashMap
在内存中是独一份的,而manager
只是外层包装,可以创建多个。
2. 代码结构分析:
进入
get()
,发现是调用的_storage
:
返回查看
_storage
,发现是static
静态声明。所以AssociationsHashMap
确实是内存中独一份
。
2 关联值value是否存在
2.1 value存在(赋值)
-
返回结构如下:
try_emplace
创建空ObjectAssociationMap
去取
查询的键值对
-
进入
try_emplace
查看源码:(不管是否存在,都会返回true)
运行代码。断点查询,发现
没有
这个key
就插入
一个空的BucketT
进去并返回true
-
进入
LookupBucketFor
,发现有两个同名方法
,是重载方法
,唯一区别是第二个入参
的是否有const
-
我们观察外部
try_emplace
源码,入参TheBucket
是没有const
声明的,所以进入的是第二个LookupBucketFor
:
-
回到第一个
LookupBucketFor
,循环查找key
对应的buckets
:
通过
setHasAssociatedObjects标记
对象存在关联对象
- 查看
setHasAssociatedObjects
:
Q:请问
关联对象
是否需要手动释放
?
A:指针优化的isa
中的has_assoc
记录了是否有关联属性
,在析构函数
触发时,会检查是否有关联属性
并主动释放
。
- 查看
hasAssociatedObjects
:
继续往下执行,我们在第二次
try_emplace
前后检查refs
:-
第二次
try_emplace
前:插入的Bucktes
是空桶,所以还没值:
-
第二次
try_emplace
后:插入的Bucktes
已经有值了:
往下走,到达
association.swap(result.first->second)
时,我们用当前policy策略
和value值
组成了一个ObjcAssociation
替换原来BucketT中的空
:
- 观察内容,此时赋值操作已完成。
2.2 value不存在(移除):
-
首先,
寻找类对
:
查看
find
内部:找到
了返回buckets
,没找到
返回end()
。
- 先找到
类对
,再找到当前类的关联属性对
,将当前关联属性对
质空,buckets
计数更新
3.2.4 旧值release
- 接下来查看
releaseHeldValue()
,发现是完成了旧值
的retain
:
小总结:
AssociationsHashMap
内有多个类对
key-value结构,而每个类
对应的value
,又包含多个关联属性对
key-value结构。- 所以我们不管
插入
还是移除
,都是先通过类
信息找到相应的类对
,再从类对
的value
中,通过关联属性key
找到对应的关联属性
,进行相应操作。其中复杂的
DisguisedPtr
和ObjcAssociation
结构,都只是类
和关联属性
信息的一层包装,负责记录信息
并统计计数
而已。
至此,我们对类的加载,分类和拓展、关联属性,都已经非常熟悉了。