Designated Initializer(指定初始化器)在Objective-C里面是很重要的概念,但是在日常开发中我们往往会忽视它的重要性,以至于我们写出的代码具有潜藏的Bug,且不易发现。保证良好的编写Designated Initializer的风格,可以让我们节约很多时间。 前段时间@吴发伟Ted分享了一篇Twitter团队的一篇博客,里面讲述了Designated Initializer正确的模板以及需要注意的问题。但是里面关于initWithCoder描述不是很清晰,且随后@an00na给出了不同的看法。我会在接下来的文章讲述验证他们给出的编写Designated Initializer的原则,并对initWithCoder的分歧做一个分析,了解其背后的机制。
准备工作
为了能够跟踪代码的实际调用顺序,在下面的实例分析中,我将会使用Xtrace。这是一个在调试上非常强大的一个库,实现原理是通过Hook的方式跟踪消息。详细可以看Github上的说明。 需要注意的是,Xtrace里面设定了不跟踪initWithCoder,由于我们后面分析的需要我们需要把Xtrace.h里面的一段代码做一些小改动:
1 2 3 |
#define XTRACE_EXCLUSIONS "^(initWithCoder|_UIAppearance_|"\ "_(initializeFor|performUpdatesForPossibleChangesOf)Idiom:|"\ "timeIntervalSinceReferenceDate)|(WithObjects(AndKeys)?|Format):$" |
这段代码标示了不跟踪的@selector,我们需要将initWithCoder删除,才能跟踪这个方法。
接下来,我们需要在AppDelegate.m里面加入一段代码:
1 2 3 4 5 6 7 8 9 10 |
- (id)init{ if (self=[super init]){ [Xtrace includeMethods:@"^init"];//1. [Xtrace traceClass:[ViewController class]]; [Xtrace traceClass:[TestInitView class]]; [Xtrace traceClass:[SubTestInitView class]]; [Xtrace traceClass:[NSObject class]];//2. } return self; } |
- 由于每个类对象调用的方法很多,为了不被干扰,声明我们只跟踪以init开头的方法。
- 这几行声明了我们将会跟踪的类。也就是说,一旦这些类调用了我们跟踪的方法,就会有信息输出。
分析代码
我会先给出我认为应该遵循的原则,并对每个原则做实际分析。
- 每个类的正确初始化过程应当是按照从子类到父类的顺序,依次调用每个类的Designated Initializer。并且用父类的Designated Initializer初始化一个子类对象,也需要遵从这个过程。
- 如果子类指定了新的初始化器,那么在这个初始化器内部必须调用父类的Designated Initializer。并且需要重写父类的Designated Initializer,将其指向子类新的初始化器
TestInitView是一个继承于UIView,它重新指定的初始化器为initWithFrame:andName:。现在,假设这个类的初始化器如下:
1 2 3 4 5 6 7 8 |
//Designated Initializer - (instancetype)initWithFrame:(CGRect)frame andName:(NSString *)name{ //incorrect if (self = [super init]){ self.name=name; } return self; } |
可以看到在里面并没有调用UIView的Designated InitializerinitWithFrame:。那么会有什么后果呢?
我们用这个Designated Initializer生成一个TestInitView对象:
1 |
TestInitView *testView = [[TestInitView alloc] initWithFrame:CGRectZero andName:@""]; |
运行程序,我们会看到Xtrce的跟踪记录如下:
1 2 3 4 5 6 7 8 |
| [<TestInitView 0x8c6c840> initWithFrame:{} andName:<__NSCFConstantString 0x6d278>] | [<TestInitView 0x8c6c840>/UIView init] | [<TestInitView 0x8c6c840>/UIView initWithFrame:{}] | [<TestInitView 0x8c6c840>/NSObject init] | -> <TestInitView 0x8c6c840> (init) | -> <TestInitView 0x8c6c840> (initWithFrame:) | -> <TestInitView 0x8c6c840> (init) | -> <TestInitView 0x8c6c840> (initWithFrame:andName:) |
咦,似乎没有问题啊。整个继承链的初始化器都被调用了。等等,如果我们用UIView的Designated Initializer生成一个TestInitView对象会怎样呢?
1 |
TestInitView *testView = [[TestInitView alloc] initWithFrame:CGRectZero]; |
运行代码后,我们得到的调用过程如下:
1 2 3 4 |
| [<TestInitView 0xa16cd40>/UIView initWithFrame:{}] | [<TestInitView 0xa16cd40>/NSObject init] | -> <TestInitView 0xa16cd40> (init) | -> <TestInitView 0xa16cd40> (initWithFrame:) |
这时会发现TestInitView的初始化器initWithFrame:andName:没有被调用。
我们再修改下代码:
1 2 3 4 5 6 7 8 |
//Designated Initializer - (instancetype)initWithFrame:(CGRect)frame andName:(NSString *)name{ //incorrect if (self = [super initWithFrame:frame]){ self.name=name; } return self; } |
我们依然使用UIView的Designated Initializer,然后运行程序得到下面的结果:
1 2 3 4 |
| [<TestInitView 0xa81d3f0>/UIView initWithFrame:{}] | [<TestInitView 0xa81d3f0>/NSObject init] | -> <TestInitView 0xa81d3f0> (init) | -> <TestInitView 0xa81d3f0> (initWithFrame:) |
TestInitView的初始化器依然没有被调用。原因就是没有我们没有重写父类UIView的Designated Initializer。修改后我们的最终代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//Designated Initializer - (instancetype)initWithFrame:(CGRect)frame andName:(NSString *)name{ //incorrect if (self = [super initWithFrame:frame]){ self.name=name; } return self; } //super override - (id)initWithFrame:(CGRect)frame { return [self initWithFrame:frame andName:@""]; } |
继续测试我们的代码,得到的结果如下: