iOS runtime 源码分析 + load 和 + initialize 原理讲解和总结
load 源码分析
runtime 源码从官网上面可以下载到,下面是我下载的objc4-756.2版本的runtime源码
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
classref_t *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
schedule_class_load(remapClass(classlist[i]));
}
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
这些代码做了什么呢?首先是准备好被调用的类和分类,
static void schedule_class_load(Class cls)
{
if (!cls) return;
assert(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
可以看到一个很关键的代码 schedule_class_load(cls->superclass);
又递归调用了,含义就是每次都回去查找父类,然后调用 add_class_to_loadable_list
方法,将类添加到 loadable_classes
存储,接下来有调用
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
意思是加载所有的category,然后调用add_category_to_loadable_list,将category装到loadable_categories
里面。接下来
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// 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;
}
从这段代码中可以看出,是创建了一个autoreleasepool,然后先调用主类的方法 call_class_loads();
,再调用 more_categories = call_category_loads();
分类的方法,
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
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, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
在这里我们可以看到是通过方法的首地址去调用的方法。
+load 总结
1.+ load
只要你动态加载或者静态引用了这个类,那么load方法就会执行,他不需要你去初始化才会执行,只会执行一次
2.从上面的源码分析可以看出,是先调用父类中的load,然后调用分类中的load,顺序是,
1.当前类父类load
2.当前类load
3.当前类分类load
然后我们再main中和appdelegate中打断点,发现,刚执行到main,就已经打印了,所以执行顺序为
1.当前类父类load
2.当前类load
3.当前类分类load
4.main
5.applegate
说明一编译,load就加载了,
2019-12-19 15:54:13.613320+0800 blogTest[96113:4739867] vc load
2019-12-19 15:54:13.614405+0800 blogTest[96113:4739867] 父类load
2019-12-19 15:54:13.614579+0800 blogTest[96113:4739867] 子类load
2019-12-19 15:54:13.614855+0800 blogTest[96113:4739867] 分类load
2019-12-19 15:54:13.615101+0800 blogTest[96113:4739867] url load
(lldb)
我又加了一些打印,那么这些打印能否改变顺序呢?指的是没有继承关系的,平级的,答案是可以,既然取决于我们的编译,那么我们的工程配置中的 compile sources,不就是编译的文件吗?,我们试着拖一拖文件的顺序面试一下
2019-12-19 15:58:28.754907+0800 blogTest[96242:4756770] vc load
2019-12-19 15:58:28.755627+0800 blogTest[96242:4756770] 父类load
2019-12-19 15:58:28.755734+0800 blogTest[96242:4756770] 子类load
2019-12-19 15:58:28.755912+0800 blogTest[96242:4756770] url load
2019-12-19 15:58:28.756158+0800 blogTest[96242:4756770] 分类load
发现 url 的分类和我们测试的分类,顺序变了,是因为我拖动了他们的顺序,是不是很神奇?但是主类和分类的调用顺序是一定的,不取决于编译顺序
load 总结和注意事项
总结
- load 方法调用在main之前,并且不需要我们初始化,程序启动就会把所有文件加载
- 主类的调用优先于分类,分类的调动优先于当前类优先于分类
- 主类和分类的调用顺序跟编译顺序无关
- 分类之间加载,也就是平级之前加载取决于编译顺序,谁先编译就先加载谁
注意事项
1.我们发现。load 的加载比main 还要早,所以如果我们再load方法里面做了耗时的操作,那么一定会影响程序的启动时间,所以在load里面一定不要写耗时的代码。
2.不要在load里面取加载对象,因为我们再load调用的时候根本就不确定我们的对象是否已经初始化了,所以不要去做对象的初始化
调用顺序延伸(category)
我之前的文章中讲过,分类中的同名方法,源码中是按照逆序加载的,也就是说后编译的分类方法会覆盖前面所有的同名的方法,分类还有一个特性就是,不管把声明写在主类还是分类,只要分类中实现了就可以找到,我们可以自己做测试
+ initialize
源码太长,就先不放了
initialize 方法会在类收到第一个消息时候调用,是一个懒加载的模式,如果一直没有收到消息,那么就一直不会调用,这样也是为了节省资源,从源码中我们可以看出来,当我们想对象发送消息的时候,如果没有初始化,会调用 _class_initialize,+initialize本质为objc_msgSend,如果子类没有实现initialize则会去父类查找,如果分类中实现,那么会覆盖主类,和runtime消息转发逻辑一样
我的测试代码
// 这个是父类
@implementation Test
+(void)load{
NSLog(@"父类load:%@",[self class]);
}
+(void)initialize{
NSLog(@"父类 initialize : %@",[self class]);
}
@end
// 这个类继承 test,同时也初始化了另一个类
@interface Forwarding : NSObject
@end
@implementation Forwarding
- (void)print{
NSLog(@"forwarding to print");
}
+(void)load{
NSLog(@"forwarding load");
}
+(void)initialize{
NSLog(@"forwarding initialize");
}
@end
@implementation TestClass
+(void)load{
NSLog(@"子类load:%@",[self class]);
}
+(void)initialize{
Forwarding *f = [[Forwarding alloc] init];
[f print];
NSLog(@"子类initialize");
}
然后我们看下打印结果,多余的不用管
2019-12-19 17:10:31.260399+0800 blogTest[97767:4993277] vc load
2019-12-19 17:10:31.260997+0800 blogTest[97767:4993277] forwarding load
2019-12-19 17:10:31.261284+0800 blogTest[97767:4993277] 父类 initialize : Test
2019-12-19 17:10:31.261383+0800 blogTest[97767:4993277] 父类load:Test
2019-12-19 17:10:31.261473+0800 blogTest[97767:4993277] forwarding initialize
2019-12-19 17:10:31.261543+0800 blogTest[97767:4993277] forwarding to print
2019-12-19 17:10:31.261625+0800 blogTest[97767:4993277] 子类initialize
2019-12-19 17:10:31.261735+0800 blogTest[97767:4993277] 子类load:TestClass
2019-12-19 17:10:31.261808+0800 blogTest[97767:4993277] block load
2019-12-19 17:10:31.261880+0800 blogTest[97767:4993277] url load
2019-12-19 17:10:31.262189+0800 blogTest[97767:4993277] 分类load
2019-12-19 17:10:31.262363+0800 blogTest[97767:4993277] 分类2load
解释原因
load 的打印顺序:
我们只是把类加载到项目中,并没有写任何的代码,跑起来就有打印是为什么呢?,因为我们的程序一编辑,就会调用load方法,而load的调用顺序是先父类再当前类,所以肯定先打印父类load,然后子类load,最后分类load,可以从我们的打印中看到,这三个打印确实是这个顺序
initialize 打印:
因为先调用父类的 load,而我们在父类的load里面,调用了[self class],这行代码,其实就代表了,给当前类发消息了,前面说过,当第一次给这个类发消息的时候,就会调用 initialize,所以当我们在load里面写了[self class],之后就是发送了消息,就会调用initialize方法,所以可以看到我们的打印顺序为
父类 initialize : Test
父类load:Test
,然后接下来调用子类的load方法,同样在子类中也发送了消息,所以会调用子类的initialize,,在子类又给forwarding对象发送了消息,所以会滴啊用forwarding的initialize方法,然后调用initialize方法,紧接着回到子类的initialize,这样一整流程就结束了。
那如果我们将代码改成这样呢,将子类的 initialize 去掉,打印结果为
2019-12-19 17:23:13.313125+0800 blogTest[98099:5025080] 父类 initialize : Test
2019-12-19 17:23:13.314034+0800 blogTest[98099:5025080] 父类load:Test
2019-12-19 17:23:13.319165+0800 blogTest[98099:5025080] 父类 initialize : TestClass
2019-12-19 17:23:13.319688+0800 blogTest[98099:5025080] 子类load:TestClass
可以发现调用了两遍父类的 initialize,所以当发现子类中没有实现initialize方法之后,就去去父类查找,也证明了initialize方法会调用多次,这个流程就和我前面文章写得runtime消息转发流程一致,如果是这样我们猜想,如果分类中实现了initialize,会不会覆盖子类的?我们测试一下
2019-12-19 17:26:45.895701+0800 blogTest[98211:5038894] 父类 initialize : Test
2019-12-19 17:26:45.896573+0800 blogTest[98211:5038894] 父类load:Test
2019-12-19 17:26:45.897588+0800 blogTest[98211:5038894] 分类中 initialize
2019-12-19 17:26:45.898289+0800 blogTest[98211:5038894] 子类load:TestClass
从打印结果可以发现,我们根本就没有走这个子类中的这个方法
+(void)initialize{
Forwarding *f = [[Forwarding alloc] init];
[f print];
NSLog(@"子类initialize");
}
所以这和我们的猜想是一样的,initialize 其实就是 objc_msgSend
, 和消息转发流程是一样的,是不是觉得瞬间豁然开朗,如果没有,多读几遍,多测试几遍,你就明白了。
总结:1.initialize 会在类第一次接收到消息的时候调用
2.先调用父类的 initialize,然后调用子类。
3. initialize 是通过 objc_msgSend 调用的
4.如果子类没有实现 initialize,会调用父类的initialize(父类可能被调用多次)
5.如果分类实现了initialize,会覆盖本类的initialize方法
网友评论