精简initialize与load的实现代码

来源 ——《Effective Objective-C 2.0 》Matt Galloway

有时候,类必须先执行某些初始化操作,然后才能正常使用。在OC中,绝大多数类都继承自NSObject这个根类,而该类有两个方法,可用来实现这种初始化操作。

首先要讲的是load方法,其原型如下:

对于加入运行期系统中的每个类(class)及分类(category)来说,必定会调用此方法,而且仅调用一次。当包含类或分类的程序载入系统时,就会执行此方法,而这通常就是指应用程序启动的时候,若程序是为iOS平台设计的,则肯定会在此时执行。Mac OS X应用程序更自由一些,它们可以使用“动态加载”(dynamic loading)之类的特性,等应用程序启动好之后再去加载程序库。如果分类和其所属的类都定义了load方法,则先调用类里的,再调用分类里的。

load方法的问题在于,执行该方法时,运行期系统处于“脆弱状态”(fragile state)。在执行子类的load方法之前,必定会先执行所有超类的load方法,而如果代码还依赖了其他程序库,那么程序库里相关类的load方法也必定会先执行。然而,根据某个给定的程序库,却无法判断出其中各个类的加载顺序。因此,在load方法中使用其他类是不安全的。比方说,有下面这段代码:

此处使用NSLog没问题,而且相关字符串也会照常记录,因为Foundation框架肯定在运行load方法之前已经载入系统了。但是,在EOCClassB的load方法里使用EOCClassA却不太安全,因为无法确定在执行EOCClassB的load方法之前,EOCClassA是不是已经加载好了。可以看到:EOCClassA这个类,也许会在其load方法中执行某些重要操作,只有执行完这些操作之后,该类实例才能正常使用。

有个重要的事情需注意,那就是load方法并不像普通的方法那样,它并不遵从那套继承规则。如果某个类本身没实现load方法,那么不管其各级超类是否实现此方法,系统都不会调用。此外,分类和其所属的类里,都可能出现load方法。此时两种实现代码都会调用,类的实现要比分类的实现先执行。

而且load方法务必实现得精简一些,也就是要尽量减少其所执行的操作,因为整个应用程序在执行load方法时都会阻塞。如果load方法中包含繁杂的代码,那么应用程序在执行期间都会变得无响应。不要在里面等待锁,也不要调用可能会加锁的方法。总之,能不做的事情就别做。实际上,凡事想要通过load在类加载之前执行某些任务的,基本都做的不太对。其真正用途仅在于调试程序,比如可以在分类里编写此方法,用来判断该分类是否已经正确载入到系统中。也许此方法一度很有用处,但现在完全可以说:时下编写OC代码时,不需要用它。

想执行与类相关的初始化操作,还有个办法,就是复写下列方法:

对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。它是由运行期系统来调用的,绝不会通过代码直接调用。其虽与load相似,但却有几个非常重要的微妙区别。首先,它是“惰性调用的”,也就是说,只有当程序用到了相关的类时,才会调用。因此,如果某个类一直都没有使用,那么其initialize方法就一直不会运行。这也就等于说,应用程序无须把每个类的initialize都执行一遍,这与load方法不同,对于load来说,应用程序必须阻塞并等待所有类的load都执行完,才能继续。

此方法与load还有个区别,就是运行期系统在执行该方法时,是处于正常状态的,因此,从运行期系统完整度上来讲,此时可以安全使用并调用任意类中的任意方法,而且,运行期系统也能确保initialize方法一定会在“线程安全的环境”(thread-safe environment)中执行,也就是说,只有执行initialize的那个线程可以操作类或类实例。其他线程都要先阻塞,等着initialize执行完。

最后一个区别是:initialize方法与其他消息一样,如果某个类未实现它,而其超类实现了,那么就会运行超类的实现代码。这听起来并不稀奇,但却经常为开发者所忽略。比方说有下面两个类:

即便EOCSubClass类没有实现initialize方法,它会收到这条消息。由各级超类所实现的initialize也会先行调用。所以,首次使用EOCSubClass时,控制台会输出如下消息:

你可能认为输出的内容有些奇怪,不过这完全符合规则。与其他方法(除了load)一样,initialize也遵循通常的继承规则,所以,当初始化基类EOCBaseClass时,EOCBaseClass中定义的initialize方法要运行一遍,而当初始化子类EOCSubClass时,由于该类并未复写此方法,因而还要吧父类的实现代码再运行一遍。鉴于此,通常都会这么来实现initialize方法:

加上这条检测语句之后,只有当开发者所期望的类载入系统时,才会执行相关的初始化操作。如果把刚才的例子照此改写,那就不会打印出两条记录消息了,这次只输出一条:

看过load和initialize方法的这些特性之后,又回到了早前提过的那个主要问题上,也就是两个方法的实现代码要尽量精简。在里面设置一些状态,使本类能够正常运作就可以了,不要执行那种耗时很久或需要加锁的任务。对于load方法来说,其原因已在前面解释过了,而initialize方法要保持精简的原因,也与之相似。首先,大家都不想看到应用程序“挂起”(hang)。对于某个类来说,任何线程都可能成为初次用到它的那个线程,并导致应用程序无响应。有时很难预测到底哪个线程会先用到这个类,强令某线程去初始化该类,显然不是好办法。

其二,开发者无法控制类的初始化时机。类在首次使用之前,肯定要初始化,但编写程序时不能令代码依赖特定的时间点,否则会很危险。运行期系统讲来更新了之后,可能会略微改变类的初始化方法,这样的话,开发者原来如果假设某个类必定会在某个具体时间点初始化,那么现在这条假设可能就不成立了。

最后一个原因是,如果某个类的实现代码很复杂,那么其中可能会直接或间接用到其他类。若那些类尚未初始化,则系统会迫使其初始化。然而,本类的初始化方法此时尚未运行完毕。其他类在运行其initialize方法时,有可能会依赖本类中的某些数据,而这些数据此时也许还未初始化好。例如:

若是EOCClassA先初始化,那么EOCClassB随后也会初始化,它会在自己的初始化方法中调用EOCClassA的doSomethingThatUsesItsInternalData,而此时EOCClassA内部的数据还没准备好。在实际编码工作中,问题不能想此处说的那么明显,而且牵涉到的类可能也不止两个。因此,当代码无法正常运行时,想要找出错误就更难了。

所以说,initialize方法只应该用来设置内部数据。不应该在其中调用其他方法,即便是本类自己的方法,也最好别调用。因为稍后可能还要给那些方法里添加更多功能,如果在初始化过程中调用它们,那么还是有可能导致刚才说的那个问题。若某个全集状态无法在编译期初始化,则可以放在initialize里来做。下例代码演示了这种用法:

整数可以在编译期定义,然而可变数组不行,因为它是个OC对象,所以创建实例只之前必须先激活运行期系统。注意,某些OC对象可以在编译期创建,例如NSString实例。然而,创建下面这种对象会令编译器报错:

编写load和initialize方法时,一定要留心这些注意事项。把代码实现得简单一些,能节省很多调试时间。除了初始化全局状态之外,如果还有其他事情要做,那么可以专门创建一个方法来执行这些操作,并要求该类的使用者必须在使用本类之前调用此方法。比如说,如果“单例类”(singleton class)在首次使用之前必须执行一些操作,那就可以采用这个方法。

Tips

1.在加载阶段,如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类中的先调用。与其他方法不同,load方法不参与复写机制。

2.首次使用某个类之前,系统会向其发送initialize消息。由于此方法遵从普通的复写规范,所以通常应该在里面判断当前要初始化的是哪个类。

3.load与initialize方法都应该实现得精简一些,这有助于保持应用程序的响应能力,也能减少引入“依赖环”(interdependency cycle)的几率。

4.无法在编译期设定的全局常量,可以放在initialize方法里初始化。



发表评论

电子邮件地址不会被公开。 必填项已用*标注