第33章 init
向新创建的对象发送init消息,它就会初始化其下的实例变量。也就是说,alloc负责分配对象空间,init负责初始化对象。请注意init是实例方法,返回的是初始化后的对象地址。
- (instancetype)init {
//调用NSObject的init方法
self = [super init];
//是否返回非nil的值?
if (self) {
// 为实例变量赋值
_voltage = 120;
}
return self;
}```
这个init方法会返回一个instancetype类型的值。instancetype这个关键字会告诉编译器方法返回什么类型的对象。你编写的或是覆盖的任何初始化方法都应该返回instancetype类型的值。
在还没有引入instancetype之前,初始化方法返回的都是id类型。然而,使用instancetype是更好的选择,它除了灵活解决子类的问题,还可以让编译器检查返回值的类型。
在上述的init方法中,头两行代码是做检查:
* 这个init方法的第一行,将父类的init方法所返回的对象赋给self。
* 检查父类的初始化方法的返回值,确定不是nil并且有效。
这些检查有什么用处呢?少数类的初始化方法需要做一些特殊处理。下面举两个例子。
* 出于优化考虑,init方法会释放已经分配了内存的对象,然后创建另一个新对象并返回之。
对于这种情况,苹果公司要求的做法是:将父类的init方法所返回的对象赋给self。
* init方法在执行过程中发生了错误,所以会释放对象并返回nil。
而对于这种情况,苹果公司建议的做法是:检查父类的初始化方法的返回值,确定不是nil并且有效。如果对象不存在,就没有必要执行自定义的初始化方法。
***
创建子类时,通常只需要初始化新的实例变量。此外,也需要调用父类的初始化方法,初始化父类的实例变量。
***
每个类都有一个指定初始化方法(designated initializer)。init 是 NSObject 的指定初始化方法。
指定初始化方法扮演的是单一入口的角色。任何类都有且只有一个指令初始化方法。如果某个类还有其他初始化方法,那么这些方法应该(直接地或间接地)调用指定初始化方法。
创建新类时,如果指定初始化方法的方法名和父类的不同,就需要在类的头文件中做出说明。加入适当的注释。
编写初始化方法时,应该遵循以下规则:
* 其他的初始化方法都应该(间接地或直接地)调用指定初始化方法。
* 指定初始化方法应该先调用父类的指定初始化方法,然后再对实例变量进行初始化。
* 如果某个类的指定初始化方法和父类的不同(这里指的是方法名不同),就必须覆盖父类的指定初始化方法,并调用新的指定初始化方法。
* 如果某个类有多个初始化方法,就应该在相应的头文件中明确地注明哪个方法是指定初始化方法。
***
假设要创建一个NSObject的子类。出于安全考虑,必须为其中的一个实例变量赋值,不能使用默认值。
最佳的解决方案是覆盖父类的指定初始化方法,然后通过某种途径告知程序员不能调用这个方法,并提供修改建议:
- (instancetype)init
{
[NSException raise:@"BNRWallSafeInitialization"
format:@"Use initWithSecretCode:, not init"];
}```
这类问题会造成程序的崩溃。如果不按上述方法修改,控制台就会输出崩溃的错误信息。
第34章 再谈属性
属性的存取类型:
任何一个属性可以声明为readwrite或 readonly,默认为readwrite。readwrite代表:程序应该自动创建存方法和取方法。如果无需创建存方法,则可以将属性声明为readonly:
@property (readonly) int voltage;
属性的生命周期类型:
首先我们先区分一下在MRC和ARC下的属性都有使用哪些关于生命周期管理的修饰符。
MRC: assign, copy, retain
ARC: strong, weak, unsafe_unretained, copy
assign: 简单赋值,不更改引用计数。一般用于基础类型的数据(NSInteger)和C语言类型数据(int , float , double , char , bool)。其在MRC下是默认值。
copy: 会拷贝传入的对象(即创建一个引用计数为1的新对象,但是内容与传入对象相同),并把新对象赋值给实例变量。常用与NSString,NSArray,NSDictionary,NSSet等。
retain: 释放旧对象,并使传入的新对象引用计数+1。此属性只能用于NSObject及其子类,而不能用于Core Foundation(因为其没有使用引用计数,需要另外使用CFRetain和CFRelease爱进行CF的内存管理)。
ARC加入的属性修饰符如下。
strong: 强引用,类似于retain。要求保留传入的对象,并放弃原有对象。一个对象只要被至少一个强引用指向,则其不会被释放,而当没有强引用指向时则会被释放。其在ARC下是对象类型的默认值。
weak: 弱引用,要求不保留传入的属性(既不会使传入的对象引用计数+1)。类似于assign,但与assign不同的是,当它们指向的对象被释放后,weak会被自动置为nil,而assign则不会,所以assign会导致“野指针”的出现,weak可以避免悬空指针。
unsafe_unretained: 其实质等同于assign。与weak的区别就是指向的对象如果被释放,其不会被置为nil,而导致悬空指针的出现。它是ARC模式下非对象属性的默认值。
所以综上所述,属性的默认值主要有以下情况。
- MRC:(atomic, readwrite, assign)
- ARC下对象类型属性:(atomic, readwrite, strong)
- ARC下非对象类型:(atomic, readwrite, unsafe_unretained)
其它对比:
- copy/retain
答:copy会拷贝创建一个新的对象,并使得它的引用计数为1。retain则是Release旧值,retain新值,其本质是指针复制(浅复制),引用计数加1,而不会导致内容被复制。
如:一个NSString对象,内存地址为:0x1111,内容为@“Hello”。
(1)copy到另外一个NSString后,地址为0x2222,内容相同(新建一个内容,内容拷贝),新的对象引用计数为1,旧的对象内容没有改变,引用计数-1。
(2)retain到另外一个NSString后,地址相同(新建一个指针,指针拷贝),内容相同,对象的引用计数+1。
2)assign/retain(MRC情况下)
答:assign只是简单的赋值,如果它引用的对象被释放了,则会造成悬空指针的出现,此时再通过该引用访问对象则会导致程序crash。retain则是在引用计数的基础上,对对象引用计数+1,以获取对象的拥有权,这样只有当对象的引用计数为0时才会被释放(既没有别的引用指向它),这样可以避免访问一个被释放的对象。
3)assign/weak(ARC情况下,因为assign类似于unsafe_unretained,所以也可以说是weak和unsafe_unretained的区别)
答:assign不同的是,当它们指向的对象被释放后,weak会被自动置为nil,而assign则不会,所以assign会导致“野指针”的出现。
扩展:
上面那些属性描述符都是针对类中定义的属性而言的,实际上对于局部变量也有类似的关键字来修饰变量,常用主要有
__strong,__weak, __unsafe_unretained, __autoreleasing
__strong
: 是默认引用类型的关键字。
__weak
: 声明一个可以自动置nil的弱引用。
__unsafe_unretained
: 弱引用,但是当指向对象被释放时,不会被置nil。所以会导致野指针的出现。
__autoreleasing
:用来修饰一个函数的参数,这个参数会在函数返回的时候被自动释放。
对象的拷贝:
有些类有两个版本:一个是可修改的,另一个是不可修改的。无论是哪个版本,copy方法都会返回不可修改的版本。例如,NSMutableString的copy方法会返回NSString实例。如果要拷贝可修改的对象,就要使用mutableCopy。
本章以上这部分是摘抄自http://blog.csdn.net/linyousong/article/details/50762199 之内容。由于原书中多以MRC为例,所以找了这个有关ARC和MRC对比版本的帖子。
copy
copy特性要求拷贝传入的对象,并将新对象赋给实例变量。
对不可修改的对象进行复制,好像是在做无用功。
NSObject的copy方法其实仅仅是调用copyWithZone:,并将nil作为实参传入。不可修改的类通常会覆盖copyWithZone:方法,以优化拷贝过程。以NSString为例,它的copyWithZone:方法的示例代码如下:
- (id)copyWithZone:(NSZone *)z {
return self;
}```
也就是说,NSString对象不会真的拷贝出一个新对象。
如果要拷贝出可修改对象,就要使用mutableCopy方法。
Objective-C没有为属性提供mutableCopy这样的特性。如果某个存方法需要复制传入的对象,并且要求新对象是可修改的,就必须自己编写代码实现(向传入的对象发送mutableCopy消息),而不能依赖属性机制。
######再谈对象拷贝
大多数不是来自苹果公司的Objective-C类并没有实现copyWithZone:方法。
NSObject类的copy方法和mutableCopy方法的实现代码大致如下:
- (id)copy {
return [self copyWithZone:NULL];
} - (id)mutableCopy {
return [self mutableWithZone:NULL];
}```
copyWithZone:方法以及mutableWithZone:方法分别在NSCopying与NSMutableCopy协议中进行了声明。
如果希望你的类可以使用copy特性的属性,就需要确保它们符合NSCopying协议。
实现存取方法
如果你声明一个属性,手动实现存取方法,编译器就不会合成实例变量。
但如果你需要实例变量,就必须自己创建。创建的方法是在类的实现文件中添加@synthesize。代码如下:
@property (nonatomic , copy) NSString *listName;
...
@implementation ClassName
@synthesize listName = _listName;
@synthesize指令会告诉编译器有一个叫做_listName的实例变量,它是listName以及setListName的实例变量,如果它不存在,就要将它创建出来。
但如果只写一个@synthesize指令,编译器就会警告说_listName是未经定义的。
声明一个只读属性时,编译器会自动合成一个取方法和一个实例变量。因此,如果手动给只读属性实现取方法,效果和读/写属性实现存取方法是一样的。编译器不会合成实例变量,需要手动合成它。
声明属性仍然是声明存取方法的快捷方法,而且它会给代码带来视觉上的连贯性。
第35章 KVC
KVC(key-value coding)能够让程序通过名称直接存取属性。因为与KVC有关的方法都是在NSObject中定义的,所以凡是继承自NSObject的类都具备KVC功能。
[a setValue:@"Washing Machine" forKey:@"productName"];
在这行代码中,setValue: forKey:方法会查找名为setProductName:的存方法。如果对象a没有setProductName:方法,就会直接为实例变量赋值。
[a valueForKey:@"productName"];
在这段代码中,valueForKey:方法会查找名为productName的取方法。如果对象没有productName方法,就会直接返回相应的实例变量。
如果输错了属性的名称,编译器并不会发出警告,但是在运行时会发生错误。
为什么需要KVC,有什么好处?当苹果公司提供的某个框架需要向你编写的对象写入数据时,会使用setValue:forKey:
。当苹果公司提供的某个框架需要从你编写的对象读取数据时,会使用valueForKey:
。以CoreData框架为例(CoreData框架能够将对象保存在SQLite数据库中,并在需要时将其还原成对象),这套框架会通过KVC来管理自定义的数据对象。
虽然程序没有实现针对_productName 的存取方法,但是,通过KVC,其他(对象外部的)方法一样可以存取_productName。这明显违背了对象封装(object encapsulation)理念。所谓的对象封装是指对象的方法可以公开,但是实例变量应该保持私有。KVC是一个例外。
非对象类型
KVC只对对象有效,但是有些属性的类型并不是对象,例如int或float。如何通过KVC存取这些属性?答案是使用NSNumber对象。
[a setValue:[NSNumber numberWithInt: 240] forKey:@"voltage"];
Key路径
大部分应用最后都会有一个相对复杂的对象表。
使用key路径(key path),可以让系统帮你遍历关系。将你想要的key排成一个长串,以点分隔。注意顺序很重要,第一个想要遍历的对象放在第一个:
NSString *numberToDial = [a valueForKeyPath:@"manager.emergencyContact.phoneNumber"];
第36章 KVO
键-值观察(key-value observing)是指当指定的对象的属性被修改时,允许对象接受通知的机制。虽然它不是很常用,但它是Cocoa bindings以及CoreData的关键组成部分。
[a addObserver:observer
forKeyPath:@"lastTime"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
// lastTime属性在类a的里面
// 在observer类里面实现发生变化时的回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
NSLog(@"%@",keyPath);
NSString *oldValue = [change objectForKey:NSKeyValueChangeOldKey];
NSString *newValue = [change objectForKey:NSKeyValueChangeNewKey];
}
在KVO中使用context
假如父类(设为ClassA)和子类(设为ClassB)都监听了同一个对象肿么办?
可以创建一个单独的指针,在开始观察的时候将它作为context,每次收到通知的时候将它和context进行对比。静态变量的地址可以很好地工作。因此,如果子类化某个使用了KVO的类时,可以编写如下代码:
static int contextForKVO;
...
[obj addObserver:self
forKeyPath:@"fido"
options:NSKeyValueObservingOptionNew
context:&contextForKVO];
...
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
NSLog(@"%@",keyPath);
NSString *oldValue = [change objectForKey:NSKeyValueChangeOldKey];
NSString *newValue = [change objectForKey:NSKeyValueChangeNewKey];
// 这不是我的?
if (context != &contextForKVO) {
// 将它传递给父类
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}else {
// 处理其它变化
}
}
显示触发通知
如果使用存取方法来设置属性,那么系统会自动通知观察者。但如果出于某些原因,你选择不使用存取方法呢?这是可以通过willChangeValueForKey:
和didChangeValueForKey:
方法通知系统某个属性的值即将/已经发生变化。
- (void)updateLastTime:(NSTimer *)t {
[self willChangeValueForKey:@"lastTime"];
_lastTime = @"newValue";
[self didChangeValueForKey:@"lastTime"];
}
独立的属性
如果你不想观察_lastTime而想观察_lastTimeString,该怎么办?
[a addObserver:observer
forKeyPath:@"lastTimeString"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
系统不知道当_lastTime发生变化的时候,_lastTimeString也会发生变化。
为了修复这个问题,你可以告诉系统_lastTime会影响_lastTimeString,可以通过实现一个类方法来做这项工作。
+ (NSSet *)keyPathsForValuesAffectingLastTimeString {
return [NSSet setWithObject:@"lastTime"];
}
请注意这个方法的名字:它是keyPathsForValuesAffecting
加上首字母大写的键的名字。类似属性的存方法是set加上首字母大写的属性名。
系统会在运行时找到它。
关于KVO的其它一些博客:
http://southpeak.github.io/2015/04/23/cocoa-foundation-nskeyvalueobserving/
这上面记载的也很不错。
第37章 范畴
通过使用范畴(Category),程序员可以为任何已有的类添加方法。
范畴的方法会替换之前存在的方法。所以命名的时候增加前缀是一个很好的习惯。
应该使用范畴来给已存在的类增加新方法,而不要在范畴中替换已存在的方法;这种情况下应该创建该类的子类。
网友评论