iOS 全网最新objc4 可调式/编译源码
编译好的源码的下载地址
序言
在前面文章《iOS底层之类的加载》中探索了类的加载流程,本篇将对分类
展开探索,从分类的结构到分类的加载流程,来探索分类的本质。
Runtime
优化
在《WWDC 2020 关于Runtime的优化》中介绍了关于Runtime的优化内容,核心内容是对rw
扩展出rwe
,来优化整个运行时的性能。
我们的应用程序装载到设备时,系统会为安装程序分配一段内存,这段内存是不可变的称为clean memory
也就是ro
,当程序运行启动时,系统会开辟新的内存来运行程序,这段内存是可变化的称之为dirty memory
也就是rw
,因为系统内存有限,所以rw
这段内存是比较宝贵的。
但是在rw
中的数据很多是不会改变的,直接从ro
读取即可,需要改变的数据通过Runtime
运行时操作的数据,比如类的方法、属性、协议等,将这些数据存放在rwe
上,这样就可以达到对dirty memory
的优化。
分类
的意义就是要动态的给类添加方法等,那么分类的探索就从rwe
入手。
分类的结构
自定义类LGPerson
和分类LGPerson (Cat)
,然后通过clang
生成cpp
文件查看
@interface LGPerson : NSObject
{
NSString * name;
}
@property (nonatomic, copy) NSString * nickName;
- (void)instanceMethod;
+ (void)classMethod;
@end
@implementation LGPerson
- (void)instanceMethod {
NSLog(@"%s", __func__ );
}
+ (void)classMethod {
NSLog(@"%s", __func__ );
}
@interface LGPerson (Cat)
@property (nonatomic, copy) NSString * lg_nickName;
- (void)lg_categoryInstanceMethod;
+ (void)lg_categoryClassMethod;
@end
@implementation LGPerson (Cat)
- (void)lg_categoryInstanceMethod {
NSLog(@"%s", **__func__** );
}
+ (void)lg_categoryClassMethod {
NSLog(@"%s", **__func__** );
}
@end
主类的cpp
代码
在
LGPerson
的主类cpp
中有实例方法
、类方法
、以及属性的set
和get
方法;
分类的cpp
代码
到在分类中有
实例方法
和类方法
,并没有属性lg_nickName
的set
和get
方法
让LGPerson (Cat)
分类遵守协议NSObject
,重新生成cpp
分类的类型是_category_t
,通过category_t
在源码中可以找到分类
的结构定义
struct category_t {
const char *name;
classref_t cls;
WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;
WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
protocol_list_t *protocolsForMeta(bool isMeta) {
if (isMeta) return nullptr;
else return protocols;
}
};
name
: 分类名称;cls
:主类;instanceMethods
:实例方法;classMethods
: 类方法;protocols
: 所遵守的协议;instanceProperties
:实例属性,并没有set
和get
方法;_classProperties
:类属性
通过
分类
的结构可以看出,分类
是没有元类
的,主类
的类方法是在元类
中,分类
的类方法是在classMethods
中。
分类的加载
rwe
是通过类中的extAllocIfNeeded
方法创建,如果已有值直接返回rwe
,如果没有则通过extAlloc
创建。
在源码中全局搜索extAllocIfNeeded
,调用的方法有
attachCategories
分类、class_setVersion
设置版本、addMethods_finish
动态添加方法、class_addProtocol
添加协议、_class_addProperty
添加属性、objc_duplicateClass
类的复制、demangledName
修改类名
这些方法中有关分类的只有attachCategories
方法
attachCategories
方法探究
// 将方法列表、属性和协议从类别附加到类。假设cats中的类别都是按加载顺序加载和排序的,最旧的类别优先。
static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)
{
/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
* 只有少数类在启动期间具有超过64个类别。这使用了一个小堆栈,并避免了malloc。
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
* 类别必须以正确的顺序添加,即从后到前。
* 为了实现分块,我们从前到后迭代cats_list,向后构建本地缓冲区,
* 并对块调用attachList。attachLists预先准备好列表,
* 因此最终结果按预期顺序排列。
*/
constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];
uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS); // 是否为元类
auto rwe = cls->data()->extAllocIfNeeded(); // 初始化rwe
// cats_count分类数量,循环处理分类
for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];
// 方法处理
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
if (mcount == ATTACH_BUFSIZ) { // 第一次mcount = 0
prepareMethodLists(cls, mlists, mcount, NO, fromBundle, __func__ );
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
//++mcount,将mlist放在mlists的最后
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
//++propcount,将proplist放在proplists的最后
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}
protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}
if (mcount > 0) {
// 向类中添加方法并排序
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount,
NO, fromBundle, __func__ );
// rwe中添加方法
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) {
flushCaches(cls, __func__ , [](Class c){
// constant caches have been dealt with in prepareMethodLists
// if the class still is constant here, it's fine to keep
return !c->cache.isConstantOptimizedCache();
});
}
}
// rwe中添加属性
rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);
// rwe中添加协议
rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
分析一下方法
- 初始化
rwe
;- 通过分类数量
cats_count
,循环处理分类中的方法
、属性
、协议
;- 循环中按倒序插入法,将所有的方法存在
mlists
,协议存放在protolists
,属性存放在proplists
;- 如果
mcount
大于0,说明有分类方法,通过prepareMethodLists
向类中添加方法并排序,然后rwe
中的methods
调用attachLists
,添加分类方法到rwe
;rwe
中的properties
调用attachLists
,添加分类属性到rwe
;rwe
中的protocols
调用attachLists
,添加分类协议到rwe
;
rwe
中的方法
、属性
和协议
都是调用的attachLists
插入数据,是因为他们的定义都继承list_array_tt
,我们看一下list_array_tt
中的attachLists
方法
attachLists
方法探究
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) { // 已有数据且为多条
// many lists -> many lists
uint32_t oldCount = array()->count; // 取出旧值
uint32_t newCount = oldCount + addedCount; // 总数据数量
array_t *newArray = (array_t *)malloc(array_t::byteSize(newCount)); // 开辟新空间
newArray->count = newCount;
array()->count = newCount;
for (int i = oldCount - 1; i >= 0; i--) // 将旧值从后往前取出,插入到新数组的后面
newArray->lists[i + addedCount] = array()->lists[i];
for (unsigned i = 0; i < addedCount; i++) // 将新值从前往后添加到新数组
newArray->lists[i] = addedLists[i];
free(array());
setArray(newArray);
validate();
}
else if (!list && addedCount == 1) { // 没有数据且插入数据只有一条
// 0 lists -> 1 list
list = addedLists[0];// 直接插入
validate();
}
else { // 没有数据且插入多条数据
// 1 list -> many lists
Ptr<List> oldList = list; // 取出旧值
uint32_t oldCount = oldList ? 1 : 0; // 有旧值oldCount=1否则oldCount=0
uint32_t newCount = oldCount + addedCount;// 总数据数量
setArray((array_t *)malloc(array_t::byteSize(newCount)));// 开辟新空间
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList; //将旧值存放在最后的位置
for (unsigned i = 0; i < addedCount; i++) // 从前往后循环插入新值
array()->lists[i] = addedLists[i];
validate();
}
}
算法分析:根据已有数据做不同情况处理
0 -> 1
:无数据且新插入数据为1条
- 直接插入到
list
1 list -> many lists
: 只有1条数据或插入多条数据
- 取出旧值;
- 对
oldCount
赋值:有旧值oldCount=1
否则oldCount=0
;- 计算插入后数据总量
newCount = oldCount + addedCount
;- 开辟新空间;
- 将旧值存放在新空间最后;
- 根据
addedCount
从前往后插入新值;
many lists -> many lists
: 已有多条数据
- 取出旧值;
- 计算插入后数据总量
newCount = oldCount + addedCount
;- 开辟新空间;
- 根据
oldCount
循环从后往前取出旧值,存放在新空间的后面位置;- 根据
addedCount
从前往后插入新值;
根据hasArray
和setArray
判断,只要调用setArray
或array()->count
中的count
值大于0,hasArray
即为YES
。
所有分类加载后,rwe
的methods
结构应该为
分类加载实例探究
在类的加载探索中,我们知道了类的加载时机区分为懒加载类
和非懒加载类
,即是否实现+load
方法,分类的加载我们同样按照懒加载
和非懒加载
的形式探索。
- 主类和分类都为懒加载;
- 主类非懒加载,分类懒加载;
- 主类懒加载,分类非懒加载;
- 主类和分类都非懒加载;
实例类为LGPerson
,就在attachCategories
方法中通过类名mangledName
比较LGPerson
来精确加断点、打印输出调试
1.主类和分类都为懒加载
主类LGPerson
声明实例方法sayHello
和sayByeBye
分类CatA
中声明实例方法sayHello_A
,以及重写主类方法sayHello
在main
函数中调用sayHello
并没有进入attachCategories
函数中,同时可以看到sayHello
是调用的分类
的方法,由此可以知道如果主类
和分类
都未实现+load
方法,分类的方法
等信息是在编译时
就和主类编译在一起了,在类的加载流程中验证
在main
函数中调用LGPerson
的alloc
方法开始加载类,这里的method_list_t
是从ro
中读取的数据,输出查看
此时,list
中的方法数主类和分类的方法集合
,主类方法放在集合的最后位置,但这里方法还没有经过排序处理,通过prepareMethodLists
会对list
进行排序修复
。
通过断点跟进,再输出一下经过fixupMethodList
处理后的list
处理后,主类的sayHello
方法排在了分类sayHello
后面。在调用sayHello
时,消息查找流程在对排序好的list
进行二分法查找,并且会通过while
循环找到最前面的同名方法,这样分类方法
就覆盖了主类方法
。
2.主类非懒加载、分类懒加载
LGPerson
的主类实现+load
方法,分类不实现
在main
函数中调用sayHello
也没有进入attachCategories
函数中,sayHello
是调用的分类
的方法,由此可以知道如果主类
非懒加载和分类
懒加载,分类的方法
等信息也是在编译时
就和主类编译在一起了。
下面在类的加载流程中验证
这里可以看出
LGPerson
是在程序启动时_read_images
实现加载的,- 分类的方法也是
编译时
就和主类方法编译在一起了,这里是通过ro
获取的method_list_t
,分类方法也在ro
中。
3.主类懒加载、分类非懒加载
主类LGPerson
不实现+load
方法,分类实现
运行查看
也没有进入attachCategories
函数中,sayHello
是调用的分类
的方法,由此可以知道如果主类
懒加载和分类
非懒加载,分类的方法
等信息也是在编译时
就和主类编译在一起了。
在类的加载流程中验证
LGPerson
是在程序启动时_read_images
实现加载的,分类非懒加载
会导致主类
被迫成为非懒加载类
;- 分类的方法也是
编译时
就和主类方法编译在一起了,这里是通过ro
获取的method_list_t
,分类方法也在ro
中。
4.主类和分类都为非懒加载
主类LGPerson
和分类CatA
都实现+load
方法,运行查看
通过输出打印可以看出,
主类的加载
和分类的加载
是在不同的流程中
主类加载
可以看出主类的加载是在
_read_images
流程,这里从ro
读取的method_list_t
中只有主类的两个方法。
分类的加载
分类的加载流程是
load_images
->loadAllCategories
->load_categories_nolock
->attachCategories
,在attachCategories
中会创建rwe
,并调用prepareMethodLists
对分类方法进行排序处理,然后调用rwe
中methods
的attachLists
插入分类的mlist
这里的LGPerson
的分类有3个,只在分类CatA
中实现了+load
方法,我们调用分类CatC
的方法
发现加载分类的时候,并没有输出分类CatC
的名字,也就是没有分类CatC
的加载,为什么可以调用sayHello_C
成功了呢?
猜测:分类
CatC
没有实现+load
,会不会是编译时和主类编译在一起了
再跟踪一下主类的加载流程
这里看到,主类
ro
中只有主类自己实现的两个方法,所以猜想不成立。
再跟踪一下分类的加载流程
这里看到,在加载分类
CatB
时,确有分类CatC
的方法,他们的共同点是都没有实现+load
,系统在编译时会把未实现+load
方法的分类合并成一个分类来处理,从而简化分类的加载流程。
我们再增加一个分类CatD
做验证
可以看到,分类加载时把未实现+load
方法的CatB
、CatC
和CatD
一起加载的,验证了系统在编译时会把未实现+load
方法的分类合并成一个分类来处理。
总结
分类的加载原理
和类同样区分为是否为懒加载
,两者组合可分为4种情况
以上是对分类的加载流程
探索过程的总结,难免有不足和错误之处,如有疑问请在评论区留言吧