本页所使用的objc runtime 756.2,来自 Apple 开源文档
类的加载探寻系列:
1、【类的加载】-(1)类的启动
一、懒加载类
1.1 概念
懒加载(lazy loading)又称为延迟加载,它是指系统不会在初始化是就加载某个对象,而是在第一次调用(使用 get 方法)时才加载这个对象到内存。它的实现方法实质上就是覆写该对象的 get 方法,并将该对象在初始化时需要实现的代码在 get 方法中实现。
而对于数据结构而言,惰性加载是指从一个数据对象通过方法获得里面的一个属性对象时,这个对应对象实际并没有随其父数据对象创建时一起保存在运行空间中,而是在其读取方法第一次被调用时才从其他数据源中加载到运行空间中,这样可以避免过早地导入过大的数据对象但并没有使用的空间占用浪费。
1.2 优点
- 不需要在 viewDidLoad 中实例化对象,简化代码,使结构清晰
- 提升初始化加载速度
- 减少内存占用
1.2 区分
通过 load()
方法,在编译器就已经处理好类
1.2.1 非载类的加载步骤:
找到类的指针:
classref_t *classlist =
_getObjc2NonlazyClassList(hi, &count);强转为
Class
类实例for (i = 0; i < count; i++) {
Class cls = remapClass(classlist[i])添加到内存:
addClassTableEntry(cls);
实现非懒加载类——实例化类的信息,如
rw
:realizeClassWithoutSwift(cls);
1.2.2 懒加载类的加载步骤
查找懒加载类的方法
向类发送消息,方法查找该类
lookUpImpOrForward
系统向一个没有实现的类的方法发送消息,通过
isa
查找方法缓存失败后,会进入到慢速查找 方法lookUpImpOrForward
里面,类是否实现过的判断:IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}对方法进行实现
接下来,
realizeClassMaybeSwiftAndLeaveLocked
方法的实现会落实到realizeClassMaybeSwiftMaybeRelock
这个方法,会返回真实的类的结构:static Class
realizeClassMaybeSwiftAndLeaveLocked(Class cls, mutex_t& lock)
{
return realizeClassMaybeSwiftMaybeRelock(cls, lock, true);
}static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
{
lock.assertLocked();
if (!cls->isSwiftStable_ButAllowLegacyForNow()) {
// Non-Swift class. Realize it now with the lock still held.
// fixme wrong in the future for objc subclasses of swift classes
realizeClassWithoutSwift(cls);
if (!leaveLocked) lock.unlock();
} else {
// Swift class. We need to drop locks and call the Swift
// runtime to initialize it.
lock.unlock();
cls = realizeSwiftClass(cls);
assert(cls->isRealized()); // callback must have provoked realization
if (leaveLocked) lock.lock();
}
return cls;
}可以看到,在这个类里面,进到
!cls->isSwiftStable_ButAllowLegacyForNow
这个选择,也就是非swift 类的情况下,对类cls
进行了方法属性的实现,对的,没错,实现放方法,就放在realizeClassWithoutSwift
里,这也是之前讨论过的非懒加载来初次实现的方法。
二、分类及其加载
2.1 分类的结构
为了搞清楚分类在底层的特点,先用代码一步一步来摸索
生成一个分类:
@interface NSObject (Eat) |
输入编译代码,得到相应的.cpp 文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc NSObject+Eat.m |
得到对应的cpp文件
打开这份.cpp 文件,熟悉的内容扑面而来
下面看到的是分类的结构:
其中包含了
- name - 主类的名字
- cls - 分类的名字
- instanceMethods - 实例方法列表
- classMethods - 类方法列表
- protocols - 分类遵循的协议列表
- properties - 属性列表
接下来看其中方法method_list
的结构:
得到了两个结构体,原结构体如下:
static struct /*_method_list_t*/ { |
下面实现的结构体为:
_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = { |
两个结构体,其实是一一对应关系
占用内存:entsize —— sizeof(_objc_method)
方法数量:method_count - 2
方法列表:method_list -
{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_NSObject_Eat_eat},
{(struct objc_selector *)"swallow", "v16@0:8", (void *)_I_NSObject_Eat_swallow}即 eat 和 swallow 两个方法
继续往下看,可以看到分类 _OBJC_$_CATEGORY_NSObject_$_Eat.cls
这个类指针指向了 主类OBJC_CLASS_$_NSObject
的地址
查询源码762,分类的结构如下:
struct category_t { |
2.2 分类的加载流程
2.3.1 初始化
void _objc_init(void) |
2.3.2 处理dyld镜像
void |
2.3.4 解锁处理镜像
void |
可以看到读取镜像后,核心业务代码为_read_images
这个方法, 处理了所有的类。
2.3.5 实现分类方法
进到_read_images
方法,看到关于分类的在这里
这里逐一解释做了什么操作:
将分类与类表中存储。使用的是addUnattachedCategoryForClass 方法,看看具体如何实现的:
如图所示,分别是:获取存放分类的表、分类扩容、分类插入表里。
而分类插入到表里,是 分类——类进行key-value 对应存入的,具体的函数为:
void *NXMapInsert(NXMapTable *table,
const void *key,
const void *value)对方法的重新实现:remethodizeClass,来看看这个方法的实现
这是啥?脚趾头都知道,核心业务方法是
attachCategories(cls, cats, true /*flush caches*/);
,点开继续看。往类里添加分类 - attachCategories
所要做的,都写在上面了,就是做了3件事情:
- 开辟内存空间
- 遍历分类列表,把方法、属性、协议写到3组数组里
- 通过attatchList 方法, 依次写入到类的rw 的methods、properties、protocols 里
attachLists方法 - 粘贴方法/属性/协议的具体实现:
具体的代码如下:
这里可以看到,往类里添加分类的方法、属性、协议,一般有3种情况,处理如下:
- 多对多。处理方式是为新内容创建空间,将旧内容平移到后面,将新内容拷贝放置最前面。
- 0对1。即之前没有方法、属性或协议。这种比较简单,直接插入即可。
- 一对多。这种和多对多类似,将新的内容拷贝放入内存最前面
内存移动和拷贝
这里着重分析一下,平移,与拷贝。在C函数里,他们是这样的
/* dst : destination :目标地址
src : source :被移动的内存首地址
n : 被移动的内存长度
memmove 将 src内存中移动 len长度部分到 dest
memcpy 将 src内存中拷贝 n 长度部分到 dest
*/
void *memcpy(void *__dst, const void *__src, size_t __n);
void *memmove(void *__dst, const void *__src, size_t __len);第一步平移操作:
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));- dst:array()->lists + addedCount,即总数组的长度
- src:被移动的数组为原内容(方法列表、属性、协议)
- n:移动了的部分:array()->lists,即偏移原来的部分
第二步操作
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));- dst:array()->lists + addedCount,即总数组的长度
- src:被拷贝的数组为原内容(方法列表、属性、协议)
- len:拷贝了的部分:array()->lists,即偏移原来的部分
从这张图,可以清晰的看出,分类中的方法、属性和协议的添加次序,都是讲旧的移走,新的方法排最前面。
这也解释了为何分类会覆盖类原有的方法,原理是因为相应的方法、属性和协议,在内存段中,排列靠前。
2.3 分类与懒加载的搭配
2.2.1 分类实现load 方法(非懒分类)
懒加载的类 + 非懒加载的分类
先实现非懒加载类的分类
具体是读取镜像
read_images
以及:
addUnattachedCategoryForClass
将读到的分类插入到分类的表里,备用请见代码实现:
static void addUnattachedCategoryForClass(category_t *cat, Class cls,
header_info *catHeader)
{
runtimeLock.assertLocked();
// DO NOT use cat->cls! cls may be cat->cls->isa instead
NXMapTable *cats = unattachedCategories();
category_list *list;
list = (category_list *)NXMapGet(cats, cls);
if (!list) {
list = (category_list *)
calloc(sizeof(*list) + sizeof(list->list[0]), 1);
} else {
list = (category_list *)
realloc(list, sizeof(*list) + sizeof(list->list[0]) * (list->count + 1));
}
list->list[list->count++] = (locstamped_category_t){cat, catHeader};
NXMapInsert(cats, cls, list);
}分类督促懒加载类实现,不然分类无处依附。
这里使用的是
prepare_load_methods
方法实现主类后,将分类粘贴到主类上
先进行
realizeClassWithoutSwift
,然后unattachedCategoriesForClass
贴到主类上具体的实现步骤如下:
read_images
prepare_load_methods
realizeClassWithoutSwift
unattachedCategoriesForClass
非懒加载的类 + 非懒加载的分类
直接编译获取到。
这种情况是最普遍的,就是直接取 class 里的data()->ro,在ro 里找到对应的方法
获取到过程是:
- read_images - 读取镜像
- realizeClassWithoutSwift(实现非懒加载类)
- methodizedClass - 实现方法
- unattachedCategoriesForClass - 把内存里的类插入到表里
- attachCategories - 往类后面粘贴分类结构
2.2 分类未实现load 方法(懒分类)
如果分类并不主动实现 +load() 方法,就由编译时实现类与分类的查找实现
非懒加载类 + 懒加载的分类
通过镜像来查找实现
- 读取镜像 read_images
- realizeClassWithoutSwift(实现非懒加载类)
- methodizedClass
- 读取类里的 data->ro 信息
懒加载的类 + 懒加载的分类
因为懒加载的类在编译时并不会主动实现,所以通过方法查找一步一步找到
- 方法查找的消息传递 - lookupimporforward
- realizeClassWithoutSwift
- methodizedClass
2.3 总结
懒加载的分类: 编译时就已经决定处理好。
非懒加载分类:运行时再来处理。
以上和懒加载的类是刚刚相反。