正确编写Designated Initializer的几个原则

1747 查看

Designated Initializer(指定初始化器)在Objective-C里面是很重要的概念,但是在日常开发中我们往往会忽视它的重要性,以至于我们写出的代码具有潜藏的Bug,且不易发现。保证良好的编写Designated Initializer的风格,可以让我们节约很多时间。 前段时间@吴发伟Ted分享了一篇Twitter团队的一篇博客,里面讲述了Designated Initializer正确的模板以及需要注意的问题。但是里面关于initWithCoder描述不是很清晰,且随后@an00na给出了不同的看法。我会在接下来的文章讲述验证他们给出的编写Designated Initializer的原则,并对initWithCoder的分歧做一个分析,了解其背后的机制。

准备工作

为了能够跟踪代码的实际调用顺序,在下面的实例分析中,我将会使用Xtrace。这是一个在调试上非常强大的一个库,实现原理是通过Hook的方式跟踪消息。详细可以看Github上的说明。 需要注意的是,Xtrace里面设定了不跟踪initWithCoder,由于我们后面分析的需要我们需要把Xtrace.h里面的一段代码做一些小改动:

这段代码标示了不跟踪的@selector,我们需要将initWithCoder删除,才能跟踪这个方法。
接下来,我们需要在AppDelegate.m里面加入一段代码:

  1. 由于每个类对象调用的方法很多,为了不被干扰,声明我们只跟踪以init开头的方法。
  2. 这几行声明了我们将会跟踪的类。也就是说,一旦这些类调用了我们跟踪的方法,就会有信息输出。

分析代码

我会先给出我认为应该遵循的原则,并对每个原则做实际分析。

  • 每个类的正确初始化过程应当是按照从子类到父类的顺序,依次调用每个类的Designated Initializer。并且用父类的Designated Initializer初始化一个子类对象,也需要遵从这个过程。
  • 如果子类指定了新的初始化器,那么在这个初始化器内部必须调用父类的Designated Initializer。并且需要重写父类的Designated Initializer,将其指向子类新的初始化器

TestInitView是一个继承于UIView,它重新指定的初始化器为initWithFrame:andName:。现在,假设这个类的初始化器如下:

可以看到在里面并没有调用UIView的Designated InitializerinitWithFrame:。那么会有什么后果呢?
我们用这个Designated Initializer生成一个TestInitView对象:

运行程序,我们会看到Xtrce的跟踪记录如下:

咦,似乎没有问题啊。整个继承链的初始化器都被调用了。等等,如果我们用UIView的Designated Initializer生成一个TestInitView对象会怎样呢?

运行代码后,我们得到的调用过程如下:

 

这时会发现TestInitView的初始化器initWithFrame:andName:没有被调用。
我们再修改下代码:

我们依然使用UIView的Designated Initializer,然后运行程序得到下面的结果:

 

TestInitView的初始化器依然没有被调用。原因就是没有我们没有重写父类UIView的Designated Initializer。修改后我们的最终代码如下:

继续测试我们的代码,得到的结果如下: