这篇文档包含了一系列在 iOS 代码中推荐的指导方针、编码惯例和编写代码的最佳实践,主要是为了提高代码的可读性。我们应该知道,永远是读代码的时间比写代码的时间多,我们超过80%的时间都是在维护代码。
有一点是需要注意的,编写本篇规范不是为了说服所有人下面所写的每一条都是编码最好的方式,而是为了推荐大家一系列的准则来保证我们的代码风格更加一致、可理解并且远离晦涩的取向。
一、编码的格式及惯例
空格、空行
- 缩排使用4个空格(一般 tab 就是4个空格)
- 确保 Xcode 配置中
Automatically trim trailing whitespace
和Including whitespace-only lines
已经勾选(译者注:我觉得包括空行这项值得商榷)
方法声明
我们应该延续 Xcode 中使用的方法声明模板:-
后面使用一个空格,除了参数之间不应该有其他的地方存在空格。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
}
流程控制语句(条件判断/循环等等)
括号应该同条件在同一行:
if (foobar) {
...
} else {
...
}
for (foo; foo; foo) {
...
}
单行流程控制语句依然应该使用括号(不要省略)。
命名
这里需要大家看一下 Apple's Coding Guidelines 特别是方法命名指南这一块,举例:
使用 - (NSSize)cellSize
而不是- (NSSize)getCellSize
。
指针声明
对于指针来说,应该将星号放置于变量名的前面,例如:NSString *foo
。
使用字面量
尽可能使用 Objc 字面量来定义字典、数组和 number,例如:使用@42
而不是[NSNumber numberWithInt:42]
或者:
NSDictionary *dictionary = @{
@”k1” : @”v1”,
@”k2” : @”v2”,
};
而不是
[NSDictionary dictionaryWithObjectsAndKeys:@”v1”, @”k1”, @”v2”, @”k2”, nil]
只在用@property 声明的属性上使用点语法
只在声明了的属性或者结构上使用点语法,不要这样用:foobar.release
或者foobar.count
原因是因为:点语法是围绕 getter/setter 调用的语法糖,只要用在属性上才是符合惯例的。
ivar 实例命名使用下划线 _ 前缀
ivar 实例命名应该使用下划线前缀,例如:NSString *_foobar
默认 property 生成的 ivar 就是以下划线作为前缀,我们应该保持风格统一。
声明属性时限定语要明确
对于属性的限定语是 nonatomic/atomic、strong/weak 要明确。
不要使用共有变量(public ivar)
外部调用应该无法直接修改变量,如有需要,提供 getter/setter 方法给外部调用;
私有变量放到@implementation 代码块中
例如:
@implementation
{
NSString *_foobar;
}
对于#import 引用应该进行排序
对于引用的头文件按照字符次序进行排序,并且本地文件的引用放在前面(对于 .m 文件,对于自己的头文件的引用放在第一位),全局文件的引用放在后面,例如对于 DBFoo.m:
#import "DBFoo.h"
#import "DBFoo+Protected.h"
#import "DBAnotherFile.h"
#import "DBSomeOtherFile.h"
#import <CoolFramework/Header.h>
#import <FoobarFramework/Foo.h>
使用#pragma mark 来根据功能标记一组代码
使用#pragma mark
来标记一组方法的逻辑含义,例如实现的一个特定协议的方法,或者对象的 setter/getter 方法。在一个区域的子区域中,可以使用#pragma mark -
来创建 Xcode 的导航分隔标记。例如,在一个 tableViewController 中我们可以这样使用:
#pragma mark - Lifecycle // for init and dealloc methods
#pragma mark - Appearance
#pragma mark view initializations
#pragma mark view utilities
#pragma mark - UIViewController overrides
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
为了使代码易读、易查找,方法应该按照功能进行划分,而不是根据方法的可访问范围。例如一个私有的类方法可以放在两个共有的实例方法之间。
审视 switch 代码,避免包罗万象的现象(catch-alls)
对于复杂的 switch-case 代码块,将每个 case 代码使用括号标明代码块,例如:
switch (foo) {
case 0: {
...
}
break;
...
}
如果可以的话,尽量避免使用 default 代码块来代表“其他情况”,应该使用default 代码块代表异常的情况。
为私有方法和分类方法添加前缀
例如:- (void)db_flushQueue
原因:这样做可以很容易的识别出这个方法是否是一个私有方法,并也可以尽量减少子类无意中重写父类私有方法的可能性。
如果私有方法不依赖实例状态的话,将其改写为类方法
如果一个私有方法没有使用成员变量,将其改写为类方法。
TODO/FIXME
使用姓名、时间来标记 TODO 和 FIXME:
TODO:(rich) 2013-09-13 finish the style guide!
使用TODO:(<name>) <date>
的格式来保证风格一致、便于查找。
使用 FIXME 来标记在 push 之前急需修改的内容。
对于常量使用 static const
使用 static const 来代替#define 语句:
static const CGFloat kFooBar = 123.0;
原因:这样编写可以代表常量的作用域,是类型安全的,并且这样编码常量是通过变量符号装载进内存,在调试模式下便于访问。
使用 NSUIntegers/NSIntegers 代替 int(CGFloat 代替 float)
除非是特定的 API 库指定类型为 int/float,我们应该使用 NSUIntegers/NSIntegers/CGFloat。
原因:第一,大多数 cocoa 库偏向使用 NSUIntegers/NSIntegers/CGFloat,我们在使用 cocoa 的时候使用NSUIntegers/NSIntegers/CGFloat 更加便捷;第二,int 类型占用的位数会根据系统架构不同而变化,使用 NSUIntegers/NSIntegers/CGFloat 更加可靠。
枚举声明
就像2012年 WWDC 中介绍的那样,我们应该使用NS_ENUM
来声明枚举:
typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
UITableViewCellStyleDefault,
UITableViewCellStyleValue1,
UITableViewCellStyleValue2,
UITableViewCellStyleSubtitle
};
原因:
NS_ENUM
有更好的类型校验和更好的代码补全。
位移常量
与NS_ENUM
相似,在 iOS6 以后使用NS_OPTIONS
:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
类名前面添加前缀
例如:自定义的视图控制器命名为:DBFoobarViewController
。
原因:OC 不支持命名空间。
对比使用下标,偏向使用 enumerator 进行遍历
使用enumerateObjectsUsingBlock
或者for-in
遍历代替for-index
遍历。
二、最佳实践,远离一些坑
使用 NSCAssert
进行断言
我们应该使用NSCAssert
而不是assert
或者NSAssert
来进行断言。
原因:
NSAssert
是一个宏,实际上,它内部对self
有一个引用,也就是说,如果在 block 中使用的断言的话,可能会在不注意的情况下对self
产生一个引用,我们可以在无所谓引不引用self
的地方使用NSAssert
。但是因为NSAssert
和NSCAssert
作用大致相同(都包含文件名、方法名和行数,但是NSCAssert
不打印self
的实际类型),我们可以直接简单的使用NSCAssert
。
存储 block 的时候使用 copy
当在一个方法的外部你希望引用一个 block 变量的时候(例如通过把它存储为一个 ivar),应该对其进行 copy。
原因:block 开始被创建在栈内存上,如果在超出作用域对 block 进行引用,应该将其 copy 到堆上。
对 NSString 使用 copy
原因:对于外部访问者传入的 NSString 使用 copy 是为了防止传入 NSMutableString。
永远不要将整型数据强制转换为 BOOL 类型
例如常见的将一个数组的长度赋给一个 BOOL 类型的变量。
原因:即使整型值大于0,当其二进制后8位全都是0的时候,BOOL 值依然为0。
对于私有变量不要使用@property 声明属性
在.m文件中不要使用@property
,使用 ivar 成员变量就好。
有一种例外情况:少有的需要使用atomic 属性的时候,不需遵循以上原则。
在 dealloc 方法中,清除指向对象的 delegate 或者 observer
如果一个对象被设置为另一些对象的代理或者观察者(包括通知对象),在dealloc
方法中应该将其设置为nil
。
注意:即使在ARC 环境下,delegate 属性被设置为 weak,delegate 对象的指针被设置为 nil 依然很重要,原因是因为里面有很多的情况依然是按照 assign 的规则编译,如果出现这种情况,再访问的时候就会出现崩溃。
对局部变量进行初始化
ivar 会被自动的初始化为 nil 或者0,所以你不需要显式的将其声明为 nil,但是对于局部变量则不然,你需要手动的将其初始化。
视图控制器中,在viewDidLoad
之前不要碰 view
调用self.view
会导致 view 的加载,通常在viewDidLoad
之前并不需要这么做。
相比 target-selector 代理模式更偏向使用 block 来完成回调
对于简单的回调,我们应该倾向使用 block 来代替 target-selector代理模式来完成回调。
原因:这样做能够降低代码的分离、碎片性,比如想想对于一个简单的回调,相比定义代理协议、遵循协议、实现协议,如下使用 block 会显得更加清晰:
[op startWIthCompletionHandler:^() {
// ... stuff to be done after the op finishes ...
}];
对于支持多线程操作的类、方法、变量应该添加更加详细的注释文档
通常情况下我们会假定所有的方法仅仅只会在主线程上执行,如果一个方法是预期在后台线程上执行的话(例如:某些苹果的通知,像ALAssetsLibraryChangedNotification
),请确保添加相对应的注释并且如果可能的话添加断言(例如:DBAssertMainThread
)。
同样的,我们通常会假定所有的实例变量只会从主线程访问,如果某个变量需要在线程间共享使用(或者仅仅只在一个特定的线程、队列中使用),请对变量确保添加了相应的注释。
对于类来说,如果是被设计成含有线程安全的接口,需要添加对应的注释来说明其工作的情况。
如无必要,避免使用原子属性
默认情况下,所有的属性都是原子属性的,需要你显式的将其标明为非原子属性。
原因:设置原子属性会对 setter/getter 方法加锁,这样会严重影响性能。
使用 GCD 来代替performSelectorInBackground
通常来说,永远也不要使用performSelectorInBackground
。
原因:使用 GCD 会让后台线程执行的代码与执行代码放到一起更加有利于理解和阅读。
避免头文件中不必要的引用
如果可以的话,使用前向声明来代替头文件的引用,例如:@class DBFoo
。
原因:减少编译时间和不必要的依赖。
对指定构造方法进行文档注释
对于指定的构造方法要标注清楚,只有这样,子类对构造方法进行重写的时候才知道重写哪些构造方法能够确保子类的构造方法被调用。
子类应该对父类的指定构造方法进行重写
如果方法调用方不应该再调用这个构造方法时,应该使用抛出断言进行警告。
通知中心的观察者方法应该带有NSNotification
参数
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(db_updateStuffWithNotification:)
name:@"SomeNotificationName"
object:theSender];
原因:即使并不需要参数的情况下(例如:观察者的方法不需要参数),这样做可以使代码阅读者更加明显的发现这个方法是被通知中心所调用。
Block 及循环引用
Block 中访问self
或者使用变量间接引用了self
的情况下,Block 会隐式的创建self
的强引用。
换句话说,在使用 Block 的时候可能会引起循环引用。一个通常的解决方案是:举个例子来说,比如你使用了一个BlockAlertView
在回调用的 block 里面引用了self
,并且self
又强引用了这个BlockAlertView
,这样就会引起一个self -> alert view -> block -> self
的引用循环,为了避免这种情况的发生,我们可以使用一种通常被称为strong-weak dance
的方法:
__weak DBFoo *weakSelf = self;
[_someRandomViewIOwn setObserverBlock:^{
DBFoo *strongSelf = weakSelf;
// NOTE: strongSelf may point to nil if weakSelf is already nil'ed out prior to the block being called!
[strongSelf doSomeStuff];
}];
原因:通过使用
weakSelf
,block 不再强持有self
,但是还是有可能出现在 block 调用之前weakSelf
变成nil 的情况,更糟的是,如果 block 是在多个线程中被调用的,weakSelf
还有可能是在 block 调用过程中被置为 nil。
为了避免在 block 执行过程中出现self
突然消失的情况,我们人为的给weakSelf
添加一个强引用strongSelf
,虽然strongSelf
仍然有可能为 nil,但是起码在 block 执行过程中会保持始终如一,不必再担心会随意的置为 nil。
但是,你也不应该滥用这种处理方式,在绝大多是情况下,你想使用延迟执行的 block,(例如:动画、dispatch_after
),不应该使用这种方式。