阅读视图
iOS底层之分类的加载
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种情况
以上是对分类的加载流程探索过程的总结,难免有不足和错误之处,如有疑问请在评论区留言吧