2.2 初始化
在Objective-C中,创建一个对象是通过alloc和init两步来实现的,alloc是为该对象分配内存空间,init才是真正将对象创建为实例。可以理解为alloc是按照该类的数据结构,在内存中开辟出相应的大小并设置引用计数为1,最后通过init才真正完成初始化的操作。所以通过alloc创建的对象虽然为instancetype,却是不可用的,所以我们自定义初始化方法,一般不会去重写alloc,而是通过重写init方法,或者自定义一些初始化init方法。
在自定义的初始化方法中,为了保证实例被真正创建,需要调用父类的一个init方法,或者间接调用父类的初始化方法(例如某类有两个初始化方法initWithA和initWithB,initWithA调用了父类的初始化方法,initWithB实现中调用了initWithA来创建)。为何要调用父类的初始化方法呢?因为没人保证实例对象只调用真正的初始化方法,同时也不能保证类的属性变量被正确地初始化。在Objective-C中,初始化方法其实并不是安全的,因为对初始化方法没有限制,开发者可以任意来调用以及实现某个类的初始化方法。例如我们经常使用的控件UITableView,创建一个UITableView的初始化方法是:
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style NS_DESIGNATED_INITIALIZER;
这是我们创建UITableView的初始化方法,作为开发者应该对这个非常清楚,但是由于在Objective-C中对初始化方法没有很好的约束,所以可能会有“滥用”的这样一种情况。同样的,如果有开发者在创建UITableView时,忘记使用上面这个方法,而是通过init方法来创建,那么会怎么样?不只是UITableView,很多UIKit的组件在设计时都会考虑到开发者调用的任意性,所以做了比较完善的处理,我们可以通过Runtime的交换方法,来交换UITableView的-initWithFrame:style:方法,如果我们还是通过init方法来创建,可以看到-initWithFrame:style:也被调用了,这就很好理解了,UITableView的内部重写了init方法,其实现大致如下。
其实在上面介绍-initWithFrame:style:方法的时候,可以看到这个方法是一个NS_DESIGNATED_INITIALIZER方法,也就是表明该方法是一个Designated方法,那么什么是Designated方法呢?除了Designated方法,其他初始化方法又称为什么?这其中如何关联?
如果读者了解Swift语言,那么对Swift的初始化方法的严格性肯定有比较深刻的印象。其实对于iOS开发来说,Objective-C和Swift只是不同的语言,对于开发语言来说有着很多共性。所以在对象初始化这块也是类似的,只不过Swift有着更加严格的初始化流程和要求,并加强了Designated初始化方法的地位。
一个类的Designated初始化方法是通过调用父类的Designated初始化方法来实现的,例如在刚刚的例子中有一个UITableView的子类,其子类重写的init方法如上所示,这其实可以看作一个Convenience初始化方法,因为它调用了自身的Designated方法,然而如果例子中不去调用UITableView的Designated方法-initWithFrame:style:,而是调用父类的[[super alloc]init],或者不重写这个方法,那么此时init的方法又可以看作一个Designated的方法,这种Objective-C中定义划分得并不清晰,所以导致很多Objective-C开发者对初始化方法的定义并不是很明确,而在Swift中,一切都需要井然有序地进行,即使跟Objective-C类似,在init()方法前加不加convenience关键字会使其内部的实现不同,但这有可能是Objective-C迁移过来的一个缺陷。
那么到底Designated和Convenience初始化方法之间有什么关系呢?可以这样理解,一个类的Designated方法是对外提供的标准的初始化方法,在这个Designated方法中,我们已经做好了一切该做的初始化的事情,外部并不需要知道这些具体做了什么。如果该类还需要创建一些补充的初始化方法,那么必须要在内部调用当前类的一个Designated方法,例如UITableView的init方法就是在内部通过调用-initWithFrame:style:方法来实现的,而这些补充的方法,称为Convenience初始化方法。但是对于Convenience方法的使用来说,Objective-C和Swift并不相同,在Objective-C中,如果子类并没有提供自己的Designated初始化方法,则是沿用父类的一切初始化方法,包括Designated和Convenience,这也是Objective-C中对初始化方法分类模糊的原因;而在Swift中,子类只能使用父类的Designated初始化方法,并不能使用其Convenience方法,如果需要使用父类的Convenience方法,需要对父类Convenience方法中使用的Designated初始化方法在子类中进行重写才能使用父类的Convenience方法。
接下来论证一下初始化方法在Swift中的创建和使用。
对于UIView来说,可以通过其定义看出,其有两个Designated方法:
public init(frame: CGRect) public init?(coder aDecoder: NSCoder)
现在假设要继承UIView,自定义一个MyColorView,并需要提供一个属于MyColorView的初始化方法:
init?(coder aDecoder: NSCoder)是必须要实现的方法,如果不打算支持Xib或者StoryBoard,则不用在意它的存在。我们主要关注第一个初始化方法init(backgroundColor:UIColor),这是MyColorView的Designated方法(和Convenience初始化方法不同,其没有前缀修饰)。
在init(backgroundColor: UIColor)方法的实现中,可以看到是通过调用父类的Designated的初始化方法,这是init(backgroundColor: UIColor)成为Designated初始化方法的必要条件,如果实现换成了父类调用Convenience方法,或者自己调用Designated方法都是会报错的。
对于第二种错误示例代码,通过错误提示,系统认为开发者可能是想实现一个Convenience的初始化方法。所以如果init(backgroundColor: UIColor)不是Designated初始化方法,则可以在前面加上一个convenience修饰符,表示这个初始化方法是一个Convenience方法,但是我们之前提到过,Convenience初始化方法的实现需要调用当前类的Designated方法,所以MyColorView还需要重写init(frame: CGRect)方法。
这两种错误的初始化方法是很多刚接触Swift的开发者经常容易犯错的地方,并且对于Designated和Convenience两种初始化方法的概念和区别不清楚。
本节小结
一个类的初始化方法,可能是通过当前类的另一个初始化方法实现的,也可能是通过其父类的初始化方法实现的,所以在一个类中,其他的初始化方法都会调用自身的Designated方法,而自身的Designated方法又会调用父类的Designated方法,所以不管类的初始化方法如何扩展,其都会汇聚到一起,而这个路径则可保证类的层级关系中所有必要变量的初始化操作。