编写高质量iOS与OS X代码的52个有效方法
第二遍看这本书,想边看边做一些记录,记录本书的一些重要知识点(分章节)
1.熟悉Objective-C
1.1了解OC起源
运行期组件本质上就是一种与开发者所编代码相链接的动态库,其代码能把开发者编写的所有程序粘合起来,因此只需要更新运行期组件,即可提升应用程序性能。
所有oc对象都必须要通过指针的形式声明,因为对象所占的内存总是分配在堆空间中,不能在栈上分配oc对象。
有些不带*的变量,比如CGRect,因为其中保存的不是oc对象,而是一些如int、float、double、char等非对象类型,使用结构体就可以了,因为创建对象需要额外的开销(比如分配及释放堆内存)
1.2在类的头文件里尽量少引入其他头文件
.h文件中通过@class 类名,来向前声明一个类(通常出现在一个类的属性里有一个另一个类,这时候不需要知道这个类的实现细节,只需要知道有这个类就行) 在实现的.m文件中再去引入
将引入头文件的时机尽量延后,可以减少类的使用者所需引入的头文件数量,可以降低类的耦合,减少编译时间。
1.3多用字面量语法
使用@来使使代码更加简洁清晰,使用字面量语法创建出来的对象都是不可变的,要想可变可以mutableCopy一份
1.4多用类型常量,少用#define预处理指令
常量有类型,有助于编写文档,并且编译器可以检查是否进行了修改,若常量局限于文件之内,则在前面加k,若在类外可见,则以类名为前缀。
常量的位置,预处理的不适合加在头文件中,因为其它引入这份文件的可能也会出现这个名字,容易引起冲突。
类型常量也是,如果仅限于类内,就不应该放在头文件里,放在头文件里会变成一个全局变量。
在头文件中使用extern来声明全局常量,这种常量会出现在全局符号表中。
用static const来定义,只在编译单元可见(static的作用)的常量(const),不用static的话会导致多个编译单元会声明同名变量,会有错误信息。
1.5用枚举表示状态、选项、状态码
实现枚举所用的数据类型取决于编译器,其二进制位要能完全表示下枚举编号,比如1个字节的char能表示256种枚举变量。
枚举变量系统会自动从0开始编号,如果你手动将第一个编成1,那么就会从1开始。
在定义选项的时候,若某些选项可以彼此组合,可以通过按位或操作符 | 来组合,其对应的枚举定义变量可以使用1 <<0 ,1 << 1, 1 << 2这种方式来定义,就可以判断出哪个禁用哪个开启了。
在switch中判断枚举时,最好不要加default,这样的话如果以后加了新状态编译器会有提示。
2 对象、消息、运行期
2.6 理解属性这一概念
OC中把实例变量当作一种存储偏移量的特殊变量,会在运行期查找,所以甚至可以在运行期向类中新增实例变量,称为稳固的ABI,可以将某些变量从接口的public段中移走,保护其实现的内部信息。
属性特质:原子性、读写权限、内存管理语义、方法名(具体不展开)
2.7 在对象内部尽量直接访问实例变量
在写入实例变量时,通过设置方法来做,读取的时候直接访问,这样即可以提高读取操作的速度,又能控制对属性的写入操作。通过设置方法来做可以保证其“内存管理语义”能够得以贯彻。
在初始化方法中应该直接访问实例变量,否则子类可能会覆写设置方法
惰性初始化:一个属性的对象相当复杂,创建的成本很高,并且不常用,需要在获取方法中进行惰性初始化,如果没用获取方法直接访问的话,是不行的。
2.8理解对象等同性
==操作符比较的是两个指针本身,而不是其指的对象
NSObject里有一个isEqual来判断两个对象的等同性。
有些类自带了判断等同性的方法,比如NSString,有一个isEqualToString,这种方法会比单纯的isEqual快因为后者要判断类型等等。
两个对象的hash值一样不一定相等,如果是同一个类的不同对象,则hashcode相同一定相同。和hash函数有关系
2.9以类族模式隐藏实现细节
比如UIButton,创建按钮的时候,要调用一个类方法,然后这个类方法里根据不同的按钮类型进行绘制,绘制方法放在子类里,一个基类,然后很多子类,是一种工厂模式。但是oc不能指定某个基类是抽象的,一般基类接口会没有init方法。
2.10在既有类中使用关联对象存放自定义数据
可以通过关联对象机制来把两个对象连起来,定义关联对象时可指定内存管理语义,只有在其他方法不行的时候才考虑关联对象,因为这种做法容易引入bug
2.11理解 objc_msgSend
C语言是静态绑定,在编译期就能决定运行时所应调用的函数。
接收者 选择子 参数 选择子和参数合起来称为消息。
objc_msgSend会将匹配结果缓存到快速映射表里(fast map),每个类都有一块这样的缓存
这只是部分消息的调用,还有些 objc_msgSend_stret _fpret Super等 针对不同的消息(返回结构体 返回浮点数 给超类发消息)
每个类里都有一张表格,其中的指针都会指向函数,选择子的名称是查表所用的键
2.12 理解消息转发机制
消息转发两大阶段:第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个未知的选择子,这叫做动态方法解析,第二阶段涉及完整的消息转发机制。
动态方法解析:返回bool值,表示这个类能否新增一个实例方法用以处理此选择子。包括resolveInstanceMethod和resolveClassMethod
备援接收者:能不能把这条消息转发给其它接收者处理 forwardingTargetForSelector
完整的消息转发:创建NSInvocation对象,把尚未处理的消息的全部细节装进去,包含选择子,目标,参数 forwordInvocation
2.13 用方法调配技术调试黑盒方法
方法调配 黑魔法 (method swizzling) 交换方法实现
通常用来为黑盒方法添加日志记录功能,利于程序调试。只需要新编写一个方法,然后在此方法中实现所需的附加功能,并调用原有实现。
2.14 理解类对象的用意
每个oc对象实例都是指向某块内存数据的指针,每个对象结构体的首个成员是class类的变量,定义了对象所属的类,isa指针
Class本身也是oc对象,其中存放类的元数据,例如类的实例实现了几个方法,具备多少个实例变量等信息。
类对象的isa指向另一个类叫元类,其中存放着类方法
isMemberOfClass能够判断出对象是否为某个特定类的实例,isKindOfClass能够判断对象是否为某类或者其派生类的实例
类对象是单例,每个类的Class只有一个实例。
对于代理对象,调用class的时候,会返回这个代理对象本身(NSProxy的子类),如果调用isKindOfClass的查询方法,那么这条消息会被转发给接受代理的对象,两者结果不同
3.接口与API设计
3.15 用前缀避免命名空间冲突
3.16 提供全能初始化方法
有多个不同初始化方法的,将其中一个设为全能初始化方法,其它方法都调用他,这样以后要更改会比较方便
3.17 实现description方法
“%@”,object 该对象会收到description消息。自定义类实现这个方法返回其描述
还有个debugdescription,用来在调试的时候输出更详尽的信息
3.18 尽量使用不可变对象
如果要内部修改,可以在分类中扩展为readwrite属性,可变的collection也不应该作为属性公开,而应该提供相应的方法,来修改,通常提供一个readonly属性供外界使用,该属性返回一个不可变的内部可变set的拷贝。
3.19 使用清晰而协调的命名方式
方法与变量名:驼峰式大小写 首字母小写
类名:首字母大写
继承子类时注意要和父类协调
3.20 为私有方法名加前缀
比如使用p_XXX命名私有方法,用于区分
3.21 理解oc错误模型
ARC默认不是异常安全的,抛出异常那么不会自动释放,可以通过设置编译器标志来实现,-fobjc-arc-exception
现在使用的方法是,只在极其罕见的情况下才抛出异常,并且抛出异常程序会直接退出,也就无须考虑恢复问题,就不需要额外的异常安全代码了。
对于其他错误,oc会让方法返回nil/0或是使用NSError,表明其中有错误发生。
NSError中封装了三条信息:
domain 错误范围 XXXErrorDomain
code 错误码 自己定义的话最好定义为枚举类型
User info 用户信息 错误链
常见方法是通过委托协议来传递错误,错误发生时,当前对象会把经由协议中的某个方法传给其委托对象。
也可以使用输出参数的方法把NSError对象回传给调用者
3.22 理解NSCopying协议
该协议只有一个方法,copyWithZone,因为以前内存是分成不同的区的,对象会在某个区里,现在只有一个区,默认区
深浅拷贝区别
4.协议与分类
4.23 通过委托与数据源协议进行对象间通信
有些是必须实现的,有些是可选的@optional 可选方法 内部通过respondsToSelector来判断委托对象是否实现了相关的方法,如果频繁检测的话,只有第一次的结果是有用的,因此可以把这个结果缓存起来,优化效率。
缓存的方式:使用了带有位段的结构体,用于存放能否响应的信息
4.24 将类的实现代码分散到便于管理的数个分类之中
便于调试,在调试器的回溯信息中很容易能够定位方法所在的功能区。
把私有方法放入private分类中,隐藏其实现细节。
4.25 总是为第三方类的分类名称加前缀
多个分类会覆盖,以最后一个为准,所以通过给分类加前缀的方式,减少冲突(给第三方类添加分类的时候)
4.26 勿在分类中声明属性
分类中的属性是不能自动生成存取方法和实例变量,虽然可以通过关联对象的方式在分类中合成实例变量。但是这样在内存管理上容易出错(实现存取方法的时候没有遵循内存管理语义)。
属性只是定义实例变量及存取方法所用的语法糖。
4.27 使用“class-continuation分类”隐藏实现细节
class-continuation是唯一能声明实例变量的分类,其分类没有名字,没有特定的实现文件,其中的方法都应该定义在类的主实现文件中。
放在其中可以隐藏起来只供本类使用。
.mm文件 通过objc++编译,在其头文件里可以引入c++的头文件,通过class-continuation的方式,可以将其放入分类中,使得其头文件里没有C++,这样就可以使引入的人不再需要用objc++编译,一些系统库都是这么做的,比如WebKit,CoreAnimation,其中有很多后端代码都是C++编写的,但对外公布的是一套纯objc接口。
class-continuation还可以用来将public中声明为只读的属性扩展为可读写(重新声明一遍),这样既能令外界无法修改对象,又能在内部按照需要修改。
还可以在该分类中声明遵循的私有协议
4.28 通过协议提供匿名对象
匿名类 id 用于协议声明受委托者的属性时,那么任何类都能充当这一属性。
数据库等中用于隐藏类名。
有些时候对象类型不重要,也可以用id,表名其类型不重要
5 内存管理
5.29 理解引用计数
autorelease:待稍后清理自动释放池时再递减其引用计数(通常是在下一次事件循环时递减)
MRC中,在解除分配之后,只是放回可用内存池,如果释放之后继续对其进行访问,而此时该内存没有被覆写,那么还是有效的。
所以一般release之后都会清空指针
在一些方法返回的地方,直接release那么返回不了,用autorelease,可以让对象稍后释放,延长对象生命期,能够传回调用者
5.30 以ARC简化引用计数
ARC会自动调用保留与释放,还会简化能够互相抵消的retain、release、autorelease
ARC只负责管理oc对象的内存,CF对象要开发者自己管理
5.31 在dealloc方法中只释放引用并解除监听
不要做其它的事情,因为此时对象正在回收的状态
5.32 编写异常安全代码时留意内存管理问题
try catch中容易忘记释放内存 可以加入finally然后释放 (MRC)
ARC中比较复杂,因为不能调用release,所以需要开启一个编译器的标志,默认关闭,但会降低运行效率。在objc++时的时候默认开启,因为C++频繁使用异常
5.33 以弱引用避免保留环
MAC OS X的objc程序可以选择启用垃圾收集器,能够检测保留环,但从10.8开始就被废弃了
使用weak使用
5.34 以自动释放池块降低内存峰值
@autoreleasepool:最外围捕捉
可以用来控制内存峰值
5.35 用僵尸对象调试内存管理问题
NSZombieEnabled环境变量设为YES,之后所有已经回收的实例就会转化为特殊的僵尸对象,而不会真正回收,僵尸对象收到消息之后就会抛出异常,其中准确说明了发送过来的消息和回收之前的对象,是调试内存管理问题的最佳方式。
系统在即将回收对象时会检测这个环境变量,看是否转化为僵尸对象。(isa指针指向对应该类的僵尸类)
NSZombie类和NSobject一样都是以一个根类
5.36 不要使用retainCount
只是某个时间点上的值,并为考虑自动释放池稍后清空的影响。单例对象的保留计数都会很大,系统会尽量把NSString实现成单例对象。
引入ARC之后retainCount彻底废止,使用会报错
6 块与大中枢派发
6.37 理解块这一概念
在它声明的范围内,所有变量都可以为其捕获,声明变量的时候可以加上__block,那么值可以在块内修改。
块也是对象,也有引用计数
块内部结构 isa,flag,reserved,invoke,descriptor,还有捕获到的变量 invoke指向实现代码,descriptor指向结构体,其中声明了块对象的总体大小,辅助指针
6.38 为常用的块类型创建typedef
typedef int(^EOCBlock)(BOOL flag,int value);
※6.39 用handler块降低代码分散程度
异步方法在执行完任务之后,需要以某种手段通知相关代码,一种是通过委托协议的方式,另一种是使用块,把completion handler定义为块,可以把业务逻辑和对象放在一起
6.40 用块引用其所属对象时不要出现保留环
6.41 多用派发队列,少用同步锁
用GCD会更加简单,将同步与异步派发结合起来可以实现与普通加锁机制一样的同步行为,又不会阻塞执行异步派发的线程。
使用同步队列加栅栏块,可以使同步行为更加高效。
※6.42 多用GCD,少用performSelector方法
performSelector在编译期不知道要执行的选择子是什么的情况下,ARC下编译代码会不添加释放操作,可能会引起内存泄露。
如果想把任务放到另一个线程上执行,最好用GCD而不用performSelector(可以选择执行的线程或者延迟执行 这些GCD都可以实现)
6.43 掌握GCD及操作队列的使用时机
操作队列即NSOperationQueue,能实现一些更加复杂的操作
6.44 通过Dispatch Group机制,根据系统资源状况来执行任务
6.45 使用Dispatch Once来执行只需运行一次的线程安全代码
6.46 不要使用dispatch_get_currnt_queue
ios 6之后已经启用了 容易死锁
7 系统框架
7.47 熟悉系统框架
Cocoa,ios上Cocoa Touch,本身并不是框架,但是里面集成了一批创建应用程序时经常会用到的框架。
Foundation框架中的类使用NS前缀,还有CoreFoundation,CFNetwork,CoreData.....
7.48 多用块枚举,少用for循环
for循环
OC1.0 NSEnumerator 遍历
OC2.0 快速遍历
最新的块遍历 enumerateObjectUsingBlock,既能获取对象又能知道下标,还能终止遍历
7.49 对自定义其内存管理语义的collection使用无缝桥接
__bridge transfer和retain 用来在Foundation中的oc对象与CoreFoundation中的C语言数据结构中来回转换
7.50 构建缓存时选用NSCache而非NSDictionary
NSCache在资源将要耗尽时会自动删减缓存
NSCache不会拷贝键,而很多时候键都是由不能拷贝的对象充当的
NSCache是线程安全的
NSPurgeableData与NSCache搭配使用,如果某NSPurgeableData对象为系统丢弃时,也会自动从缓存中移除
7.51 精简initialize与load的实现代码
load:只会调用一次,问题在于,执行该方法时,运行期系统处于脆弱状态,在load中使用其他类是不安全的,因为不知道那个类是否已经加载好了
而且某个类的load如果没实现,不管其超类有没有实现,都不会调用,如果分类里也出现load,那么两个都会调用,类的先调用分类后调用
initialize:惰性调用,用到了这个类才调用,为什么要精简?
initialize的时候一定要线程安全,因此其它线程会阻塞,所以其要精简,减少阻塞;开发者无法控制类的初始化时机;如果用到了其它类,就会迫使其初始化,而那个类可能用到了本类的数据,而本类此时没有初始化完毕。
7.52 别忘了NSTimer会保留其目标对象
使用invalidate让计时器失效来防止保留环,可以通过块来解决(定义一个弱引用然后块去捕获他)