资讯专栏INFORMATION COLUMN

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

lvzishen / 2002人阅读

摘要:五当遇到指定初始化函数对了还有一个特例苹果官方文档中明确规定翻译一下如父类没有实现协议,那么应该调用父类的指定初始化函数。

聊聊 iOS Designated Initializer(指定初始化函数)
一、iOS的对象创建和初始化

iOS 中对象的使用时分两步完成:

分配内存

初始化对象的成员变量

对应着我们最常见的创建对象过程,如下图:

苹果官方有一副图片更生动的描述了这个过程:

今天我们重点看一下初始化的过程。

对象的初始化是一个很重要的过程,通常在初始化的时候我们会支持成员变量的初始状态,创建关联的对象等。例如对于如下对象:

@interface ViewController : UIViewController

@end


@interface ViewController () {
    XXService      *_service;
}

@end

@implementation ViewController

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        _service = [[XXService alloc] init];
    }
    
    return self;
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    [_service doRequest];
}

...

@end

上面的VC中有一个成员变量XXService,在viewWillAppear的时候发起网络请求获取数据填充VC。

大家觉得上面的代码有没有什么问题?

带着这个问题我们继续往下看

上面的代码中我们只看到了VC的实现,没有看到VC是怎么创建的,我们分两种情况:

1. 手动创建

通常为了省事,我们创建VC的时候经常使用如下方式

ViewController *vc = [ViewController alloc] init];
ViewController *vc = [ViewController alloc] initWithNibName:nil bundle:nil];

使用如上两种方式创建,我们上面的那一段代码都可以正常运行,因为成员变量_service被正确的初始化了。

2. 从storyboard加载或者反序列化出来的

先来看一段苹果官方的文案:

When using a storyboard to define your view controller and its associated views, you never initialize your view controller class directly. Instead, view controllers are instantiated by the storyboard €”either automatically when a segue is triggered or programmatically when your app calls the instantiateViewControllerWithIdentifier: method of a storyboard object. When instantiating a view controller from a storyboard, iOS initializes the new view controller by calling its initWithCoder: method instead of this method and sets the nibName property to a nib file stored inside the storyboard.

从Xcode5以后创建新的工程默认都是Storyboard的方式管理和加载VC,如果你在新创建的工程的VC中加上如上代码的话,那么很不幸,doRequest请求并不会发出,调试之后可以发现,对象的初始化压根没有调到:initWithNibName:bundle: 方法,而是调用到了 initWithCoder: 方法。由于_service对象没有被正确初始化,还是nil,所以请求无法发出。

至此第一个问题大家心中应该已经有了答案,下面让我们再去看看问题背后的更深层的原因。

正确的运行结果不代表着代码的逻辑也是正确的,有时候可能正好是巧合而已

二、Designated Initializer (指定初始化函数)

UIViewController的头文件中我们可以看到如下两个初始化方法:

- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

细心的同学可能已经发现了一个宏 “ NS_DESIGNATED_INITIALIZER ”, 这个宏定义在NSObjCRuntime.h这个头文件中,定义如下:

#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

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

通过上面的定义,我们可以看到"NS_DESIGNATED_INITIALIZER"其实就是给初始化函数声明的后面加上了一个编译器可见的标记,不要小看这个标记,他可以在编译时就帮我们找出一些潜在的问题,避免程序运行时出现一些奇奇怪怪的行为。

听着神乎其神,编译器怎么帮我们避免呢?
答案是:⚠️⚠️⚠️警告

如下图:

不要笑,就是警告,可能你会想工程有编译警告不是一件很正常的事儿吗?!

这一类警告真的很重要,它可以协助我们发现对象初始化可能不正常的漏洞,当然其他的警告也很重要。我们应该重视项目中的警告,至少是自己写的代码部分的警告。在写完代码之后花一点儿时间执行以下Analytics,消除一下项目中的警告,可能会为后省下不少时间。

编译器为什么报警告,因为我们写的代码不够规范,所以多花点时间规范自己的代码,消除项目中的警告,说不定就能避免后面项目上线后可能出现的奇奇怪怪的问题。

要是这个Bug能重现就好了???

三、NS_DESIGNATED_INITIALIZER 正确使用姿势是什么?

有了指定初始化函数,那么相应的是否会有非指定初始化函数?

Bingo, 你猜对了,的确有这么一类初始化函数,我们通常称它们为“便利初始化函数”。

指定初始化函数对一个类来说非常重要,通常参数也是最多的,试想每次我们需要创建一个自定义类都需要一堆参数,那岂不是很痛苦。莫慌,便利初始化函数就是用来帮我们解决这个问题的,可以让我们比较的创建对象,同时又可以保证类的成员变量被设置为默认的值。

不过需要注意,为了享受这些“便利”,我们需要遵守一些规范,否则可能会因为变量没有正确初始化导致一些奇怪的问题

规范很简明,官方文档链接:

Objective-C: Object Initialization, Multiple initializers

Swift: Swift Initialization

指定初始化函数实现规范:
1. 子类如果有指定初始化函数,那么指定初始化函数实现时必须调用它的直接父类的指定初始化函数。

2. 如果子类有指定初始化函数,那么便利初始化函数必须调用自己的其它初始化函数(包括指定初始化函数以及其他的便利初始化函数),不能调用super的初始化函数。

基于第2条的定义我们可以推断出:所有的便利初始化函数最终都会调到该类的指定初始化函数

原因:所有的便利初始化函数必须调用的其他初始化函数,如果程序能够正常运行,那么一定不会出现直接递归,或者间接递归的情况。那么假设一个类有指定函数A,便利初始化函数B,C,D,那么B,C,D三者之间无论怎么调用总的有一个人打破这个循环,那么必定会有一个调用指向了A,从而其他两个也最终会指向A。

示意图如下(图画的比较丑,大家明白意思就好):

3. 如果子类提供了指定初始化函数,那么一定要实现所有父类的指定初始化函数。

当子类指定了指定初始化函数之后,要求实现父类的所有指定初始化函数,其实就一个目的: “保证子类新增的变量能够被正确初始化。

因为我们不能限制使用者通过什么什么方式创建子类,例如我们在创建UIViewController的时候可以使用如下三种方式:

UIViewController *vc = [[UIViewController alloc] init];
UIViewController *vc = [[UIViewController alloc] initWithNibName:nil bundle:nil];
UIViewController *vc = [[UIViewController alloc] initWithCoder:xxx];

如果实际使用子类没有重写父类的所有初始化函数,而使用者恰好直接使用父类的初始化函数初始化对象,那么子类的成员变量就没有初始化。

当子类定义了自己的指定初始化函数之后,父类的指定初始化函数就“退化”为子类的便利初始化函数。

四、举个栗子

以上三条规范理解起来可能有点儿绕,我写了个简单的例子有助于理解该规范,代码如下:

@interface Animal : NSObject {
    NSString *_name;
}

- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;

@end

@implementation Animal

- (instancetype)initWithName:(NSString *)name
{
    self = [super init];
    if (self) {
        _name = name;
    }
    
    return self;
}

- (instancetype)init
{
    return [self initWithName:@"Animal"];
}

@end


@interface Mammal : Animal {
    NSInteger   _numberOfLegs;
}

- (instancetype)initWithName:(NSString *)name andLegs:(NSInteger)numberOfLegs NS_DESIGNATED_INITIALIZER;

- (instancetype)initWithLegs:(NSInteger)numberOfLegs;

@end

@implementation Mammal

- (instancetype)initWithLegs:(NSInteger)numberOfLegs
{
    self = [self initWithName:@"Mammal"];
    if (self) {
        _numberOfLegs = numberOfLegs;
    }
    
    return self;
}

- (instancetype)initWithName:(NSString *)name andLegs:(NSInteger)numberOfLegs
{
    self = [super initWithName:name];
    if (self) {
        _numberOfLegs = numberOfLegs;
    }
    
    return self;
}

- (instancetype)initWithName:(NSString *)name
{
    return [self initWithName:name andLegs:4];
}

@end


@interface Whale : Mammal {
    BOOL    _canSwim;
}

- (instancetype)initWhale NS_DESIGNATED_INITIALIZER;

@end

@implementation Whale

- (instancetype)initWhale
{
    self = [super initWithName:@"Whale" andLegs:0];
    if (self) {
        _canSwim = YES;
    }
    
    return self;
}

- (instancetype)initWithName:(NSString *)name andLegs:(NSInteger)numberOfLegs
{
    return [self initWhale];
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"Name: %@, Numberof Legs %zd, CanSwim %@", _name, _numberOfLegs, _canSwim ? @"YES" : @"NO"];
}

@end

配套上面的代码,我还画了一张类调用图辅助大家理解,如下:

现在对照着代码和上面的类图,上午例子总共声明了三个类:Animal(动物)Mammal(哺乳动物)Whale(鲸鱼),三个类按照Apple对于指定初始化函数的三条规则实现了它们的指定初始化函数。我们创建一个Whale对象,如下代码:

Whale *whale1 = [[Whale alloc] initWhale];                  // 1
NSLog(@"whale1 %@", whale1);

Whale *whale2 = [[Whale alloc] initWithName:@"Whale"];     // 2
NSLog(@"whale2 %@", whale2);

Whale *whale3 = [[Whale alloc] init];                       // 3
NSLog(@"whale3 %@", whale3);

Whale *whale4 = [[Whale alloc] initWithLegs:4];            // 4
NSLog(@"whale4 %@", whale4);

Whale *whale5 = [[Whale alloc] initWithName:@"Whale" andLegs:8];    // 5
NSLog(@"whale5 %@", whale5);

执行结果为:

whale1 Name: Whale, Numberof Legs 0, CanSwim YES
whale2 Name: Whale, Numberof Legs 0, CanSwim YES
whale3 Name: Whale, Numberof Legs 0, CanSwim YES
whale4 Name: Whale, Numberof Legs 4, CanSwim YES
whale5 Name: Whale, Numberof Legs 0, CanSwim YES

whale1 使用 Whale 的指定初始化函数创建,初始化调用顺序为: ⑧ -> ⑤ -> ③ -> ①,初始化方法的实际执行顺序恰好相反: ① -> ③ -> ⑤ -> ⑧,即从根类的开始初始化,初始化的顺序正好和类成员变量的布局顺序相同,有兴趣的可以自行上网查查。

whale5 使用Whale的父类Mammal的指定初始化函数(对于Whale类来说退化为便利初始化函数)创建实例,初始化调用顺序为: ⑦ -> ⑧ -> ⑤ -> ③ -> ①。
创建出来的实例完全正确,

注:⑦ 代表 Whale 类的实现,其内部实现调用了自己类的指定初始化函数 initWhale。 ⑤ 代表 Mammal 类的实现。

细心地朋友可能已经发我们创建的第四条鲸鱼,神奇的长了4条腿,让我们看看创建过程的调用顺序:
⑥ -> ④ -> ⑦ -> ⑧ -> ⑤ -> ③ -> ①, 可以看到对象的初始化也是完全从跟到当前类的顺序依次初始化的,那么问题出在哪儿呢?

我们再看看 Mammal 类的 initWithLegs:函数,可以看到除了正常的初始化函数调用栈,它还一段函数体,对已经初始化好的对象的成员变量 _numberOfLegs 重新设置了值。

- (instancetype)initWithLegs:(NSInteger)numberOfLegs
{
    self = [self initWithName:@"Mammal"];
    if (self) {
        _numberOfLegs = numberOfLegs;
    }
    
    return self;
}

这已经是一个业务问题了,指定初始化函数规则只能用来保证类的所有变量能够被初始化到,仅此而已。

五、当 initWithCoder:遇到指定初始化函数

对了还有一个特例 initWithCoder:

@protocol NSCoding

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER

@end

苹果官方文档 Decoding an Object 中明确规定:

In the implementation of an initWithCoder: method, the object should first invoke its superclass’s designated initializer to initialize inherited state, and then it should decode and initialize its state.
If the superclass adopts the NSCoding protocol, you start by assigning of the return value of initWithCoder: to self.

翻译一下:

如父类没有实现NSCoding协议,那么应该调用父类的指定初始化函数。

如果父类实现了NSCoing协议,那么子类的 initWithCoder: 的实现中需要调用父类的initWithCoder:方法,

根据上面的第三部分阐述的指定初始化函数的三个规则,而NSCoding实现的两个原则都需要父类的初始化函数,这违反了指定初始化实现的第二条原则。

怎么解决?

仔细观察NSCoding协议中 initWithCoder: 的定义后面有一个注释掉的 NS_DESIGNATED_INITIALIZER,是不是可以找到一点儿灵感呢!
实现NSCoding协议的时候,我们显示的声明 initWithCoder: 为指定初始化函数,那么既满足了指定初始化函数的三个规则,也满足了NSCoding协议的三条原则,完美解决问题。

六、总结

上面关于指定初始化的规则讲了那么多,其实可以归纳为两点:

便利初始化函数只能调用自己类中的其他初始化方法

指定初始化函数才有资格调用父类的指定初始化函数

苹果官方有个图,有助于我们理解这两点:

当我们为自己的创建的类添加指定初始化函数时,必须的识别并覆盖直接父类所有的指定初始化函数,这样才能保证整个子类的初始化过程可以覆盖到所有继承链上的成员变量得到合适的初始化。

NS_DESIGNATED_INITIALIZER 是一个很有用的宏,让编译器帮我们找出初始化过程中可能存在的漏洞,增强代码的健壮性。

参考资料

Object creation: https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/ObjectCreation.html#//apple_ref/doc/uid/TP40008195-CH39-SW1

Initialization: https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/Initialization.html#//apple_ref/doc/uid/TP40008195-CH21-SW1

Multiple initializers: https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/MultipleInitializers.html

How To: Objective C Initializer Patterns: http://blog.twitter.com/2014/how-to-objective-c-initializer-patterns

Decoding an Object: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Articles/codingobjects.html#//apple_ref/doc/uid/20000948-97254

Object Initialization: https://developer.apple.com/library/mac/releasenotes/ObjectiveC/ModernizationObjC/AdoptingModernObjective-C/AdoptingModernObjective-C.html#//apple_ref/doc/uid/TP40014150-CH1-SW8

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/16196.html

相关文章

  • iOS布局渲染-UIView方法调用时机

    摘要:布局渲染方法调用时机一约束何时触发如下所示时候调用,但是要求重写以下方法并返回。标记为需要布局,下次自动调用。自己的发生改变时约束也会导致改变。视图被添加到,滚动。三显示何时触发如下所示时候调用,但是的值不能为。 iOS布局渲染-UIView方法调用时机 一、约束 - (void)updateConstraints NS_AVAILABLE_IOS(6_0) NS_REQUIRES...

    lingdududu 评论0 收藏0
  • ES6 系列之我们来聊聊装饰器

    摘要:第二部分源码解析接下是应用多个第二部分对于一个方法应用了多个,比如会编译为在第二部分的源码中,执行了和操作,由此我们也可以发现,如果同一个方法有多个装饰器,会由内向外执行。有了装饰器,就可以改写上面的代码。 Decorator 装饰器主要用于: 装饰类 装饰方法或属性 装饰类 @annotation class MyClass { } function annotation(ta...

    eternalshallow 评论0 收藏0
  • iOS实现依赖注入

    摘要:依赖注入这个词,源于,但在框架中也是十分常见的。举例来说的初始化方法这里的传入值,就是所谓的依赖,这个实例化是根据注入实现的。 依赖注入(Dependency Injection)这个词,源于java,但在Cocoa框架中也是十分常见的。举例来说:UIView的初始化方法initWithFrame - (id)initWithFrame:(CGRect)frame NS_DESIGNA...

    Taste 评论0 收藏0
  • 聊聊lettuce的sentinel连接

    摘要:而部署多个来实现高可用,假设一个挂了,则端使用下一个来获取地址 序 本文主要研究一下lettuce的sentinel连接 RedisClient.connectSentinel lettuce-core-5.0.4.RELEASE-sources.jar!/io/lettuce/core/RedisClient.java private StatefulRedisSentin...

    shusen 评论0 收藏0
  • Swift Tips

    摘要:和架构有关在位上及以前是,位上及以后为。在添加自定义操作符时,分别加上表示中位符前位符后位符。方法必须确保所有成员对象被初始化了。是强制子类必须重写的关键字。初始化方法返回后加或允许在初始化中返回来实例化对象。 Int和CPU架构有关 在32位CPU上(iphone5及以前)是Int32,64位上(5s及以后)为Int64。UInt同理。 可选链式调用 可选链式调用失败时,等号右侧的代...

    Bmob 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<