<<Effective Objective C 2.
<<Effective Objective-C 2.0编写高质量iOS与OS X代码的52个有效方法>>这本书相信很多人都读过,最近又重温了一遍,这本书很薄,只有209页,几个小时就可以看完。虽然很薄,但是写的很好,这本书的内容结构是这样的,先是摆出结论,然后再花几倍的笔墨来解释为什么要这样。如果作者去除解释部分,估计本书可以压缩到80页。这本书我看了好几遍了,但是还是感觉意犹未尽,于是自己就根据它的每个章节提了一些问题。(有的章节其实没必要提问题的,就是了解一下就够了,但是由于惯例,所以这些不太适合提问题的章节还是提问了。) 其中的少数问题其实”超纲”了,也就是仅凭本书是答不出或者答不完整的,还需要查很多的资料才能完全答出来。有兴趣的同学可以看看自己能答出来多少。由于本人水平有限,有的问题出的不是很好。
可以点击这里下载PDF版
第一章:熟悉Objective-C
第一条:了解Objective-C语言的起源
- 如何理解OC的动态绑定?
- 说说[xxObj doSomething]从开始执行到结束的过程。
- 从运行程序到main方法的调用,这之间发生了什么?
建议大家好好看看runtime component的实现过程,runtime源码链接,重点看看objc-runtime-new.mm类。
第二条: 在类的文件中尽量少引用其他头文件
- 你有用过@class吗?请说一下它的常用情景。
- #import和#include的区别。
第三条:多用字面量语法,少用与之等价的方法
- 字面量语法相比传统创建方法的优势?
第四条:多用类型常量,少用#define预处理指令
- 类型常量相对于#define预处理指令的优点?
- 请手写一个通知名称的声明和实现。
第五条:多用枚举表示状态、选项、状态码
- NS_OPTIONS为什么能实现组合多个选项的功能?
- c语言的enum不是够用了吗?Foundation框架为啥要再弄一个NS_ENUM的宏?
第二章:对象、消息、运行期
第六条:理解“属性”这一概念
- 为什么说atomic不能保证属性在多线程中是安全的?请举个例子说明。你如何保证iOS中多线程中访问可变数组,可以得到正确的数据?
- 请分别说明iOS开发中@property的属性特征中的原子性,读写权限和内存管理语义的默认情况?(如什么都不写的话,是nonatomic还是atomic?)
- weak是如何实现变量销毁时指向nil的?
- @synthesize和@dynamic有什么区别?你有使用过它们吗?什么情况下会需要使用它们?请举例
- 你有在.h文件中使用readonly吗?为啥要这么设计?一般还需要在.m中做些什么来配合readonly使用?
第七条:在对象内部尽量直接访问实例变量
- 什么时候使用属性,什么时候使用实例变量?分别说说使用它们的优缺点。
- 如果在初始化方法和dealloc方法中使用了属性,可能会出现什么问题?说一说出现这种问题的原因。
第八条:理解“对象等同性”这一概念
- 自定义对象,如何实现isEqual?
- hash方法存在的意义?hash方法如何实现?为什么?为什么说相同的对象一定具有相同的哈希码,而两个哈希码相同的对象却未必相同?
第九条:以“类族模式”隐藏实现细节
- Foundation框架和UIKit框架中都有类应用了“类族模式”设计,请至少说出2个采用了这种模式的类。
- “类族模式”有什么好处?请你设计一个采用了“类族模式”的类。
第十条:在既有类中使用关联对象存放自定义数据
- 分类中的@property属性跟普通类中使用@property声明属性有什么不同?如何在分类中实现跟普通类中使用@property一样的效果?
- 你在项目中或者看到别人使用过关联对象技术吗?
第十一条:理解“objc_msgSend”的作用
- SEL的本质是什么?
- 给对象发送消息是如何”动态消息派发系统”派发的?说一说这个过程(消息传递机制)。
- 苹果为动态消息派发系统做了哪些优化来提高查找方法列表的速度?
第十二条:理解消息转发机制
- 请说说消息转发机制?你在项目中有使用过消息转发吗(在开发中的具体应用场景)?
第十三条:用“方法调配技术”调试“黑盒方法”
- 如何动态添加方法和替换方法实现?
第十四条:理解“类对象”的用意
- 说说你对类对象的理解?清楚它的内存布局吗?
第三章:接口与API设计
第十五条:用前缀避免命名空间冲突
- 你在项目中创建类和设计分类方法时,如何避免类的重复定义及分类方法的相互覆盖?
第十六条:提供“全能初始化方法”
- 如果让你设计一个类,提供多个初始化方法,你的.h文件怎么设计接口?
- 为什么在init方法中要调用super init,不调用行不行?请说明理由。
第十七条:实现description方法
- 有使用过LLDB吗?
- 在Xcode的打印含有中文字符串的字典和数组时,会出现编码和不是正常的文字,你是怎么解决的?
第十八条:尽量使用不可变对象
- 在某个类的头文件中,有个属性
@property (nonatomic, strong) NSMutableArray *friends;
,这么写有什么问题吗?如果有问题,请说明问题,如何修改?
第十九条:使用清晰而协调的命名方式
- 你平时是怎么样为方法和类与协议命名的?
第二十条:为私有方法名加前缀
- 为什么要给私有方法加前缀?
第二十一条:理解Objective-C错误模型
- 第三方崩溃日志收集平台的设计原理是什么?如果让你自己设计一个,你怎么设计出类似的功能。
第二十二条:理解NSCopying协议
- 什么是深拷贝,什么是浅拷贝,有什么区别?
- 如何实现自定义对象支持拷贝功能?
第四章:协议与分类
第二十三条:通过委托与数据源协议进行对象间通信
- 什么是委托(代理)模式?这个模式的作用是什么?
- UITableView为什么有了delegate还要增加dataSource属性,这2个属性为什么都是weak?
- UITableView的某些代理方法调用的非常频繁,频繁判断某个委托对象是否响应代理方法比较耗费性能,请问如何优化?
第二十四条:将类的实现代码分散到便于管理的数个分类之中
- 某个类的.m过于臃肿,杂糅了太多的方法,如何重构它?
- 分类里实现了与本类相同的方法,为什么会调用分类里面的方法而不调用本类的原来方法?
第二十五条:总是为第三方类的分类名称加前缀
- 增加分类和分类方法时要注意什么?(主要是命名上)
第二十六条:勿在分类中声明属性
- 为什么分类中的@property不能生成实例变量?
第二十七条:使用“class-continuation”分类隐藏实现细节
- 匿名分类跟分类有什么区别?
第二十八条:通过协议提供匿名对象
- 协议除了用于代理模式,还有什么作用?
第五章:内存管理
第二十九条:理解引用计数
- 说说你对引用计数的理解?苹果设计引用计数的用意是什么?
第三十条:以ARC简化引用计数
- ARC和MRC有什么区别?ARC是如何实现的?
- 使用了ARC还会出现内存泄露吗?请举例说明。
- weak是如何做到自动置为nil的?
第三十一条:在delloc方法中只释放引用并解除监听
- ARC下我们在dealloc方法中应该做什么事情?
第三十二条:编写“异常安全代码”时留意内存管理问题
- Java等语言中经常大量用到异常,为什么OC中比较少出现异常机制的代码?
第三十三条:以弱引用避免保留环
- weak和unsafe_unretained有什么区别?哪些情况下会用到weak,请举例。
第三十四条:以 “自动释放池块” 降低内存峰值
- 项目中有用过自动释放池吗?有什么作用?
第三十五条:用“僵尸对象”调试内存管理问题
- 如何监测项目中出现的“僵尸对象”?
第三十六条:不要使用retainCount
- 为什么不要用retainCount? retainCount不准的原因在哪里?
第六章:块与大中枢派发
第三十七条:理解”块”这一概念
- block有哪几种?它的内部结构是什么样的?
第三十八条:为常用的块类型创建typedef
- 请手写一个block属性,并分别使用typedef重新定义它。
第三十九条:用handler块降低代码分散程度
- 使用block代替代理有啥有点和缺点?分别在何种情况使用比较合适?
- NSNotificationCenter在发送通知的时候,是同步还是异步?
第四十条:用块引用其所属对象时不要出现保留环
- 什么时候容易出现block的循环引用?出现这种情况除了使用weak,还有其他的解决方法吗?
第四十一条:多用派发队列,少用同步锁
- atomic的实现原理是咋样的?
- 如何用GCD解决访问可变数组过程中的多线程安全问题?
第四十二条:多用GCD,少用performSelector系列方法
- performSelector方法有什么局限性和缺点? 如何解决。
第四十三条:掌握GCD以及操作队列的使用时机
- GCD和NSOperation各有哪些特点?你平时喜欢用哪个?
第四十四条:通过Dispatch Group机制,根据系统资源状况来执行任务
- 使用过dispatch group吗?它的作用是什么?
- GCD大概有哪些比较常用的函数?
第四十五条:使用dispatch_once来执行只需要运行一次的线程安全代码
- 请分别用2种方式来实现单例。
- dispatch_once为什么说是线程安全的?
第四十六条:不要使用dispatch_get_current_queue
- 什么情况会出现死锁?请用GCD模拟一种情况。
第七章:系统框架
第四十七条:熟悉系统框架
- 除了Foundation和UIKit框架,你还知道哪些系统框架?
第四十八条:多用块枚举,少用for循环
- 你知道哪些遍历的方法?各有什么特点?
第四十九条:对自定义其内存管理语义的collecion使用无缝桥接
- 你了解桥接技术吗?为什么需要桥接?
第五十条:构建缓存时选用NSCache而非NSDictionary
- 你了解NSCache吗?它相比于NSDictionary有哪些优点?
第五十一条:精简initialize与load的实现代码
- 有听过load和initialize方法吗?说说它们的特点。
第五十二条:别忘了NSTimer会保留其目标对象
-
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(repeatDoSomething) userInfo:nil repeats:YES];
这句代码有什么问题?如何解决?
本书精简版
1. 了解Object-C 语言的起源
消息结构和函数调用的区别:
使用消息结构的语言,其运行时所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定。另外,CGRect结构体,储存在栈区
2. 在类的头文件中尽量少引用其他头文件
如果不是非用不可,尽量用@class(向前声明)代替#import,加快编译速度。
3. 多用字面量语法,少用与之等价的方法
id object1 = @"1";
id object2 = nil;
id object3 = @"3";
NSArray *arrayA = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSArray *arrayB = @[object1, object2, object3];
上面的代码我们发现,arrayA虽然能创建出来,但是其中却只有object1一个对象,原因在于“arrayWithObjects:”方法会依次处理各个参数,直到发现nil为止,由于object2是nil,所以该方法会提前结束,按字面量语法创建arrayB时会抛出异常。
所以:
使用字面量语法创建数组、字典...更安全,语法更简洁。
4. 多用类型常量,少用#define 预处理指令
仅在“编译单元(.m文件内)”内使用
define ANIMATION_DURATION 0.3 // 不建议
static NSTimeInterval const kANIMATION_DURATION = 0.3; // 建议
需要外露使用
// EOCAnimatedView.h
extern const NSTimeInterval EOCAnimateViewAnimationDuration;
// EOCAnimatedView.m
const NSTimeInterval EOCAnimateViewAnimationDuration = 0.3;
上述宏定义,预处理指令会把源代码中的ANIMATION_DURATION字符串替换成0.3。假设此指令声明在某个头文件中,那么所有引入了这个头文件的代码,其ANIMATION_DURATION都会被替换。
涉及知识点:
- 常量命名:若常量局限于某“编译单元”(也就是实现文件.m)之内,则在前面加上字母k;若常量在类之外可见,则通常以类名为前缀。
- 宏:
1.作用:在预编译阶段替换;
2.其他:在预编译阶段执行,不做编译检查,不会报编译错误; - const:
1.作用:仅仅用来修饰右边的变量,被const修饰的变量是只读的;
2.其他:在编译阶段执行,会做编译检查,会报编译错误; - static的作用:
1.修饰局部变量:(1). 延长局部变量的声明周期,程序结束才会销毁;(2).局部变量只会生成一份内存,只会初始化一次;(3). 改变局部变量的作用域;
2.修饰全局变量:(1). 只能在本文件中访问,修改全局变量的作用域,生命周期不会改变;(2). 避免重复定义全局变量; - extern:
1.作用:只是用来获取全局变量(包括全局静态变量)的值,不能用于定义变量;
2.原理:先在挡圈文件中查找有没有全局变量,没有找到,才会去其他文件查找; - static与const联合使用:
1.作用:声明一个只读的静态变量;(在每个文件都需要定义一份静态全局变量)
2.使用场景:在一个文件中经常使用的字符串常量; - extern与const联合使用:
1.作用:只需要定义一份全局变量,多个文件共享;
2.使用场景:在多个文件中经常使用同一个字符串常量;
5. 用枚举表示状态、选项、状态码
在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。
6. 理解“属性”这一概念
@interface EOCPersion: NSObject {
@public
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end
编写时使用实例变量有个问题:就是对象布局在编译期就已经固定了,只要碰到访问_firstName变量的代码,编译器就把其替换为“偏移量”(offset),这个偏移量是“硬编码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远,这样做目前没问题,但是如果又加了一个实例变量,如:
@interface EOCPersion: NSObject {
@public
NSString *_dateOfBirth;
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
}
@end
那么原来_firstName的偏移量现在却指向_dateOfBirth了,把偏移量硬编码于其中的那些代码都会读取到错误的值。所以,修改之后必须重新编译,否则就会出错。Obejcet-C的做法是,把实例变量当做一种存储偏移变量所用的“特殊变量”,交由“类对象”保管,偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。另外还有一种解决办法就是,尽量不要直接访问实例变量,而应该通过存取方法来做。
atomic一定安全吗?
具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性,但这无法保证绝对的“线程安全”,例如:一个线程在连续多次读取某个属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读到不同的属性值。
7. 在对象内部尽量直接访问实例变量
在对象之外访问实例变量时,总是应该通过属性来做,然后在对象内部访问实例变量是该如何?作者建议
在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做。
8. 理解“对象等同性”这一概念
重写hash的一种思路,相对高效
- (NSUInteger)hash
{
NSInteger firstNameHash = [_firstName hash];
NSInteger lastNameHash = [_lastName hash];
NSInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}
一般重写“isEqual:”方法
- (BOOL)isEqualToPerson:(EOCPerson *)otherPerson
{
if (self == otherPerson) return YES;
if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
if (_age != otherPerson.age) return NO;
return YES;
}
-(BOOL)isEqual:(id)object
{
if ([self class] == [object class]) {
return [self isEqualToPerson:(EOCPerson *)object];
} else {
return [super isEqual:object];
}
}
9. 以“类簇模式”隐藏实现细节
10. 在既有类中使用关联对象存放自定义数据
可以把关联对象想象成NSDictionary,只不过设置关联对象时用到的键(key)是个“不透明的指针”。
11. 理解objc_msgSend的作用
如果某函数的最后一项操作是调用另外一个函数,那么久可以运用“尾调用优化”技术。编译器会生成调转至另一函数所需要的指令码,而且不会向调用堆栈中推入新的“栈帧”。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另做他用时,才能执行“尾调用优化”。这项优化对objc_msgSend非常关键,如果不这么做的话,那么每次调用Object-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”,大家在“栈踪迹”中可以看到这种“栈帧”。此外,若是不优化,还会过早的发生“栈溢出”现象。
12. 理解消息转发机制
image基本就是消息转发的大体流程,需要注意的一点就是步骤越往后,处理消息的代价就越大,最好能在第一步就处理完,这样的话,运行期系统就看可以将此方法缓存起来,如果这个类的实例稍后还受到同样的选择子(方法选择器),那么根本无须启动消息转发流程。
13. 用“方法调配技术”调试“黑盒方法”
利用动态特性,运行时动态添加、交换方法
14. 理解“类对象”的用意
尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。11~14 全是老成长谈的runtime机制,这个一时半会且三言两语是无法讲解清楚的(单拿出一整篇文章来讲都不见得能面面俱到
15. 用前缀避免命名空间冲突
16. 提供“全能初始化方法”
17. 实现description方法
- 实现description方法返回一个有意义的字符串,用以描述该实例;
- 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法;
18. 尽量使用不可变对象
这个意思是说:
尽量把对外公布出来的属性设为只读,而且只有在确有必要的时候才将属性对外公布。
尽管把属性设置成readOnly之后,可以防止外部乱改属性值,但并不是完全不能改,外界仍然可以通过“键值编码”(KVC)技术设置这个属性值。
19. 使用清晰而协调的命名方式
别怕麻烦,要见名知意,方法命名可参考系统方法名。
20. 为私有方法名加前缀
作者建议:
编写类的实现代码时,经常要写一些只在内部使用的方法。应该为私有方法的名称前加上某些前缀,这有助于调试,因为据此很容易就能把公共方法和私有方法区别开。可以用“p_”打头。(不要单单只用一个下划线做私有方法的前缀,因为这种做法是预留给苹果大大用的)。
21. 理解Object-C错误模型
当出现“不那么严重的错误”时,Object-C语言所用的编程范式为:令方法返回nil/0,或者使用NSError。
NSError对象里封装了三条信息:
- Error domain(错误范围,其类型为字符串);
- Error code(错误码,其类型为整数);
- User info(用户信息,其类型为字典)
22. 理解NSCoding协议
想要实现复制功能,需要遵从NSCoding协议,实现copyWithZone:方法;
深拷贝:
深拷贝:在拷贝对象自身时,将其底层数据也一并复制过去。Foundation框架中所有collection类在默认情况下都执行浅拷贝。
23. 通过委托与数据源协议进行对象间通信
这里主要是讲了使用delegate的场景以及相关的注意事项,比如使用weak防止循环引用等。这里有一个优化点:(不过笔者很少这么干,不是很少,是压根没有😶)
我们通常把委托对象能否响应某个协议方法这一信息缓存起来,以优化程序效率。 将方法响应能力缓存起来的最佳途径是实用“位段”数据类型。这是一项乏人问津的C语言特性,但在此处用起来却正合适。
// 协议声明部分
@class EOCNetworkFetcher;
@protocol EOCNetworkFetcherDelegate
@optional
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didUpdateProgressTo:(float )progress;
@end
在.m文件中 定义一个结构体_delegateFlags用来缓存方法响应能力
@interface EOCNetworkFetcher ()
{
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlags;
}
@end
@implementation EOCNetworkFetcher
- (void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate
{
_delegate = delegate;
_delegateFlags.didReceiveData = [_delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
_delegateFlags.didFailWithError = [_delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];
_delegateFlags.didUpdateProgressTo = [_delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}
// 在使用的时候 直接判断
if (_delegateFlags.didUpdateProgressTo) {
[_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
}
@end
24. 将类的实现代码分散到便于管理的数个分类之中
使用分类机制可以把类的实现代码划分成易于管理的小块;
25. 总是为第三方类的分类名称增加前缀
- 向第三方类中添加分类时,总应该给其名称加上你专用的前缀;
- 向第三方类中添加分类时,总应该给其中的方法名加上你专用的前缀;
26. 勿在分类中声明属性
把封装数据所用的全部属性都定义在主接口里。不建议在分类中添加属性,虽然通过关联对象能够解决在分类中不能合成实例变量的问题。但不是最理想的,且在内管理上容易出错,分类只是一种手段,目的是在于扩展类的功能,而非封装数据。
27. 使用“class-continuation分类” 隐藏实现细节
其实就是类扩展
- 通过“class-continuation分类”向类中新增实例变量;
- 如果某属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation分类”中将其扩展为“可读写”。
- 把私有方法的原型声明在“class-continuation分类”里面;
- 若想使类所遵循的协议不为人所知,则可于“class-continuation分类”中声明。
28. 通过协议提供匿名对象
29. 理解引用计数
- (void)setFoo:(id)foo
{
[foo retain];
[_foo release];
_foo = foo;
}
此方法将保留新值释放旧值,然后更新实例变量,令其指向新值。顺序很重要。假如还未保留新值就先把旧值释放了,而且这两个值又指向同一个对象,那么,限执行的release操作就有可能导致系统将此对象永久回收。而后续的retain操作则无法令这个已经彻底回收的对象复生,于是实例变量就成了悬挂指针。
30. 以ARC简化引用计数
- ARC在调用这些方法时,并不通过普通的Object-C消息派发机制,而是直接调用其底层的C语言版本,这样性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期,比如ARC会调用与retain等价的底层函数objc_retain。
- 在编译期,ARC会把能够相互抵消的retain、release、autorelease操作简约。如果发现在同一个对象上执行多次的“保留”与“释放”操作,那么ARC有时可以成对的移除这两个操作;
- ARC也包含运行期组件。用于检测当前方法返回之后即将执行的那段代码。如果发现那段代码要在返回的对象上执行retain操作,则设置全局数据结构中的一个标志位,而不执行autorelease操作。
- ARC最在dealloc方法中自动生成.cxx_destrucet方法,释放对象
31. 在dealloc方法中只释放引用并解除监听
简单说就是在dealloc方法里,只应该释放引用,或者移除监听者(KVO或NSNotificationCenter),不要做其他事。
32. 编写“异常安全代码”时留意内存管理问题
- 捕获异常时,一定要注意将try块内所创建的对象清理干净;
- 在默认情况下,ARC不生成安全处理异常所需的清理代码,开启编译器标志后(-fobjc-arc-exceptions),可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。
33. 以弱引用避免保留环
使用weak。weak所指向的实例回收后,weak属性指向nil,而unsafe_unreatined属性仍然指向那个已经回收的实例。
34. 以“自动释放池块”降低内存峰值
for (int i = 0; i < 100000; i++) {
[self doSomethingWithInt:i];
}
如果“doSomethingWithInt:”方法要创建临时对象,那么这些对象很可能放在自动释放池里。但是,这些对象在调用完方法之后就不再使用了,他们也依然处于存活状态,因为目前还在自动释放池里,等待线程执行下一次事件循环时才会清空。这就意味着在执行for循环时,会持续有新对象创建出来,加入到自动释放池中。所有这些对象要等到for循环执行完之后才会释放,所以会造成内存高峰,即:内存用量持续上涨,然后突然下降。
NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray array];
for (NSDictionary *record in databaseRecords) {
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
}
35. 用“僵尸对象”调试内存管理问题
- 僵尸对象:已经被销毁的对象(不能再使用的对象);
- 野指针:指向僵尸对象(不可用内存)的指针
- 给野指针发消息会报EXC_BAD_ACCESS错误
- 系统在回收对象时,可以不将其真的回收,而是把它转成僵尸对象;
- 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸对象能够响应所有的选择器,响应方式:打印一条包含消息内容及其接受者的消息,然后终止应用程序。
开启“僵尸对象”的检测
在Xcode中设置Edit Scheme -> Diagnostics -> Zombie Objects
从汇编的调用顺序可以阿盖总结僵尸对象生成过程:
//1、获取到即将deallocted对象所属类(Class)
Class cls = object_getClass(self);
//2、获取类名
const char *clsName = class_getName(cls)
//3、生成僵尸对象类名
const char *zombieClsName = "_NSZombie_" + clsName;
//4、查看是否存在相同的僵尸对象类名,不存在则创建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
//5、获取僵尸对象类 _NSZombie_
Class baseZombieCls = objc_lookUpClass(“_NSZombie_");
//6、创建 zombieClsName 类
zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
//7、在对象内存未被释放的情况下销毁对象的成员变量及关联引用。
objc_destructInstance(self);
//8、修改对象的 isa 指针,令其指向特殊的僵尸类
objc_setClass(self, zombieCls);
那么Zombie Object是如何被触发的?
//1、获取对象class
Class cls = object_getClass(self);
//2、获取对象类名
const char *clsName = class_getName(cls);
//3、检测是否带有前缀_NSZombie_
if (string_has_prefix(clsName, "_NSZombie_")) {
//4、获取被野指针对象类名
const char *originalClsName = substring_from(clsName, 10);
//5、获取当前调用方法名
const char *selectorName = sel_getName(_cmd);
  
//6、输出日志
NSLog(''*** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);
 //7、结束进程
 abort();
36. 不要使用retainCount
- 对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数”都无法反映对象生命期的全貌;
- ARC下调用该方法会编译报错。
37. 理解“块”这一概念
块和函数类似,只不过是直接定义在另一个函数里,和定义它的那个函数共享同一范围内的东西。
block的内存管理:
- 默认情况下block的内存是在栈中(不需要手动去管理block内存),它不会对所引用的对象进行任何操作;
- 如果对block进行了copy操作, block的内存会搬到堆里面,它会对所引用的对象做一次retain操作。
为什么加上__block就可以修改外部的变量了?
真正的原因是这样的:我们都知道:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
38. 为常用的块型创建typedef
以typedef重新定义块类型,可令变量用起来更加简单
39. 用handler块降低代码的分散程度
40. 用块引用其所属对象时不要出现保留环
41. 多用派发队列,少用同步锁
为啥@synchronized(self)效率低?
同步行为针对的对象是self。然而共用同一个锁的那些同步块,都必须按顺序执行,若是在self对象频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码。
手动实现atomic特性,通常会这么写:
- (NSString *)someString
{
@synchronized(self) {
return _someString;
}
}
- (void)setSomeString:(NSString *)someString
{
@synchronized(self) {
_someString = someString;
}
}
这么写虽然能提供某种程度的“线程安全”,但却无法保证访问该对象时绝对的线程安全,在同一个线程上多次调用获取方法(getter),每次获取到的结果却未必相同。在两次访问操作之间,其他线程可能会写入新的属性值。
优化后:
- (NSString *)someString
{
__block NSString *localSomeString;
dispatch_sync(__syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString
{
dispatch_barrier_async(__syncQueue, ^{
_someString = someString;
});
}
image
42. 多用GCD,少用performSelector
performSelector系列方法有局限性。如:具备延后执行功能的那些方法(performSelector: withObject: afterDelay:)都无法处理带有两个参数的选择器。而能够指定执行线程的那些方法(performSelector: onThread: withObject: waitUntilDone:)也不是特别通用,如果要用这些方法,就得把许多参数都打包到字典里,然后在受调用的方法里将其提取出来,这样会增加开销且可能出bug。
43. 掌握GCD及操作队列的使用时机
使用NSOperation及NSOperationQueue的好处如下:
- 取消某个操作。在运行任务之前,可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表名此任务不需要执行。
- 指定操作间的依赖关系;
- 通过键值观察机制(KVO)监控NSOperation对象的属性。比如isCancelled(是否已经取消)、isFinished(是否已完成);
- 指定操作的优先级。GCD的队列确实有优先级,不过那是针对整个队列来说的,而不是针对每个块来说的;
- 重用NSOperation对象。
44. 通过Dispatch Group机制,根据系统资源状况来执行任务
- 使用场景:这个比较适合比如下载多张图片然后再合成一张图片,或者请求C需要依赖请求A和请求B的结果的情况;
- 注意事项:调用dispatch_group_enter与dispatch_group_leave须成对出现;
- dispathc_apply所用的队列可以是并发队列,然而dispathc_apply会持续阻塞,知道所有任务都执行完毕。
45. 使用dispathc_once来执行只需要运行一次的线程安全代码
单例
46. 不要使用dispathc_get_current_queue
- dispathc_get_current_queue函数行为常常与开发者所预期的不同。
- dispathc_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。
47. 熟悉系统框架
48. 多用块枚举,少用for循环
“块枚举法”本身就能通过GCD来并发执行遍历操作。
49. 对自定义其内存管理语义的collection使用无缝桥接
主要讲了下CF的一些底层函数,比如CFDictionary
50. 构建缓存时选用NSCache而非NSDictionary
相比NSDictionary,优势在于:自动删减功能,而且是“线程安全的”,它与字典不同,并不会拷贝键。
51. 精简initialize与load方法
load是在编译阶段执行,是方法地址执行,不走消息发送机制。首次使用某个类之前,系统会向其发送initialize消息; load方法:
执行顺序:父类 -> 子类 -> 分类。都执行
initialize方法:(执行顺序)
- 分类会覆盖本类。
- 父类 -> 子类
- 如果子类没有执行initialize 那么父类的initialize可能会执行多次。
52. 别忘了NSTimer会保留其目标对象
书中是用分类扩展,用“块”来实现的。也可以通过关联对象来实现。