Objective-C中的数据封装
对象可以通过属性来封装数据,下面我们就来看一下对象是如何封装属性以及这些属性是如何通过同步或者异步的方式来进行读取以及设置的。如果属性是通过实例变量存取的,那么就需要在初始化的方法中正确地给予设置。如果对象需要通过属性链接到对另外一个对象的引用,那么一定要处理好两个对象之间的关系。虽然Automatic Reference Couting可以帮你处理好大部分的内存管理事宜,但是我们自己编码的时候也还是需要注意避免强引用的死循环而引起的内存泄露。我们还会介绍一些对象的生命周期管理以及如何管理好对象之间的关系图。
属性封装了对象的值
大部分对象都需要保存一些信息才能完成某些特定的工作。某些对象就是设计出来用于保存数据的,例如Cocoa NSNumber保存值类型数据,或者自定义的XYZPerson用来保存姓氏和名字。还有一些对象的范围更加广泛,例如响应用户的交互及需要显示的信息,这些对象都需要保存用户的界面元素以及一些相关模型对象的信息。
为公开数据定义公共属性
Objective-C中提供了定义封装属性的语法,属性可以在接口中声明,如下:
@interface XYZPerson : NSObject @property NSString *firstName; @property NSString *lastName; @end
这里XYZPerson定义了两个属性用来保存姓氏和名字。因为在面向对象编程中非常重要的一点是隐藏接口中属性和方法的内部实现,因此就需要通过对象暴露出来方法进行属性的读取与设置而不是直接获取其内部变量的值。
使用方法设定和获取属性值
可以通过属性方法来获取或者设置属性的值:
NSString *firstName = [somePerson firstName]; [somePerson setFirstName:@"Johnny"];
默认情况下,这些属性方法编译器会自动帮你同步,因此只需要在接口中适用@property关键字声明就可以了,不需要专门书写。但是同步方法遵循特定的命名规范,用来获取属性的getter方法的名字与属性的名字相同,即获取属性firstName的getter方法也是firstName,用来设置属性值的setter方法需要在属性前面加上set关键字然后再将属性的第一个字母大写,因此设置firstName的方法名称为setFirstName。如果你不希望属性的值被更改,那么就可以适用readonly关键字:
@property (readonly) NSString *fullName;
与之前的属性声明方式一样,它不仅告诉了编译器如何与属性交互,还暗示了应该如何同步属性方法,这里,编译器会自动仅仅同步fullName的getter方法,而没有setter方法。
如果想要使用不同的方法名称来命名属性方法,可以向属性添加自定义名称来表示属性方法的名称。对于布尔值的属性,一般都是再getter方法前面加is,如果getter方法为finished,那么可以自定义为isFinished。可以通过向属性添加属性来完成:
@property (getter=isFinished) BOOL finished;
如果需要添加多个属性,可以用逗号隔开:
@property (readonly, getter=isFinished) BOOL finished;
这里编译器仅仅会同步isFinished的属性方法,而不会同步setFinished方法。
使用.符号设定和获取属性值
不仅仅可以适用属性方法来获取和设置属性的值,在Objective-C中还提供了相同功能的.符号用来操作属性值:
NSString *firstName = somePerson.firstName; somePerson.firstName = @"Johnny";
.符号的方式纯粹是属性方法的简写,如上的操作方式还是需要通过调用属性方法来完成目的。这也就意味着.方式的语法也是由属性的声明方式来决定的,例如如果属性声明为readonly,在通过.符号进行属性设置的时候会得到编译错误。
大部分属性是通过实例变量存储
默认情况下,属性是readwrite标示的,通过一个实例变量,这个变量是编译器再同步属性方法的时候自动添加的。实例变量是存在于对象声明周期的一个变量,在对象通过alloc创建的时候就已经分配好内存控件,当对象deallocated的时候是放掉。除非自己手动设定,否则实例变量的名称与属性的名称是一样的,只不过前面多了一个下划线,如属性firstName的实例变量对应的是_firstName。虽然最好的习惯是通过使用属性方法来获取和设置属性的值,但是有些情况下需要在类实现的过程中使用到该实例变量。在实例变量的前面加上下划线可以有助于区分我们是使用的属性方法还是操作的实例变量:
- (void)someMethod { NSString *myString = @"An interesting string"; _someString = myString; }
在这个例子中可以很清楚的区分出myString是本地变量,_someString是实例变量。通常即使是在类实现方法的时候,也最好是通过属性方法或者.操作符来操作实例变量或属性,可以使用self关键字:
- (void)someMethod { NSString *myString = @"An interesting string"; self.someString = myString; // or [self setSomeString:myString]; }
有一种特殊的情况要直接操作实例变量,那就是在初始化、deallocation或者自定义属性方法的时候。
自定义同步的实例变量名称
如果希望使用不同规则的名称来表示属性的实例变量,可以如下书写:
@implementation YourClass @synthesize propertyName = instanceVariableName; ... @end
@synthesize firstName = ivar_firstName;
在这个例子中,属性的名字还是firstName,可以通过firstName和setFirstName方法或者.符号来操作,但是在对象的内部,该属性值是通过实例变量ivar_firstName存储的。如果在使用@synthesis的时候没有指定实例变量的名称,如下:
@synthesize firstName;
在这种情况下,实例变量的名称与属性的名称将会保持一致,也就是实例变量的名字也是firstName,而没有前面的下划线。
定义非属性的实例变量
当需要跟踪某个对象的某个指或者对象中引用到的其他对象的时候最好是通过添加属性来实现。但是也可以不通过定义属性而直接定义自己需要的实例变量,在类的借口或者实现文件的开头使用括号来声明这些变量:
@interface SomeClass : NSObject { NSString *_myNonPropertyInstanceVariable; } ... @end
@implementation SomeClass { NSString *_anotherCustomInstanceVariable; } ... @end
从初始化方法中直接操作属性
setter方法可能会在某些情况下产生副作用,有可能会触发KVC提示,或者在自定义方法的时候产生一些问题。在初始化方法里面必须直接操作属性对应的实例变量,因为在为属性赋值的时候对象很有可能并没有完全初始化完成。即使你不自定义属性方法或者自己也很清楚在初始化函数中适用setter方法的后果,但是你不能保证别人在继承了你的类并重写方法的时候不会出现问题。一个典型的init方法如下:
- (id)init { self = [super init]; if (self) { // initialize instance variables here } return self; }
对象在初始化之前首先应该先调用父类的初始化方法,并将结果返回给自己,父类在初始化的时候有可能出现问题并返回nil,因此需要判断这个时候self的值是否为nil,如果不为nil才可以开始进行本身的初始化工作。通过调用[super init]方法,在继承链上将会从根部开始依此完成各个父类的初始化工作,以XYZShoutingPerson为例:
初始化方法可以不使用参数,也可以带有参数,这里可以将XYZPerson类的初始化函数改成设定姓氏和名称参数;
- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName;
- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName { self = [super init]; if (self) { _firstName = aFirstName; _lastName = aLastName; } return self; }
被代理的主方法
如果类生命了多个初始化方法,那么就需要指定一个被代理的主方法。通常这个方法是参数最多的初始化方法,由哪些为了简便而书写的初始化方法调用。当在其他的初始化方法中调用被代理的主方法的时候提供合适的默认参数。如果XYZPerson还提供了一个出生日期的属性,那么被代理的初始化方法可以是:
- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName dateOfBirth:(NSDate *)aDOB;
这个方法提供了所有属性的参数,同时可能还需要提供一个仅仅设置姓氏和名字的初始化方法,在书写这个初始化方法的时候可以代理上面的主方法:
- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName { return [self initWithFirstName:aFirstName lastName:aLastName dateOfBirth:nil]; }
还有可能需要一个没有任何参数的初始化方法:
- (id)init { return [self initWithFirstName:@"John" lastName:@"Doe" dateOfBirth:nil]; }
当需要继承一个用友多个初始化方法的类的时候,你可以选择重写负父类的主方法来完成你的初始化工作,也可以添加自己的初始化方法。不管使用哪种方法,你都应该首先调用父类的的主方法,在[super init]的位置,然后才能开始书写自己的初始化逻辑。
自定义属性获取和设置方法
属性不一定非要使用实例变量来存储,XYZPerson也可以定义一个只读的属性来表示人的全名:
@property (readonly) NSString *fullName;
如果再去定义一个fullName的实例变量,那么在每次修改姓氏或者名字的时候都需要更新fullName的值,我们可以自定义一个fullName的属性获取方法:
- (NSString *)fullName { return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName]; }
如果需要对使用了实例变量的属性进行自定义属性方法,那么就需要在属性方法的内部直接使用实例变量,例如,通常是在属性第一次被请求的时候才去实例化它,我们称之为延迟获取,如下:
- (XYZObject *)someImportantObject { if (!_someImportantObject) { _someImportantObject = [[XYZObject alloc] init]; } return _someImportantObject; }
通常在定义了属性之后,编译器都会自动同步一个或者两个属性方法,如果你已经针对readwritable属性自定义了getter和setter方法或者针对readonly属性自定义了getter方法,那么编译器将会自动认为你已经完全控制了该属性的实例变量,也就不会再自动同步属性的实例变量了。如果需要使用到实例变量的时候,就需要使用自己定义的实例变量:
@synthesize property = _property;
属性莫认为原子性的
默认情况下,属性是具有原子性的;
@interface XYZObject : NSObject @property NSObject *implicitAtomicObject; // atomic by default @property (atomic) NSObject *explicitAtomicObject; // explicitly marked atomic @end
这意味着在属性方法被执行的总是被某个线程完全拥有,在同步请求的时候不会被中断。因为原子性的属性方法是私有的,因此不可以将同步的属性方法与自定义的属性方法混合使用,编译器会报错。也就是对一个atomic readwrite属性来讲,不可以一方面自己提供一个setter方法,然后让编译器自动同步getter方法。当使用nonatomic关键字的时候,我们只能够保证简单地获取和设置属性的值,但是在有其他线程发起同步请求的时候,不能够保证发生什么情况。因为这个原因,使用nonatomic要比atomic效率更高,而且也能够将自动同步的setter与自定义的getter方法组合使用:
@interface XYZObject : NSObject @property (nonatomic) NSObject *nonatomicObject; @end
@implementation XYZObject - (NSObject *)nonatomicObject { return _nonatomicObject; } // setter will be synthesized automatically @end
但是这里也需要注意,atomic并不等于是线程安全的。以XYZPerson为例,姓氏和名字都是使用atomic的属性方法,当一个线程在操作的过程中有另外一个线程也发起相同的操作,原子性的getter方法将会返回一个完整的字符串,但是并不能够保证这个值是正确的。如果姓氏是在第二个线程介入之前进行了读取,名字是在第二个线程修改了名字的属性之后进行了读取,那么姓氏和名字将会造成错乱。
通过所有关系和指责管理对象关系图
Objective-C的对象内存是动态分配的,需要使用指针来跟踪对象的地址。与值类型不同,对象的生命周期与指向它的指针的生命周期并不总是相互对应,只要对象仍然需要被其他对象引用,那么就必须保证他在内存中能够被找到或者被引用。而且在编程的时候除了考虑对象的生命周期之外,还需要考虑到对象之间的关系。以XYZPerson对象为例,两个字符串属性firstName和lastName属性都属于XYZPerson实例,这意味着只要XYZPerson对象在,这两个字符串就要在。
如果类型的情况发生在两个对象之间,也就是一个对象的生命周期要依赖于另一个对象才有意义,那么就应该使用强引用,在Objective-C中只要某个对象被强引用,那么只要当强引用他的对象还存在他就会继续存在,在XYZPerson实例的例子中,关系如下:
当XYZPerson对象被deallocate的时候,这两个属性在没有被其他对象强引用的时候也会被释放掉。
现在增加系统的复杂情况,看下图表示的对象关系:
当用户点击update按钮的时候显示面板的信息会随之更新,此时的对象关系如下所示:
显示面板负责显示信息,维持着与元John字符串的强引用,虽然XYZPerson对象已经拥有了不同的firstName属性,但是John字符串仍然存在于内存中,被显示面板用来显示名字信息。当用户点击update按钮的时候,控制面板接到通知来更改原来的字符串信息:
这个时候,原来的John字符串不再被强引用,从而被从内存中移除。默认情况下,Objective-C中的属性和变量都是与宿主之间保持着强引用的关系,这在大多数情况下都是没有问题的,但是有些情况下会出现强引用循环。
避免强引用循环
对于单向关系的对象之间可以使用强引用,但是对于双向工作的那么就需要小心处理,如果这两个对象都都不被外部对象强引用的话,那么这两个对象将会出现一种互相强引用的情况。一个例子就是表试图对象与它的代理。为了增加表示图的通用性,会将一些方法代理给外部对象,着意味着它将表示图内部应该显示什么数据以及如何与用户交互交给外部对象来决定,这个时候就需要表示图拥有对外部对象的引用,而外部对象也需要拥有对该表示图对象的引用:
使用强引用和若引用管理所属关系
默认请款下属性如下声明:
@property id delegate;
默认是强引用类型的,如果需要声明若引用类型,则如下声明
@property (weak) id delegate;
本地变量默认情况下也是强引用的关系,因此下面的代码将会按照预想的方式执行
NSDate *originalDate = self.lastModificationDate; self.lastModificationDate = [NSDate date]; NSLog(@"Last modification date changed from %@ to %@", originalDate, self.lastModificationDate);
这个例子中本地变量originalDate维持了一个指向刚开始lastModificationDate指向对象的引用,当lastModificationDate属性更改的时候,该属性不再指向原来的对象,但是原来的对象会倍originalDate保存。当变量维持强引用的时候表示该引用一直持续到变量的声明周期结束或者该变量指向了其他对象的引用。如果不希望变量维持强引用可以使用_weak关键字:
NSObject * __weak weakVariable;
因为若引用并不能保证对象的有效性,因此有可能弱引用还继续存在但是被引用的对象已经被释放掉了,因此为了避免这种情况的发生,当被若引用的对象是放掉之后若引用变量会自动被设置为nil。如下:
NSDate * __weak originalDate = self.lastModificationDate;
self.lastModificationDate = [NSDate date];
original变量会倍设置为nil。当self.lastModificationDate重新指向某个对象的时候讲不再维持对原有对象的强引用,因此当没有其他强引用指向该对象的时候之前的对象会被是放掉,originalDate会被设置为nil。弱引用也有可能造成一些麻烦:
NSObject * __weak someObject = [[NSObject alloc] init];
因为新创建的对象没有强引用,因此会被马上销毁,因此someObject会被立刻设置为nil。
有时候当在方法内部使用弱属性的时候也需要注意:
- (void)someMethod { [self.weakProperty doSomething]; ... [self.weakProperty doSomethingElse]; }
这样的情况下最好先将该若引用的对象保存到某个强引用本地变量中,这样就能够保证代码的正确执行;
- (void)someMethod { NSObject *cachedObject = self.weakProperty; [cachedObject doSomething]; ... [cachedObject doSomethingElse]; }
在这个例子中,cachedObject变量维持了一个对弱引用属性指向的对象的强引用,因此只要cachedObject生命周期没有结束,该对象都不可能被释放掉。还需要记住的是如果需要使用若引用对象仅仅判断是否为nil是不够的,如:
if (self.someWeakProperty) { [someObject doSomethingImportantWith:self.someWeakProperty]; }
因为在多线程应用程序中,在判断和执行方法之间的空隙中有可能会插入其他的线程来对该属性进行了修改,因此需要声明一个强引用本地变量来保存该值:
NSObject *cachedObject = self.someWeakProperty; //1 if (cachedObject) { //2 [someObject doSomethingImportantWith:cachedObject]; //3 } //4 cachedObject = nil; //5
在这个例子中,首先创建了强引用,这意味着能够保证在if判断和方法执行过程中,该对象是始终存在的,最后cachedObject被设置为nil放弃了强引用。如果之前的对象没有被其他的变量强引用的话,那么这个时候该对象就会被是放掉并且soemWeakproperty也会被设置为nil。
对某些类使用unsafe_unretained引用
在Cocoa或Cocoa Touch中有一些类不支持弱引用,也就是不能通过weak关键字来声明这种类型的属性或变量,如NSTextView,NSFont和NSColorSpace等。如果需要对这些类型使用弱引用,必须使用非安全的引用,对于属性或变量可以使用unsafe_unretained关键字:
@property (unsafe_unretained) NSObject *unsafeProperty;
NSObject * __unsafe_unretained unsafeReference;
一个不安全的引用类似于弱引用,不能够保证对象被回收,但是不同的是当被引用的对象回收的时候,指针不会被设置为nil,这也就意味着留下了一个悬空的指针,而这个指针所指向的内存空间可能内容已经开始更改了,因此它是不安全的,向这种指针指向的内存地址发送消息很可能造成系统的崩溃。
复制属性
在某些情况下,一个对象可能需要对赋值给他的属性的对象进行复制,从而保存一个在对象自己内部的副本。例如XYZBadgeView类可以如下:
@interface XYZBadgeView : NSView @property NSString *firstName; @property NSString *lastName; @end
对象保存对两个字符串属性的强引用,然后考虑如下使用该属性的一种情况:
NSMutableString *nameString = [NSMutableString stringWithString:@"John"]; self.badgeView.firstName = nameString;
这样的赋值完全可以,因为NSMutableString是NSSTtring的子类,因此该类型的对象也可以赋值给NSString属性,但是实际上该属性强引用到的是一个NSMutableString,因此该属性所指向的对象可能会发生变化:
[nameString appendString:@"ny"];
这样虽然刚开始的时候名字是John,并将这个值赋值给firstName属性,但是现在他已经变成了Johnny,因为他指向的NSMutableString是可以动态更改的。因此,这个时候可能需要该对象保存一个firstName和lastName属性的副本,这样就能够保存在属性赋值的时刻他所接收到的对象的引用。为了实现这点,可以添加copy关键字:
@interface XYZBadgeView : NSView @property (copy) NSString *firstName; @property (copy) NSString *lastName; @end
这个时候该对象就保存了自己对两个字符串属性的副本值,当NSMutableString发生改变的时候对象的属性值也不会改变,仍然是刚开始为属性赋予的那个值:
NSMutableString *nameString = [NSMutableString stringWithString:@"John"]; self.badgeView.firstName = nameString; [nameString appendString:@"ny"];
这样的话firstName属性的值就不会受到后来nameString变化的影响,仍然是刚开始的John。属性的复制意味着属性的强引用,因为他指向了一个他自己创建的新的对象。
如果需要直接操作设定了copy关键字的属性的实例变量的时候,例如在初始化函数中的时候,需要赋值原对象的副本,如下:
- (id)initWithSomeOriginalString:(NSString *)aString { self = [super init]; if (self) { _instanceVariableForCopyProperty = [aString copy]; } return self; }