说说 iOS 中的 initializer patterns

704 查看

导语:前段时间在完成项目的过程中,曾碰到过一个困惑:当一个类定义有多个init方法时,为什么可调用其中任何一个,会执行其类继承体系中的所有init方法,以及如何保证有序地执行? 因此,便探索了一下其背后的机制。

一、Designated initializer & Secondary initializer

ios对于方法的命名有不少约定,开发时需要注意遵循。如以alloc/new/copy/mutableCopy起始的方法返回对象时,调用方是生成且持有该返回对象,而类似通过[NSArray array]这类方法取得对象则仅是持有对象。以init起始的方法苹果文档则有更严格的约束:

These initializers are instance methods that begin with “init” and return an object of the dynamic type id. Other than that, they follow the Cocoa conventions for multiparameter methods, often using WithType: or FromSource: before the first and most important parameter.

Designated initializer一般是一个类里带较多参数和完成大部分初始化工作的构造器,它确保实例初始化时会调用其父类的designated initializer,从而保证实例初始化的完整性, 如UIView里的initWithFrame:方法 。关于Designated initializer和Secondary initializer在继承体系中的区别:

When designing the initializers of your class, keep in mind that designated initializers are chained to each other through messages to super; whereas other initializers are chained to the designated initializer of their class through messages to self.

二、Initializer patterns

当我们想设计一个类的init方法时,可以归纳为以下四种情形,若设计时遵循initializer patterns,则可以满足调用类里的任何一个init方法,其继承体系中的所有designated initializers均会被有序地执行。

  • 重写父类的designated initializer。
    必须重写直系父类的designated initializer。(反例见三)
  • 创建本类自定义的designated initializer。
    自定义的designated initializer里须调用直系父类的designated initializer,同时重写直系父类的一个designated initializer并在其中调用本类自定义的designated initializer。
  • 创建本类自定义的secondary initializer。
    自定义的secondary initializer可以是实例方法,也可以是类方法,但只能通过self调用本类的designated initializer。
  • 不创建任何initializer。
    这种情形会继承父类的initializer。

以上图中CityServiceView为例,初始化对象时无论调用 initWithFrame:,还是 initWithCityName:cityCode:,亦或是init ,均可以保证其它的所有designated initializer有序调用到。比如调用 initWithFrame:时:

调用init时:

同样可以保证继承体系中的所有init方法被执行,且子类的init总是在父类的init之后。

三、Bad case

苹果文档对于designated initializer的实现建议可能比较陈旧,在ARC之前编纂,更多聚焦在内存管理上。

  • Always invoke the superclass (super) initializer first.
  • Check the object returned by the superclass. If it is nil, then initialization cannot proceed; return nil to the receiver.
  • When initializing instance variables that are references to objects, retain or copy the object as necessary (in memory-managed code).
  • After setting instance variables to valid initial values, return self unless:
    It was necessary to return a substituted object, in which case release the freshly allocated object first (in memory-managed code).
    A problem prevented initialization from succeeding, in which case return nil.

Initializer patterns其实是一种最佳实践,如二中所述。比较容易忽视的一点是在自定义designated initializer调用的父类和重写父类designated initializer时,所指的父类均须为直系父类。如果不是直系父类,则很容易造成继承体系中有部分类的designated initializer未被执行到,或者未有序执行到。如果父类的init方法执行在子类的init方法之后,最终初始化出来的对象便可能因缺乏某些元素而未完全初始化。这种缺陷在很多时候下并不会暴露问题,但一旦出现了则难以追踪。

The proper order of initialization is critical because the later initializations of subclasses may depend on superclass-defined instance variables being initialized to reasonable values.

如果CityServiceView的designated initializer这样定义:

当CityServiceView的对象以init:来初始化时会正常初始化,然而当对象以initWithFrame:来构造时:

CityServiceView在自身init做的初始化工作将不会被执行到,这也是平时我们继承UIView的子类习惯于用initWithFrame:initWithCoder:的原因吧。

如果CityServiceView的对象一直以init来初始化未暴露bug,将来若有一个View类继承自CityServiewView,则其无论调用继承体系中哪一个designated initializer,其对象都不会被完整地初始化。

四、一点思考

UIView的父类并不是NSObject,而是UIResponder。而UIView的designated initializer是在自定义的initWithFrame:里调用[super init],然后重写init并在其中调用[self initWithFrame:],其自定义designated initializer调用的并非其直系父类,而是NSObject的designated initializer,由此感觉可以推测一个结论:

在initializer patterns基础上,当一个类的直系父类没有自定义的designated initializer时,可以越过其视直系祖父作为其super chain或重写的目标。若直系祖父也没有自定义的designated initializer,则以此在继承链上递推。