本文翻译自Mike Ash的Friday Q&A 2009-05-22: Objective-C Class Loading and Initialization
译者:代培
地址:http://blog.csdn.net/dp948080952/article/details/53346379
转载请注明出处
欢迎回到周五问答。在几周的休息之后,我准备回到正常的日程上来,我们将看到这意志能影响我多久,不过我很乐观。这周我将听从Daniel Jalkut的建议,谈谈Objective-C中类的加载和初始化(loading&initialization)。
Objective-C中类(class)实际上是如何加载到内存中,对于一个程序员来说不是一个经常需要考虑的事情。这是一系列由运行时的链接器完成的工作,而且在你的代码开始执行前很久就已经完成。
译者注:这里所说的运行时链接器(runtime linker)应该就是动态链接器:Dynamic linker
对于大多数类来说,你的代码是你所需关注的全部。但是有些类想要做更多的事,比如运行一些代码从而在创建的时候表现出一些不同,一个类可能需要初始化一个全局的表,一个缓存在user default中的值,或者其他许多任务。
在Objective-C的运行时中提供了两个方法来实现这个功能:+initialize
和+load
当类真正被加载时会调用+load
函数,只要这个类实现了这个函数。这个发生的特别早。如果你在一个应用中或是一个链接到这个应用的framework实现了+load
函数,+load
将在这个应用的main()
函数前调用,如果你在一个可以加载的bundle中实现了+load
函数,+load
函数将会在这个bundle加载的过程中被调用。
+load
可能会很难使用,因为它运行的太早了。很明显一些类需要先于其他类加载,所以你无法确定你要用的其他类已经被加载了,比着更糟的是你应用(framework、plugin)中的C++静态加载(C++ Static initialization)还未完成,如果你运行了任何依赖这些东西的代码,程序将会崩溃,好消息是你添加到项目中的framework可以保证已经完全加载,所以可以安全的使用。需要时刻谨记的是,通常在加载的时候是没有autorelease pool的,当你调用一些Objective-C的东西,你要控制好你自己的代码。
一个有趣的特点是当一个类和其分类同时实现了+load
时,该函数在运行时中的调用十分特殊。具体来说就是如果你同时在一个类和其分类中实现了+load
函数时,这两个函数都会被调用。这可能与你所知道的Category的工作原理完全相悖,不过那是因为+load
不是一个普通的方法。这个特点意味着这是一个十分合适的位置去做一些邪恶的事情比如方法交换(Method Swizzling)。
译者注:苹果的官方文档中说,
+load
函数的调用顺序是父类先于子类,主类先于分类。
+initialize
方法在一个相对正常的环境里被调用,通常来说这是一个比+load
方法更好的地方去写一些代码。有趣的是+initialize
使用的是懒加载的方式,甚至都有可能根本不会被调用。当一个类第一次加载时,+initialize
是不会被调用的。当一个类接收到一条消息时,运行时首先会检查这个类的+initialize
是否被调用过,如果没有,则先调用再完成消息的发送。在概念上你可以理解为下述代码:
id objc_msgSend(id self, SEL _cmd, ...)
{
if(!self->class->initialized)
[self->class initialize];
...send the message...
}
译者注:我看过runtime的源代码,实际上是在
lookUpImpOrForward
进行的这个判断,下面是源代码:
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
}
实际上肯定要比这复杂很多,因为线程安全和很多其他有趣的事情,但这是最基础的逻辑。
+initialize
每个类会调用一次,而且是在第一个消息被送到前调用。和+load
一样,+initialize
在调用前总是先检查父类是否调用过。
这让+initialize
使用起来更加安全,因为通常它在一个更兼容的环境里被调用。很明显发送一条消息发送所依赖的环境至少是在NSApplicationMain()
被调用之后。
因为+initialize
懒加载的方式,显然不是一个好的地方去做注册一个在其他地方没有被使用的类。举个栗子,NSValueTransformer 或 NSURLProtocol的子类不能用父类的+initialize
来注册自己,因为你制造了一个鸡生蛋还是蛋生鸡的场景。
当类在加载的过程中,这是一个能做任何事情的好地方。他运行在一个更兼容的环境里意味着你可以更加自由的书写你的代码,而且懒加载的方式意味着你没有在设置你的类上浪费资源,知道他真正被使用。
关于+initialize
还有一个需要小心的地方。在我上面的伪代码中我写到:[self->class initialize]
,这表明了Objective-C中一个比较通常的消息分发的原则:如果你没有实现它,那个父类的+initialize
将会替代你去执行。这也是事实上发生的。因此,+initialize
需要这样来写:
+ (void)initialize
{
if(self == [WhateverClass class])
{
...perform initialization...
}
}
译者注:这个思想肯定是没错的,但个人认为此处这样写不是很有比要,因为苹果底层的代码会首先去调用父类的initialize。
没有额外的检查,你的初始化可能会进行两次如果你有一个子类没有实现自己的+initialize
,这不仅仅是理论上的担忧,甚至如果你没有任何子类,苹果的KVO会创造没有重新实现+initialize
的动态子类
译者注:实际上苹果在调用此方法时都会先检查是否已经调用过,这个担忧似乎有些多余。
Objective-C提供了两种方式去自动的运行创建类的代码,+load
可以保证调用的足够早,对于需要在类刚刚加载时就调用的代码来说非常有用。但这也很危险,因为是在一个很不友好的环境下。
+initialize
对于一些创建的任务更加合适,因为他是懒加载而且在一个很好的环境下,你可以在这做很多事情,只要不是需要依赖类接收的消息的事情。