013-iOS底层原理-类的加载(category)

一、引言

上篇文章讲述的是类的加载中的本类加载,本文将接着探索、反推分类的加载。在此之前,先了解几个概念。

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)<NSObject>
@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、属性:
QLPerson+NB1.cpp,无getter/setter

如上图所示,我们在category内部定义的属性cat_name,cat_age并没有像本类一样实现,是因为底层编译成.cpp没有实现对应的getter/setter方法。会想我们之前的类的探索的时候,将QLPerson.m编译后成.cpp的文件,内部定义的属性,有实现getter/setter方法。对于category内的属性,可以通过关联对象来设置。
QLPerson.cpp,有getter/setter

2.2 objc源码

打开objc4_818_2工程,搜索category_t {,找到category源码,如下:

struct category_t {
    const char *name;
    classref_t cls;
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
    WrappedPtr<method_list_t, PtrauthStrip> 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;
    }
};

小结

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()源码。


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()加入本类中。做了如下处理:


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、这篇博客真难写,写的差强人意,后面会花时间去补充完整,调试的过程贴出来。

推荐阅读更多精彩内容