本书读完,我还是挺有一些感悟,虽然很多知识之前就有所了解或有所用到。但是读完这本书之后,对一些知识点理解也更加深入了一些。至此我在读第二遍的时候,记录52个要点中一些印象比较深刻和自己有所新领悟的要点。 以下标注的点和书中的点对应。
3、多用字面量语法,少用与之对应的等价方法
这个点提出来的目的就单是提醒自己,尽量在潜意识中高速自己多用字面量语法,写出来的代码更加简明扼要。
5、用枚举表示状态、选项、状态码
这一点着重在于使用二进制的按位操作巧妙实现枚举的组合使用。
enum UIViewAutoresizing{
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
}
从图上可知,每一个枚举对应一个二进制位。如:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight,用按位与操作符即可以判断出是否已经启动,此时对应的二进制是 010010。
具体详情如有遗忘可回翻书本。
6、理解属性这一概念
这一块相当于是复习基础,不过值得一提的是属性并不等同于实例变量,这一点在我先前很长一段时间我都是比较混淆的一个概念。接下来我来捋一捋这之间的关系。
首先先说一下几个属性关键词
readwrite // 默认属性,自动生成setter 和 getter方法
readonly // 只读属性,自动生成getter方法,不生产setter方法
@property (nonatomic, strong) NSString * str;
-(void)str {
return _str;
}
-(void)setStr:(NSString *)str {
_str = str;
}
其实看一下setter 和 getter 方法,大致就已经明白了property定义的属性,其实就是通过这两个方法去访问带下划线的实例变量。str 自动生成setter 和 getter方法,可以通过点语法直接调用str,通过setter和getter方法,修改和获取到实例变量_str的值。
由于实例变量只能内部访问,所以这也就是一般类,设置readonly属性,即可保证类以外的文件无法直接访问。
_str = @"test"; // 直接访问实例变量的方式
self.str = @"test"; // 间接通过点语法以getter方法访问到实例变量
当然属性包含很多知识,各种属性关键词的作用和使用场景在这里就不一一列举了。
8、理解“对象等同性”
== 操作符并不能总是得到我们预期想要的结果,因为该操作符比较的是其指针而非其对象。
NSObject协议中有两个用于判断等同性的关键方法
NSObject中的 isEqual: 方法判断两个对象是否相等,该方法的默认实现是当且仅当"指针值"完全相等时,两个对象才相等。如果isEqual 方法判断两个对象相等,那么其hash方法也必须返回同一个值。但是如果两个对象的hash方法返回同一个值,isEqual方法也未必会认为两者相等(这句话我的理解是,有可能存在其他不等同的对象hash返回了相同的值,因为hashmap是两个,所以存在相同值的情况)
(BOOL)isEqual:(id)object ;
(NSUInteger)hash ;
NSString 有自己专门的isEqualToString: 方法,NSArray也有isEqualToArray:,NSDictionary对应isEqualToDictionary:等。
如果有自定义类比较的需求可以重写isEqual: 方法,自定义等同性判断方法,可以根据需求决定是根据整个对象来判断还是根据其中哪几个字段来判断。
列举书中一个例子
//person 类
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if ([self class] != [object class]) {
return NO;
}
Person *p = (Person*)object;
if (![_firstName isEqualToString:p.firstName]) {
return NO;
}
if (![_lastName isEqualToString:p.lastName]) {
return NO;
}
if (_age!=p.age) {
return NO;
}
return YES;
}
- (NSUInteger)hash {
// return 6452; 第一种
// NSString *stringToHash = [NSString stringWithFormat:@"%@:%@:%i",_firstName,_lastName,_age];
// return [stringToHash hash]; 第二种
NSUInteger fistNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return fistNameHash^lastNameHash^ageHash; // 第三种
}
书后要点归纳:
1、若想检测对象的等同性,请提供isEqual 和 hash 方法。
2、相同的对象必须具有相同的哈希值,但是两个哈希值相同的对象未必相同。
3、不要盲目地逐个检查每条属性,根据自己的需要来制定检测方案。
4、编写hash方法时,应该使用计算速度快,并且不容易哈希冲突的算法。
11、理解objc_msgSend的作用
objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”(message forwarding)操作。
上文是书中描述msgSend 的大致流程,这一章值得一提的是讲到的尾调用优化技术。
文中对于尾调用优化提及的内容如下:
如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“ 尾调用优化 ”技术。编译器会生成调转至另一函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧”(frame stack)。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行“ 尾调用优化 ”。
这项优化对objc_msgSend非常关键,如果不这么做的话,那么每次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”,大家在“栈踪迹”(stack trace)中可以看到这种“栈帧”。此外,如果不优化,还会过早地发生“栈溢出”(stack overflow)现象。
尾调用(Tail Call):某个函数的最后一步仅仅只是调用了一个函数(可以是自身,可以是另一个函数)。注意 “仅仅” 两个字。
尾调用优化实现原理:当函数A的最后一步仅仅是调用另一个函数B时(或者调用自身函数A),这时,因为函数A的位置信息和内部变量已经不会再用到了,直接把函数A的栈帧交给函数B使用。
// 不是尾调用1:
- (NSInteger)funcA:(NSInteger)num {
NSInteger num = [self funcB:(num)];
return num;// 不是尾调用->最后一步是返回一个值,而不是调用一个函数
// 不是尾调用2:
- (NSInteger)funcA:(NSInteger)num {
return [self funcB:(num)] + 1;// 不是尾调用->原因:最后一步不仅调用了函数还有 +1 操作
}
// 尾调用:
- (NSInteger)funcA:(NSInteger)num {
/* Some codes... */
if (num = 0) {
return [self funcA:num];// 尾调用->自身
}
if (num > 0) {
return [self funcB:num];// 尾调用->函数funcB
}
return [self funcC:num];// 尾调用->函数funcC
}
实际上的意思就是描述了栈帧的复用,节省内存空间。特别应用是在递归调用中这里节约了大量的栈帧空间。
12、理解消息转发机制
上一节讲的是消息传递机制。这一节的重点在于,对象在接收到无法解读的消息后,会发生什么事做什么事。
第一步:对象在收到无法解读的消息后,首先会调用+(BOOL)resolveInstanceMethod:(SEL)sel或者+ (BOOL)resolveClassMethod:(SEL)sel, 询问是否有动态添加方法来进行处理,处理实例如下
//People.m
void speak(id self, SEL _cmd){
NSLog(@"Now I can speak.");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(sel));
if (sel == @selector(speak)) {
class_addMethod([self class], sel, (IMP)speak, "V@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
当People类收到了未知 speak选择子的消息的时候,如果是实例方法会首选调用上文的resolveInstanceMethod:方法,方法内通过判断选择子然后通过class_addMethod方法动态添加了一个speak的实现方法来解决掉这条未知的消息,此时消息转发过程提前结束。
但是当People 收到fly 这条未知消息的时候,第一步返回的是No,也就是没有动态新增实现方法的时候就会调用第二步。
第二步:既然第一步已经问过了,没有新增方法,那就问问有没有别人能够帮忙处理一下啊,调用的是- (id)forwardingTargetForSelector:(SEL)aSelector这个方法。
上文我们说到People接收到了一条选择子为fly的未知消息,我们可以看到控制台已经打印了resolveInstanceMethod: fly,代表第一步已经问过了,那么第二步问一下是否有别的类能帮忙处理吗?代码如下:
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector: %@", NSStringFromSelector(aSelector));
Bird *bird = [[Bird alloc] init];
if ([bird respondsToSelector: aSelector]) {
return bird;
}
return [super forwardingTargetForSelector: aSelector];
}
// Bird.m
- (void)fly {
NSLog(@"I am a bird, I can fly.");
}
通过- (id)forwardingTargetForSelector:(SEL)aSelector的处理,bird能够处理这条消息,所以这条消息被bird成功处理,消息转发流程提前结束。控制台打印。
forwardingTargetForSelector: fly
I am a bird, I can fly.
但是如果- (id)forwardingTargetForSelector:(SEL)aSelector也找不到能够帮忙处理这条未知消息,那就会走到最后一步,这步也是代价最大的一步。
第三步:调用- (void)forwardInvocation:(NSInvocation *)anInvocation,在调用forwardInvocation:之前会调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法来获取这个选择子的方法签名,然后在-(void)forwardInvocation:(NSInvocation *)anInvocation方法中你就可以通过anInvocation拿到相应信息做处理,实例代码如下
当People 收到一条 选择子为code 的消息的时候,前两步发现都没办法处理掉,走到第三步:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation: %@", NSStringFromSelector([anInvocation selector]));
if ([anInvocation selector] == @selector(code)) {
Monkey *monkey = [[Monkey alloc] init];
[anInvocation invokeWithTarget:monkey];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"method signature for selector: %@", NSStringFromSelector(aSelector));
if (aSelector == @selector(code)) {
return [NSMethodSignature signatureWithObjCTypes:"V@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
这时候控制台打log
resolveInstanceMethod: code
forwardingTargetForSelector: code
method signature for selector: code
forwardInvocation: code
I am a coder.
那么最后消息未能处理的时候,还会调用到
- (void)doesNotRecognizeSelector:(SEL)aSelector这个方法,我们也可以在这个方法中做些文章,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来。
11,12节的内容比较重要,也是OC编程的基础要点。必须要理解这些,后面学习OC才能更容易理解不同的各种OC机制。
18、尽量使用不可变对象
尽量把对外公布出来的属性设置为可读,只在确有必要才将属性对外公布。对象中的属性值可以读出,但是无法写入,这就能保证使用对象时能肯定其底层数据不会改变,对象本身的数据结构也不可能出现不一致的现象。
这是一点开发习惯。以作提醒。
34、以自动释放池块降低内存峰值
释放对象有两种方式:一种是调用release方法,使其保留计数立即递减,另一种是调用autorelease方法,将其加入自动释放池中。
自动释放池用于存放那些需要再稍后某个时刻释放的对象,清空自动释放池时,系统会向其中的对象发生release消息。
创建自动释放池代码:
@autoreleasepool {
//...
}
1.自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。
2.合理运用自动释放池,可降低应用程序的内存峰值。
3.@autoreleasepool这种新式写法能创建出更为轻便的自动释放池。
35、用僵尸对象调试内存管理问题
iOS开发中有时会遇到EXC_BAD_ACCESS报错崩溃的问题,其实这个就是给已经释放了内存的指针发送消息导致的。 僵尸对象一种用来检测内存错误(EXC_BAD_ACCESS)的对象,它可以捕获任何对尝试访问坏内存的调用。
如何开启Zombie Object检测
僵尸对象的工作原理是什么呢?它 的实现代码深植于OC的运行期程序库,Foundation框架及CoreFoundation框架中。系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是将对象转化为僵尸对象而不彻底回收。
ZombieObjectDemo[1357:84410] before release!
ZombieObjectDemo[1357:84410] self:People - superClass:NSObject
ZombieObjectDemo[1357:84410] after release!
ZombieObjectDemo[1357:84410] self:_NSZombie_People - superClass:nil
对象所属类由People 转变成了_NSZombie_People。这个_NSZombie_People其实就是在运行期生成的,当首次碰到People类的对象要变成僵尸对象时,就会创建这么一个类,这个类没有多少事情可做,只是充当一个标记。
系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnable可开启此功能。
系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序。
41、多用派发队列,少用同步锁
同步块:
- (void)synchronizedMethod{
@synchronized(self){
// self
}
}
这种写法会根据给定的对象, 自动创建一个锁,并等待块中的代码执行完毕,执行到这段代码的结尾处, 锁就释放了, 在本例中,同步行文所执行的对象就是 self, 这么写通常没有错, 因为它可以保证每个对象实例都能不受干扰的运行其 synchronizedMethod 方法, 然而,滥用 @synchronized (self) 会降低代码效率, 因为共用同一个锁的那些同步块. 都必须按顺序执行,若是在 self 对象上频繁的加锁, 那么程序可能要等另一段与此无关的代码执行完毕, 才能继续执行当前代码, 这样做其实没有必要。
NSLock:
_lock = [[NSLock alloc] init];
- (void)synchronizedMethod{
[_lock lock];
//self
[_lock unlock];
}
也可以使用 NSRecursiveLock 这种 '递归锁',线程能够多次持有该锁,而不会出现死锁现象。这两种方法都很好,但是都有缺陷。有种简单而高效的方法是使用串行同步队列。将读取操作及写入操作都安排在一个队列里,就可以保证数据同步了。
_syncQueue = dispatch_queue_create("com.effectiveobjectivec,syncQueue",NULL);
- (NSString *)someString{
__block NSString *localSomeString;
dispatch_sync(_syncQueue,^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
dispatch_sync(_syncQueue,^{
_someString = someString;
});]
}
然而还能进一步优化。
_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;
});
}
在队列中,栅栏块必须单独执行,不能与其他块并行,这只对并发队列有意义,因为串行队列总是按照顺序逐个执行的。并发队列如果发现接下来要处理的块是个栅栏块,就一直等当前所以并发块都执行完毕,才会单独执行这个栅栏块,待栅栏块执行完之后,在按照正常方式向下处理。
所以可以使用栅栏块来实现属性的设置方法,在设置方法使用栅栏块之后,对属性的读取操作可以并发执行,但是写入操作必须单独执行。
1、派发队列可以用来表述同步语义,这种做法要比使用@synchronized块,或NSLock更简单。
2、将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
3、使用同步队列及栅栏块,可以使同步行为更加高效。