关于Objective-C
对象初始化的一些基本知识已经在《Objective-c下的对象初始化》一文中写过,这里不再赘述。这篇文章主要是对最近进行iOS开发时的涉及对象的初始化有关的东西的一点个人的总结。
编码规范
首先,有一个良好的编码规范是一个好的开始,这一点应该是特别需要注意的。
因此,对于编写Objective-C
类的初始化方法时应当遵守以下几条规范:
格式
初始化器应当使用下面的模板编写:- (instancetype) init { self = [super init];//调用父类的初始化器防止从父类继承的属性未被初始化 if(self) //判断是否成功初始化父类,防止在父类初始化失败的时候对自身进行初始化 { //自定义的初始化代码 } return self; }
在初始化方法内直接使用实例变量对属性进行赋值
- (instancetype) initWithName:(NSString *)personName age:(int)personAge //指定初始化器 { self = [super init]; if(self) { _name = personName; //不要使用self.name = personName; _age = personAge; //同上 } return self; }
以上代码假定类有name与age两个属性,在初始化时使用实例变量进行读写可以防止因对象的不稳定状态引起的错误。
返回
instancetype
而不要返回id
返回instancetype
可以给编译器提供有利信息以便使编译器能够帮助我们进行类型检查。
关于指定初始化器需注意的问题
对于子类而言,创建指定初始化器有三种方式:
- 什么都不做,直接继承父类的指定初始化器
- 重写父类的指定初始化器
- 重新自定义一个指定初始化器
对于1而言,几乎什么都不需要做,只需要确认子类的其他初始化器调用了父类的指定初始化器即可。
当需要在初始化器中加入子类的初始化逻辑时,可以选择重写父类的指定初始化器,这时只需要按照上面的模板写好自己的初始化逻辑并确保调用父类的初始化器,最后再确认子类的其他初始化器调用了子类的指定初始化器即可。
当需要在子类中重新定义一个指定初始化器时,需要特别注意完成一下步骤:
- 定义子类的指定初始化器并确保其调用了父类的指定初始化器
- 重写父类的指定初始化器并让其调用子类的新定义的指定初始化器或让父类的指定初始化器失效
- 为子类新的指定初始化器编写文档
下面是一个例子,假定父类为Persion
类,拥有name
和age
属性,指定初始化器如下:
- (instancetype) initWithName:(NSString *)personName
age:(int)personAge //指定初始化器
{
self = [super init];
if(self)
{
_name = personName;
_age = personAge;
}
return self;
}
它有一个子类叫做People
,它有一个表示性别的属性peopleSex
,现在需要重新定义一个指定初始化器initWithSex:name:age:
,定义以及重写父类的指定初始化器如下:
#import "People.h"
@interface Persion ()
@property (nonatomic, readwrite, strong) NSString *sex;
@end
@implementation People
- (instancetype)initWithSex:(NSString *)sex name:(NSString *)persionName age:(NSInteger)persionAge
{
self = [super initWithName:persionName age:persionAge];
if(self)
{
_peopleSex = sex;
}
return self;
}
- (instancetype)initWithName:(NSString *)persionName age:(NSInteger)persionAge
{
return [self initWithSex:@"man" name:@"unknow" age:0];
}
@end
在上面的例子中,首先新定义了子类的指定初始化器,在子类的初始化器中,调用了父类的指定初始化器并实现了自己独有的初始化逻辑。然后又重写了父类的指定初始化器,这样,当用户使用父类的指定初始化器以及间接初始化器初始化对象的时候也能够调用到子类的指定初始化器并执行子类独有的初始化逻辑。
接下来只需要为子类的指定初始化器编写文档就完成了自定义指定初始化器工作。
当然,还需要注意一点的就是千万不要在指定初始化器中调用间接初始化器。因为间接初始化器会调用指定初始化器,而指定初始化器又会调用间接初始化器,这样便会引起调用死循环。
特例-initWithCoder:
一般而言,一个类的指定初始化器只有一个。但是,这有个特例,那就是这个类实现了NSCoding
协议,NSCoding
协议定义了两个必须实现的方法:
- (id)initWithCoder:(NSCoder *)decoder;
- (void)encodeWithCoder:(NSCoder *)encoder;
第一个方法用于从压缩文件中解压缩并初始化类的属性,第二个方法用来把类的各个属性压缩到压缩文件中。
例如上面的例子,要让Persion
类和People
类拥有把属性保存到压缩文件和从压缩文件中初始化对象,就需要实现NSCoding
协议,需要实现的代码如下:
首先声明Persion
类实现NSCoding
协议
#import <Foundation/Foundation.h>
@interface Persion : NSObject <NSCoding> //声明实现NSCoding协议
@property (nonatomic, readonly, strong) NSString *name;
@property (nonatomic, readonly) NSInteger age;
- (instancetype)initWithName:(NSString *)persionName age:(NSInteger)persionAge;
- (instancetype)init;
- (instancetype)initWithCoder:(NSCoder *)aDecoder;
- (void)encodeWithCoder:(NSCoder *)aCoder;
@end
然后分别实现协议中的方法:
//Persion.m
#pragma mark - NSCoding Protocol
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super init]; //这里调用了父类的指定初始化器
if(self)
{
//按压缩时的key进行解压
_name = [aDecoder decodeObjectForKey:@"name"];
_age = [aDecoder decodeIntegerForKey:@"age"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
//按照名值对方式进行压缩,最终压缩到一个xml文件中。
//因此是按照xml的key-value进行编码
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.age forKey:@"age"];
}
#pragma mark - archive
//压缩解压缩方法,用来存储、或从文件中读取类
- (BOOL)savePropertys //把属性存储到文件
{
NSString *path = [self archivePath];
return [NSKeyedArchiver archiveRootObject:self toFile:path];
}
- (NSString *)archivePath //获取用户沙盒目录
{
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [documentDirectories firstObject];
return [documentDirectory stringByAppendingPathComponent:@"items.archive"];
}
- (id)unarchive //从文件中读取
{
NSString *path = [self archivePath];
return [NSKeyedUnarchiver unarchiveObjectWithFile:path];
}
//People.m
#pragma mark - NSCoding Protocol
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder]; //这里调用了父类的方法
if(self)
{
_peopleSex = [aDecoder decodeObjectForKey:@"sex"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[super encodeWithCoder:aCoder];
[aCoder encodeObject:self.peopleSex forKey:@"sex"];
}
从上面的代码也可以注意到,当使用initWithCoder:
初始化器时,也需要按照模板来,并保证所有属性都能够初始化,当然更需要注意父类是否实现了NSCoding
协议:
- 父类实现了
NSCoding
协议
这时候当在编写子类的initWithCoding:
方法时,需要调用父类的initWithCoding
方法 - 父类没有实现
NSCoding
协议
此时在编写子类的initWithCoding:
方法时,需要调用父类的指定初始化器。
当然,initWithCoding:
方法不需要其他间接初始化器进行调用,它通常都是由系统或系统提供的类进行调用。
UIViewController的初始化方法
UIViewController
的初始化器有以下几个:
- (instancetype)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle;
-(instancetype)init;
- (instancetype)initWithCoder:(NSCoder *)decoder;
其中,
- (instancetype)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle;
是UIViewController
的指定初始化器。
那么,又应该在什么时候去调用这三个初始化器呢?这就需要看UIViewController
是如何设计的:
使用StroyBoard设计
当UIViewController
使用StoryBoard
设计的时候,UIViewController
从StoryBoard
初始化的时候就会调用initWithCoder:
初始化器。待UIViewController
初始化完成后,会继续对其上设置了Outlet
的视图调用initWithCoder:
初始化器进行初始化。注意:调用
UIStoryBoard
的instantiateViewControllerWithIdentifier:
方法获取UIViewController
实例的时候也会调用initWithCoder:
初始化方法进行初始化。使用xib文件方式设计
当UIViewController
使用xib
文件设计的时候,我们就可以使用initWithNibName:
对UIViewController
进行初始化。当使用initWithNibName:
对UIViewController
初始化完毕后,同样会对设置了Outlet
的UIView
调用initWithCoder:
方法进行初始化。当然,因为
initWithNibName:
是指定初始化器,所以也可以调用init
方法进行初始化。在这里更推荐使用
init
方法进行初始化。使用代码方式设计
使用纯代码方式设计UIViewController
时,直接调用init
方法进行初始化即可。
因此,当需要在UIViewController
的子类中加入子类的初始化逻辑时,需要根据视图控制器的设计方式选择重写相应的初始化方法:
- 使用
StoryBoard
设计时需要重写initWithCoder:
方法 - 使用
xib
文件设计时需要重写initWithNibName:
方法 - 使用纯代码设计时,需要重写
init
方法
当然,更好地方式是把初始化逻辑写在viewDidLoad
方法中。 因为这样无论使用什么方式设计的UIViewController
都可以保证调用到加入的初始化逻辑。
关于初始化的设计模式
单例模式
单例模式就是让某个类只有一个实例的一种设计模式,为了实现这一点,通常来说会用下述代码实现:
+ (instancetype)sharedStore
{
static INSStore *store = nil;
if(!store) //如果对象还没被创建
{
store = [[INSStore alloc] init];
}
return store;
}
上述代码放在单线程环境中是没有问题的,能够保证每次返回的都是同一个对象。但是如果放到多线程环境中执行就难以保证每次返回的都是同一个。因此,在编写单例模式时,应该尤其注意线程安全的问题。
那么,又应该如何编写一个线程安全的单例方法?
答案是利用dispath_once()
,GCD中的dispath_once()
函数可以保证有它执行的代码在所有线程中仅执行一次,因此,将上面的单例方法改写如下:
+ (instancetype)sharedStore
{
static INSStore *store = nil;
static dispatch_once_t count = 0;
//用来判断dispatch_once的block中的代码是否执行过
if(!store)
{
dispatch_once(&count, ^{
store = [[INSStore alloc] init];
});
}
return store;
}
这样,就保证了单例方法能够在多线程的环境下仅创建一次对象并总是返回同一个对象。
不言而喻,在编写单例模式的时候应该使用第二种方式。
类簇
Objective-C
中的类簇设计模式其实就是Java
中的抽象工厂设计模式。
上次在《理解Objective-C的类与对象》中说过,在使用类簇设计模式的类的实例上调用isMemberOfClass:
方法会往往得不到预料的结果。
这是因为你在其上调用初始化方法的类往往是抽象类,而真正返回的类则是抽象类的某个子类。例如如下代码:
NSArray *array = [[NSArray alloc] init];
NSLog(@"%@",[array class]);//输出array地类型
得到的输出却是:
2015-10-16 17:00:21.352 initSample[2381:84466] __NSArrayI
输出地类型显然不是我们预料中的NSArray
。
可见,NSArray
类只是一个抽象类,调用它的初始化方法返回的则是它的某个子类,在这个例子中是__NSArrayI
。
那么,使用类簇设计模式又有什么好处呢?
好处当然是有的,要不然干嘛要用它呢?一个显而易见的好处就是可以屏蔽掉底层的一些对类的使用者而言无关的细节而统一成一个统一的抽象接口。
例如我们有若干问题,问题的类型有填空题、选择题,现在我们需要分别为他们编写相应的视图来显示他们。这样,在使用这些视图显示问题的时候我们就需要这样使用:
INSQuestionView *view = nil;
if([Question.type isEqualToString:@"choice"]) // 如果是选择题
{
view = (INSQuestionView)[INSChoiceView alloc]
initWithQestion:Question]; //初始化选择题
}
if([Question.type isEqualToString:@"blank"]) //如果是填空题
{
view = (INSQuestionView)[INSBlankView alloc]
initWithQestion:Question]; //初始化填空题
}
上面的INSChoiceView
和INSBlankView
都是INSQuestionView
的子类。
不仅麻烦、暴露过多细节而且还不容易修改,如果再增加个简答题类型,那又得到处修改代码。
因此,我们可以将Question
类改为使用类簇模式初始化并把判断逻辑放到初始化器中。为Question
类添加的初始化器代码如下:
+ (id)ViweForQuestion:(Question *)question
{
if([self isMemberOfClass:Question.class])
{
if([Question.type isEqualToString:@"choice"]) // 如果是选择题
{
return [INSChoiceView alloc] initWithQestion:Question]];
}
if([Question.type isEqualToString:@"blank"]) //如果是填空题
{
return [INSBlankView alloc] initWithQestion:Question]];
}
}
return nil;
}
NOTE:这里最好使用
enum
表示Question
的类型。
总结
- 编写初始化器要遵守编码规范。
- 子类中编写指定初始化器需要注意是否对父类的初始化器处置妥当
- 编写视图控制器的初始化器需要因地制宜(看视图控制器是通过哪种方式设计的)
- 最好将视图控制器的初始化代码放到
viewDidLoad
方法中 - 编写单例模式注意线程安全
- 当需要按照某种条件初始化同一个类的各个子类时,可以考虑使用类簇模式