「OC」源码学习——类和分类的加载
文章目录
前言
+load
和 +initialize
是两个常用的类方法,它们在不同的时间点被调用,并且有不同的用途。在消息转发的流程之中,有一个很重要的方法就是lookUpImpOrForward
,其中使用了realizeAndInitializeIfNeeded_locked
对懒加载类进行初始化,而经过一系列的调用其实就会调用realizeClassWithoutSwift
的方法
realizeClassWithoutSwift
的测试
探究懒加载和非懒加载进入实现的时机
测试一
JCClass没有实现 + load
方法,是懒加载类,主程序调用[JCClass alloc]
的初始化方法。走lookUpImpOrForward
去实现类的实现
结论:懒加载类会在第一次调用的时候进行加载,加载的时机是在消息查找流程中的lookUpImpOrForward方法中。
测试二
父类JCClass实现 + load
方法,子类JCSubClass不实现 + load
方法。清理缓存,主程序子类
调用初始化方法。
结论:父类实现
+ load
, 子类不实现+ load
。父类是非懒加载类,子类是懒加载类。
测试三
父类JCClass不实现 + load
方法,子类JCSubClass实现 + load
方法。清理缓存,主程序子类
先调用的初始化方法,父类
再调用的初始化方法。
发现父类没有进入!cls->isRealized()
, 父类是懒加载类。因为递归调用realizeClassWithoutSwift
完善继承链并处理当前类的父类、元类;如果有父类,就通过addSubclass
把当前类放到父类的子类列表中去
结论:如果子类实现
+ load
,那么父类也会在子类被加载的时候,一起被加载。原因是子类在加载的时候会对父类和元类进行处理。
+load
方法设计
+load方法在类或分类加载到内存之中进行一次性的初始化操作,例如:在load方法之中实现方法交换。因为这个应用程序的生命周期早期完成,这种操作不需要重复进行,只需调用一次,这个方法运行时间过早,可能有些类还没加载,我这个我们无法保证
调用时机
当OC在运行,类和分类被加载的时候,就会调用,_objc_init
对函数进行初始化,在map_images
调用之后,使用load_images
函数进行加载,而在load_images
内部,调用流程如下
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// 如果这里没有+load方法,则不带锁返回。
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// 发现 load 方法
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh); // 准备load方法 - 加载load到内存
}
// 调用 +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
+load
被加载到内存:prepare_load_methods
+load
被调用:call_load_methods
往下步入函数
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
lockdebug::assert_locked(&loadMethodLock);
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
static void call_class_loads(void)
{
int i;
/* 1. 分离当前可加载的类列表 */
struct loadable_class *classes = loadable_classes; // 获取全局待加载类列表
int used = loadable_classes_used; // 获取已使用的类数量
loadable_classes = nil; // 清空全局类列表指针
loadable_classes_allocated = 0; // 重置已分配内存大小
loadable_classes_used = 0; // 重置已使用计数器
/* 2. 遍历执行所有+load方法 */
for (i = 0; i < used; i++) {
Class cls = classes[i].cls; // 获取类对象
load_method_t load_method = (load_method_t)classes[i].method; // 类型转换为函数指针
if (!cls) continue;
// 调试模式下打印加载日志
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, @selector(load)); // 直接调用类方法
}
/* 3. 释放分离出来的类列表内存 */
if (classes) free(classes);
}
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// 分离当前可加载分类列表
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil; // 清空全局分类列表指针
loadable_categories_allocated = 0; // 重置已分配内存大小
loadable_categories_used = 0; // 重置已使用计数器
// 遍历调用所有分离出的分类的 +load 方法
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat); // 获取分类所属的类对象
if (cls && cls->isLoadable()) { // 检查类是否可加载
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, @selector(load)); // 直接调用分类的 +load 方法
cats[i].cat = nil; // 标记该分类已处理
}
}
// 压缩列表(保留顺序,移除已处理的分类)
shift = 0;
for (i = 0; i < used; i++) {
if (cats[i].cat) {
cats[i - shift] = cats[i]; // 前移未处理的分类
} else {
shift++; // 记录已处理分类的空位数量
}
}
used -= shift; // 更新有效分类数量
// 合并新增的可加载分类到分离的列表中
new_categories_added = (loadable_categories_used > 0);
for (i = 0; i < loadable_categories_used; i++) {
if (used == allocated) { // 动态扩容策略
allocated = allocated * 2 + 16;
cats = (struct loadable_category *)realloc(cats, allocated * sizeof(struct loadable_category));
}
cats[used++] = loadable_categories[i]; // 追加新增分类
}
// 释放旧全局列表内存
if (loadable_categories) free(loadable_categories);
// 重新附加更新后的列表(若仍有未处理分类)
if (used) {
loadable_categories = cats;
loadable_categories_used = used;
loadable_categories_allocated = allocated;
} else { // 无剩余分类则释放内存
if (cats) free(cats);
loadable_categories = nil;
loadable_categories_used = 0;
loadable_categories_allocated = 0;
}
return new_categories_added; // 返回是否有新增分类
}
我们可以看到,+load
方法的调用不是通过消息转发的流程来进行的,我们直接获取他的IMP来进行调用。同样也不难看出,我们在分类重写+load
的时候,会先加载原本重写+load
,再加载分类之中重写的+load
,我们用例子展示一下。
- load方法在程序启动的时候自动调用——即在
read_images
进行类的实现之后 - 先调用类的
+load
方法,再调用分类的方法 - 如果有多个分类重写
+load
方法,分类的load方法是按照编译顺序执行 - 每个类的 load 方法和每个分类的 load 方法只会调用一次。
+initialize
顾名思义,+initialize
就是类的初始化方法,会在类的初次调用的时候自动调用,在这个时候系统的初始化完成,可以执行一些关联其他类的操作。+initialize
方法是通过objc_msgSend()
进行调用的。那我们看看我们源码,通过一系列方法的溯源,我们可以看到
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
asm("");
}
initialize方法的调用是线程安全的,即在多线程环境中,initialize方法只会被调用一次,不会产生竞争条件。
调用顺序
我们分别在子类和父类之中实现initialize
方法,以及在父类实现子类不实现,分类实现initialize
方法,查看一下调用流程
子类不实现:
父类和子类都实现:
实现分类(父类分类和类之中都实现,子类不实现)
归纳
-
initialize
走普通的消息发送机制。所以分类覆盖主类,当有多个分类都实现了initialize
方法,执行最后被加载到内存中的分类的方法。 -
initialize
在类或者其子类的第一个方法被调用前(发送消息前)调用 -
如果父类和子类都实现了
initialize
方法,在调用子类时,-
如果
父类的initialize
方法调用过,则只调用子类的initialize
方法; -
如果
父类的initialize
没用过,则先调用父类的initialize
方法,在调用子类的initialize
方法。(此时,再初始化父类的时候,不会再调用initialize方法)
-
-
父类实现,子类不实现,调用子类时,会调用两次
父类的initialize
方法
源码实现
void initializeNonMetaClass(Class cls)
{
// 确保当前类是非元类(普通类)
ASSERT(!cls->isMetaClass());
// 强制父类先完成初始化(避免死锁)
Class supercls = cls->getSuperclass();
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls); // 递归初始化父类
}
// 获取当前类的初始化锁(线程安全)
lockClass(cls);
// 三种可能的初始化状态:
// 1. 已初始化:直接返回
// 2. 正在初始化:
// A. 当前线程已持有锁(可重入)
// B. 在fork子进程中,父进程线程已终止
// 3. 首次初始化:当前线程赢得竞争
if (cls->isInitialized()) {
unlockClass(cls);
return;
}
if (cls->isInitializing()) {
if (!MultithreadedForkChild || _thisThreadIsInitializingClass(cls)) {
unlockClass(cls);
return;
} else {
// 处理fork子进程中的初始化竞争
lockClass(cls);
_setThisThreadIsInitializingClass(cls);
performForkChildInitialize(cls, supercls);
}
}
// 设置类为初始化中状态,并收集回调函数
cls->setInitializing();
SmallVector<_objc_willInitializeClassCallback, 1> localWillInitializeFuncs;
{
mutex_locker_t lock(classInitLock);
localWillInitializeFuncs.initFrom(willInitializeFuncs);
}
_setThisThreadIsInitializingClass(cls);
if (MultithreadedForkChild) {
// 在fork子进程中跳过+initialize调用
performForkChildInitialize(cls, supercls);
return;
}
// 触发所有注册的"will initialize"回调
for (auto callback : localWillInitializeFuncs)
callback.f(callback.context, cls);
// 调用类的+initialize方法(可能递归触发父类)
@try {
callInitialize(cls); // 实际调用+initialize方法
} @catch (...) {
// 异常处理:标记初始化完成
@throw;
} @finally {
// 完成初始化并释放锁
lockAndFinishInitializing(cls, supercls);
}
}
分类与类
分类的加载通过查看源码我们知道,实现加载分类全部都在realizeClassWithoutSwift
这个方法之中完成
懒加载类与非懒加载类的实现在于,他们是否对+load
方法进行重写,如果重写的话非懒加载类就会在map_images
的调用类的实现,将rw
的内容提前加载。
懒加载类 + 懒加载分类
我们可以看到我们的懒加载类是在main函数调用之后,通过消息转发到realizeClassWithoutSwift
之中完成类的实现
非懒加载类+懒加载分类
我们可以看到这个类的实现在main
函数调用之前就调用了,我们现在需要看的是分类的方法是否被添加到其中
通过ro打印出来的方法列表数量为2,我们此时可以断定了类和分类的方法都被加入到了ro之中
结论:不管是懒加载类或是非懒加载类,懒加载分类的方法在编译时就确定了。
懒加载类+非懒加载分类
根据我们之前学习的流程,我们可以知道懒加载类是在第一次发送消息的时候才被加载,我们可以看到流程应该与懒加载类 + 懒加载分类
的实现一样
即使主类未实现 +load
方法,只要其 任意一个分类实现了 +load
,编译器就会将主类标记为 非延迟加载类(Non-Lazy Class),并写入 Mach-O 的 __objc_nlclslist
段。在_getObjc2NonlazyClassList
获取非懒加载类
列表,read_image阶段就对类进行了实现。
非懒加载类+非懒加载分类
不难看出调用栈和之前的非懒加载类没有什么区别,都是在read_images
之中进行实现
我们接下来要查看的是,我们的分类方法这时候有没有被加入到ro
之中,很明显count = 1,分类的方法没有被加入到ro
之中。我们知道,非懒加载的分类需要通过 attachCategories
函数动态附加到类的方法列表中,而这一过程会创建或更新 rwe
。带这个这个合理的猜测我们在源码之中进行一下实验
具体的源码流程我们可以在attachCategories
,加入以下代码进行调试,加上断点看看是否能进入这个内容。
const char *className = cls->mangledName();
const char *targetName = "JCClass";
bool isTargetClass = (className && strcmp(className, targetName) == 0);
if (isTargetClass) {
printf("===== Attaching categories to JCClass =====\n");
printf("Class address: %p\n", cls);
printf("Category count: %u\n", cats_count);
printf("Is metaclass? %s\n", (flags & ATTACH_METACLASS) ? "YES" : "NO");
// 触发断点(调试时启用)
// asm("int3");
}
经过我的测试,就是只有类和分类都为非懒加载时JCClass
才会进入这个方法的断点,与我们之前的推测相符
小结
- 只有在非懒加载类和非懒加载分类的情况下,
ro
保存了本类的,rwe
保存本类和分类的; 其它情况下ro
保存了本类和分类的,没有rwe
。 Runtime API
对类进行更改也会产生rwe
。
懒加载类调用流程
这里给出两种不同的类的具体调用流程,有兴趣的读者可以通过源码自行进行调试
map_images
-> map_images_nolock
-> _read_images
-> 类对象第一次接收到消息
-> cache里没有找到 _objc_msgSend_uncached
-> lookUpImpOrForward
-> realizeClassMaybeSwiftMaybeRelock
-> 加载类 realizeClassWithoutSwift
-> 分类添加到类 methodizeClass
非懒加载类调用流程
map_images
-> map_images_nolock
-> _read_images
-> 发现类 readClass
-> 加载类 realizeClassWithoutSwift
-> 分类添加到类 methodizeClass
-> attachCategories