熟悉Objective-C
1. Objective-C 语言的起源
Objective-C语言由Smalltalk演化而来,smalltalk是消息型语言的鼻祖。消息型语言运行时所执行的代码由运行环境来决定。函数调用的语言由编译器来决定。
Objective-C为C语言添加了面相对象特征,是C语言的超集。Objective-C使用动态绑定的消息结构,也就是说在运行时才回去检查对象类型。接受一条消息后,究竟应执行何种代码,由运行期环境而非编译器来决定。
2. 类中的头文件尽量少引入其他头文件
除非确有必要,否则不要引入头文件。一般来说,应在某个类额头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件,这样做可以尽量降低类之间的耦合
有时无法使用向前声明,比如要声明某个类型遵循某一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把该协议单独放在一个头文件中,然后将其引入。
3. 多用字面量语法,少用与之等价的方法
使用该字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
应该通过取下标来访问数组下标或字典中的健所对应的元素
用字面语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里面不包含nill对象。
4. 多用类型常量,少用#define预处理指令**
不要用预处理指令定义常量。这样定义出来的常量不包含类型信息,编译器只会在编译钱根据执行找与替换操作。几遍有人重定义了常量值,该编译器也不会产生警告信息,浙江导致应用中常量不一致。
在实现文件中庸static const来定义“只在编译单元内可见的常量”。由于该常量不在全局符号号表中,所以无需为其添加前缀。
在头文件中用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以前缀用来区分,通常用用与之相关类名做前缀。
5. 用枚举表示状态、选项、状态码
应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
如果传递个某个方法的选项表示枚举类型,而多个选项又可以同时使用,那么将各选项定义成2的幂,以便通过按位或操作将其组合起来。
用NS_ENUM和NS_OPTIONS宏来定义枚举的类型,并指明起底层数据类型。这样做可以明确确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。
在处理枚举类型中的switch语句不要实现default分支,这样的话,加入新枚举之后,编译器就会提示我们switch中有未处理的枚举。
对象、消息、运行期
6. 理解“属性”这一概念
属性是Objective-C的一项特性,用于封装对象中的数据。属性性质有:原子性、读/写权限、内存管理语义(assign、strong、weak、unsafe_unretained、copy)、方法名(getter=<name>、setter=<name>)
可以用@property语法来定义对象中所封装的数据
通过“特质”来指定存储数据所需的正确语义
在设置属性所对应的实例变量时、一定要遵从该属性所声明的语义
开发iOS 12程序时使用nonatomic属性,因为atomic属性严重影响性能
7. 在对象内部尽量直接访问实例变量
属性访问与直接访问的区别:
- 直接访问实例变量的速度比属性快,因为编译器所生成的代码会直接访问保存对象实例变量的那块内存,省去了Objective-C的method dispatch步骤
- 直接访问实例变量不会调用其设置方法,这就绕过了属性相关的内存管理语句,比如声明一个copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值
- 直接访问不会触发键值观察(KVO)通知,这样是否会产生问题取决于具体的对象行为
- 通过属性访问有助于排查与之相关的问题,因为会用到Getter和Setter方法
要点: - 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据则应通过属性来写
- 在初始化及dealloc方法中,总是应该通过实例变量来读写数据
- 有时用到懒加载初始化数据时,应该通过属性来读取数据
8. 理解“对象等同性”这一概念
NSObject协议中有两个用于判断等同性的关键语句方法
- (BOOL)isEqual:(id)object
- (NSUInteger)hash
要点:
- 特定类具有等同性判断的方法
- 等同性判断执行的深度,有时候无需将所有数据进行比较,只根据部分数据即刻判断二者是否等同,比如说数据库中的“唯一标识符”
- 容器中可变类的等同性,如果把某对象放入collection之后改变其内容将会造成意想不到的后果,比如NSSet中加入两个可变对象,加入之后再改变其中一可变对象与另一可变对象相同,那么NSSet就有两个一样的对象了,这与NSSet设计明显不符合。
要点: - 若想检测对象的等同性,需要提供“isEqual”和“hash”方法
- 相同对象必须具有相同的哈希码,但相同的哈希码的对象不一定相同
- 不要盲目逐个比对每条属性,应该依照具体需要来指定特定方案
- 便携hash方法时,应该使用计算速度快而且哈希碰撞几率低的算法
9. 以“类族模式”隐藏实现细节
类族可以隐藏抽象基类背后实现的细节。比如UIButton、Cocoa里大部分都是类族
类族规则:
- 子类应该继承自类族中的抽象基类
- 子类应该定义自己的数据存储方式
- 子类应该覆写超类文档中知名需要覆写的方法
要点:
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面
- 系统框架中经常使用类族模式
- 从类族的公共抽象基类中继承子类时应当心,若有开发文档则需要先阅读
10. 在已有类中使用关联对象存放自定义数据
关联对象(Associated Object)可以为已有类存储对象值,这些对象是通过键来区分的,该键通常为全局静态变量
objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>)
objc_getAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>)
objc_removeAssociatedObjects(<#id _Nonnull object#>)
要点
- 可以通过“关联对象”机智来把两个对相关联起来
- 定义关联对象科指定内存管理语句,泳衣模仿定义属性是所采用的“拥有关系”和“非拥有关系”
- 只有在其他方法不可行的时候才选用关联对象策略,因为出现bug难以查找
11. 理解objc'_msgSend的作用
在objective-C中,如果向对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息后,该调用哪个方法取决于运行期间绝对,甚至可以在程序运行期间改变,这些特性使得Objective-C成为一门真正的动态语言。
objc_msgSend(id self,SEL cmd,...)
objc_msgSend_stret()
objc_msgSend_fpret()
objc_msgSendSuper()
如果某函数的最后一项是调用另外一个函数,那么就可以运用“尾调用优化”技术。编译器会生成调用另一函数所需的指令码,而且不需要调用堆栈中推入新的“帧栈”。只有当某函数的最后一个操作仅仅是调用其他函数而且不会将其返值另作他用时,才能执行“尾调用优化”,这项技术对objc_msgSend非常关键,解决了帧栈溢出的问题。尾调用优化讲解
要点
- 消息由接受者、方法选择器、及参数构成。给对象“发送消息”也就是相当于在该对象上“调用方法”
- 发给某对象的全部消息都有“动态消息派发系统”来处理,该系统会查出相应的方法,并执行代码。
12. 理解消息转发机制
消息转发大致分为两大阶段:
- 第一阶段先征询接受者,所属的类,看起是否能动态添加方法,已处理当前这个"未知选择子"
- 第二段设计"完整的消息转发机制",如果接受者无法动态的新增方法的手段来响应这个未选择子的消息,此时运行期系统会请求看看有没有其他对象来处理这条消息,若有,则运行期间会把消息传递给那个对象,于是消息转发结束,一切正常。若没有“备援的接受者”,则启动完整的消息转发机制,运行期系统会把与消息相关的全部细节封装到NSInvocation对象中,再给接受者最后一次机会,另其设法解决当前还未处理的这条消息。处理消息越到后面代价越大。
/// 对象收到无解读的消息后,首先将调用其类的下列类方法
+ (BOOL)resolveClassMethod:(SEL)sel
///备援接受者
- (id)forwardingTargetForSelector:(SEL)aSelector
/// 完整的消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation
要点:
- 若对象无法响应某个选择子,则进入消息转发机制
- 通过运行期动态方法解析功能,我们可以在需要用到某个方法的地方再将其加入类中
- 对象可以把其无法解读的某些选择子交给其他对象来处理
- 经过上述步骤,若还是没处理未选择子,那就启动完整的消息转发机制
13. 用方法调配技术调试黑盒方法
开发者可以为那些“完全不知道其具体实现”的黑盒方法增加日志记录功能,这非常有助于调试
要点:
- 在运行期间,可以向类中新增或替换选择子所对应的实现方法
- 使用另一份实现来替代原有方法实现,这道工序叫做“方法调配”,开发者经常用词技术向原有功能添加新功能
- 一般来说,只有调试程序中才需要在运行期修改方法实现,这种做法不宜滥用
14. 理解“类对象”的用意
在运行期间检查对象类型这一操作也叫类型信息查询,这个强大而有用的特性内置于Foundation框架中的NSObject协议里,凡是有公共根类(NSObject和NSProxy)继承而来的对象都要遵从此协议。在比较对象所属类的时候建议使用类型信息查询
struct objc_class {
Class _Nonnull isa ;/**< 该对象所属的类 元类metaClass*/
Class _Nullable super_class;/**< 该对象所属类的超类 确立了继承关系 super_class*/
const char * _Nonnull name;/**< 类名 */
long version;/**< 版本信息 */
long info;
long instance_size;
struct objc_ivar_list * _Nullable ivars;/**< 属性信息集合 */
struct objc_method_list * _Nullable * _Nullable methodLists;/**< 方法信息集合 */
struct objc_cache * _Nonnull cache;/**< 缓存信息集合 */
struct objc_protocol_list * _Nullable protocols;/**< 遵从的协议集合 */
} OBJC2_UNAVAILABLE;
isMemberOfClass判断出对象是否为某个特定类的实例
isKindOfClass判断对象是否为某类或其派生类的实例
要点:
- 每个实例都有一个指向Class对象的指针,用意表明其类型,这些Class对象构成了类的继承关系
- 如果对象类型无法再编译器确定,那么久应该使用该类型信息查询方法来探知
- 尽量使用类型信息查询方法来确定对象类型,而不要直接使用比较(==)类对象,因为某些对象可能实现了消息转发功能
接口与API设计
15. 用前缀避免命名空间冲突
Objective-C语言没有内置的命名空间机制,因此我们开发的时候需要变相的实现命名空间(为所有名称前都加上适当的前缀,应用程序所有名称前都应该加上前缀)
要点:
- 选择与你的公司、应用程序或者二者皆有关联的之名称作为类名的前缀,并在所有代码中均使用该前缀
- 若自己所开发的程序中用到了第三方库,则应为七中的名称加上前缀
16. 提供“全能初始化方法”
提供全能初始化方法,其余初始化方法都需要调用此全能初始化方法,只有在全能初始化方法中才可以存储数据。因此当底层数据放生改变时,只需要修改此方
法的代码就好,无需修改其他初始化方法。
要点:
- 在类中提供一个全能初始化方法,并在文档中指明。其他初始化方法均调用此方法
- 在全能初始化方法与超类不同,则需要覆盖超类的对应的方法
- 如果超类全能初始化方法并不适应于此类,那么应该覆写这个超类的方法,并在其中抛出异常
17. 实现description方法
- (NSString *)description {
return [NSString stringWithFormat:@"<%@:%p,width=%.2lf,height=%.2lf",[self class],self,_width,_height];
}
///控制台输出信息
2019-04-12 00:06:13.139550+0800 Effective Objective-C Demo[816:18588] <MJTestModel:0x600002edc240,width=100.00,height=100.00
- 实现description方法返回一个有意义的字符串,用以描述该类
- 若想在打印时打印出更详细的对象信息,则应实现debugDescription方法
18. 尽量使用不可变对象
在设计类的时候应该充分运用属性来封装数据,而在使用属性的时候可将其设置为只读,
要点:
- 尽量创建不可变对象
- 若某属性仅可对于对象内部修改,则在“class-continuation”分类中将其readonly属性扩展为readwrite
- 不要把可变的collection作为属性公开,而应该提供相关的方法,以此修改对象中的可变collection
19. 使用清晰而协调的命名方式
方法命名:清晰的方法名是从左至右读起来好似一段文章。
- 如果方法的返回值是新创建的,那么方法名的首个词应该是返回值类型,除非前面还有修饰语,例如localizedString。然而属性的存储方法不遵守这种命名方法。
- 应该把参数类型的名词放在参数前面
- 如果方法要在当前对象上执行操作,那么应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或者多个名词
- 不要使用str这种简称,应使用string这样的全称
- Boolean属性应加上is前缀。如果方法返回非属性的Boolean值,则应该根据其功能,选用has或is当前缀
- 将get这个前缀留给那些借助由“输出参数”来保存返回值的方法,比如说,把返回值填充到“C语言数组”里的那种方法就可以使用这个词做前缀
类与协议的命名:应该为类与协议加上前缀避免命名冲突,而且应该想给方法起名一样从左至右读起来通畅。
要点:
- 起名时应遵从标准的Objective-C命名规范,这样创建出来的接口更容易为开发者所理解
- 方法名要言简意赅,从左至右读起来通畅易懂
- 方法名里面不应该使用缩略后的类型名称
- 给方法起名是第一要务就是确保其风格与你自己的代码或索要集成的框架相符合
20. 为私有方法名加前缀
为私有方法加前缀有助于调试,而且还能与公开的方法区分开。
要点:
- 给私有方法的名称加上前缀,很容易与公开方法区分开
- 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的
21. 理解?Objective-C错误模型
Objectview-C中“自动引用计数”在默认情况下不是“异常安全”的。因为抛出异常会导致本应在作用域末尾需要释放的对象不会自动释放,如果还要“异常安全”的代码,需要打开编译器标志-fobjc-arc-exceptions。
Objective-C语言现在采用的办法是:只有在机器罕见的情况下抛出异常,异常抛出后,无需考虑恢复问题,而且app此时也应该退出。
Objective语言对于不那么严重的错误处理的方法是返回“nil/0”,或者使用NSError以表明其中有错误发生。
NSError:用法灵活,用次对象我们可以将错误原因高速调用者。里面封装了三条信息
1. Error domain(错误范围,字符创类型):通常用一个特有的全局变量来定义。比方说处理URL的子系统(URL-handing subsystem)再从URL中解析或取得数据时出错了,那么就使用NSURLErrorDomain来表示错误范围
2. Error code(错误码 整型):独有的错误码用以指明在某个范围了发生了何种错误
3. User info(用户信息,其类型为字典):有关错误额外信息,其中包含“本地化描述(localized description)”,或许还含有导致该错误信息发生的另一个错误
我们可以通过委托协议来传递错误,也可以经由方法的“输出参数”返回给调用者
要点:
- 只有发生了可使整个程序崩溃的严重错误时,才应该使用异常
- 在错误不那么严重的情况下,可以指派“委托方法”来处理错误,也可以把错误信息存放在NSError对象里,经由“参数输出”返回给调用者
22. 理解NSCopying协议
如果某个类想执行拷贝功能,则需要该类声明遵从NSCopying协议,并实现
-(id)copyWithZone:(NSZone *)zone
方法
要点
- 若想令自己写的对象具有拷贝功能,则需要实现NSCopying协议
- 如果自定义的对象分为可变版本和不可变版本,那么就需要同时实现NSCopying和NSMutableCopying协议
- 复制对象时绝对采用浅拷贝还是深拷贝,一般情况下尽量采用浅拷贝
- 如果你写的对象需要深拷贝,那么可以新增一个专门执行深拷贝的方法
协议与分类
23. 通过委托与数据源协议进行对象中通信
Objective-C广发采用“委托模式”的编程设计模式来实现对象之间的通信,该模式的宗旨就是定义一套接口,某对象若想接受另一对象的委托,则需要遵从此接口,以便成为其“委托对象”。这“另一对象”则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。此模式可以将数据与业务逻辑解耦。
在代理相关方法调用很多次时,可以使用位段数据数据类型进行优化
要点:
- 委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。
- 将委托对象应该支持的接口定义成为协议,在协议中可能需要处理的事件定义成方法
- 当某对象需要从另外一个对象中获取数据时,可以使用委托模式。在这种情况该模式也称为“数据源协议”
- 若有必要,可现实含有位段的借口蹄,将委托对象是否能响应相关协议缓存吵起来。
24. 将类的实现代码分散到便于管理的数个分类之中
通过分类机制可以把类代码分成多个易于管理的小块,以便单独检视。
要点:
- 使用分类机制把类的实现划分成易于管理的小块
- 将应该视为“私有”的方法归入名叫Private的分类中,以隐藏实现细节
25. 总是为第三方的分类名钱加前缀
使用前缀可以大大降低自己为第三方库加分类的时候与第三方分类方法名重名的几率。
要点:
- 向第三方类中添加分类时,总是给其名称加上你的专用前缀
- 向第三方类中添加分类时,总应该给其中的方法名加上你的专用前缀
26. 勿在分类中声明属性
属性是封装数据的方式,而分类的目的在于扩张类的功能,而非封装数据。
要点
- 把封装数据所用的全部属性定义在主接口里
- 在class-continuation分类中,可以定义存储方法,但尽量不要定义属性
27. 使用“class-continuation”分类隐藏实现细节
在“class-continuation分类”中或“实现快”中可以将其隐藏起来,只供本类使用。还可以在public声明为“只读”的属性扩展为“可读写”,以便在类的内部设置其值。
- 通过“class-continuation分类”向类中新增实例变量
- 如果某属性在主接口声明为“只读”,而类的内部又要用设置方法修改此属性,那么就在"class-continuation分类中"将其扩展为可读写
- 把私有方法的原型声明在“class-continuation分类”里面
- 若想把类所遵循的协议不为人所知,则可于“class-continuation分类中声明”
28. 通过协议提供匿名对象**
- 协议可在某种程度上提供匿名类型。具体的对象类型可以从淡化遵从从某协议的id类型,协议里规定了对象所应实现的方法
- 使用匿名对象来隐藏类型名称(或类名)
- 如果具体类型不重要,重要的是对象能响应(定义在协议里的)特定方法,那么可以使用匿名对象来表示
内存管理
29. 理解引用计数
Objective-C语言使用引用计数来管理内存,每个对象都有个可以递增或者递减的计数器。如果让某个对象继续存活,那么引用计数就递增;用完之后引用计数就递减。计数为0就销毁该对象。
引用计数工作原理:在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想另此对象存活,这在?Objective-C中叫做“保留计数(引用计数)”。NSObject协议中声明了三个方法用于操作计数器,以递增或递减其值:
retain 递增引用计数
release 递减引用计数
autorelease 待稍后清理“自动释放池(autorelease pool)”时在递减其引用计数。
属性存储方法中的内存管理:
- (void)setFoo:(id)foo {
[foo retain];
[_foo release]
_foo = foo;
}
此方法是保留新值释放旧值,然后更新实例变量,另其指向新值。顺序很重要。假如还未保留新值就先把新值释放了,而且两个值又指向同一个对象,那么,先执行的release操作可能导致系统将此对象回收。后续的retain操作则无法另这个已经彻底回收的对象复生,于是实例就成了“悬挂指针”。
自动释放池:在Objective-C的引用计数架构中,自动释放池是一项很重要的特性。调用release会立刻递减引用计数而且还可能导致该对象被系统回收。然后有时候我们可以调用autorelease,此方法会在稍后递减引用计数,通常是下一回合“时间循环(evetn loop)”时递减,不过也有可能执行的更早。由此可见autorelease能延长对象生命期,使其在跨界方法调用边界后依然可以存活一段时间。
保留环(循环引用):使用引用技术机制时,经常要注意一个问题就是“循环引用”,这会导致“内存泄漏”。解决这个问题我们通常采用weak来打破循环解决此问题。
要点
- 引用计数机制通过递增、递减的计数来管理内存。若对象创建后,其引用计数为至少为1.若引用计数大于0,则该对象存活。当引用计数为0的时候,对象就需要被销毁释放了
- 在对象生命周期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减引用计数
30. 以ARC简化引用计数
使用ARC时引用计数还是在执行的,只不过保留与释放操作都是ARC为你添加。由于ARC自动执行retain、release、autorelease等操作,因此不能手动调用retain、release、autorelease、dealloc,
使用ARC时必须遵从的方法命名规则:以alloc、new、copy、mutableCopy命名的方法返回的对象归调用者持有。如果不是以以上4种方法开头所返回的对象不归调用者持有,会调用autorelease来让它跨界方法调用边界后依然有效。
_myPerson = [ECOPerson personWithName:@"Bob Smith"];
上述代码调用了personWithName:方法返回新的ECOPerson对象,而此方法在返回对象之前为其调用了autorelease方法。由于_myPerson是strong类型,所以编译器在设置其值时还需要执行一次保留操作。因此前面那段代码等效如下:
EOCPerson * tem = [ECOPerson personWithName:@"Bob Smith"];
_myPerson = [tem retain];
此时personWithName:
方法里面autorelease与上述代码中retain
都是多余的。实际上ARC在运气器检测到这一对多余操作,为了优化代码,在方法返回自动释放对象时,要执行一个特殊函数,此时不会直接调用autorelease
,而是该调用objc_autoreleaseReturnValue
函数。此函数会检测当前方法返回之后要立即执行的那段代码。若发现那段代码要在返回的对象上执行retain操作,则设置全局数据结构(此数据结构的具体内容因处理器而已)中的一个标志位,二不执行autorelease操作。与之相似入股方法返回了一个自动释放池对象,而调用方法的代码要保留此对象,那么此时不直接执行retain操作,而是改调用objc_retainAutoreleaseReturnValue
函数。此函数检测刚才那个标志位,若位置已置位,则不执行retain操作。设置并检测标志位,比调用autorelease和retain更快。
变量的内存管理语义:ARC也出处理局部变量与实例变量的内存管理。默认情况下都是指向对象的强引用。
_strong:默认语义,保留此值
_unsafe_unretained:不保留次值,这么做不安全,因为等到下次再用它时,他可能已经被系统回收了
_weak:不保留此值,但是变量可以安全使用,因为系统如果把这个对象回收了,那么会将此对象置为nil
_autorelease:把对象“按引用传递” 给方法时,使用这个特殊的修饰符,此值在方法返回时自动释放
要点
- 在ARC之后,程序员不必担心内存管理问题了。使用ARC编程可省去类中许多样板代码
- ARC管理对象声明周期方法基本是:在合适的地方插入“保留”与释放操作在ARC环境下,变量管理语义可以通过修饰符只能,二元开需要手工执行“保留”与“释放”规则
- 由方法所返回的对象,其内存管理语句是通过方法名来实现的,ARC将次必须要遵守这个规则
- ARC只负责对Objective-C对象的内存,二coreFoundation对象不归ARC管理,开发者必须实时调用CFRetain/CFRelease
31. 在dealloc方法中释放并解除监听
对象经历其生命周期后,被系统回收时需要执行dealloc方法,此方法有且只执行一次,也就是在引用计数为0的时候执行。一旦调用dealloc后对象就不再有效了。在此方法中会释放对象所拥有的引用,但是CoreFoundation对象需要手动释放,还有就是移除观测行为(observation behavior)。开销大或系统内稀缺的资源不一定在dealloc方法中释放。那些通常是在实现一方法。在调用完资源后立即调用刚刚实现的方法关闭资源。在dealloc中不要调用属性的存储方法,因为有人可能会覆写这些方法,并于其中做一些无法再回首阶段安全执行的操作。
要点:
- 在dealloc方法中,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观察(kvo)”或者NSNotificaitonCenter等通知,不要做其他事情。
- 如果对象持有文件描述符等系统资源,那么应该编写一个方法释放资源。这样的类要和其他使用者约定:用完资源必须调用关闭方法
- 执行异步任务的方法不应该在dealloc里调用,只有在正常状态下执行的那些方法也不应在dealloc中调用,因为此时对象已经处于正在回收的状态了
32. 编写“异常安全代码”时留意内存管理问题
ARC开启安全处理异常的附加代码:-fobjc-arc-exceptions
这个编译器标志。这个标志默认是关闭的,因为如果应用程序即将终止,那么发生内存泄漏已经无关紧要了,在应用即将终止的情况下还去添加安全处理异常所用的附加代码已经没有意义了。
要点:
- 捕获异常时,一定要注意将try块内所创立的对象清理干净
- 默认情况下,ARC不生成安处理异常所需的清理代码。开启编译器标志后,可以生成这种代码,不过会导致应用程序变大,而且会降低运行效率
33.以弱引用避免循环引用
避免循环引用最佳方式是弱引用,这种“非拥有关系”。
weak与unsafe_unretained的区别:weak在对象被回收后会置为nil,而unsafe_unretained则仍指向那个已经被回收的对象。显然用weak时更安全的做法
要点:
- 将某些引用设为weak,可避免循环引用
- weak可以自动清空,也可以不自动清空。自动清空(autonilling)是随着ARC而引入的新特性,由运行期系统自动实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种弱引用不会指向已经被回收过得对象
34. 以“自动释放池块”降低内存峰值
自动释放池嵌套用的好处就是可以降低内存峰值,自动释放池机制就像“栈(stack)”一样,系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相对于将其从栈中弹出。在对象上执行自动释放池,就等于将其放入栈顶的那个池中。
要点:
- 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。
- 合理利用自动释放池,可以降低应用程序的峰值
- @autoreleasepool这种新式写法能创建出更为轻便的自动释放池
35. 用“僵尸对象”调试内存管理问题
僵尸对象是调试内存问题的最佳方式,僵尸对象的原理是实现代码深值于Objective-C运行期程序库、Foundation框架、CoreFoundation框架中。系统在将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤,这一步就是把对象转换成僵尸对象,而不是彻底回收,然后再dealloc中通过swizzle生成僵尸对象的代码(NSZombieEnabled环境已打开),关键之处在于对象内存没有(通过调用free()方法)释放,因此这块代码不可复用。
要点
- 系统在回收对象时,可以不将其真的回收,二十把他转换成僵尸对象,通过环境变量NSZombieEnabled可开启此功能
- 系统会修改对象isa指针,另其指向特殊的僵尸对象类,从而是该对象变为僵尸对象。僵尸类能响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序。
36. 不要使用retainCount
- 对象的保留计数看似有用实则不然,因为任何给定时间上的“绝对保留计数”(absolute retain count)都无法反应对象生命期的全貌
- 引入ARC之后,retainCount方法就正式废弃了,在ARC调用该方法会导致编译器异常报错
网友评论