@property详解

在这篇教程中,我们将谈一谈objc的property。我们将介绍property是什么,它是怎样工作的,有一些什么样的规则,以及使用它们可以用来避免大部分与内存相关的问题。

  如果你还没有本系列教程的样例工程的话,可以点击这里下载,我们将从这个工程开始。

Retain Your Memory

  让我们先回顾一下,本项目需要管理内存的地方在哪。

  目前RootViewController有两个实例变量:_sushiTypes, 和 _lastSushiSelected.

@interface RootViewController : UITableViewController {
    NSArray * _sushiTypes;
    NSString * _lastSushiSelected;
}
 
@end 

  对于_sushiTypes,我们是在viewDidLoad里面通过alloc/init的方式来创建的,然后在viewDidUnload和dealloc里面release。

// In viewDidLoad.  Afterwards, retain count is 1.
_sushiTypes = [[NSArray alloc] initWithObjects:@"California Roll", 
               @"Tuna Roll", @"Salmon Roll", @"Unagi Roll", 
               @"Philadelphia Roll", @"Rainbow Roll",
               @"Vegetable Roll", @"Spider Roll", 
               @"Shrimp Tempura Roll", @"Cucumber Roll",
               @"Yellowtail Roll", @"Spicy Tuna Roll",
               @"Avocado Roll", @"Scallop Roll",
               nil];
 
// In viewDidUnload and dealloc.  Afterwards, retain count is 0.
[_sushiTypes release];
_sushiTypes = nil; 

  对于_lastSushiSelected,它是在用户选中table view的一行时被赋值的。它在两个地方有release。一个是在赋值之前,还有一个是在dealloc时面,请看下面代码:

[_lastSushiSelected release];
_lastSushiSelected = [sushiString retain];
 
// In dealloc
[_sushiTypes release];
_sushiTypes = nil; 

  这种方法肯定是可行的,但是,它需要你很认真的思考,每一次你给一个变量赋值的时候,都要认真考虑与之相关的内存问题。要不要先release后再赋值啊,要不要retain啊,总之,当变量一多,项目一大起来,各种内存问题就随之而来了。

  因此,接下来,我会向你介绍一种简单的方法---使用property来管理内存。

搬把椅子过来,开始编码吧

  如果你熟悉其它编程语言,比如java或者c#,于对getters和setters的概念肯定不陌生。当你拥有一个_sushiTypes的 实例变量的时候,你经常需要让其它类的对象来访问这个变量。但是,如果直接使用.号的方式去访问不太好,它破坏了封装性的原则,把类的实现爆露给外面的, 编程大师说的。不管你信不信,反正我是信了。:)

  因此,你需要一个方法,叫做 “getSushiTypes”(或者仅仅是 “sushiTypes” ,这样少打了3个字母),同时,还需要一个方法,叫做“setSushiTypes”.通过使用这两个方法来访问类的实例变量。这是一个好的编码习惯,因 为你可以改变实例变量的名字,但是你不会影响到其它类,因为接口没变。所以,我们编码代码的时候,也要多针对接口编码,少针对实现编码。当然,使用 getter和setter还有其它好处,你可以在里面用NSLog输出一些内容,这样你就知道有没有人想窥探你的私有变量啦。相当于一个保镖。

  像上面我所说的那样,为每个的的实例变量定义相应的getter和setter方法,当然,前提是你想让外部访问这个变量你才定义,你别搞得把 全部变量都公开,那样封装的意义在哪里呢?这样,将会使内存管理的工作变得轻松。接下来,让我们看看,我是如何给这两个变量添加getters和 setters的。

  首先,在RootViewController.h里面,声明下面四个方法:

- (NSArray *)sushiTypes;
- (void)setSushiTypes:(NSArray *)sushiTypes;
- (NSString *)lastSushiSelected;
- (void)setLastSushiSelected:(NSString *)lastSushiSelected; 

  然后,在RootViewController.m底部添加其实现:

- (NSArray *)sushiTypes {
    return _sushiTypes;
}
 
- (void)setSushiTypes:(NSArray *)sushiTypes {
    [sushiTypes retain];
    [_sushiTypes release];
    _sushiTypes = sushiTypes;
}
 
- (NSString *)lastSushiSelected {
    return _lastSushiSelected;
}
 
- (void)setLastSushiSelected:(NSString *)lastSushiSelected {
    [lastSushiSelected retain];
    [_lastSushiSelected release];
    _lastSushiSelected = lastSushiSelected;
}

  这里的getter方法很简单,它们只是返回各自的变量而已。

  而setter方法,首先把传入的参数引用计数加1,同时把之前的实例变量引用计数减1,然后再把输入的变量赋值给实例变量。(译者:这里的写 法其实不好,没有考虑自赋值的情况。如果大家也过C++的String类,那么写拷贝构造函数和赋值操作符的时候,是一定要考虑自赋值的情况的,不然会出 问题。但是,上面作者的写法不会有问题。因为它先retain的,后release的。如果你写反了,先release,那么就出问题了。但是,如果我考 虑自赋值的情况,那么我就不用考虑这种先后顺序的问题了。具体写法请参照我的原创,objc @property详解)。通过这种方式,新传入的参数被实例变量所引用,因为是所有者,所以符合“谁拥有,谁retain”的原则。

  你可能会奇怪,为什么setter方法要先调用retain/release,然后再赋值,而且顺序不能变。当然啦,肯定是防止自赋值的情况啦。如果你还是搞不懂,那就算了吧,有些事情,总有一天你会明白的。:)

  注意,这里为什么要把实例变量的命名前面加一个下划线呢?这样做一是可以使得getter和setter方法的参数命名获得方便。如果我们的实 例变量命名为 “sushiTypes”,那么我们的setSushiTypes函数的参数名就不能再是“sushiTypes”,因为那会引起冲突,编 译会报错的。同时,如果你把所有的实例变量都加一个下划线,你的同事看你的代码的时候,也马上就知道,这是一个实例变量,我使用时得小心。当然,还有 apple的kvc和kvo机制,也靠下划线去搜索key,具体我不展开说了,看书吧。

  最后,注意,这里的getter和setter方法不是线程安全的,但是,对于本应用程序来说,getter和setter方法只会在主线程里面访问,所以“线程安全不安全”,跟咱没关系!

现在,你地基有了,开干吧!

  现在,你有新的getter和setter了,修改本类中的其它代码,开始使用getter和setter吧。让我们先从sushiTypes开始:

// In viewDidLoad
self.sushiTypes = [[[NSArray alloc] initWithObjects:@"California Roll", 
               @"Tuna Roll", @"Salmon Roll", @"Unagi Roll", 
               @"Philadelphia Roll", @"Rainbow Roll",
               @"Vegetable Roll", @"Spider Roll", 
               @"Shrimp Tempura Roll", @"Cucumber Roll",
               @"Yellowtail Roll", @"Spicy Tuna Roll",
               @"Avocado Roll", @"Scallop Roll",
               nil] autorelease];
 
// In viewDidUnload and dealloc
self.sushiTypes = nil; 

  调用“self.sushiTypes = xxx”和调用 “[self setSushiTypes:xxx]”,这两者完全等价--这里的“.”号,对于我来说,就是“好看”而已。

  因此,我们不是去直接访问_sushiTypes实例变量,而是用setter来设置它的值。回想一下,setter会把传入的对数引用计数加 1.因此,现在,我们不能直接把alloc/init得到的值直接赋给_sushiTypes了(因为,我们通过setter访问的时候,那这个 alloc/init创建的变量的引用计数会是2,那会有问题,因为它的所有者只有一个,那意味着,在将来,只会被所有者release一次。但是,此时 引用计数还是1,永远也不会被释放掉了。也就是说,恭喜你!内存泄露了!)所以,在调用alloc/init之后,我们还需要调用一下 autorelease。

  在viewDidUnload和dealloc方法里同,我们不再是手动地relase再设置为nil了。我们只需要使用setter,一句设计self.xxx = nil就搞定。如果你用self._sushiTypes = nil的话,那么会生成下列的代码:

[nil retain];  // Does nothing
[_sushiTypes release];
_sushiTypes = nil; 

  顺便说一下,给您提个醒----一些人可能跟你说过“永远不要在init或者dealloc方法里面使用getter或者setter方法”。 他们这样说是为什么呢?因为,如果你在alloc或者dealloc函数里面使用setter或getter,但是,它的子类重写了getter和 setter,因为objc所有的方法都是“虚方法”,也就是说可以被重写。那么子类init方法调用[(self = [super init]))的时候,先调父类的init方法,而里面使用了getter和setter,而正好这两个方法又被你覆盖了,如果你在这两个覆盖的方法里面 干了一些事,那么就会有问题了。仔细想想,为什么!因为,你的子类还没有初使化完毕啊!!!你现在还在调用父类的“构造函数”,但是,你已经使用了子类的 方法!!!但是,我想说的是,我在这里违反了某些人提供的原则。为什么,因为我知道会可能有副作用。所以,我不会轻易重载父类的getter或 setter方法。我们这里这样写,可以帮助我们简化代码。这当然是我个人意见,仅供参考。

  现在,修改代码,让lastSushiSelected也使用setter方法来赋值:

self.lastSushiSelected = sushiString;
 
// In dealloc
self.lastSushiSelected = nil; 

  哇---现在,我们对于内存问题的担心少了很多了,不是吗?你没有开动脑筋使劲想,哪里需要retain啊,哪些需要release啊。这里的setter方法,它在某种程度上替你完成了内存管理的工作。

一个简单的建议

  因此,写getter和setter可以方便其它类访问你类里面的实例变量,同时,有时候也会使你的内存管理工作变得更加轻松。

  但是,一遍又一遍地写一大堆这些getter和setter方法,那么我会疯掉的。搞java的为什么没疯?因为eclipse自动可以生成。 搞c++的为什么也没疯,因为,可以直接public。但是,objc 的@public是没用的。不用担心,没有人会想一遍又一遍地干重复的事情的,所以objc2.0提供了一个新的,非常有用的特性,叫做 @property,也叫属性。

  我们可以把我们前面写的那些getter和setter方法全部注释掉,只需要写上下面两行代码就够了。自己动手试一下吧,打开RootViewController.h,然后找到getter和setter声明的地方,把它换成下面的2行代码:

@property (nonatomic, retain) NSArray * sushiTypes;
@property (nonatomic, retain) NSString * lastSushiSelected; 

  这是使用属性的第一步,创建属性声明。

  属性的声明以@property关键字开始,然后在括号里面传入一些参数(比如atomic/nonatomic/assign/copy/retain等)。最后,你指明了属性的类型和名字。

  propetyceov是一些特殊的关键字,它可以告诉编译器如何生成getter和setter。这里,你指定了两个参数,一个是 nonatomic,它是告诉编译器,你不用担心多线程的问题。还有一个是reatin,它是告诉编译器,在把setter参数传给实例变量之前,要先 retain一下。

  在其它情况下,你可能想使用“assign”参数,而不是reatin,“assign”告诉编译器不要retain传入的参数。或者,有时你还需要指定“copy”参数,它会在setter参数赋值给实例变量之前,先copy一下。

  好了,为了完成property的使用,你先转到RootViewController.m,删除之前写的getter和setter,然后在文件的顶部添加下面2行代码:

@synthesize sushiTypes = _sushiTypes;
@synthesize lastSushiSelected = _lastSushiSelected; 

  上面的代码是告诉编译器,请你基于我前面定义的property及其参数,请为我生成相应的getter和setter方法。你使用 @synthesize关键字开始,然后给出属性名字,(如果属性名字和实例变量名字不一样的话),那么一定要写上=于号,这样在生成setter方法的 时候,编译才知道,传入的参数要赋值给谁。切记一次要写上等于号!!!如果实例变量名和属性名一样,那就没必须了。

  就这么多!编译并运行代码吧,一样很ok,运行得很好。但是,和之前你写的那堆代码相比较,是不是更容易理解了呢?同时也会使得出错的概率下降。

  到目前为止,你应该了角propety的用法,以及具体是如何工作的吧!接下来,我将给出一些使用property的建议。

一般性的策略

  我想在这篇教程里面添加一些这样的策略,因为,它能够帮助我在管理objc内存的时候,更加轻松,更加不容易犯错误。

  如果你遵守这些规则的话,那么,在大部分时候,你会远离内存相关问题的烦恼。当然,盲目地背下这些规则而不去理解,为什么会有这些规则,为什么 这些规则就能够有作用。这肯定是不行的啦!但是,如果你是新手的话,你可以按照我给的这些规则去做,这样你会避免大量的内存相关的错误,使得你的日常编程 活动更加轻松。

  我先把这些规则一条条列出来,接下来,我再详细依个讨论。

  1. 总是为所有的实例变量定义属性。
  2. 如果它是一个类,那么就设定“retain”为属性参数,否则的话,就设置为assign。
  3. 任何时候创建一个类的实例,请使用alloc/init/autorelease 的方式创建。
  4. 任何时候,当给一个变量赋值的时候,总是使用e “self.xxx = yyy”。换句话说,就是使用property。
  5. 对于你的每一个实例变量,在dealloc函数里面调用 “self.xxx = nil”。如果是一个outlet的话,那么在viewDidLoad里面创建,要记得在viewDidUnload里面销毁。

  好,现在开始逐条讨论!

  规则1:通过为每一个实例变量定义property,你可以让编译器为你写内存相关的代码。缺点很明显了,你破坏了类的封装性,这样的话,可能会使你的类的耦合度变得更高,更不利用维护和代码复用。

  规则2:通过把类的property参数指明为retain,那么在任何时候,你可以都可以访问它们。你们你还保存有它们的一个引用计数,内存不会被释放掉的,你是拥有者,你负责释放。

  规则3:当你创建一个类对象的时候,使用alloc/init/autorelease惯用法(就像你之前创建sushiTypes的数组那样 的)。这样的话,内存会被自动释放。如果你想让它不释放的话,那么在要赋值的实例变量,在声明其property的参数那里,声明一个retain吧。

  规则4:不管什么时候给实例变量赋值,都使用 self.xxx的语法,这样的话,当你给实例变量赋值的时候,会先把老的值释放掉,并且 retain新的变量值。注意,有些程序员担心在init和dealloc函数里面使用getter和setter函数会带来副作用,但是,我认为这没什 么。只要你对内存管理规则完全清楚,你不会去做“在子类里重写父类的getter和setter”方法的事的。或者,就算实际需要,必须重写父类的 getter和setter,你也会十分注意,不在重写的过程中,给代码带来任何副作用的,对吧?

  规则5:在dealloc函数里面,使用“self.xxx = nil” ,这样可以通过property使用其引用计数减1.不用要忘了还有viewDidUnload!

与cocos2d有关的简单策略

  我知道,现在我的博客上有一大批cocos2d的忠实粉丝,因此,接下来的tips是特意为你们准备的!

  上面提出的5点规则,对于coocs2d来说,有点太严格了,或者直接说,太死了。因为,大部分时候,我们的对象都加到层里面去了,我们在类里 面定义一些实例变量,仅仅是为了在除init方法之外的其他方法里面方法使用。(其实,很多人喜欢定义tag,然后在addChild的时候指定一个 tag,然后在其他方法里同,使用[self getChildByTag:xxx]来获得你想要的对象。因为层里面有个CCArray的数组,它用来保存层的所有孩子结点,当调用addChild的 时候,其实是调用CCArray的addObject方法,所以,加到层里面的孩子,其引用计数会加1.

  因此,为了避免定义一些不必要的property,下面是我对于cocos2d的使用者的一些建议:

  1. 从来不使用property。
  2. 把你创建的sprite实例直接赋值给你定义的实例变量。
  3. 因为这些精灵都会加到当前层里面去,coocs2d会自动retain,使其引用计数加1.
  4. 当你把一个对象从当前层中移除出去的时候,记得把它赋值为nil。

  我个人觉得用上面4条方法来开发cocos2d游戏,感觉还不错,简单,快捷。

  注意,如果某个对象没有加到当前层里面去,比如action。那么,就不会使用上面4个规则了。手动去reatin/release吧。

  记住,规则是死的,人是活的!只要理解了objc的内存管理规则,你可以忘记上面所有的规则!


你可能感兴趣的:(iphone平台相关问题)