参考资料
iOS: 聊聊 Designated Initializer(指定初始化函数)
《编写高质量iOS与OS X代码的52个有效方法》中第16条:提供“全能初始化方法”
对象的创建
在 Objective-C 中,对象的创建分为两步,分配内存和初始化成员变量。
NSObject *object = [[NSObject alloc] init];
首先调用类方法+ alloc
,其根据要创建的实例对象对应的类来分配足够的内存空间。除了分配内存空间,其实+ alloc
方法还做了其他事情,包括将对象的引用计数记为1,将对象的isa
指针指向对应的运行时类对象,以及将对象的成员变量置为对应的0值(0、nil、NULL)。
+ alloc
方法返回的对象还是不可用的,在之后完成初始化方法的调用后,对象的创建工作才算完成。初始化方法会设置对象的成员变量为一个正确的合理的值,以及获取一些其他额外的资源。
对象的初始化
Designated Initializer 指定初始化方法
所有对象都是要初始化的,而且很多情况下,对象在初始化时是需要接收额外的参数,这就可能会提供多个初始化方法。根据规范,通常选择一个接收参数最多的初始化方法作为指定初始化方法,真正的数据分配和其他相关初始化操作在这个方法中完成。而其他的初始化方法则作为便捷初始化方法去调用这个指定初始化方法。这样当实现改变时,只要修改指定初始化方法就可以了。便捷初始化方法接收的参数更少,它会在内部调用指定初始化方法时,直接设置未接收参数的默认值。便捷初始化方法也可以不直接调用指定初始化方法,它可以调用其他便捷初始化方法,但不管调用几层,最终是要调用到指定初始化方法的,因为真正的实现操作是在指定初始化方法中完成的。所有初始化方法统一以- init
开始。
- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
如上例代码所示,- initWithTimeIntervalSinceReferenceDate
方法是一个指定初始化方法,而其他初始化方法最终是要调用它的。
子类实现指定初始化方法
当子类继承父类后实现了新的指定初始化方法,此时如果调用父类中的指定初始化方法则无法调用到子类新实现的初始化逻辑,所以子类同时还要重写父类的指定初始化方法,将其变为一个便捷初始化方法,最终去调用子类自己的指定初始化方法。而为了保证父类初始化逻辑的执行,在子类指定初始化方法中,首先要通过关键字super
调用父类的指定初始化方法。
@interface Rectangle : NSObject
@property (nonatomic) float width;
@property (nonatomic) float height;
@end
@implementation Rectangle
- (instancetype)init {
return [self initWithWidth:5 Height:5];
}
- (instancetype)initWithWidth:(float)width Height:(float)height {
if (self = [super init]) {
self.width = width;
self.height = height;
}
return self;
}
@end
@interface Square : Rectangle
@end
@implementation Square
- (instancetype)initWithWidth:(float)width Height:(float)height {
float dimension = MAX(width, height);
return [self initWithDimension:dimension];
}
- (instancetype)initWithDimension:(float)dimension {
return [super initWithWidth:dimension Height:dimension];
}
@end
如上例代码所示,Rectangle
类继承自NSObject
类,它实现了新的指定初始化方法- initWithWidth:Height:
,而- init
方法是NSObject
类的指定初始化方法,如果不重写则该初始化方法不会设置新增属性的值。所以在Rectangle
类中的- init
方法通过self
关键字调用了指定初始化方法。而在指定初始化方法中,通过[super init]
调用了父类的初始化逻辑。同理,Square
类又继承自Rectangle
类,它新实现了指定初始化方法- initWithDimension:
, 并调用了父类的指定初始化方法,所以要重写方法- initWithWidth:Height:
,而此时因为- init
方法已重写为便捷方法,会最终调用到新的指定初始化方法,所以不需要重写了。
在子类实现新的指定初始化话方法时,除了将父类的指定初始化方法重写为便捷方法外,也可以在重写实现中抛出异常,即告诉外界在子类中是不提供这种初始化方式的。
如果子类不需要实现自己的指定初始化方法,或者子类的指定初始化方法就是重写父类的指定初始化方法,则其他的子类便捷初始化方法,就调用子类中这个与父类指定初始化方法的同名方法即可。
- initWithCoder:
框架中的很多类实现了<NSCoding>
协议(如:UIViewController
),这个协议定义了初始化方法- initWithCoder:
,一般这个方法里的初始化逻辑与其他的指定初始化方法中是不同的,如UIViewController
通过该方法解码 XML 格式的 NIB 文件。所以子类中可以有不止一个指定初始化方法,- initWithCoder:
也是一个指定初始化方法。- initWithCoder:
中的相关实现规则是,如果父类也实现了<NSCoding>
协议,首先要调用父类的- initWithCoder:
方法,如果父类没有实现,则调用父类的指定初始化方法。
NS_DESIGNATED_INITIALIZER
当在接口中指定初始化方法的后面加上该宏,编译器就会检查我们实现的初始化调用链是否符合规则,并提示相应的警告。另外NS_DESIGNATED_INITIALIZER
也起到了标明指定初始化方法的注释作用。
- (instancetype)init NS_DESIGNATED_INITIALIZER;
总结
指定初始化方法的机制保证了对象会依次执行从父类到子类的所有初始化逻辑,实现的规则为:
- 便捷初始化方法只能调用本类中的其他初始化方法,并最终调用到指定初始化方法。
- 子类的指定初始化方法要调用父类的指定初始化方法,以保证父类的初始化逻辑可以执行。
- 当子类实现了自己的指定初始化方法后,父类的指定初始化方法要重写为便捷初始化方法,以保证所有初始化方法都能调用到子类的初始化逻辑。