二、对象、消息、运行期(1)
- 『对象 object』是使用面向对象语言编程时的『基本构造单元』。
- 开发者可以通过对象存储、传递数据,对象之间传递数据并执行任务的过程成为『消息传递 Messaging』。
- Objective-C Runtime 是在程序运行期间提供相关支持的代码,它提供了使得对象之间能够发送消息的重要函数,并且包含类对象创建的全部逻辑。
6、理解『属性』这一概念
- 『属性 property』是 OC 用于封装对象中的数据而提供的一个特性。OC 对象通常会把其需的数据保存为各种实例变量。实例变量一般通过『存取方法』来访问。
getter
方法用于读取,setter
方法用于写入。 - 在 OC 2.0 中引入属性这一特性,开发者可令编译器自动生成属性的存取方法。此特性还引入了『点语法』是开发者更容易访问对象中的数据。
- 类似 Java 和C++ 的写法,OC可以这样定义一个类:
@interface EOCPerson : NSObject {
@public
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end;
这种写法的问题是:对象布局在编译器就已经固定了。只要是访问_firstName变量的代码,编译器就把其替换为『偏移量 offset』,这个偏移量是『硬编码』,表示该变量距离存放对象的内存区域的起始位置有多远。这样的话,如果在_firstName前又添加一个变量NSDate *_dateOfBirth;
,那么原来_firstName的偏移量现在会指向_dateOfBirth,会读取到错误的值。所以这种写法在修改了类定义之后必须重新编译,以确保编译器计算正确的偏移量,倘若两份代码一份使用旧的类定义,一份使用新的,就会出现不兼容的现象。
- OC 的解决办法是,把实例变量当做一种存储偏移量的特殊变量,交由『类对象』管理,偏移量会在运行期查找,如果类定义变了,偏移量也会变化。这样不仅可以确保偏移量的正确性,还可以在运行期向类中添加实例变量,这就是稳固的『应用程序二进制接口 ABI』。得益于稳固的 ABI,我们可以在类的 class-continuation 分类(即空名的分类)或实现文件中定义实例变量。
- 另一个解决办法,就是不要直接访问实例变量,而是通过存取方法来访问,这时『属性』就派上用场了。通过属性,可以访问封装在对象里的数据。所以可以把属性当做是编译器的一系列操作的简称,因为使用了属性这一特性后,编译器会自动生成一套存取方法以及对应给定名称的实例变量。
// 通过属性定义类的的实例变量
@interface EOCPerson : NSObject {
@property NSString *firstName;
@property NSString *lastName;
}
@end;
// 对于使用者来说, 以上代码等同于:
@interface EOCPerson : NSObject {
- (NSString*)firstName;
- (void)setFirstName:(NSString*)firstName;
- (NSString*)lastName;
- (void)setLastName:(NSString*)lastName;
}
@end;
- 使用属性的优势:
- 可以使用点语法访问属性,和使用存取方法没有差别。
- 编译器在编译期会自动编写访问属性的存取方法,此过程叫『自动合成』。
- 编译器在编译期会自动向类中添加适当类型的实例变量,实例变量的名称为属性名前加下划线。
- 我们可以使用
@synthesize
语法来自己指定实例变量的名称。使用@dynamic
语法指定属性不自动生成存取方法和实例变量,即使编译期时编译器没有发现存取方法也不会报错,因为它认为这些方法在运行期能找到。
- 属性特质:属性的不同特质会影响编译器所生成的存取方法。属性的特质可以分四类:
- 原子性:默认情况下,由编译器所生成的存取方法会通过添加同步锁的机制确保属性的原子性
atomic
(在多线程中同一时间只能有一个线程执行属性的存取操作)。 如果属性具备nonatomic
,则不使用同步锁。 - 读/写权限:
- readwrite 属性拥有
setter
和getter
方法; - readonly 属性只有
getter
。可以使用此特质把某个属性对外公开为只读属性,然后在class-continuation
分类中将其重新定义为读写属性。
- readwrite 属性拥有
- 内存管理语义: 属性用于封装数据,而数据应具有『具体所有权语义』。下面一组特质只会影响
setter
方法。如果是自己编写的setter
方法,则必须与属性具备的特质相符。- assign
setter
方法只会执行针对『纯量类型』(scalar type 如 CGFloat, NSInteger 等基本数据类型)的简单赋值操作。 - strong 表明该属性定义了一种『拥有关系』,为该属性设置新值时,
setter
方法会先保留新值,并释放旧值,然后将新值设置上去。 - weak 表明该属性定义了一种『非拥有关系』,为该属性设置新值时,
setter
方法既不会保留新值,也不会释放旧值,和 assign 类似,只是简单的赋值。但是在该属性所指对象被销毁时,属性值也会被清空。 - unsafe_unretained 和 assign 类似,但是它适用于『对象类型』,和 weak 类似,也是定义一种『非拥有关系(unretained)』,但是当目标对象被销毁时,属性值不会自动清空(unsafe)。
- copy 和 strong 类似,但是
setter
方法不会保留新值,而是将其拷贝。多用在 NSString* 类型,来保护其封装性。因为传给setter
方法的新值可能一个 NSMutableString 类型的可变字符串,其值可能会被修改。所以拷贝一份不可变的字符串,可以确保属性对象的值不会遭人修改。一般来说,只要属性所用的对象是可变的,就应该在setter
方法中对其进行拷贝。
- assign
- 方法名: 可以指定存取方法的方法名
- getter=
如 UISwitch 类的的 on 属性
- getter=
- 原子性:默认情况下,由编译器所生成的存取方法会通过添加同步锁的机制确保属性的原子性
@property (nonatomic, getter=isOn) BOOL on;
// 使用时
switch.isOn;
- **setter=** 不常见。
- 在我们为类自定义了一个初始化方法时,也要遵循属性的特质来为其赋值,如我们为类定义了一个 具有copy 特质字符串,又定义了一个用该字符串初始化一个类的实例的方法。
@property (copy) NSString *name;
- (id)initWithName:(NSString *)name;
初始化方法的实现可以这样写:
- (id)initWithName:(NSString *)name
{
if(self = [super init]) {
_name = [name copy];
}
return self;
}
为什么这里不直接调用setter
方法呢?第7条会做详细解释。
- 如果使用了
readonly
,编译器只会创建getter
,即便如此,还是要写上内存管理语义,以表明在初始化方法中设置这些属性所用的方式。否则使用者可能会在初始化之后自行拷贝,这种操作是多余而且低效的。 - 使用
atomic
的好处是,如果有两个线程读写同一属性,那么不论何时,各个线程中总能得到有效的属性值。如果使用nonaotmic
,其中一个线程读写时,另一个线程突然闯入读取或修改了尚未修改好的值,最后得到的值可能会不对。但是为什么通常开发中都是用nonatomic
,原因是,在 iOS 中使用同步锁的开销较大,会带来性能问题。此外,就算使用了atomic
也不一定能保证线程安全。例如,一个线程连续多次读取某个属性的值的过程中,有别的线程在同时改写该值,那么最后得到的值还是会可能出错。要保证线程安全,需要采用更深层的锁定机制。不过在 macOS 中,使用atomic
不会有性能问题。
7、在对象内部尽量直接访问实例变量
- 直接访问和通过属性访问的区别是:
- 直接访问不经过 OC 的『方法派发』(见11条),速度会比较快,相当于直接访问保存对象实例变量的内存。
- 直接访问不会调用
setter
方法,这会绕过为属性定义的『内存管理语义』,如在 ARC 下直接访问一个copy
属性,并不会拷贝该属性。 - 直接访问不会触发『键值观察 KVO』通知。
- 通过属性访问可以给存取方法设置断点,这有助于排插错误。
- 作者建议在读取实例变量时直接访问,而在设置时通过属性来做,以尽可能提高效率。这种做法需要注意的问题:
- 在初始化方法中应该总是采用直接访问,因为子类可能会重写
setter
方法。但是,如果待初始化的实例变量声明在超类中,我们无法在子类中直接访问此实例变量,就只能调用setter
方法来初始化。 - 如果某个属性采用了懒加载,则必须通过存取方法访问属性。否则,实例变量永远不会被初始化。
- 在初始化方法中应该总是采用直接访问,因为子类可能会重写
8、理解『对象等同性』这一概念
- 使用 『==』操作符比较的是两个指针本身是否相同,或者说内存地址是否相同。
- 比较两个对象的同等性应当使用 NSObject 协议中声明的
isEqual:
方法。 - 有的类提供了专门的比较方法,如 NSString 的
isEqualToString:
方法,应当优先使用,这比调用isEqual
方法快,因为isEqual
需要执行额外的步骤来判断比较对象的类型。 - NSObject 协议中用于判断等同性的关键方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
这两个方法的默认实现是:当且仅当其『指针值(或内存地址)』完全相等时,这两个对象才相等。若要在自定义对象中重写这两个方法,则必须保证:如果isEqual
方法判断两个对象相等,则 hash
方法也必须返回相同的值。但是如果hash
返回相同的值,那么isEqual
方法未必认为两个对象相等。一种较好的计算 hash 值的方式:
// 以前面提到的 EOCPerson 类为例:
- (NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
-
hash
方法返回的值对 Set 集合的性能有很大的影响,因为 Set 在检索哈希表时,会用对象的哈希值做索引。Set 可能会根据哈希值把对象分装到不同的数组,想 Set 中添加对象时,要根据其哈希值找到对应的数组,依次检查其中的各个元素,判断是否和新对象相等。所以,如果每个对象返回的 hash 值相同,那么 Set 需要将所有对象扫描一边,会影响性能。 - 特定的类有特定的等同性判断方法,如系统为 NSString、NSArray、NSDictionary 等提供了特别的判断方法,但由于 OC 在编译期不做强类型检查,传入这些方法的参数类型需要保证正确,否则会抛出异常。可以为自定义类编写类似的方法,并一并复写
isEqual:
方法,后者常见的实现方法是如果传入的参数类型正确就使用自己编写的判断方法,否则交由超类比较。 - 等同性的执行深度:在我们为自定义类实现判断方法时,可以根据不同需求针对部分或全部的字段做比较。如从数据库读取的数据可以只比较主键 id 是否相同。
- 容器中如果存在可变类,可能会影响容器等同性的结果。例如,在 Set 里添加一个可变数组,再添加一个与之不同的可变数组,接着修改第二个可变数组同第一个相等,此时 Set 里依然有两个完全相同的数组,这与 Set 的语义相违背。并且,如果此时复制一份该 Set,得到的结果是只有一个数组的 Set。所以,要么保证添加到 Set 里的对象的 hash 值不是根据可变字段计算的,要么就保证这个对象是不可变的。
9、以『类族模式』隐藏实现细节
- 使用『类族』模式,可以隐藏『抽象基类』的实现细节。OC 的系统框架中普遍使用了这种模式,如 UIButton,其提供的工厂方法
buttonWithType:
可以根据传入的参数生成不同类型的 button,但是这写不同类型的 button 都继承自基类 UIButton。 - 由工厂方法返回的实例对象并不是一定是基类的实例对象,所以
[button isMemberOfClass:[UIButton class]];
返回的是 NO。 - 除了 UIButton,大部分的集合类都是类族,通过 class 返回的类型便可知道,如一个 NSArray 对象执行
[array class];
返回的类可能是__NSArrayI
、__NSArrayM
、__NSArray0
等,绝不可能返回NSArray
类本身。所以使用如下的代码是错误的。
if ([maybeAnArray class] == [NSArray class]) {
// do someting
}
应当使用类型信息查询方法
if ([maybeAnArray isKindOfClass:[NSArray class]]) {
// do someting
}
- 编写类族的子类时,有以下几条规则:
- 子类应该继承自类族中的抽象基类。
- 子类应该定义自己的数据存储方式。因为像 NSArry 本身只不过是包在其他隐藏对象外面的壳,仅仅定义了所有数据都需要具备的一些接口,并没有定义数据的存储方式。对于自定义的数组来说,可以用 NSArray 来保存它的实例。
- 子类应当覆写超类文档中明确指明需要覆写的方法。如 NSArray 的子类必须覆写
- count
和- objectAtIndex:
方法。
10、在既有类中使用关联对象存放自定义数据
- 同过使用『关联对象』的特性,可以在某个对象中存储一些额外的数据。可以给某个对象关联多个其他对象,这些对象通过『键』来区分。关联时可以指明对象的内存管理语义。
关联类型 | 等效@property中的 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic,retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic,copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
下列方法可以管理关联对象:
// 设置对象value为object 的键为*key, 存储策略为 policy 的关联对象
- void objc_setAssociatedObject (id object, void *key, id value, objc_AssociatedPolicy policy)
// 获取object对象中的键为 *key 的关联对象值.
- id objc_getAssociatedObject (id object, void *key)
// 移除 object 的所有关联对象
- void objc_removeAssociatedObject(id object)
- 只有在其他办法不可行时才去使用关联对象,因为如果滥用会令代码失控,难于调试。