您的位置:首页 > 移动开发 > Objective-C

Programming with Objective-C(二)

2016-04-20 02:01 585 查看
今天的博文是苹果的官方文档 Programming with Objective-C 的 Encapsulating Data 这一章,主要是讲到了类中各个数据的知识。

在 OC 中提到类中包含的数据,大概第一反应就是 property 了。property 可以称得上是 OC 的一种特性了,在类里面,如果我们需要哪些属性,可以直接通过 @property 来声明一个属性,编译器会根据这个属性的名字自动为我们生成 getter 和 setter,同时,来自外部的访问,将会被解释为对 setter 和 getter 的调用。用下面的代码来打个比方:

@interface XYZPerson : NSObject
@property NSString *firstName;
@end


XYZPerson 使我们定义的类,而 firstName 则是我们定义的属性。当编译器编译并解释的时候,这个类中就会自动生成两个方法,一个是 firstName,这个方法用于从外部访问 firstName,另一个是 setFirstName,这个方法用于从外部来设置 firstName 的值。实际上,在以前的时候,单纯地声明 property 是不会生成这两个方法的,还需要我们在类的实现汇中加入 @synthesized 才行,但是现在苹果已经为我们做好了这一切。所以在声明的时候,我们需要注意的地方,就在于属性的访问修饰符了,这里也是非常容易让人感到疑惑的地方。

说到访问修饰符的话,首先提到的就是关于访问的问题,比如一个属性是可读可写的,还是只读的。这个时候就可以在 property 中加入修饰符,比如:

@property (readonly) NSString *fullName;


这个时候声明的就是一个只读的属性,编译器会生成 getter,但是不会生成 setter,并且,在编译器是可以检查出来这种错误的。同时,借助于访问修饰符,我们可以自己来编写 setter 和 getter,比如:

@property (getter=isFinished) BOOL finished;


这个时候就声明了一个 getter 方法为 isFinished 的属性 finished。

需要注意的一点:如果自定义属性的访问方法的话,方法的名称应当符合相应的规范,或者说,应该能兼容 KVC 模式。

关于属性的访问,可以通过 . 操作符来实现,不过该操作符的使用实际上也是对 setter 和 getter 的调用。

说到这里,中间稍微停一下,之前关于属性讲了很多,但是有一点依旧没有提及,而且这里也会让人产生疑惑。那就是,我们通过 @property 到底声明了什么,我们声明了一个属性,那么它的值到底保存在哪里?实际上,我们在声明属性的时候,也就相当于声明了一个变量,属性的值实际上就是保存在这个变量中,这个变量的默认名称就是下划线加上属性名。尽管我们有了这么一个变量,但是就算在类的内部,我们访问属性的时候也基本使用属性名来访问,也就是说,我们依旧是通过 setter 和 getter 来访问的,这个时候,变量的意义就变得有些微妙了。实际上,这个变量我们基本上是用不到的,但是有个例外的情况,那就是我们在初始化的时候。如果我们对一个类进行初始化的时候,从初始化的意义来考虑,我们应该直接对变量进行赋值,而不是去使用 setter。关于初始化的问题之后再谈,现在继续看属性,对于属性变量,其实我们也可以自己给它命名,只要使用 @synthesize 就可以了,比如:

@synthesize firstName = instanceVariableName;


上面这个例子我们就将属性对应的变量名称改成了 instanceVariableName,另外,如果我们使用了 @synthesize 但是并没有赋予变量名,那么变量名就会和属性名称一样。在此处个人还有一个小疑问,那就是当变量名和属性名称一致的时候,在类的内部对属性的访问,到底是通过 setter/getter 来访问,还是直接访问相应的变量。

上面谈到了关于初始化的问题,按照苹果的官方推荐,当你在初始化的方法中访问属性的时候,应该直接访问变量。主要的原因,是因为 setter 方法会存在边缘效应,实际上,在最开始的时候,我们也提到了,关于函数的命名应该符合 KVC 的规范,原因也就是在这里。当我们使用 setter 方法的时候,会触发 KVC 通知,也有可能你自己重写了 setter 方法,这时候就会触发 setter 中的其他行为,有些时候,这些额外的行为会涉及到其他对象,但是这个时候这些对象可能还没初始化,而且在初始化的时候,我们仅仅只想将相应变量赋予合适的值,所以在这个时候去调用 setter 是不明智的。(这个地方可以说是必须要记住的一点,因为即使能够保证当前类中我们的 setter 不会执行额外的操作,也很难保证以后项目扩大之后我们是不是会在子类中去改写 setter)

对于属性,其实它的属性修饰符还有很多可以谈,上面只说到了访问修饰符。除了访问修饰符之外,我们还有原子访问修饰符,说到原子访问,其实就涉及到了多线程的问题。我们可以先假定这样一个情景:某一个对象可能被多个线程操作,那么如果同时有多个线程访问了这个对象,并且要修改它的属性,那么这个时候该对象的属性到底该听从哪个线程的命令呢?为了解决这个问题我们引入了原子变量的概念,也就是说,对该变量的访问和操作是一体的,一旦某个线程访问了这个变量,那么在该线程结束访问前,其他的线程是无法访问这个变量的。这个时候,多线程的问题就解决了,同样地,在 OC 中我们也会面临这个问题,以为不论是 iOS 还是 Mac OS,他们都是支持多线程的。所以属性就有了 atomic 这样一个修饰符,也就是该属性是不是原子型的。一般默认情况,一个属性是非原子的。但是需要注意一个问题,原子并不意味着线程安全。我们可以继续以 XYZPerson 为例,它有两个属性,firstName 以及 lastName,假如一个线程现在想要获取 XYZPerson 的全名称,那么它就需要获取 XYZPerson 的两个属性,但是这个时候另一个线程也来访问这两个属性,假设第一个线程已经访问了 firstName,后一个线程先访问了 lastName,那么当第一个线程结束了访问的时候,它访问 firstName 时对应的 lastName 可能已经发生过改变了,在这种情况下,很显然,并不是线程安全的。真想做到线程安全,我们就必须加一个线程锁,由此可以发现,其实加了原子属性并不见得有什么作用,因为真正需要保证线程安全的时候我们还是需要加上线程锁。所以一般情况下我们都用 noatomic,毕竟这样对应属性的访问速度会比原子型要快不少。

说完原子之后才是属性中的重点,也就是一直以来,让人疑惑的 strong 和 weak,单看书上的描述,总会觉得有些难以理解。到目前为止,有关内存管理的内容我们其实都不曾真正涉及,因为苹果已经帮我们完成了这一工作,我们只需要声明对象然后去使用就可以了。但是当对象中包含对象的时候,问题就来了,如果两个对象,彼此包含,我们应该怎么去销毁?按照引用计数的观点,只要一个对象有指向它的指针,也就是引用计数不为零,那就不会释放,很显然,当对象彼此包含的时候引用计数就遇到问题了。因为两个对象都至少会有一个指针指向自己,并且这个指针还不会被销毁。那么这个时候怎么去打破这样的一个循环呢?苹果为我们提供的方案就是 weak 属性,平常我们在对象中声明的对象都是 strong 的,但是我们自己可以声明一个 weak 的对象,这样在循环引用的时候就可以释放循环中的对象。我们以 tableView 作为一个例子来说明一下,对于一个 tableView 来说,如果我们想实现它的功能,那么光靠一个 tableView 是不够的,我们需要一个 delegate 来帮助 tableView 去实现一部分功能。这个时候,很明显,tableView 需要一个指向 delegate 对象的指针,而 delegate 对象也需要指向 tableView 才能为具体的 tableView 工作。这个时候如果没有 weak 指针,那么显然会出现强引用循环。weak 指针的作用就是,让当前的对象可以保有对其他对象的指针,同时,这个指针不会像 strong 指针一样,会声明一种类似于拥有权一样的关系,这个时候引用计数就不会产生计数加一的效果了。在 tableView 和 delegate 之间我们需要的是 tableView,所以 tableView 指向 delegate 的应该是 weak 属性的指针,而 delegate 指向 tableView 的应该是 strong 属性的指针。这样,delegate 就会先被销毁,然后等到其他指向 tableView 的 stong 指针也被销毁之后 tableView 就会被销毁。实际上 weak 和 strong 并不是局限于属性中,即时是局部变量,也存在着 weak 以及 strong,比如一个局部指针变量,它默认是 strong 的。我们也可以手动将它声明为 weak 属性的指针,比如这样:

NSObject * __weak weakVariable;


这个时候这个指针就是 weak 属性的。但是在局部使用一个 weak 属性的指针很容易引发悬挂指针,因为 weak 属性并不会让一个对象保持存活状态,比如我们用一个 weak 属性指向了一个变量,稍后这个变量被销毁了,那么从这里开始,到代码块结束为止,我们的指针都不能使用了,因为它所指向的对象已经被销毁了。尤其是,我们的局部指针所指向的,本来也是一个 weak 修饰的属性的时候,这种情况就很容易出现了。这个时候,我们就应该用一个 strong 类型的指针来保存这个 weak 修饰的属性,这样就可以保证当前的作用域期间这个属性是不会销毁的,以此来完成我们的功能。代码例子就像下面这样:

- (void)someMethod {
NSObject *cachedObject = self.weakProperty;
[cachedObject doSomething];
...
[cachedObject doSomethingElse];
}


上面就是通过 cachedObject 保存了对于 weak 属性的一个强引用,以保证当前作用域下这个属性不会被释放。另外,对于 weak 修饰的属性,我们在直接使用的时候一般也需要进行判断,判断它是否为 nil,因为很有可能在进入当前作用域之前它就已经被销毁了。

关于属性的修饰符,还剩下最后一个部分,那就是涉及到赋值的时候。如果我们什么修饰符都不声明,那么在给对象赋值的时候就是直接将相应的对象赋值过来了,乍一看之下好像没有什么问题。但是,在 OC 中,有些类,比如 NSString,它分了 NSString 和 NSMutableString,如果我们把一个可变的直接赋值给了不可变的,那么可变和不可变之间的界限不就被打破了么。所以针对有些对象,我们在赋值的时候,应该是给它一份副本,也就是使用 copy 修饰符。但是在使用 copy 修饰符的时候需要注意一点,能够使用 copy 修饰的变量必须只是 NSCopying 协议。

上面已经提到了初始化,那么接下来就继续讲讲初始化的问题。一般来说,初始化的格式都像是下面这样的:

- (id)init {
self = [super init];

if (self) {
// initialize instance variables here
}
return self;
}


对于每个函数的 init 函数,都应该先调用其父类的 init 方法,然后再执行其他的操作。这样做的原因在于,首先要基于父类进行变量的初始化,因为父类中的某些变量可能在子类中是没办法直接看到的,所以要确保这些变量的正确初始化,你应该先调用父类的 init 方法。当然,在父类初始化的时候,也可能会失败,这时候就会返回 nil,所以我们在初始化具体的变量之前,先要确保当前的对象是否正确初始化了。这样的话,我们的一个对象生成的时候,其实相当于有一条调用链存在,这个调用链从我们需要的对象开始,顺着它的父类一直向上追溯,直到 NSObject 类为止,然后从上向下依次调用 init 方法。

实际上在初始化的时候,因为我们所需要的参数的不同,我们是可以定义多个初始化函数的,但是这个时候我们就需要一个主要的初始化函数,其他的初始化函数都可以通过调用这个函数来实现,这个函数就是指定初始化函数。指定初始化函数的参数应该是最全的,也就是说初始化这个对象可能需要的所有参数,它都应该包含,这样的话,其他的初始化函数只需要提供它们所负责的参数,然后其他参数提供默认值就可以了。同时,还需要注意另一件事,当引入指定初始化函数之后,那么在子类中,初始化之前,应当先调用父类的指定初始化函数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: