单例模式
单例简介
单例是生命周期与程序生命周期相同,仅能生成一次、且不能被销毁的唯一实例;
需要确保应用中的一个特定类有且仅有一个实例(对象);
单例可在程序任何位置被访问,且一直存在;
单例实例获取方法命名规则:一般以standard…、shared…、default….开头;
单例必须至少满足以下条件:
只初始化一次;
全局存在;
唯一,对象不可改;
方便获取,任何位置可访问
系统单例
UIApplication:应用程序单例
NSNotificationCenter:通知中心单例
NSUserDefaults:用户默认单例
NSFileManager:文件管理单例
单例优点
共享信息;
实例控制:Singleton会阻止其他对象实例化其自己的Singleton对象的副本,从而确保所有对象都访问唯一实例;
灵活性:因为类控制了实例化过程,所以类可以更加灵活地修改实例化过程;
自定义单例创建常用方法
首先创建一个继承于NSObject的类Singleton,其次在Singleton.m文件中实现如下代码;
- 方法1:一般创建
#import "Singleton.h"
// 静态变量保证单例对象始终存在
static Singleton *singleton = nil;
@implementation Singleton
+ (instancetype)defaultSingleton {
// 仅在单例对象为空时初始化一次,之后直接返回唯一的单例对象
if (singleton == nil) {
singleton = [[Singleton alloc] init];
}
return singleton;
}
@end
- 方法2:GCD创建(推荐使用)
#import "Singleton.h"
static Singleton *singleton = nil;
@implementation Singleton
+ (instancetype)defaultSingleton {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
singleton = [[Singleton alloc] init];
});
return singleton;
}
@end
自定义单例创建完整方法
一般创建单例方法可能会出现一些问题,我们通过不同的途径得到不同的对象,显然是不行的。我们必须要确保对象的唯一性,所以我们就需要封锁用户通过alloc/init/copy以及release/retain等方法来构造或操作对象这条道路。
我们知道,创建对象的步骤分为申请内存(alloc)、初始化(init)这两个步骤,我们要确保对象的唯一性,因此在第一步这个阶段我们就要拦截它。当我们调用alloc方法时,oc内部会调用allocWithZone这个方法来申请内存,我们覆写这个方法,然后在这个方法中调用defaultSingleton方法返回单例对象,这样就可以达到我们的目的。拷贝对象也是同样的原理,覆写copyWithZone方法,注意需遵守
<NSCopying>
协议,然后在这个方法中调用defaultSingleton方法返回单例对象,其他示例直接参考如下代码:
static Singleton *singleton = nil;
+ (instancetype)defaultSingleton {
if (!singleton) {
singleton = [[Singleton alloc] init];
}
return singleton;
}
// alloc
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
@synchronized(self) {
if (!singleton) {
singleton = [super allocWithZone:zone];
return singleton;
}
return nil;
}
}
// copy
- (id)copyWithZone:(NSZone *)zone {
return self;
}
// retain
- (instancetype)retain {
return self;
}
// release
- (oneway void)release {
}
// autorelease
- (instancetype)autorelease {
return self;
}
// retainCount
- (NSUInteger)retainCount {
return NSUIntegerMax;
}
单例使用
首先在main.m文件中,引入“Singleton.h”,其次创建两个单例实例,并且打印对象信息;
我们可以看到,两个实例对象的地址是一致的,由此可见,单例的唯一性。
可利用单例的唯一性进行传值,但必须满足一个前提条件就是,在传值时,必须保证单例存在并且单例属性确实有值。
现在在Singleton.h文件中声明属性name,其次创建一个Person类,为其设置属性name,并且重写init方法,通过单例对name直接复制,如下所示:
#import "Person.h"
#import "Singleton.h"
@implementation Person
- (instancetype)init {
self = [super init];
if (self) {
// 获取单例属性值赋给name
self.name = [Singleton defaultSingleton].name;
}
return self;
}
@end
切换到main.m文件中,创建单例实例,并且给name属性复制,其次实例化Person对象,并打印person.name,如下所示:
#import <Foundation/Foundation.h>
#import "Singleton.h"
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
#pragma mark - 单例模式
Singleton *singleton = [Singleton defaultSingleton];
singleton.name = @"Admin";
Person *person = [[Person alloc] init];
NSLog(@"Person's name is '%@'.", person.name); // Person's name is 'Admin'.
}
return 0;
}
从上述代码中可以看出,可通过单例进行传值操作。
KVC 键值编码
概述
KVC是KeyValueCoding的简称,它是一种可以直接通过字符串的名字(key)来访问类属性(实例变量)的机制。而不是通过调用Setter、Getter方法访问。通常我们使用
valueForKey
来替代getter()
方法,setValue:forKey
来代替setter()
方法。当使用KVO、Core Data、CocoaBindings、AppleScript(Mac支持)时,KVC是关键技术。
常用方法
- 取值方法
// 1、
valueForKey:,传入NSString属性的名字。
// 2、
valueForKeyPath:,传入NSString属性的路径,xx.xx形式。
- 设值方法
// 1、
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 2、
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 3、- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
// 4、
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
KVC 读取方式
KVC使用起来比较简单,但是它如何查找一个属性进行读取呢?具体查找规则(假设现在要利用KVC对name进行读取):
如果是动态设置属性,则优先考虑调用setName方法,如果没有该方法则优先考虑搜索成员变量_name,如果仍然不存在则搜索成员变量name,如果最后仍然没搜索到则会调用这个类的setValue:forUndefinedKey:方法(注意搜索过程中不管这些方法、成员变量是私有的还是公共的都能正确设置);
如果是动态读取属性,则优先考虑调用name方法(属性name的getter方法),如果没有搜索到则会优先搜索成员变量_name,如果仍然不存在则搜索成员变量name,如果最后仍然没搜索到则会调用这个类的valueforUndefinedKey:方法(注意搜索过程中不管这些方法、成员变量是私有的还是公共的都能正确读取);
案例
1、声明 KVCClass
类,并在其.h文件中声明几个全局变量,无需定义成属性及生成存(setter)取(getter)方法,代码如下:
#import <Foundation/Foundation.h>
@interface KVCClass : NSObject {
NSString *_name; /**< 姓名 */
NSString *_nationality; /**< 国籍 */
NSInteger _age; /**< 年龄 */
}
@end
2、在 “KVCClass.m”文件中,重写description
方法与dealloc
方法,代码如下:
#import "KVCClass.h"
@implementation KVCClass
- (void)dealloc {
[_name release]; _name = nil;
[_nationality release]; _nationality = nil;
[super dealloc];
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%p name:%@, age:%ld, address:%@>", self, _name, _age, _nationality];
}
@end
3、在main.m文件中,导入KVCClass.h
,实例化对象,并通过KVC为其设值,代码如下:
#import <Foundation/Foundation.h>
#import "KVCClass.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
#pragma mark - KVC
// 1、逐一赋值
KVCClass *kvcObject1 = [[KVCClass alloc] init];
[kvcObject1 setValue:@"Charles" forKey:@"name"];
[kvcObject1 setValue:@"China" forKey:@"nationality"];
[kvcObject1 setValue:@22 forKey:@"age"];
NSLog(@"%@", kvcObject1);
// 取值
NSLog(@"Name is '%@'.", [kvcObject1 valueForKey:@"name"]);
// 2、统一赋值
KVCClass *kvcObject2 = [[KVCClass alloc] init];
NSDictionary *dic = @{@"name":@"William", @"nationality":@"USA", @"age":@30};
[kvcObject2 setValuesForKeysWithDictionary:dic];
NSLog(@"%@", kvcObject2);
// 取值
NSLog(@"Name is '%@'.", [kvcObject2 valueForKey:@"name"]);
return 0;
}
控制台输出情况如下:
Tips
1、key的值必须正确,如果拼写错误,会出现异常;
2、当key的值是没有定义的,valueForUndefinedKey:这个方法会被调用,如果你自己写了这个方法,key的值出错就会调用到这里来;
3、因为类key反复嵌套,所以有个keyPath的概念,keyPath就是用.号来把一个一个key链接起来,这样就可以根据这个路径访问下去;
4、NSArray/NSSet等都支持KVC;
KVO 键值监听
概述
KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。在ObjC中要实现KVO则必须实现NSKeyValueObServing协议,不过幸运的是NSObject已经实现了该协议,因此几乎所有的ObjC对象都可以使用KVO。
在面对对象编程时,我们很多情况下直接处理某些事件会很麻烦。如常见的程序运行中电话呼入、用户突然点击home键等等,一般我们使用观察者(代理)来处理类似事件。
在使用这些模式的时候,我们需要告诉这些角色,谁来响应这些事件,这就需要使用类的引用。
类的引用本质上就是如何让另外一个角色,“知道”另外一个角色,对应到代码中,就是如何让某一个类的对象,顺利的把信息传递给另外一个对象。
处理类的引用的时候,应注意以下几点:
每个类是否能正常接收到信息(响应动作)
信息传递时接收者是否存在
每个类是否能正常释放(不会引起循环引用)
键值观察
键值观察是基于键值编码的一种技术;
利用键值观察可以注册成为一个对象的观察者,在该对象的某个属性变化时收到通知;
被观察对象需要编写复合KVC标准的存取方法;
常用方法
系统框架已经支持KVO,所以程序员在使用的时候非常简单。
1、注册指定Key路径的监听器:addObserver: forKeyPath: options: context:
2、实现回调方法: observeValueForKeyPath: ofObject: change: context:
3、删除指定Key路径的监听器:removeObserver: forKeyPath、removeObserver: forKeyPath: context:
案例
此案例将KVC与KVO结合使用
1、声明 KVOClass
类,并在其.h文件中声明全局变量_weather
;
#import <Foundation/Foundation.h>
@interface KVOClass : NSObject {
NSString *_weather; /**< 天气 */
}
@end
2、在“person.m”文件中实现如下代码:
#import "KVOClass.h"
@implementation KVOClass
- (void)dealloc {
// 移除监听
[self removeObserver:self forKeyPath:@"weather"];
[_weather release]; _weather = nil;
[super dealloc];
}
- (instancetype)init {
self = [super init];
if (self) {
// 注册监听
[self addObserver:self forKeyPath:@"weather" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
return self;
}
// 实现监听回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
NSLog(@"天气发生变化,此时天气情况:%@", change[@"new"]);
}
@end
3、在“main.m”文件中导入KVOClass.h
文件,实现如下代码,观察控制台:
#import <Foundation/Foundation.h>
#import "KVOClass.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
#pragma mark - KVO
KVOClass *kvoObject = [[KVOClass alloc] init];
[kvoObject setValue:@"晴" forKey:@"weather"];
[kvoObject release];
}
return 0;
}
控制台输出:
2015-11-17 23:02:00.860 DesignPatterns[1392:132353] 天气发生变化,此时天气情况:晴
Tips
使用KVC可以大大地减少我们的代码量,当遇到property的时候,可以多想想是否可以KVC来帮助我,是否可以用KVC来重构代码, 当需要加入observer模式时,可以考虑下KVO, 在高性能的observer里面,KVO会给我们很好的帮助。
数据持久化
iOS应用程序使用沙盒机制作为文件存储方式;
应用程序有一个独立的沙盒文件夹,程序仅能访问自己沙盒路径下的文件,任何情况下都无法访问其它程序的沙盒;
那我们可以通过什么方法将数据存入沙盒,同时也可以取出?可同 NSUserdefaults 或 属性列表实现。
数据持久化的方法还有很多,如本地数据库(CoreData、sqlite、归档)、网络数据库(服务器)等等,会在后面的博客中介绍。
NSUserdefaults
常用方法
1、存值
// 1、获取系统单例
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// 2、设值
[defaults setObject:@"William" forKey:@"name"];
// 3、同步数据
[defaults synchronize];
2、取值
// 1、获取系统单例
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// 2、取值
NSString *name = [defaults objectForKey:@"name"];
// 3、打印信息
NSLog(@"Name is '%@'.", name);
3、移除值
// 1、获取系统单例
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// 2、删除值
[defaults removeObjectForKey:@"name"];
// 3、同步数据
[defaults synchronize];
Tips
1、从上述示例中可以看出,无论是什么操作,都需获取userdefaults单例,如果是存值或删除值,一定记得调用
synchronize
方法,同步数据。2、通过userdefaults同样可以进行值的传递,因为userdefaults本质也是单例。
读写文件
我们可以自己通过代码,根据一条路径自动在桌面上创建一个文件,来存取我们的数据,注意:这个数据并非存在沙河中。
获取桌面文件路径方式:直接将桌面文件拖入Xcode工程即可,Xcode会自动获取文件路径。
NSDictionary *dict = @{@"account":@"Admin", @"password":@"123456"};
// 1、写入文件
// 将 dict 存入文件,/infomation.text 为自己给存储对象文件的命名
[dict writeToFile:@"/Users/LiHongyao/Desktop/Datas/infomation.text" atomically:YES];
// 2、读取文件
NSDictionary *infomation = [NSDictionary dictionaryWithContentsOfFile:@"/Users/LiHongyao/Desktop/Datas/infomation.text"];
NSLog(@"%@", infomation);
输出情况:
2015-11-17 23:49:39.302 DesignPatterns[1562:170884] {
account = Admin;
password = 123456;
}
Program ended with exit code: 0