一、引言
上篇文章讲述的是类的加载中的本类加载,本文将接着探索、反推分类的加载。在此之前,先了解几个概念。
####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,因为它是只读的。
    013-iOS底层原理-类的加载(category) - 图9
  • 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中,存储了只有在运行时才会生成的新数据。013-iOS底层原理-类的加载(category) - 图10
由于ro是只读的,我们要追踪类的更多信息,就得在rw中进行,这样的结果会导致内存占用得比较多,因为一个工程里少则一百多个类,多则上万个类。而在Apple的测试中,大概只有10%的类用到runtime去更改它们的方法。因此可以将rw中不常用的部分(属性、方法、协议),分离出来成为class_rw_ext_t,即rwe。这个操作可以减小rw一半的大小。对于那些确实需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它加入类中,让它使用。
rw拆分如图所示:013-iOS底层原理-类的加载(category) - 图11

2、【小结】
ro:数据是只读(read only)的,为clean Memory。从磁盘加载到内存中读取,ro的数据在编译的时候就已经确定了。
rw:数据是可读可写(read write)的,为dirty Memory。rw的数据存放的是运行时动态修改的数据。初始数据是从ro中copy一份到rw的。
rwe:对rw的拓展,优化rw。在视频介绍中可知,并不是每个类都会在运行时改变属性、方法、协议。而rwe会标记处理,针对那些不需要改变内容的数据,就去ro读取,那些需要改变内容的就去rw读取。
三者关系,如图所示:013-iOS底层原理-类的加载(category) - 图12

二、category底层原理
####2.1 oc转cpp
添加一个category,源码如下:

  1. QLPerson+NB1.h
  2. #import "QLPerson.h"
  3. @interface QLPerson (NB1)<NSObject>
  4. @property (nonatomic,copy) NSString *cat_name;
  5. @property (nonatomic,assign) int cat_age;
  6. - (void) cat_test1;
  7. - (void) cat_test2;
  8. + (void) cat_test;
  9. @end
  10. QLPerson+NB1.m
  11. #import "QLPerson+NB1.h"
  12. @implementation QLPerson (NB1)
  13. - (void) cat_test1{
  14. NSLog(@"%s",__func__);
  15. }
  16. - (void) cat_test2{
  17. NSLog(@"%s",__func__);
  18. }
  19. + (void) cat_test{
  20. NSLog(@"%s",__func__);
  21. }
  22. @end

打开终端,cd 到.m文件目录,通过以下命令,将QLPerson+NB1.m转换编译源码QLPerson+NB1.cpp,打开.cpp文件。

  1. xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc QLPerson+NB1.m -o NB1.cpp

2.1.1、分类底层结构:
搜索QLPerson_找到部分编译后的源码如下:

  1. static struct _category_t _OBJC_$_CATEGORY_QLPerson_$_NB1 __attribute__ ((used, section ("__DATA,__objc_const"))) =
  2. {
  3. "QLPerson", // 分类名(此处为什么是类名?因为这是编译阶段,还未到运行时,所以暂时用类名代替)
  4. 0, // &OBJC_CLASS_$_QLPerson, 类(此处为什么是0?因为这是编译阶段,还未到运行时,所以暂时用0代替)
  5. (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_QLPerson_$_NB1, // 实力方法列表
  6. (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_QLPerson_$_NB1, // 类方法列表(**为什么分类有类方法?因为分类没有元类!!**)
  7. (const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_QLPerson_$_NB1, // 协议列表
  8. (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_QLPerson_$_NB1, // 属性列表
  9. };

这个数据结构,对应着category_t的结构,明细如下:
为什么分类有类方法?因为分类没有元类!!

  1. struct _category_t {
  2. const char *name;
  3. struct _class_t *cls;
  4. const struct _method_list_t *instance_methods;
  5. const struct _method_list_t *class_methods;// 为什么分类有类方法?因为分类没有元类
  6. const struct _protocol_list_t *protocols;
  7. const struct _prop_list_t *properties;
  8. };

2.1.2、实例方法、类方法:
013-iOS底层原理-类的加载(category) - 图13
2.1.3、协议:
013-iOS底层原理-类的加载(category) - 图14
2.1.4、属性:
013-iOS底层原理-类的加载(category) - 图15
如上图所示,我们在category内部定义的属性cat_name,cat_age并没有像本类一样实现,是因为底层编译成.cpp没有实现对应的getter/setter方法。会想我们之前的类的探索的时候,将QLPerson.m编译后成.cpp的文件,内部定义的属性,有实现getter/setter方法。对于category内的属性,可以通过关联对象来设置。
013-iOS底层原理-类的加载(category) - 图16
####2.2 objc源码
打开objc4_818_2工程,搜索category_t {,找到category源码,如下:

  1. struct category_t {
  2. const char *name;
  3. classref_t cls;
  4. WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
  5. WrappedPtr<method_list_t, PtrauthStrip> classMethods;
  6. struct protocol_list_t *protocols;
  7. struct property_list_t *instanceProperties;
  8. // Fields below this point are not always present on disk.
  9. struct property_list_t *_classProperties;
  10. method_list_t *methodsForMeta(bool isMeta) {
  11. if (isMeta) return classMethods;
  12. else return instanceMethods;
  13. }
  14. property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
  15. protocol_list_t *protocolsForMeta(bool isMeta) {
  16. if (isMeta) return nullptr;
  17. else return protocols;
  18. }
  19. };

小结
categoryoc类一样,本质都是结构体

  • name:该category的本类名字;
  • cls:该category的本类cls
  • instanceMethods:实例方法列表
  • classMethods:类方法列表
  • protocols:协议列表
  • properties:属性列表,无getter / setter方法,需要通过关联对象来实现。
  • category 没有元类,所以category_t才会有instanceMethodsclassMethods
    ###二、category的加载
    ####【1】rwe的赋值
    1、根据前面rwe的介绍,category在类加载到内存的时候,会加载到class_rw_ext_t,在上一篇文章,分析到类的实现realizeClassWithoutSwift()methodizeClass()时,auto rwe = rw->ext();,进入ext()源码。013-iOS底层原理-类的加载(category) - 图17
    2、extAllocIfNeeded()
    如上图所示,rweextAllocIfNeeded()调用的时候赋值的,即先从从内存中查找,如果不存在,则直接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()处理完本类的数据以及rwemethods,properties,protocols通过attachLists()保存到表中,并addMethod()保存类的数据后,将category通过attachToClass()加入本类中。做了如下处理:
    013-iOS底层原理-类的加载(category) - 图18
    attachToClass()源码如下:
    013-iOS底层原理-类的加载(category) - 图19
    ####【3】attachCategories()
    ######【3.1】主类+load,分类+load
    我们在主类和分类中都实现+load方法,
    然后在attachCategories()源码中,加入我们自定义的逻辑,让断点停下来。
    如图所示:013-iOS底层原理-类的加载(category) - 图20
    我们将断点继续往下,在for循环中,遍历所有category,将其方法、属性、协议等加入到rwe中。如图所示:013-iOS底层原理-类的加载(category) - 图21
    【小结3.1】:主类+load,分类+load
    流程如下**_dyld_objc_notify_register()****load_images()****loadAllCategories()****load_categories_nolock()****attachCategories()**
    ######【3.2】主类未实现+load,分类实现+load
    013-iOS底层原理-类的加载(category) - 图22
    【小结3.2】:主类未实现+load,分类实现+load
    **_dyld_objc_notify_register()****map_images()****map_images_nolock()****_read_images()****realizeClassWithoutSwift()****methodizeClass()****attachToClass**。未调用attachCategories()
    ######【3.3】主类实现+load,分类未实现+load
    013-iOS底层原理-类的加载(category) - 图23
    【小结3.3】:主类实现+load,分类未实现+load
    **_dyld_objc_notify_register()****map_images()****map_images_nolock()****_read_images()****realizeClassWithoutSwift()****methodizeClass()****attachToClass**。未调用attachCategories()
    ######【3.4】主类未实现+load,分类未实现+load
    013-iOS底层原理-类的加载(category) - 图24
    【小结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中是否有分类。如图所示:013-iOS底层原理-类的加载(category) - 图25
LLDB调试如下:
013-iOS底层原理-类的加载(category) - 图26
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条件:013-iOS底层原理-类的加载(category) - 图27
#####【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、类和分类的懒、非懒加载多种情况搭配的加载如图所示:013-iOS底层原理-类的加载(category) - 图28
2、由于实现(不管是主类还是分类)load的时候,底层是非常耗时的、复杂的过程。因此在开发过程中,尽量少在自定义的类和分类中去实现load方法。
3、这篇博客真难写,写的差强人意,后面会花时间去补充完整,调试的过程贴出来。