一个月没更新博客了(´・_・`)最近在工作和生活上都肥肠忙碌,导致学习的时间变少了很多(╥﹏╥)。近一个月唯一的长进就是看完了《Effective Objective-C 2.0》这本书,所以就把看这本书时想到的几点问题记下来写成一篇博客好了。
向前声明
第2条:在类的头文件中尽量少引入其它文件:
如果在各自头文件中引入对方的头文件,则会导致“循环引用”(chicken-and-egg situation)。当解析其中一个头文件时,编译器会发现它引入了另一个头文件,而那个头文件由回过头来引用第一个头文件。使用#import而非#include指令虽然不会导致死循环,但却意味着这两个类里有一个无法被正确编译。
“无法被正确编译”会带来什么结果呢?尝试了一下,编译无法通过,编译器报错:
/Users/xsq/Documents/Developer/XSQReferenceDemo/XSQReferenceDemo/ViewController.h:14:31:
Unknown type name 'XSQObject'; did you mean 'NSObject'?
字面量
第3条:多用字面量语法,少用与之等价的方法:
在改用字面量语法来创建数组时会遇到这个问题。下面这段代码分别以两种语法创建数组:
id object1 = /* ... /;
id object2 = / ... /;
id object3 = / ... */;
NSArray *arrayA = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSArray *arrayB = @[object1, object2, object3];
大家想想:如果object1和object3都指向了有效的Objective-C对象,而object2是nil,那么会出现什么情况呢?按字面量语法创建数组arrayB时会抛出异常。arrayA虽然能创建出来,但是其中却只含有object1一个对象。原因在于,“arrayWithObjects:”方法会依次处理各个参数,直到发现nil为止,由于object2是nil,所以该方法会提前结束。
尝试了一下,创建arrayB的时候抛出了异常:
Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '*** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[1]'
而从堆栈信息中可以看到,字面量语法创建arrayB的时候,使用了arrayWithObjects:count:
方法,而不是arrayWithObjects:
方法。
frame #0: 0x0000000105a3ddbb libobjc.A.dylib`objc_exception_throw
frame #1: 0x0000000105e82a52 CoreFoundation`-[__NSPlaceholderArray initWithObjects:count:] + 290
frame #2: 0x0000000105edf0b4 CoreFoundation`+[NSArray arrayWithObjects:count:] + 52
hash
第8条:理解“对象等同性”这一概念:
如果“isEqual:”方法判定两个对象相等,那么其hash方法也必须返回同一个值。但是,如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。
官方文档中也有类似的介绍:
If two objects are equal, they must have the same hash value. This last point is particularly important if you define isEqual:
in a subclass and intend to put instances of that subclass into a collection. Make sure you also define hash
in your subclass.
当需要把一个对象放入一个collection中时,hash方法尤其需要注意。官方文档中也说明了,如果一个可变的对象被放入了依赖hash方法来定位的collection中,期间这个对象的hash值不应该被改变:
If a mutable object is added to a collection that uses hash values to determine the object’s position in the collection, the value returned by the hash
method of the object must not change while the object is in the collection.
类族
第9条:以“类族模式”隐藏实现细节:
在使用NSArray的alloc方法来获取实例时,该方法首先会分配一个属于某类的实例,此实例充当“占位数组”。该数组稍后会转为另一个类的实例,而那个类则是NSArray的实体子类。
关于类族深层的东西,这本书没有讲太多。但是至少可以了解到,程序员没法预料初始化得到的实例的具体类型,所以在比较类型时,使用
[maybeAnArray class] == [NSArray class]
和
[maybeAnArray isMemberOfClass:[NSArray class]]
很可能得到与预料中不一致的结果。
关联对象的key
第10条:在既有类中使用关联对象存放自定义数据:
我们可以把某对象想象成NSDictionary,把关联到该读喜庆的值理解为字典中的条目,于是,存取关联对象的值就相当于在NSDictionary对象上调用[object setObject:value forKey:key]与[object objectForKey:key]方法。然而两者之间有个重要差别:设置关联对象时用的键是个“不透明的指针”(opaque pointer)。如果在两个键上调用“isEqual:”方法的返回值是YES,那么NSDictionary就认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常使用静态全局变量做键。
虽然用静态全局变量做键已经成为了一种套路,但是以前真没想过它的键是个“不透明的指针”这个问题。
designated initializer
第16条:提供“全能初始化方法”:
如果创建类实例的方式不止一种,那么这个类就会有多个初始化方法。这当然很好,不过仍然要在其中选定一个作为全能初始化方法(designated initializer),令其它初始化方法都来调用它。
官方文档中也有类似的说法:
When you define a subclass, you must be able to identify the designated initializer of the superclass and invoke it in your subclass’s designated initializer through a message to super. You must also make sure that inherited initializers are covered in some way. And you may provide as many convenience initializers as you deem necessary. When designing the initializers of your class, keep in mind that designated initializers are chained to each other through messages to super; whereas other initializers are chained to the designated initializer of their class through messages to self.
在定义一个类的时候,程序员必须认出它父类的designated initializer,然后在子类的designated initializer中给它发送消息。程序员还必须确保继承的其它初始化方法也要被处理到,程序员还可以根据需要新增几个初始化方法。
在设计一个类的初始化方法时,确保子类的designated initializer连上了父类的designated initializer,而其它的初始化方法必须连上当前类的designated initializer。
debugDescription
第17条:实现description方法:
NSObject协议中还有个方法要注意,那就是debugDescription,此方法的用意与description非常相似。二者区别在于,debugDescription方法时开发者在调试器中以控制台命令打印对象时才调用的。
以前没有注意过,查了一下官方文档中的说明:
debugDescription: Returns a string that describes the contents of the receiver for presentation in the debugger.
实例变量
第27条:使用“class-continuation分类”隐藏实现细节:
为什么能在其中定义方法和实例变量呢?只因有“稳固的ABI”这一机制,使得我们无需知道对象大小即可使用它。
第6条:理解“属性”这一概念:
Objective-C的做法是,把实例变量当作一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。
这是一个很有趣的问题,好像我以前也从来没考虑过。在C++中,对实例变量的访问,会在编译时期被转化为对偏移量的方法。我做了个实验,用C++语言写了个类:
// BaseClass.h
#ifndef BaseClass_h
#define BaseClass_h
#include <stdio.h>
class BaseClass {
public:
char a = 'a';
char b = 'b';
char c = 'c';
void printBase();
};
#endif /* BaseClass_h */
// BaseClass.cpp
#include "BaseClass.h"
void BaseClass::printBase() {
printf("a=%c, b=%c, c=%c", this->a, this->b, this->c);
}
将BaseClass编译成静态库,然后写一个main函数:
// main.cpp
#include <iostream>
#include "BaseClass.h"
int main(int argc, const char * argv[]) {
BaseClass baseClass = BaseClass();
baseClass.printBase();
return 0;
}
能正常输出:
a=a, b=b, c=c
然后我在静态库的头文件BaseClass.h中,注释掉对a和c这两个实例变量的定义,但保持静态库的.a文件不变,再次运行,则输出了奇怪的:
a=b, b=, c=
这应该可以说明,如果使用C++语言,那么在BaseClass.cpp编译的过程中,对实例变量的访问已经被改为了对一个偏移量的访问。
但是Objective-C 2.0不是这样做的。官方文档里有这样的介绍:
The most notable new feature is that instance variables in the modern runtime are “non-fragile”:
In the legacy runtime, if you change the layout of instance variables in a class, you must recompile classes that inherit from it.
In the modern runtime, if you change the layout of instance variables in a class, you do not have to recompile classes that inherit from it.
这个特性牵涉了编译、链接和运行时,感觉如果要深入了解,还需要下很多功夫。所以这里就暂时记录这么多吧(´・_・`)
dispatch_barrier
第41条:多用派发队列,少用同步锁
在设置方法中使用了栅栏块之后,对属性的读取操作依然可以并发执行,但是写入操作却必须单独执行了。
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)someString
{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString
{
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
这里,getter方法必须同步才能读取到有效数据,而setter方法可以让存放数据异步操作,不需要等待其操作完成。使用dispatch_barrier_async是想等到此刻所有getter任务完成后再开始进行setter的操作。
但是在查阅了dispatch_barrier_async的官方文档的过程中,却发现了这样一句说明:
The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_async function.
所以书中的这个例子选用了dispatch_get_global_queue可能不是很恰当,按照苹果的说法,此处的dispatch_barrier_async其实就相当于一个dispatch_async?
NSCache
第50条:构建缓存时选用NSCache而非NSDictionary:
NSCache胜过NSDictionary之处在于,当系统资源将要耗尽时,它可以自动删减缓存。
...
NSCache并不会“拷贝”键,而是会“保留”它。
...
另外,NSCache是线程安全的。