Designated Initializer For Objective-C

在阅读 AFNetworking 源码时,发现每个类都有一个初始化方法,都有跟着一个 NS_DESIGNATED_INITIALIZER 宏。

1
2
3
4
5
6
7
8
/**
Initializes an instance of a network reachability manager from the specified reachability object.
@param reachability The reachability object to monitor.
@return An initialized network reachability manager, actively monitoring the specified reachability.
*/
- (instancetype)initWithReachability:(SCNetworkReachabilityRef)reachability NS_DESIGNATED_INITIALIZER;

接着发现 NSObjectUIViewUIViewController 也都有,才发现自己之前缺少对源码的留意,格物致知,于是决定一探究竟。

NS_DESIGNATED_INITIALIZER

1
2
3
4
5
6
7
#ifndef NS_DESIGNATED_INITIALIZER
#if __has_attribute(objc_designated_initializer)
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
#else
#define NS_DESIGNATED_INITIALIZER
#endif
#endif

NS_DESIGNATED_INITIALIZER 的宏定义中__has_attribute是Clang 的一个用于检测当前编译器是否支持某一特性的一个宏,对你没有听错,__has_attribute 也是一个宏。详细信息见: Type Safety Checking

通过上面的定义,我们可以看到NS_DESIGNATED_INITIALIZER其实是给初始化方法声明的后面加上了一个编译器可见的标记,不要小看这个标记,他可以在编译时就帮我们找出一些潜在的问题。

通过它可以让我们充分发挥编译器的特性(编译时检查,语法错误后并给出warning),进而帮我们找出初始化过程中可能存在的漏洞,增加代码的健壮性,写出更规范的代码。

NSObject 中的 NS_DESIGNATED_INITIALIZER

1
2
3
4
5
- (instancetype)init
#if NS_ENFORCE_NSOBJECT_DESIGNATED_INITIALIZER
NS_DESIGNATED_INITIALIZER
#endif
;

可以看出,如果子类没有指定 NS_DESIGNATED_INITIALIZER,则默认把init方法作为NS_DESIGNATED_INITIALIZER,如果子类有NS_DESIGNATED_INITIALIZER,那么init将只是一个普通的初始化方法。从Objective-C的继承链我们可以看出,除了如NSProxy之外的类,几乎都是派生自NSObject,所有init才成为了所有类的标配初始化方法。

AFNetworkReachabilityManager 中的 NS_DESIGNATED_INITIALIZER

1
2
3
4
5
6
// AFNetworkReachabilityManager.h
// 提供了三个构造方法
+ (instancetype)managerForDomain:(NSString *)domain;
+ (instancetype)managerForAddress:(const void *)address;
- (instancetype)initWithReachability:(SCNetworkReachabilityRef)reachability NS_DESIGNATED_INITIALIZER;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// AFNetworkReachabilityManager.m
// 构造方法的具体实现
- (instancetype)initWithReachability:(SCNetworkReachabilityRef)reachability {
self = [super init];
if (!self) {
return nil;
}
self.networkReachability = CFBridgingRelease(reachability);
self.networkReachabilityStatus = AFNetworkReachabilityStatusUnknown;
return self;
}
- (instancetype)init NS_UNAVAILABLE
{
return nil;
}
#ifndef __clang_analyzer__
+ (instancetype)managerForDomain:(NSString *)domain {
SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, [domain UTF8String]);
AFNetworkReachabilityManager *manager = [[self alloc] initWithReachability:reachability];
return manager;
}
#endif
#ifndef __clang_analyzer__
+ (instancetype)managerForAddress:(const void *)address {
SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *)address);
AFNetworkReachabilityManager *manager = [[self alloc] initWithReachability:reachability];
return manager;
}
#endif

可以看出-(instancetype)managerForDomain:-(instancetype)managerForAddress: 中都调用了 -(instancetype)initWithReachability: 来做初始化。

AFNetworkReachabilityManager 是继承与 NSObject 的,那么父类的 -init 将不会成为指定初始化方法。这里还把 -init 方法设置成 NS_UNAVAILABLE ,调用者在 -init 方法时编译器会直接抛出 waring ,从而起到限制的的作用。

开发者文档中的描述

In Objective-C, object initialization is based on the notion of a designated initializer, an initializer method that is responsible for calling one of its superclass’s initializers and then initializing its own instance variables. Initializers that are not designated initializers are known as convenience initializers. Convenience initializers typically delegate to another initializer—eventually terminating the chain at a designated initializer—rather than performing initialization themselves.

The designated initializer pattern helps ensure that inherited initializers properly initialize all instance variables. A subclass that needs to perform nontrivial initialization should override all of its superclass’s designated initializers, but it does not need to override the convenience initializers. For more information about initializers, see Object Initialization.

To clarify the distinction between designated and designated initializers clear, you can add the NS_DESIGNATED_INITIALIZER macro to any method in the init family, denoting it a designated initializer. Using this macro introduces a few restrictions:

  • The implementation of a designated initializer must chain to a superclass init method (with [super init...]) that is a designated initializer for the superclass.
  • The implementation of a convenience initializer (an initializer not marked as a designated initializer within a class that has at least one initializer marked as a designated initializer) must delegate to another initializer (with [self init...]).
  • If a class provides one or more designated initializers, it must implement all of the designated initializers of its superclass.

If any of these restrictions are violated, you receive warnings from the compiler.

If you use the NS_DESIGNATED_INITIALIZER macro in your class, you need to mark all of your designated initializers with this macro. All other initializers will be considered to be convenience initializers.

Multiple initializers

NS_DESIGNATED_INITIALIZER 使用方法

关于指定构造器和便利构造器可以看这篇文章,构造器也就是文中的方法。

NS_DESIGNATED_INITIALIZER 使用方法:

  • 子类如果有指定初始化方法,那么指定初始化方法实现时必须调用它的直接父类的指定初始化方法。
  • 如果子类有指定初始化方法,那么便利初始化方法必须调用自己的其它初始化方法(包括指定初始化方法以及其他的便利初始化方法),不能调用 super 的初始化方法。
  • 如果子类提供了指定初始化方法,那么一定要实现所有父类的指定初始化方法。

如果不遵守以上几条,编译器将会报 warning,由此可以规范化初始化方法,确保子类是调用的父类指定初始化方法,且不会出现循环的调用。

参考文献

Apple 开发者文档: Object Initialization, Multiple initializers

iOS: 聊聊 Designated Initializer(指定初始化函数)

正确编写Designated Initializer的几个原则