编写高质量iOS与OS X代码有效方法读书笔记(持续更新中)
1 类的头文件中尽量少引入其他头文件
- 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以降低类之间的耦合性以及解决循环引用头文件的问题。
- 父类以及protocol是不能使用向前声明的,必须要import。某个类必须要遵循某一项协议,这种情况下尽量把“该类遵循某协议”的这条声明移至"class-continuation"分类中。如果不行的话,最好将协议放在某个单独的文件中,然后再引入。
//移至"class-continuation"分类中,而不是放在.h文件中
@interface YQCircleViewController ()<UISearchBarDelegate,UISearchDisplayDelegate>{
}
2 多用字面量语法,少用与之等价的方法
使用字面量语法可以缩减源代码长度,使其更加易读,减少代码出错机率。字面量语法实际是一种 “语法糖”,也称 “糖衣语法”,是指计算机语言中与另外一套语法等效但是开发者用起来却更加方便的语法。
//字面数值
NSNumber *someNumner = @1;
NSNumber *intNumner = @1;
NSNumber *floatNumner = @2.5f;
NSNumber *doubleNumner = @3.14159;
NSNumber *charNumner = @'s';
//字面量数组
NSArray *array = @[@"a",@"b"@"c"];
NSString *string = array[0]; //直接使用下标操作数组
//字面量字典
NSDictionary *dict = @{@"key":@"value"};
NSString *string = dict[@"key"]; //直接使用键来操作字典
//可变数组与字典
NSMutableArray *mutable = [@[@"a",@"b"] mutableCopy];
然后应该通过下标操作来访问数组或字典中的键来获取字典中的元素,不过注意语法糖语法的局限性:
- 字面量所创建的对象必须属于Foundation 框架,如果自定义这些类的子类,则无法用字面量语法创建其对象;
- 创建出的对象都是不可变的,需要可变则需多加一步mutableCopy;
- 字面量语法创建数组或字典时,注意不能有nil否则抛出异常
3 多用类型常量,少用#define 预处理指令
- 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
- 在实现文件中使用static const 来定义 “只在编译单元内可见的常量“(translation-unitspecific constant)。由于此类常量不在全局符号表中,所以无须为其名称加前缀。
- 在头文件中使用extern 来声明全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类型做前缀。
示例:
image.png image.png4 用枚举表示状态、选项、状态码
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
- 在定义选项时,尤其是可以组合的选项时,可将各选项值定义为2的幂,以便通过按位操作将其组合起来。
typedef NS_OPTIONS(NSUInteger, UIInterfaceOrientationMask) {
UIInterfaceOrientationMaskPortrait = (1 << UIInterfaceOrientationPortrait),
UIInterfaceOrientationMaskLandscapeLeft = (1 << UIInterfaceOrientationLandscapeLeft),
UIInterfaceOrientationMaskLandscapeRight = (1 << UIInterfaceOrientationLandscapeRight),
UIInterfaceOrientationMaskPortraitUpsideDown = (1 << UIInterfaceOrientationPortraitUpsideDown),
UIInterfaceOrientationMaskLandscape = (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
UIInterfaceOrientationMaskAll = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortraitUpsideDown),
UIInterfaceOrientationMaskAllButUpsideDown = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
} __TVOS_PROHIBITED;
- 处理枚举类型的switch语句中不要实现default分支,这样的话,加入新枚举之后,编译器会提示开发者:编译器并未处理所有的异常
5 在对象内部尽量直接访问实例变量
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
- 在初始化方法及dealloc 方法中,总是应该直接通过实例变量来读写数据。
- 有时会使用惰性初始化技术配置某份数据,在这种情况下,需要通过属性来读取数据。
6 理解对象等同性的概念
- 想要检测对象等同性,需提供isEqual和hash方法
- 相同的对象必须有相同的hash码,有相同hash码的对象未必相同
- 应该根据特定需求制定检测等同性方案,NSObject默认的isEqual方法判断的是两个指针指向的内存地址完全相同才相等
- 编写hash方法时,应该使用计算速度快且哈希码碰撞几率低的算法
7 在既有类中使用关联对象存放自定义数据
- 可以通过"关联对象"机制来把两个对象关联起来
- 定义关联对象时可指定内存管理语义,用以模仿属性时所采用的"拥有关系"与"非拥有关系"
- 只有其他做法不可取时才应选用关联对象,因为这种做法通常会引入难以查找的bug
可以给某对象关联许多其他的对象,这些对象通过“键”来区分。比如某个类里要处理多个警告视图,那么就必须在警告视图的delegate方法里判断传入的alertView参数,然后选取相应的逻辑。那么可以将alertView按钮逻辑的方法关联到alertView上,如下例:
#import <objc/runtime.h>
static void *EOCMyAlertViewKey = @"EOCMyAlertViewKey";
- (IBAction)testAlertAssociation:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Quetion" message:@"" delegate:self cancelButtonTitle:@"cancel" otherButtonTitles:@"OK",nil];
void (^block)(NSInteger) = ^(NSInteger buttonIndex) {
NSLog(@"click index %i",buttonIndex);
};
objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
[alert show];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
block(buttonIndex);
}
8 尽量使用不可变对象
- 尽量创建不可变的对象。
- 不要把可变的集合类对象作为属性公开,而应提供相关方法,以此修改对象中的可变集合类对象的内容。
9 使用 “类扩展(class-continuation)”隐藏实现细节
- 通过 “类扩展” 向类中新增实例变量。
- 如果某属性在主接口中声明为 “只读”,而类的内部又要用设置方法修改此属性,那么就在 “类扩展” 中将其扩展为 “可读写”。
- 把私有方法的原型声明在 “类扩展” 里面。
- 若想使类所遵循的协议不为人所知,则可于 “类扩展” 中声明。
10 为私有方法名加前缀
编写类的实现代码时,经常要写一些只在内部使用的方法,编码时应为这些方法加上某些前缀,如p_有助于调试,以便很容易的把公有方法和私有方法区分开,而且还便于修改且不影响已公开的公有API接口。但不要单用一个下划线_来做私有方法的前缀,这种做法是预留给苹果公司用的
11总是为第三方类的分类名称加前缀
- 向第三方类中添加分类时,总应给其名称加上你专用的前缀
- 向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀
12 将类的实现代码分散到便于管理的数个分类中
- 使用分类机制把类的实现代码划分于易管理的小块
- 将应该归为私有的方法归入名为Private的分类中,以隐藏实现细节
13 在dealloc方法中只释放引用并解除监听
- 在dealloc方法里,应该做的事情就是释放指向其他对象的引用,取消原来订阅的KVO键值观测或者NSNotificationCenter等通知
- 如果对象持有文件描述等系统资源,那么应专门编写一个方法来释放此种资源。这样的类要和使用者约定,用完资源后必须调用close方法
- 执行异步任务的方法不应在dealloc方法里调用,只能在正常状态下调用的那些方法也不应在dealloc方法里调用,因为此时对象已经处于回收状态了
14 多用块枚举,少用for 循环
- 遍历集合类对象有四种方式。最基本的办法就是for 循环,其次是NSEnumerator 遍历法及快速遍历法,最新、最先进的方式则是 “块枚举法”。
- “块枚举法” 本身就能通过GCD 来并发执行遍历操作,无须另行编写代码。而采用其他遍历则无法轻易实现这一点。
15 构建缓存时选用 NSCache 而非 NSDictionay
- NSCache 是专门来处理缓存的,在系统资源将要耗尽时,它可以自动删减缓存。
- NScache 并不会 “拷贝” 键,而是会 “保留” 它。NScache 键很多时候都是由不支持拷贝操作对象充当的。NSCache 是线程安全的,不用编写加锁代码,多个线程便可以同时访问NSCache。
16 以自动释放池降低内存峰值
程序中有时会遇到大量的for循环,而占用的这些内存需要到下一次事件循环才会被释放掉,适当的增加自动释放池可以降低内存峰值,因为在新增的这个池末尾,会将临时变量释放掉。
NSMutableArray *array = [[NSMutableArray alloc] init];
for (NSInteger i=0; i<1000; i++) {
@autoreleasepool {
//加上自动释放池后,可以提早释放o这个临时变量
NSObject *o = [NSObject new];
[array addObject:o];
}
}
- 自动释放池排布在栈中,系统创建好自动释放池,就将其放在栈顶,清空释放池时,就相当于将其从栈中弹出。当对象收到autorelease消息后,系统将其放入最顶端的自动释放池中
- 合理使用自动释放池,可以减低内存峰值
- 采用@autoreleasepool这种新式写法能创建出更为轻便的自动释放池
17 为常用的块类型创建typedef
- 以typedef重新定义块类型,可令变量用起来更简单
typedef int(^EOCSomeBlock) (BOOL flag);
@property (nonatomic,copy) EOCSomeBlock block;
- 定义新类型应遵循现有命名习惯,勿使其名称与别的类型冲突
- 不妨为同一个块定义多个类型别名,如果要重构代码使用了块类型的某个别名,那么只需要修改typedef中的块签名即可,无需改动其他typedef。类型定义的签名相同,但用在不同的地方,开发者看到类型别名及签名中的参数之后,很容易就能理解此类型的用途。
typedef void(^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error);
typedef void(^ACAccountStoreRemoveCompletionHandler)(BOOL success, NSError *error);
typedef void(^ACAccountStoreRequestAccessCompletionHandler)(BOOL granted, NSError *error);
18 多用GCD,少用perfromSelector的方法
- performSelector系列方法在内存管理方面容易有疏失,它无法确定将要执行的选择子具体是什么,因为ARC编译器无法为其插入适当的内存管理方法
- performSelector系列方法所能处理的选择子太过局限,选择子的返回类型及发送给方法的参数个数比较受限制
- 如果想把任务放到另外一个线程执行,最好不要用perfromSelector系列方法,应将任务封装到块里,调用GCD相关的方法来实现
19 不要使用dispatch_get_current_queue
- dispatch_get_current_queue函数的行为常与开发者所预期的不同,此函数已经废弃,只应做调试之用
20 熟悉系统框架
- 许多系统框架可以直接使用,其中最重要的便有Foundation与CoreFoundation,这两个框架提供了构建应用程序所需的很多核心功能。能使用系统框架的地方,尽量使用系统框架。
21 精简intialize与load方法的实现代码
21.1 load
对于加入运行期系统中的每个类class及分类category来说,一定会调用load方法,而且仅调用一次。比如app启动时,肯定会执行一次,但是在执行该方法时,运行期系统处于“脆弱状态”(fragile state),在执行子类的load方法时,必先执行所有超类的load方法,如果代码还依赖了其他程序类库,那么程序库相关类的load方法也必定会先执行。然而给定一个程序库,是无法判断出各个类的加载顺序,因此在类的load方法里调用其他的类是不安全的
+ (void)load {
NSLog(@"Loading EOCClassB");
//在EOCClassB类的load方法里调用EOCClassA,但此时却不确定EOCClassA是否已经load完毕
EOCClassA *o = [EOCClassA new];
}
还有个重要的事情要注意,load方法不像普通方法遵循继承的规则,如果本类没有实现load方法,那么不管其各级超累是否实现load方法都不会执行。分类和其所属的类都可能实现load方法,那么类一定比分类的load方法先执行。而且load方法务必实现的精简一些,因为整个应用程序在执行load方法时都会阻塞。
实际上,凡是想通过load在类加载之前执行某些任务的,基本都不太对,笔者目前自己见过最多的便是在load方法里写方法交换逻辑。
21.2 initialize
如果想执行与类相关的初始化操作,还有个办法就是覆写initialize方法。initialize方法是当程序运用到了相关类,才会调用,且只调用一次,如果某个类一直没有用则initialize方法一直不会被执行,就是所谓的“惰性调用”。initialize方法执行时,运行期系统是处于正常状态的,从运行期系统完整度调用来说可以调用任何类的任意方法。而且运行期系统能确保initialize方法一定会在线程安全环境中执行,因此只有执行initialize方法的线程可以操作类或者实例,其他的线程全部都要先阻塞。
initialize方法遵循继承规则,如果某个类没有实现它,而其超类实现了,那么就会运行超类的实现代码。
initialize方法也要尽量保持精简,如果在initialize方法里调用了其他类,如果这个类没有被初始化,那么此时系统会迫使其初始化,但如果这个类又应用了本类(互相依赖),但本类此时还没初始化完,则会出现问题。
总结来说:
- 在加载阶段,如果类实现了load方法,那么系统会调用它。分类里也可以定义load方法,但是类的load方法会比分类的load方法先调用。与其他方法不同,load方法不参与覆写机制。子类的load方法时,必先执行所有超类的load方法,但子类如果没有实现load方法,则也不会调用父类的load方法。
- 首次使用某个类时,系统会向其发送initialize消息,由于此方法遵循覆写机制,因此最好在方法里区分当前初始化的是哪个类。
- load和initialize方法都应实现的精简一些,有助于保持应用程序的响应能力,也能减少依赖环的产生
- 无法在编译期设定的全局常量,可以放在initialize方法里初始化
static void *EOCMyAlertViewKey = @"EOCMyAlertViewKey";
//NSMutableArray在这里初始化编译器会报错
static NSMutableArray *ksomeObjects; //= [NSMutableArray new];
typedef int(^EOCSomeBlock) (BOOL flag);
@interface ViewController ()<UIAlertViewDelegate>{
}
@property (nonatomic,copy) EOCSomeBlock block;
@end
@implementation ViewController
+ (void)initialize {
//放在这里初始化
ksomeObjects = [NSMutableArray new];
}
@end
22 通过委托与数据源协议进行对象间通信
- 委托模式为对象提供了一套接口,使其可由将相关事件告知其他对象
- 将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的事件定义成方法
- 当某对象需要从另外一个对象中获取数据时,可以使用委托模式,这种情景下亦称“数据源协议”(data source protocol)
- 如有必要,可实现含有位段的结构体,将委托协议对象是否能响应相关协议方法这一信息缓存至其中。因为反复的通过respondToSeletor方法判断是否能响应某方法会很多余,基本第一次判断能响应则能响应,不能响应则不能响应,无需反复判断,因此建议将判断的值存储起来。