一、iOS中对象的创建和初始化iOS中对象的创建分两步完成:分配内存和初始化对象的成员变量我们最熟悉的创建NSObject的过程objects:Apple官方有一张图更形象地描述了这个过程:对象初始化是一个非常重要的过程。通常,在初始化的时候,我们会支持成员变量的初始状态,创建关联对象等等。例如,对于以下对象:@interfaceViewController:UIViewController@end@interfaceViewController(){XXService*_service;}@end@implementationViewController-(instancetype)initWithNibName:(NSString*)nibNameOrNilbundle:(NSBundle*)nibBundleOrNil{self=[superinitWithNibName:nibNameOrNilbundle:nibBundleOrNil];if(self){_service=[[XXServicealloc]init];返回自我;}-(void)viewWillAppear:(BOOL)animated{[superviewWillAppear:animated];[_servicedoRequest];}...@endTestViewControllerVC中有一个成员变量XXService,在viewWillAppear时发起网络请求获取数据填充VC。你觉得上面的代码有什么问题吗?带着这个疑问,我们继续往下看。上面只有VC的实现代码。我们不知道VC是由什么姿势创造出来的。有两种情况:1.手动创建通常是为了省事,我们在创建VC的时候,经常使用如下方法ViewController*vc=[ViewControlleralloc]init];ViewController*vc=[ViewControlleralloc]initWithNibName:nilbundle:nil];使用上面两种方法创建,上面的代码可以正常运行,因为成员变量_service被正确初始化了。2.从故事板加载或反序列化来看,让我们看一下官方的Apple副本:当使用故事板定义视图控制器及其关联的视图时,您永远不会直接初始化视图控制器类。相反,视图控制器由故事板实例化,或者在触发segue时自动实例化,或者在您的应用程序调用故事板对象的instantiateViewControllerWithIdentifier:方法时以编程方式实例化。当从故事板实例化视图控制器时,iOS在使用代码方法调用它时初始化新视图,并将nibName属性设置为存储在故事板内的nib文件。Xcode5以后默认新建工程,以Storyboard的方式管理和加载VC。对象的初始化根本没有调用initWithNibName:bundle:方法,而是调用了initWithCoder:方法。对比上面VC的实现,可以看出_service对象没有正确初始化,所以无法发出请求。此时,你的脑海中应该已经有了第一个问题的答案。让我们看看这个问题背后更深层次的原因。正确的运算结果并不代表正确的执行逻辑,有时可能只是巧合。2.DesignatedInitializer(指定初始化函数)在UIViewController的头文件中,我们可以看到如下两种初始化方法:-(nullableinstancetype)initWithCoder:(NSCoder*)aDecoderNS_DESIGNATED_INITIALIZER; Carefulstudentsmayhavediscoveredamacro"NS_DESIGNATED_INITIALIZERThisheaderfileisdefinedinNS_DESIGNATED_INITIALIZER".中,定义如下:#ifndefNS_DESIGNATED_INITIALIZER#if__has_attribute(objc_designated_initializer)#defineNS_DESIGNATED_INITIALIZER__attribute__((objc_designated_initializer))#else#defineNS_DESIGNATED_INITIALIZER#endif#endif"__has_attribute"是Clang的一个用于检测当前编译器是否支持某种特性的一个宏,对你没听错,"__has_attribute"也是一个宏。通过上面的定义,我们可以看出“NS_DESIGNATED_INITIALIZER”实际上是在初始化函数声明的末尾添加了一个编译器可见的标记。不要小看这个标记。它可以帮助我们在编译过程中发现一些潜在的问题,以避免程序运行时出现一些奇怪的行为。听起来很神奇,编译器怎么帮我们避免呢?答案是:??????警告如下图:编译器有警告,说明我们写的代码不够规范。Xcode自带的Analytics工具可以帮助我们发现程序中潜在的问题,花更多的时间规范自己的代码,消除项目中的警告,避免项目上线后出现奇怪的问题。3、NS_DESIGNATED_INITIALIZER的正确姿势是什么?指定初始化函数Vs方便的初始化函数指定初始化函数对于一个类来说非常重要,通常参数最多。想象一下,每次我们需要创建一个自定义类时,我们都需要一堆参数。那不是很痛苦吗?便捷初始化函数就是用来帮助我们解决这个问题的。它允许我们相对地创建对象,同时保证类的成员变量被设置为默认值。不过需要注意的是,要享受这些“便利”,我们需要遵守一些规范。官方文档链接如下:Objective-C:https://developer.apple.com/library/mac/releasenotes/ObjectiveC/ModernizationObjC/AdoptingModernObjective-C/AdoptingModernObjective-C.html#//apple_ref/doc/uid/TP40014150-CH1-SW8https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/MultipleInitializers.htmlSwift:https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.htmlSwift和Objective-C略有不同,我们以Objective-C的规范为例。1、如果子类有指定初始化函数,则在实现指定初始化函数时,必须调用其直接父类的指定初始化函数。2.如果子类有指定初始化函数,那么便利初始化函数必须调用自己的其他初始化函数(包括指定初始化函数和其他便利初始化函数),不能调用超初始化函数。根据第2条的定义,我们可以推导出所有便利初始化函数最终都会被调用到类的指定初始化函数原因:所有便利初始化函数都必须调用其他初始化函数,如果程序能够正常运行,那么它必须not发生直接递归或间接递归。那么假设一个类有一个指定的函数A,方便的初始化函数B、C、D,那么不管B、C、D怎么调用,总有一个人打破这个循环,那么肯定有一个调用指向A,所以另外两个也会最终指向A。示意图如下(图丑,大家看懂意思就好):3.如果子类提供了指定的初始化函数,那么它必须实现指定所有父类的初始化函数。当子类定义自己的指定初始化函数时,父类的指定初始化函数“退化”为子类的方便初始化函数。该规范的目的是:“确保子类新添加的变量能够被正确初始化。"因为我们不能限制用户创建子类的方式,比如我们在创建UIViewController时可以使用以下三种方法:UIViewController*vc=[[UIViewControlleralloc]init];UIViewController*vc=[[UIViewControlleralloc]initWithNibName:nilbundle:nil];UIViewController*vc=[[UIViewControlleralloc]initWithCoder:xxx];4.举个栗子,上面三个规范理解起来可能有点混乱,我写了一个简单的例子帮助理解规范,代码如下:@interfaceAnimal:NSObject{NSString*_name;}-(instancetype)initWithName:(NSString*)nameNS_DESIGNATED_INITIALIZER;@end@implementationAnimal-(instancetype)initWithName:(NSString*)name{self=[superinit];if(self){_name=name;}returnself;}-(instancetype)init{return[selfinitWithName:@"Animal"];}@end@interfaceMammal:Animal{NSInteger_numberOfLegs;}-(instancetype)initWithName:(NSString*)nameandLegs:(NSInteger})numberOfLegsNS_DESIGNATED_INITIALIZER;-(instancetype)initWithLegs:(NSInteger)numberOfLegs;@end@implementationMammal-(instancetype)initWithLegs:(NSInteger)numberOfLegs{self=[selfinitWithName:@"Mammal"];如果(选择f){_numberOfLegs=numberOfLegs;返回自我;}-(instancetype)initWithName:(NSString*)nameandLegs:(NSInteger)numberOfLegs{self=[superinitWithName:name];如果(自己){_numberOfLegs=numberOfLegs;返回自我;}-(instancetype)initWithName:(NSString*)name{return[selfinitWithName:nameandLegs:4];}@end@interfaceWhale:哺乳动物{BOOL_canSwim;}-(实例类型)initWhaleNS_DESIGNATED_INITIALIZER;@end@implementationWhale-(instancetype)initWhale{self=[superinitWithName:@"Whale"andLegs:0];如果(自己){_canSwim=是;返回自我;}-(instancetype)initWithName:(NSString*)nameandLegs:(NSInteger)numberOfLegs{返回[selfinitWhale];*)description{return[NSStringstringWithFormat:@"Name:%@,NumberofLegs%zd,CanSwim%@",_name,_numberOfLegs,_canSwim?@"YES":@"NO"];}@endTestDesignatedInitializer配合上面的代码,我也画了一个类调用图帮助大家理解,如下:我们声明了三个类:Animal(动物)、Mammal(哺乳动物)、Whale(鲸鱼),并根据指定初始化函数函数的说明让我们创建一些Whales(鲸鱼)来测试它们的健壮性。代码如下:Whale*whale1=[[Whalealloc]initWhale];//1NSLog(@"whale1%@",whale1);Whale*whale2=[[Whalealloc]initWithName:@"Whale"];//2NSLog(@"whale2%@",whale2);Whale*whale3=[[Whalealloc]init];//3NSLog(@"whale3%@",whale3);Whale*whale4=[[Whalealloc]initWithLegs:4];//4NSLog(@"whale4%@",whale4);Whale*whale5=[[Whalealloc]initWithName:@"Whale"andLegs:8];//5NSLog(@"whale5%@",whale5);执行结果为:whale1Name:Whale,NumberofLegs0,CanSwimYESwhale2Name:Whale,NumberofLegs0,CanSwimYESwhale3Name:Whale,NumberofLegs0,CanSwimYESwhale4Name:Whale,NumberofNumberName0Leg,Leg:WhaleCanSwimYES分析表明:whale1是使用Whale指定的初始化函数创建的,初始化调用顺序为:⑧->⑤->③->①.初始化方法的实际执行顺序正好相反:①->③->⑤->⑧,即从根类开始初始化。初始化的顺序与类成员变量的布局顺序完全一致。有兴趣的可以上网查一下。whale5使用Whale的父类Mammal指定的初始化函数来创建一个实例。初始化调用顺序为:⑦->⑧->⑤->③->①,创建的对象符合预期。注:⑦表示Whale类的实现,其内部实现调用了自己类的指定初始化函数initWhale。⑤代表Mammal类的实现。细心的朋友可能送来了我们创造的第四只鲸鱼,它神奇地长出了4条腿。我们看一下创建过程的调用顺序:⑥->④->⑦->⑧->⑤->③->①,可以看出对象的初始化也是按照下面的顺序依次完整初始化的现在的班级,那么问题出在哪里呢?Mammal类的initWithLegs:函数,??除了正常的初始化函数调用栈外,还有一个函数体,就是重新设置初始化对象的成员变量_numberOfLegs的值,从而使鲸鱼长出4条腿。-(instancetype)initWithLegs:(NSInteger)numberOfLegs{self=[selfinitWithName:@"Mammal"];如果(自己){_numberOfLegs=numberOfLegs;返回自我;初始化函数创建子类的对象,最后四个调用顺序为:⑧->⑤->③->①。指定的初始化函数规则只能保证对象创建过程从基类到子类依次初始化所有成员变量,不能解决业务问题。5、initWithCoder:遇到NS_DESIGNATED_INITIALIZER时,NSCoding协议定义如下:@protocolNSCoding-(void)encodeWithCoder:(NSCoder*)aCoder;-(nullableinstancetype)initWithCoder:(NSCoder*)aDecoder;//NS_DESIGNATED_INITIALIZER@end苹果官方文档DecodinganObject明确规定:在一个initWithCoder:方法的实现中,对象首先要调用其超类的designatedinitializer来初始化继承状态,并且然后它应该解码并初始化它的状态。如果超类采用NSCoding协议,则首先将initWithCoder:的返回值赋值给self。翻译:如果父类没有实现NSCoding协议,那么应该调用父类指定的初始化函数。如果父类实现了NSCoing协议,那么子类的initWithCoder:实现需要调用父类的initWithCoder:方法。根据上面第三部分描述的指定初始化函数的三个规则,NSCoding实现的两个原则都要求超类的初始化器违反了第二个指定初始化实现的原则。该怎么办?仔细观察NSCoding协议中的initWithCoder:定义后面跟着一个被注释掉的NS_DESIGNATED_INITIALIZER,你能找到一些灵感吗?在实现NSCoding协议时,我们可以显式声明initWithCoder:为指定初始化函数(一个类可以有多个指定初始化函数,比如UIViewController),就可以彻底解决问题,不仅满足指定初始化的三个规则function,也符合NSCoding协议的三大原则。6.小结上面关于指定初始化的规则说了这么多,但归纳起来就是两点:方便初始化函数只能调用自己类中的其他初始化方法。指定的初始化函数有资格调用父类的指定初始化函数。这张图帮助我们理解了这两点:当我们为自己创建的类添加指定的初始化函数时,一定要准确识别并重写直接父类的所有指定初始化函数,这样才能保证整个类的初始化过程子类可以覆盖继承链上的所有成员变量并正确初始化。NS_DESIGNATED_INITIALIZER是一个非常有用的宏,它充分发挥了编译器的特性,帮助我们找到初始化过程中可能存在的漏洞,增强代码的健壮性。
