一、引言
上篇文章讲述的是类的加载中的本类加载
,本文将接着探索、反推分类的加载
。在此之前,先了解几个概念。
1、脏内存、干净内存、rw、ro、rwe
在上一篇文章012-iOS底层原理-类的加载中探索到的realizeClassWithoutSwift ()
->methodizeClass ()
中,多次出现rw,ro,rwe
,他们分别代表什么。
根据Apple官方视频Advancements in the Objective-C runtime介绍可知:
-
clean memory
:是指加载后不会发生改变的内存。class_ro_t
就是属于clean memory
,因为它是只读的。
dirty memory
: 指的是进程运行时会发生改变的内存。类的结构一经使用,就会变成dirty memory
,因为在运行时会对其写入新的数据。例如创建一个新的方法缓存,并从类中指向它。
dirty memory
要比clean memory
贵的多,因为只要进程一运行,它就必须一直存在。另一个方面,clean memory
可以进行移除,从而节省更多的内存空间,换句话说就是,如果你需要clean memory
中的数据,系统可以直接从磁盘中重新加载进来。
iOS不用swap,所以dirty memory
在iOS中的代价就很大。因此dirty memory
是这个类被分成两部分的原因。可以保持清洁的数据越多越好,通过分离出那些不会改变的数据,就可以把大部分的类数据存储在clean memory
中。虽然这部分数据可以让我们了解类,但是我们需要在运行时追踪每个类的更多信息。当一个类第一次使用,runtime
会为这个类分配额外的内存,这就是class_rw_t
,用于读写数据。在rw
中,存储了只有在运行时才会生成的新数据。
由于
ro
是只读的,我们要追踪类的更多信息,就得在rw
中进行,这样的结果会导致内存占用得比较多,因为一个工程里少则一百多个类,多则上万个类。而在Apple的测试中,大概只有10%的类用到runtime去更改它们的方法。因此可以将rw
中不常用的部分(属性、方法、协议),分离出来成为class_rw_ext_t
,即rwe
。这个操作可以减小rw
一半的大小。对于那些确实需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它加入类中,让它使用。
rw拆分
如图所示:
2、【小结】
ro
:数据是只读(read only)的,为clean Memory
。从磁盘加载到内存中读取,ro的数据在编译的时候就已经确定了。
rw
:数据是可读可写(read write)的,为dirty Memory
。rw的数据存放的是运行时动态修改的数据。初始数据是从ro
中copy一份到rw
的。
rwe
:对rw的拓展,优化rw。在视频介绍中可知,并不是每个类都会在运行时改变属性、方法、协议。而rwe会标记处理,针对那些不需要改变内容的数据,就去ro读取,那些需要改变内容的就去rw读取。
三者关系,如图所示:
二、category底层原理
2.1 oc转cpp
添加一个category,源码如下:
QLPerson+NB1.h
#import "QLPerson.h"
@interface QLPerson (NB1)
@property (nonatomic,copy) NSString *cat_name;
@property (nonatomic,assign) int cat_age;
- (void) cat_test1;
- (void) cat_test2;
+ (void) cat_test;
@end
QLPerson+NB1.m
#import "QLPerson+NB1.h"
@implementation QLPerson (NB1)
- (void) cat_test1{
NSLog(@"%s",__func__);
}
- (void) cat_test2{
NSLog(@"%s",__func__);
}
+ (void) cat_test{
NSLog(@"%s",__func__);
}
@end
打开终端,cd 到.m文件目录,通过以下命令,将QLPerson+NB1.m
转换编译源码QLPerson+NB1.cpp
,打开.cpp文件。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc QLPerson+NB1.m -o NB1.cpp
2.1.1、分类底层结构:
搜索QLPerson_
找到部分编译后的源码如下:
static struct _category_t _OBJC_$_CATEGORY_QLPerson_$_NB1 __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"QLPerson", // 分类名(此处为什么是类名?因为这是编译阶段,还未到运行时,所以暂时用类名代替)
0, // &OBJC_CLASS_$_QLPerson, 类(此处为什么是0?因为这是编译阶段,还未到运行时,所以暂时用0代替)
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_QLPerson_$_NB1, // 实力方法列表
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_QLPerson_$_NB1, // 类方法列表(**为什么分类有类方法?因为分类没有元类!!**)
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_QLPerson_$_NB1, // 协议列表
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_QLPerson_$_NB1, // 属性列表
};
这个数据结构,对应着category_t
的结构,明细如下:
为什么分类有类方法?因为分类没有元类!!
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;// 为什么分类有类方法?因为分类没有元类
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
2.1.2、实例方法、类方法:
2.1.3、协议:
2.1.4、属性:
如上图所示,我们在category内部定义的属性
cat_name,cat_age
并没有像本类一样实现,是因为底层编译成.cpp
没有实现对应的getter/setter方法
。会想我们之前的类的探索的时候,将QLPerson.m
编译后成.cpp
的文件,内部定义的属性,有实现getter/setter方法
。对于category
内的属性,可以通过关联对象
来设置。
2.2 objc源码
打开objc4_818_2
工程,搜索category_t {
,找到category源码,如下:
struct category_t {
const char *name;
classref_t cls;
WrappedPtr instanceMethods;
WrappedPtr 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;
}
};
小结
category
和oc类
一样,本质都是结构体
,
-
name
:该category的本类名字; -
cls
:该category的本类cls -
instanceMethods
:实例方法列表 -
classMethods
:类方法列表 -
protocols
:协议列表 -
properties
:属性列表,无getter / setter方法
,需要通过关联对象
来实现。 -
category
没有元类,所以category_t
才会有instanceMethods
和classMethods
二、category的加载
【1】rwe的赋值
1、根据前面rwe
的介绍,category
在类加载到内存的时候,会加载到class_rw_ext_t
,在上一篇文章,分析到类的实现realizeClassWithoutSwift()
→methodizeClass()
时,auto rwe = rw->ext();
,进入ext()
源码。
2、extAllocIfNeeded()
如上图所示,
rwe
在extAllocIfNeeded()
调用的时候赋值的,即先从从内存中查找,如果不存在,则直接extAlloc()
。
全局搜索
extAllocIfNeeded
,查看调用此函数的地方如下:
-
attachCategories()
(重点) demangledName()
class_setVersion()
addMethods_finish()
class_addProtocol()
_class_addProperty()
-
objc_duplicateClass()
其中,attachCategories()
为category
相关的函数,因此我们继续探索attachCategories()
,全局搜索attachCategories
,只有以下几个地方调用此函数: -
attachToClass()
→attachCategories()
-
load_categories_nolock()
→attachCategories()
【2】attachToClass()
在attachToClass()
源码中,加入我们自定义的逻辑,让断点停下来。
在methodizeClass()
处理完本类的数据以及rwe
将methods,properties,protocols
通过attachLists()
保存到表中,并addMethod()
保存类的数据后,将category
通过attachToClass()
加入本类中。做了如下处理:
attachToClass()
源码如下:
【3】attachCategories()
【3.1】主类+load,分类+load
我们在主类和分类中都实现+load方法,
然后在attachCategories()
源码中,加入我们自定义的逻辑,让断点停下来。
如图所示:
我们将断点继续往下,在for循环中,遍历
所有category
,将其方法、属性、协议等加入到rwe
中。如图所示:
【小结3.1】:主类+load,分类+load
流程如下
_dyld_objc_notify_register()
→load_images()
→loadAllCategories()
→load_categories_nolock()
→attachCategories()
【3.2】主类未实现+load,分类实现+load
【小结3.2】:主类未实现+load,分类实现+load
_dyld_objc_notify_register()
→map_images()
→map_images_nolock()
→_read_images()
→realizeClassWithoutSwift()
→methodizeClass()
→attachToClass
。未调用attachCategories()
!
【3.3】主类实现+load,分类未实现+load
【小结3.3】:主类实现+load,分类未实现+load
_dyld_objc_notify_register()
→map_images()
→map_images_nolock()
→_read_images()
→realizeClassWithoutSwift()
→methodizeClass()
→attachToClass
。未调用attachCategories()
!
【3.4】主类未实现+load,分类未实现+load
【小结3.4】:主类未实现+load,分类未实现+load
懒加载类
推迟到第一次消息发送:
alloc()
→objc_alloc()
→callAlloc()
→objc_msgSend()
→lookUpImpOrForward()
→realizeAndInitializeIfNeeded_locked()
→initializeAndLeaveLocked()
→initializeAndMaybeRelock()
→realizeClassMaybeSwiftAndUnlock()
→realizeClassMaybeSwiftMaybeRelock()
→realizeClassWithoutSwift()
→methodizeClass()
→attachToClass()
。未调用attachCategories()
!
【4】Category加载时机
【4.1】:主类+load,分类+load(非懒加载)
根据【3.1】小结的流程,我们在load_categories_nolock()
中加入自定义逻辑,停下断点,通过lldb调试
查看ro中是否有分类。如图所示:
LLDB调试如下:
1、打印
category_t
内部结构,打印name
,在oc源码编译成c++时,会将name赋值为类名
。在运行时,则将name赋值为分类名
。
2、打印
cls
,为本类
3、打印
instanceMethods
:打印出实例方法cat_test1,cat_test2
4、调用栈为:
_dyld_objc_notify_register()
→load_images()
→loadAllCategories()
→load_categories_nolock()
5、调用
attachCategories()
为类已实现是的if条件:
【4.1小结】
主类和分类都为非懒加载
的情况下,主类的加载流程,根据012-iOS底层原理-类的加载的描述,
主类加载流程:_dyld_objc_notify_register()
→map_images()
→ map_images_nolock()
→ _read_images()
→ readClass()(保存类名+地址)
→ realizeClassWithoutSwift()(配置data())
→methodizeClass()
→ attachToClass()
接着进入分类加载流程:_dyld_objc_notify_register()
→load_images()
→loadAllCategories()
→load_categories_nolock()
→attachCategories()
【4.2】主类未实现+load,分类实现+load
不管主类是否实现load
方法,只要分类实现了load,就会要求主类提前加载(即非懒加载)。与【4.1】一样,分类的数据也是从Mach-O
加载到内存后,通过cls->data()
中获取的,即在编译时期就已经完成。
【4.3】主类实现+load,分类未实现+load(非懒加载)
与【4.1】一样,分类的数据也是从Mach-O
加载到内存后,通过cls->data()
中获取的,即在编译时期就已经完成。
【4.4】主类未实现+load,分类未实现+load
根据【小结3.4】流程,懒加载
的主类和分类,推迟到第一次消息发送
的时候加载,与【4.1】一样,分类的数据也是从Mach-O
加载到内存后,通过cls->data()
中获取的,即在编译时期就已经完成。
【4.5】主类未实现+load,2个以上
分类实现+load(非懒加载)
流程如下:load_images()
→loadAllCategories()(加载分类)
→load_categories_nolock()(处理分类数据)
→prepare_load_methods()(准备)
→realizeClassWithoutSwift()(实现类)
→methodizeClass()
→attachToClass()
→attachCategories()(添加分类)
【4.5】主类未实现+load,1个
分类实现+load,剩余分类未实现load
调试结果,与【4.2】一致
【总结】
1、类和分类的懒、非懒加载多种情况搭配的加载如图所示:2、由于实现(不管是主类还是分类)load的时候,底层是非常耗时的、复杂的过程。因此在开发过程中,尽量少在自定义的类和分类中去实现load方法。
3、这篇博客真难写,写的差强人意,后面会花时间去补充完整,调试的过程贴出来。