先回顾一下Objective-C类的定义格式:
#import <Foundation/Foundation.h> @interface MyClass : NSObject { //成员变量定义,没有修饰符的情况下,默认为@protected int i; @private float f; char *c; @protected NSString *str; @public id obj; struct { unsigned int lineBreakMode:3; unsigned int highlighted:1; unsigned int baselineAdjustment:2; } _textLabelFlags; } //属性定义 @property (nonatomic,copy) NSString *name; @property (strong, nonatomic) IBOutletUILabel *lblName; //方法定义 //Objective-C的编译器会给没有写显式的返回值函数加上一个默认的返回值,它的类型是id - promoteTo:newPosition; - (void)setTitle:(NSString *)string; + (id) getMyName:(NSString *)string; @end
#import "MyClass.h" @implementation MyClass @synthesize name; @synthesize lblName; - (id)promoteTo:(id)newPosition { returnnil; } - (void)setTitle:(NSString *)string { } +(id)getMyName:(NSString *)string { return@"myName"; } @end
定义成员变量作用域
(1) @protected -- 作用范围在自身类和继承自己的子类,默认。
(2) @private -- 作用范围只在自身类。
(3) @public -- 范围最广,可被任何类访问
实例化一个类的一般做法是:[[Class alloc] init]。这个操作其实做了两件事:alloc给对象分配内存空间,init对对象进行初始化。
上面的这个类,当外部实例化它时:
MyClass *tmpClass = [[MyClass alloc] init]; NSLog(@"%s",tmpClass->c); //error,"Instance variable 'c' is private NSLog(@"%d",tmpClass->i); //error,"Instance variable 'i' is protected id tmp = tmpClass->obj; // no-warning UILabel *tmpLabel = tmpClass.lblName; // no-warning
可以看到成员变量只有声明为@public时,才能以"->"的形式访问。而要以"."操作符来访问变量,则必须声明为@property。
@property的一个主要作用就是生成getter和setter方法,这样做简化了大部分代码,看下面的例子:
@interface Worker : NSObject { NSString *_name; } - (NSString*)name; - (void)setName:(NSString*)strName; //等价于 //@property (nonatomic,copy) NSString *name;
在实现文件(Worker.m)里:
- (NSString*)name { return _name; } - (void)setName:(NSString*)strName { if(_name!=strName) { [_name release]; _name = [strName copy]; } } //等价于 //@synthesize name;
上面的代码解释了@property和@synthesize背后的实际操作,同时也能看出来@property(copy)代表_name = [strName copy]; 如果你指定为retain,代表_name = [strName retain]; 指定为assign,代表_name = strName;
所以,@property并不仅仅是做了一个简化代码的getter/setter操作,同时它还做内存管理。
声明@property的语法为:@property(参数1, 参数2) 类型 名字。其中参数主要分为三类:
读写属性:readwrite(默认)/readonly
setter语意:assign(默认)/retain/copy
原子性:atomic(默认)/nonatomic
各参数意义如下:
readwrite:产生setter/getter方法
readonly:只产生getter,不产生setter
assign:默认类型,直接赋值而不进行retain操作
retain:先release旧值,再retain新值
copy:进行copy操作,与retain一样
atomic:开启多线程变量保护,会消耗一定的资源
nonatomic:禁止多线程变量保护,提高性能
先来研究一下readwrite和readonly:
【示例一】
#import <Foundation/Foundation.h> @interface NewClass : NSObject //这里声明为readonly,所以只会生成getter方法 @property (nonatomic,readonly,copy) NSString *nickname; @end
基于上面的头文件,当你synthesize name以后,尝试做如下操作:
NewClass *myClass = [[NewClass alloc] init]; myClass.nickname = @"abc";
这个时候编译器会报错:Assigning to property with 'readonly' attribute not allowed. 就是提示你对设置为readonly的属性赋值是被禁止的。
【示例二】
#import <Foundation/Foundation.h> @interface NewClass : NSObject //这里声明为readonly,所以只会生成getter方法 @property (nonatomic,readonly,copy) NSString *nickname; //提供了一个自定义的setter方法 - (void)setNickname:(NSString *)nickname; @end
虽然在@property定义为readonly,但是随后提供了一个自定义的setter方法,那么实际作用等同于readwrite了。当尝试对其进行赋值,编译器将通过。
【示例三】
#import <Foundation/Foundation.h> @interface NewClass : NSObject @property (nonatomic,copy) NSString *nickname; //自定义getter方法 - (NSString*)nickname; @end
#import "NewClass.h" @implementation NewClass @synthesize nickname; - (NSString*)nickname { NSMutableString *str = [[NSMutableStringalloc] initWithString:nickname]; [str appendString:@"_suffix"]; return str; } @end
- (void)viewDidLoad { [superviewDidLoad]; NewClass *myClass = [[NewClass alloc] init]; myClass.nickname = @"abc"; NSLog(@"%@",myClass.nickname); //将输出abc_suffix }
上面的例子里声明了一个属性nickname,默认为readwrite。但随后又提供了一个自定义的getter方法,并且在实现文件里将赋值后的值加上了一个后缀字符串。最终运行后,会发现自定义的getter方法生效了,它覆盖了@property自动生成的getter。
通过上面的例子同时可以得到一个结论:假如声明了一个@property,名称为nickname。那么会生成一个名为nickname的成员变量、一个名为nickname的getter方法、一个setNickname的setter方法。
再来看看atomic/nonatomic的区别。如果你用@synthesize去让编译器生成代码,那么atomic和nonatomic生成的代码是不一样的。如果使用atomic,它会保证每次getter和setter的操作都会正确的执行完毕,而不用担心其它线程在你get的时候set,可以说保证了某种程度上的线程安全。但是,仅仅靠atomic来保证线程安全是不够的。要写出线程安全的代码,还需要有同步和互斥机制。而nonatomic就没有类似的"线程安全"保证了。因此,很明显,nonatomic比atomic速度要快。这也是为什么,我们基本上所有用@property的地方,都用的是nonatomic了。
最后着重了解一下assign、retain与copy:
assign指定setter方法采用简单的赋值操作,而不更改索引计数。这也是没有明确指定setter语意的默认操作。基础数据类型(NSInteger、CGPoint)和C语言数据类型(int, float, double, char等)一般会采用assign。
- (void)setNickname:(NSString *)strName { nickname = strName; }
指定为retain后,先释放旧的(指针)对象,再设置为新的对象,并且索引计数加1。(指针拷贝)
- (void)setNickname:(NSString *)strName { if(nickname!=strName) { [nickname release]; nickname = [strName retain]; } }
指定为copy后,先释放旧的(指针)对象,再拷贝一份新的对象,不对(拷贝的)旧对象进行操作。(内容拷贝)
- (void)setNickname:(NSString *)strName { if(nickname!=strName) { [nickname release]; nickname = [strName copy]; } }
下面举例分析一下assign、retain和copy的一些不同之处:
NSString *str = [[NSString alloc] initWithString:@"test"];
这句话实际上产生了两个操作:
1. 申请了一段内存用来存储"test",这里假设内存地址为0x1111
2. 用一个名为str的指针,指向"test",假设指针的内存地址为0xaaaa
先来看assign在这种情况下的示例:
NSString *str2 = str;
此时str2和str完全相同,地址都是0xaaaa,都指向地址为0x1111的内容"test"。所以assign操作相当于是一个"别名"的概念
再来看retain的情况:
NSString *str3 = [str retain];
此时str3的地址不再是0xaaaa,可能是0xbbbb,但是仍然指向同一个对象(地址为0x1111,内容为"test")。retainCount(索引计数)增加1。
最后看看copy的情况:
NSString *str4 = [str copy];
此时会新开辟一段内存来存放内容"test",假设地址为0x2222。同时会为指针str4也分配一段新内存(假设为0xcccc),然后将str4指向地址为0x2222的"test"。操作结束后,原来地址为0x1111的"test"以及地址为"0xaaaa"的指针均不受影响;地址为0xcccc的指针指向地址为0x2222的"test"。且retainCount为1。
所以可以得出结论,retain是指针拷贝,copy是内容拷贝
copy也分为深拷贝和浅拷贝,类似于C++
(1) 深拷贝,就是新拷贝一块内存交给对象使用
(2) 浅拷贝,就是觉得拷贝内存太浪费,直接给你我的地址吧,相当于retain
在Objective-C里只有一种情况是浅拷贝,那就是不可变对象的copy,其它的都是深拷贝(包括不可变对象mutableCopy、可变对象的copy和mutableCopy)。
适用场合:
非对象的数据类型,比如int、float等基本数据类型用assign。
对象数据类型,例如NSObject用retain。
当类拥有mutable子类时,应该使用copy,而不是retain。例如:NSArray、NSSet、NSDictionary、NSData、NSCharacterSet、NSIndexSet、NSString。
虽然规范上NSString做属性都是写成copy,其实在不存在NSMutableString赋值给NSString时,是可以采用retain来避免字符串拷贝带来的消耗的。
#import <Foundation/Foundation.h> @interface NewClass : NSObject @property (nonatomic,retain) NSString *strRetain; @property (nonatomic,copy) NSString *strCopy; @end
#import "NewClass.h" @implementation NewClass @synthesize strRetain; @synthesize strCopy; @end
- (void)viewDidLoad { [superviewDidLoad]; NSMutableString *str = [[NSMutableStringalloc] initWithCapacity:100]; [str setString:@"dog"]; NewClass *myClass = [[NewClass alloc] init]; myClass.strRetain = str; myClass.strCopy = str; NSLog(@"retain string is %@",myClass.strRetain); NSLog(@"copy string is %@",myClass.strCopy); [str setString:@"cat"]; NSLog(@"retain string is %@",myClass.strRetain); NSLog(@"copy string is %@",myClass.strCopy); }
将依次输出"dog, dog, cat, dog"。这样就可以看出,当使用retain方式的时候,NSMutableString的内容变化时,语义上应该不可变的NSString也变化了,而用copy则是始终保持赋值时的内容。在实际开发中,如果此NSString不存在NSMutableString赋值的情况,就可以采用retain来代替copy。
最后说一下iOS5加入ARC机制后新加入的一些关键词:
strong -- 强引用,关键字为__strong,用该属性声明的变量将成为对象的持有者。(默认)
weak -- 弱引用,关键字为__weak,用该属性声明的变量并没有对象的所有权,并且当对象失去持有者之后,变量会被自动设置为nil。
unsafe_unretained,关键字为__unsafe__unretained,iOS5之前的版本用这个关键字来代替__weak,于_weak的区别在于是否执行nil赋值。如果所指向的对象被释放了,这个指针就是一个野指针了。
autoreleasing,关键字为__autoreleasing,用来修饰一个函数的参数,这个参数会在函数返回的时候被自动释放。换个说法:该关键字使对象延迟释放。比如你想传一个未初始化的对象引用到一个方法当中,并在此方法中实例化该对象,那么这种情况可以使用__autoreleasing。它被经常用于函数有值参数返回时的处理,比如下面的例子:
- (void)testSomething:(NSObject * __autoreleasing *)objParam { *objParam = [[NSObject alloc] init]; } - (void)viewDidLoad { [superviewDidLoad]; NSObject *obj = nil; [self testSomething:&obj]; NSLog(@"%p",obj); }
另外,下面的写法是等效的:
id *obj == id __autoreleasing * obj;
NSObject **obj == NSObject * __autoreleasing * obj;
使用strong, weak, autoreleasing限定的变量会被隐式初始化为nil。