Effective Objective-C 2.0笔记(接口设计/协议/框架)
Effective Objective-C 2.0笔记(runtime/内存/多线程)
第一章 熟悉Objective-C
之前看了《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》这本五星好书,受益颇多,在一定程度上提高了编写的代码的质量。现在就对这52个有效方法做简单笔记。
第1条:了解OC语言的起源
OC为C语言添加了面向对象特性,是其超集。OC与其他面向对象的语言在很多方面都有差别。OC语言由Smalltalk(消息型语言鼻祖)演化而来。OC语言使用动态绑定的“消息结构”而非“函数调用”,也就是说在运行时才会检查对象类型,接收一条消息后,究竟执行何种代码,由运行时环境而非编译器来决定。
第2条:在类的头文件中尽量少引入其他头文件
在类的头文件中引入其他头文件可能会有如下问题:
- A类头文件引入了B类的头文件,B类头文件又引入了A类头文件,这两个类就相互引用了,编译不过。
- A类头文件引入了C类的头文件,当B类引入A类头文件时,不需要的C类也被引入了B类。而另一个类又引入B类时,没有用到的A类C类也被引入了。如此持续下去,则要引入许多根本用不到的内容,会增加编译时间。
取而代之,我们应尽量使用向前声明@class声明,而在.m文件中引入头文件。@class让编译器知道有这样一个类,而不需要知道类的全部细节。这样将引入头文件的时机尽量的延后,只在需要的时候才引入。
但是有时候必须要在头文件中引入其他头文件,有如下情况:
- 继承至父类的子类,必须在头文件中引入父类的头文件。
- 类遵循某个协议时,该协议必须有完整的定义,编译器要知道该协议的方法,而不能使用@class。(其实也可以在.m文件中的扩展中遵循协议,那么就不用在头文件中去引入协议头文件了)
总的说来,每次在头文件中引入其他头文件之前,都要先问问自己这样做是否有必要。除非确有必要,否则不要引入,而应该使用向前声明@class。
第3条:多用字面量语法,少用与之等价的方法
Foundation框架中NSString,NSNumber,NSArray,NSDictionary都有对应的字面量语法:@"",@1,@[],@{}。字面量语法特点:
- 语法精简,缩减代码长度更为易读。
- 字面量语法有利于数组,字典的操作:可以通过下标的形式进行操作,简明扼要。
- 使用字面量创建数组/字典时,若集合中元素对象有nil,则会抛出异常,而非字面量创建时会到nil对象为止。这种差别表明,字面量语法更加安全,抛出异常总比创建的集合元素莫名的少了要好。
- 字面量创建的对象是不可变的,若想要可变的则需复制一份,类似@[].mutableCopy。
第4条:多用类型常量,少用#define预处理指令
- 预处理指令定义的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。在我们用错类型时,编译时并不能及时发现错误。如果不小心重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序的常量值不一致。
- 类型常量包含类型消息,清楚地描述了常量的含义,可以让代码更易被理解。使用时更好了利用编译器的某些特性。
- 常量命名规则:若常量局限于实现文件之内,则在前面加字母k;若常量在类之外可见,则通常以类名为前缀。
- 常量一般使用static const声明,const声明的变量可以防止被修改,如果试图修改会得到编译错误。而static修饰的变量意味着该变量仅在定义此变量的编译单元(及实现文件)中可见。
- 有时需要公开某个常量,以便可以在定义该变量的编译单元之外使用。此类常量需要放在“全局符号表”中,这就需要在头文件用extern关键字声明,而在实现文件中定义(不能使用static)。使用extern关键字,编译器无须查看其定义,即允许代码使用此常量。因为它知道,当链接成二进制文件后,肯定能找到这个常量。
- 预处理指令定义的字符串,编译器可以对相同的字符串进行优化,只保存一份到 .rodata 段。甚至有相同后缀的字符串也可以优化,"Hello world" 与 "world" 两个字符串,只存储前面一个。取的时候只需要给前面和中间的地址,如果是整形、浮点型会有多份拷贝,但这些数写在指令中。占的只是代码段而已,大量用预处理指令会导致二进制文件变大。
第5条:用枚举表示状态 选项 状态码
对于状态 选项等,使用枚举写出来的代码更易读懂。编译器为枚举分配一个独有的编号,从0开始,每个枚举递增1。也可以自己设值,不使用编译器分配的。设置的枚举值可以用二进制表示,这样通过“按位或操作符”可组合多个选项。
也可以使用typedef关键字重新定义枚举类型,使定义枚举变量的方式更简洁。Foundation框架中定义了一些辅助的宏,可以很方便的定义枚举类型,也可以指定用于保存枚举值的底层数据类型。相关的宏有两种:
#define NS_ENUM(...) CF_ENUM(__VA_ARGS__)
#define NS_OPTIONS(_type, _name) CF_OPTIONS(_type, _name)
两种宏的区别:作为选项的枚举值经常使用按位或运算组合,在运算两个枚举值时,C++认为运算的结果应该是枚举的底层数据类型也就是NSUInteger。C++不允许将底层类型“隐式转换”为枚举类型本身。若编译器按C++模式编译,而按位运算的类型与枚举底层数据类型不一致,这时使用NS_ENUM会报错。而NS_OPTIONS很好的兼容了这个问题,若非C++模式编译其和NS_ENUM一样,若是C++模式编译,NS_OPTIONS会将按位操作的结果显式转换,为我们省去类型转换的操作。鉴于此,凡是需要按位运算组合的枚举都应使用NS_OPTIONS,若不需要组合,则应使用NS_ENUM。
typedef NS_OPTIONS(NSUInteger, UIControlState) {
UIControlStateNormal = 0,
UIControlStateHighlighted = 1 << 0, // used when UIControl isHighlighted is set
UIControlStateDisabled = 1 << 1,
UIControlStateSelected = 1 << 2, // flag usable by app (see below)
UIControlStateFocused NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 3, // Applicable only when the screen supports focus
UIControlStateApplication = 0x00FF0000, // additional flags available for application use
UIControlStateReserved = 0xFF000000 // flags reserved for internal framework use
};
还有就是,在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举后,编译器会提示我们有未处理的枚举,以免遗漏。
第三章 接口与API设计
第15条:用前缀避免命名冲突
- OC没有命名空间,为了避免潜在的命名冲突,可以使用加前缀的方式
- 选择公司,应用程序或有关联的名称作为类名的前缀,并在所有代码中使用这一前缀
第16条:提供“全能初始化方法”
- 全能初始化方法:为对象提供必要信息以便其能完成工作的初始化方法,类似UITableViewCell初始化方法:
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier
这里必要信息是:style,reuseIdentifier样式及重用标识
- 有多个初始化方法时,可以在其中选定一个作为全能初始化方法,令其他初始化方法都调用他。这样,这有全能初始化方法存储内部数据,需要改变内部结构时,只需要改全能初始化方法而不用改动其他初始化方法
- 如果超类的初始化方法不适用于子类,那么应该覆写这个超类的方法,并在其中抛出异常
第17条:实现description方法
通过NSLog打印并查看对象信息时,对象会收到description消息,返回对象相关的信息。但默认的description方法返回的信息,有时并不是我们想要的,通过覆写description可以实现输出我们在定义的信息。类似的,还有debugDescription方法,它和description区别:debugDescription方法是开发者在调试器以控制台命令打印对象时调用的(LLDB "po"命令)。当我们通过LLDB "po"命令打印对象信息时,就可以覆写debugDescription返回我们需要的信息。
第18条:尽量使用不可变对象
- 使用属性时,默认情况下属性是“可读可写”的,这样设计出来的类都是“可变的”。
- 为了防止对象被更改,应该尽量把对外公布的属性设为只读,而只在必要的时候才对外公布。
- 若某属性仅可于对象内部修改,可以在分类中将readonly属性扩展为readwrite属性。
- 不要把可变的集合(collection)作为属性公开,而应提供方法,通过方法修改对象的可变collection。
第19条:使用清晰而协调的命名方式
- 方法命名:方法名使用“驼峰大小写命名法”;使用“in”,“for”,“with”等介词连接,使得代码读起来和句子一样;方法名要明确每个参数等含义,把表示参数类型的名词放在参数前面;方法有返回值时,方法名的首个词最好是返回值的类型;布尔属性应该根据其功能,选用类似has, is当前缀;不要使用类似str简称,应该使用全称;方法名也不能长得太过分,应尽量在用意表达清楚的基础上做到言简意赅;
- 类和协议的命名:类和协议的名称要加上前缀,避免命名冲突;命名应该把词汇组织好,从左至右读起来通顺;定义委托协议时,把委托接口的类名放在前面,后面加Delegate一词。
第20条:为私有方法名加前缀
为私有方法名加前缀,很容易区分公共方法和私有方法,有助于调试。使用何种前缀,由个人喜好决定,一般用p_作为前缀,尽量不要单独使用_作为前缀只是预留给苹果爸爸的。
第21条:理解错误模型
- 用异常机制处理错误
- 通过@throw抛出异常(NSException)
- 通过@try @catch捕获并处理异常
但这种机制,如果抛出异常,那么本应在作用域末尾释放的对象不会自动释放,也就是说默认情况下不是“异常安全的”;所以,异常只应该应用于极其严重的错误,异常抛出后无须考虑恢复问题,而且应用程序此时应该退出。
- NSError
- 处理不是很严重的错误,表明有错误发生,程序不用退出。
- 用法灵活,对象里封装了3条消息:Error domain错误范围;Error code错误码;Userinfo用户信息;
- NSError一个常见用法是通过委托协议传递此错误。
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
- 另一个用法是经由方法的输出参数返回调用者,传递的参数是个指针,这个指针又指向另一个指针,另一个指针指向NSError对象。
- (BOOL)createDirectoryAtPath:(NSString *)path withIntermediateDirectories:(BOOL)createIntermediates attributes:(nullable NSDictionary<NSFileAttributeKey, id> *)attributes error:(NSError **)error
第22条:理解NSCopying协议
- 若想令自己所写的对象具有拷贝功能(copy),需要实现NSCoping协议。通过实现- (id)copyWithZone:(NSZone *)zone方法返回新的对象
- 如果自定义的类分为可变和不可变版本,要同时实现NSCopying和NSMutableCopying协议
第四章 协议与分类
第23条:通过委托与数据源协议进行对象间通信
- 委托模式(代理模式)是一种实现对象间通信的设计模式,可将数据与业务逻辑解耦;主旨是定义一套接口,某一对象想接受另一对象的委托,需遵循此协议,成为其代理;另一对象可以给委托对象回传消息;
- 委托协议名通常是相关类名后加Delegate,方便理解;
- 委托协议的方法默认是@require的,使用@optional可以指明可选方法;委托对象调用可选方法,必须提前使用类型信息查询方法判断是否响应选择器:
respondsToSelector:
- 定义委托协议方法时,应该提供一个委托发起对象的实例作为参数,方便根据该实例分别执行不同的代码;
第24条:将类的实现代码分散到便于管理的数个分类之中
- 一个类的方法过多时会变得难于管理,可以使用分类把类的实现代码划分成便于管理的小块;
- 将不想公开的方法归入单独一个分类中,隐藏实现细节;
第25条:总是为第三方类的分类名称加前缀
第26条:勿在分类中声明属性
- 除了Extension外,其他分类都无法向类中新增实例变量,因此无法把属性所需的实例变量合成出来;
- 可以通过关联对象技术解决分类中不能合成实例变量的问题,实现为分类添加属性,这样做在内存管理问题上容易出错,应该尽量避免
- 有时只读属性还是可以在分类中使用,实现属性getter方法,就不会为该属性合成实例变量了;(类似于swift中计算属性的使用)
第27条:使用class-continuation
分类隐藏实现细节
class-continuation分类即Extension
- 可以通过Extension为类添加属性,Extension定义在原类的实现文件中
- 可以将“私有”的属性声明在Extension里面,这样外界既不能访问该属性也完全不知道这个属性,从而隐藏相关细节
- 类所遵循协议也可以在Extension中声明,从而使协议也不为外界所知
- 如果属性在.h文件声明为readonly,而类的内部又要改动该属性,可以在Extension中将属性声明为readwrite
第28条:通过协议提供匿名对象
匿名对象:协议可以提供匿名类型:遵从该协议的纯id类型,如委托对象delegate
- 需要隐藏类型名称,可以使用匿名对象;
- 如果具体类型不重要,只要对象能响应协议的方法,也可以使用匿名对象;
第七章 系统框架
第47条:熟悉系统框架
- 常用系统库:
CFNetwork
,CoreAudio
,AVFoundation
,CoreData
,CoreText
,CoreFoundation
,Foundation
,UIKit
- OC编程经常需要使用底层的C语言级API;C语言API,可以绕过OC的运行期系统,可以提升执行速度;想要成为优秀的OC开发者,应该掌握C语言的核心概念;
第48条:多用块枚举,少用for循环
- 遍历集合有4种方式:最基本的for循环,其次是NSEnumerator遍历及for in快速遍历法,最新,最先进的是
块枚举法
[@[@"1",@"2"] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
- 块枚举遍历,遍历时可以直接从块里获取更多信息;
- 块枚举法本身能通过GCD来并发执行遍历操作,无须另行编写代码;
- 块枚举法能修改块的方法签名,以免进行类型转换操作;如上面代码可以改写为如下:
// 明确数组是字符串数组,可以直接修改块参数类型;
[@[@"1",@"2"] enumerateObjectsUsingBlock:^(NSString *str, NSUInteger idx, BOOL * _Nonnull stop) {
}];
第49条:对自定义其内存管理语义对collection使用无缝桥接
- 通过无法桥接技术,实现Foundation框架OC对象与CoreFoundation框架的C语言数据结构之间来回转换;
- Foundation框架OC对象与CoreFoundation框架的C语言内存管理不一样,需要使用桥接技术处理:
- 使用
__bridge
将Foundation对象转换为CoreFoundation对象,ARC仍然具备这个OC对象的所有权,不用额外处理; - 使用
__bridge_retained
将Foundation对象转换为CoreFoundation对象,ARC交出对象的所有权,使用完对象后需使用CFRelease()函数手动释放对象; - 使用
__bridge_transfer
将CoreFoundation对象转换为Foundation对象,ARC获得该对象所有权,不用手动释放;
第50条:构建缓存时选用NSCache而非NSDictionary
- NSCache类似NSDictionary,但更好:
- 当系统资源将要耗尽时,它可以自动删减缓存;
- NSCache时线程安全的:不需要加锁代码,多个线程可以同时访问NSCache;
- 可以给NSCache对象设置上限,限制缓存的对象个数及大小;
第51条:精简initialize与load的实现代码
有这样的场景:类必须执行某些初始化操作,然后才能正常使用;initialize与load方法可用来实现这种操作;initialize与load方法由系统调用,绝不应该通过代码手动调用;
-
+ (void)load
:
- 加入运行期系统的每个类及分类(就算没有使用这个类),必定会调用这个方法,而且仅会调用一次;
- 在子类的load方法调用前,必定会调用所有超类的load方法;
- 在load方法里使用其他类是不安全的,因为其他类可能还没执行load方法即还没加载好;
- 如果某个类本身没有实现load方法,不管各级超类是否实现了load方法,系统都不会调用;
-
+ (void) initialize
:
- 该方法在程序首次用该类之前调用,且也是只能调用一次;
- initialize方法是首次使用类时才调用,可以安全使用并调用其他类;
- initialize是在线程安全的环境中执行的,其他线程都会先阻塞等着initialize执行完;
- 和大部分方法一样:如果某个类本身没有实现该方法,如果各级超类是实现了该方法,那么就会调用超类的方法;
- 使用场景:
- 一般在分类中的load方法执行method swizzle;
- 若某个全局变量无法在编译期初始化,则可以放在initialize方法里做;
- initialize与load都应该实现的精简一些,尽量不用使用这两个方法;
第52条:别忘了NSTimer会保留其目标对象
NSTimer会保留其目标对象容易形成循环引用;
之前也总结过:NSTimer/CADisplayLink那些事