iOS开发规范(内部使用)

前言

随着公司业务的不断增加,功能的快速迭代,app的业务线越来越多,代码体积变得越来越庞大。同时,项目投入的开发者也越来越多,不同的开发者的code风格千差万别。加之公司开发者人员有可能存在变动,为了保证app稳定性,阅读性和扩展性,保证开发效率,统一开发风格。因此,急需一篇iOS开发规范。

约定

开发规范暂时划分成两个等级,分别是【必须】、【建议】。

  • 【必须】:必须遵守。是不得不遵守的规范。
  • 【建议】:建议遵守。有助于维护系统的稳定和提高合作效率的规范。

·规范一旦实施,CodeReview的时候如果不符合,一定要重新修改后,才可提交测试

本文参考了苹果官方编码指南github上一些知名的编码规范,也算是取众人之所长。主要由命名规范编码规范构成:

一、命名规范

1.1 通用命名规则

一般情况下,通用命名规则适用于变量、常量、属性、参数、方法、函数等。
【必须】自我描述性。属性/函数/参数/变量/常量/宏 的命名必须具有自我描述性。杜绝中文、拼音与英文混写、过度缩写、或者无意义的命名方式。

【必须】驼峰命名方式。参数名、成员变量、局部变量、属性名都要采用首字母小写的驼峰命名方式。特殊情况除外。

【建议】一致性。属性/函数/参数/变量/常量/宏 的命名应该具有上下文或者全局的一致性,相同类型或者具有相同作用的变量的命名方式应该相同或者类似。
说明:具体来讲,不同文件中或者不同类中具有相同功能或相似功能的属性的命名应该是相同的或者相似的。好处在于:方便后来的开发者减少代码的阅读量和提高对代码的理解速度。比如:

// stockCode同时定义不同类中的,该属性都代表同一个意思,即合约代码。
@property (readonly) NSUInteger stockCode;

【必须】清晰性。属性/函数/参数/变量/常量/宏 的命名应该保持清晰+简洁,如果鱼和熊掌不能兼得,那么清晰更重要。

命名 说明
removeObjectAtIndex: 规范的写法
removeObject: 规范的写法,因为参数指明了要移除一个对象
remove: 不清晰,移除什么?

【建议】一般情况下,不要缩写或省略单词,建议拼写出来,除非有共识的缩写(btn,bgColor,VC)。当然,在保证可读性的同时,for循环中遍历出来的对象或者某些方法的参数可以缩写。

命名 说明
customButton 规范写法
custBut 不清晰

1.2 缩写规范

通常,我们都不应该缩写命名(参考General Principles)。然而,下面所列举的都是一些众所周知的缩写,我们可以继续使用这些古老的缩写。在其他情况下,我们需要遵循下面两条缩写建议:

  • 允许使用那些在C语言时代就已经在使用的缩写,比如allocgetc
  • 我们可以在命名参数的时候使用缩写。其他情况,尽量不要使用缩写。

1.3 Method命名规范

【必须】方法名也要采用小写字母开头的驼峰命名方式。特殊情况除外。

【建议】类、协议、函数、常量、枚举等全局可见内容需要添加三个字符作为前缀。苹果保留对任意两个字符作为前缀的使用权。所以尽量不要使用两个字符作为前缀。禁止使用的前缀包括但不限于:NS,UI,CG,CF,CA,WK,MK,CI,NC

【必须】禁止在方法前面加下划线“ _ ”。Apple官网团队经常在方法前面加下划线"_"。为了避免方法覆盖,导致不可预知的意外,禁止在方法前面加下划线。

【必须】自我描述性。方法的命名也应该具有自我描述性。杜绝中文拼音、过度缩写、或者无意义的命名方式。

【必须】一致性。方法的命名也应该具有上下文或者全局的一致性,相同类型或者具有相同作用的方法的命名方式应该相同或者类似。

【必须】所有参数前面都应该添加关键字,参数之前的单词尽量能描述参数的意义。

【必须】如果当前子类创建的方法比从父类继承来的方法更加具体明确。本身提供的方法更具有针对性。则不该重写类本身提供的方法。而是应该单独的提供一个方法,并在新的方法后面添加上必要的关键参数。

// UIView提供的方法
- (instancetype)initWithFrame:(CGRect)frame
// 更具针对性的方法
- (instancetype)initWithFrame:(CGRect)frame mode:(int)aMode cellClass:(Class)factory Id numberOfRows:(int)rows numberOfColumns:(int)cols;

1.4 Delegate方法命名规范

如果delegate对象实现了另一个对象的delegate方法,那么这个对象就可以在它自己某个指定的事件发生时调用delegate对象的delegate方法。delegate方法的命名有一些与众不同的格式:
【建议】以触发消息的对象名开头,省略类名前缀并且首字母小写:

- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename;

1.5 Category命名规范

【必须】category中一般不要声明属性和成员变量。

【必须】避免category中的方法覆盖系统方法。可以使用前缀来区分系统方法和category方法。

1.6 Class命名规范

【建议、待定】class的名称应该由两部分组成,前缀+名称。即,class的名称应该包含一个前缀和一个名词。

1.7 Notification命名规范

【必须】notification的命名使用全局的NSString字符串进行标识。命名方式如下:
[相关类的名称] + [Did | Will] + [可以标识唯一的名称] + Notification
例如:

NSApplicationDidBecomeActiveNotification

NSWindowDidMiniaturizeNotification

【必须】object通常是指发出notification的对象,如果在发送notification的同时要传递一些额外的信息,请使用userInfo,而不是object。

注:苹果的初衷是通过这个object来限定哪些观察者响应通知的,意即object通常是指发出notification的对象,观察者如果指定的object的内存地址等于发送者的object,才会响应通知(可以通过字符串测试推测出,苹果的处理大概就是根据这个object内存地址去判断的)

1.8 常量命名规范

1.8.1 枚举常量

【必须】使用枚举类型来表示一组相关的整型常量。

typedef enum {
    /**新闻 1*/
    FSTabTypeNews = 1,
    /**精评 2*/
    FSTabTypeJingPing,
    /**解盘 3*/
    FSTabTypeJiePan,
    /**概述 4*/
    FSTabTypeGaiShu
} FSTabType;

1.8.2 使用const关键字创建常量

【必须】使用const关键字创建浮点型常量。你也可以使用const来创建和其他常量不相关的整型常量。否则,请使用枚举类型来创建。即,如果一个整型常量和其他常量不相关,可以使用const来创建,否则,使用枚举类型表示一组相关的整型常量,如项目中经常有的协议ID、pageID、控件固定尺寸等:

#define PROTOCAL_ID  1232
#define VIEW_WIDTH  200

更换为:

typedef NS_ENUM(NSInteger,Count){
    PROTOCAL_ID  = 1232,
    VIEW_WIDTH  = 200
};

1.8.3 字符串常量与宏定义

目前项目中埋点名称等用的都是宏定义,需要优化。
【必须】通常情况下,不要使用#define预处理命令创建常量。正如上面所说,对于整型常量,使用枚举创建;对于浮点型常量,使用const修饰符创建,字符串常量使用static修饰符创建。

【建议】通知的名字和字典的key,应该使用字符串常量来定义。使用字符串常量编译器可以进行检查,这样可以避免拼写错误

字符串常量应该在.h头文件中暴露给外部,而字符串常量真正的赋值是在.m文件中。如下:

.h文件
extern NSString *const KReachablityChangedNotification;
.m文件
NSString * const KReachablityChangedNotification= @"KReachablityChangedNotification";

1.9 宏定义的使用及其命名规范

1.9.1 宏定义的使用

定义系统控件或屏幕尺寸、判断系统版本、通知名称、通用工具类,沙盒等文件路径等。

1.9.2 宏定义的命名规范

字母全大写,单词直接用“_”连接。

1.9 图标命名规范

基本原则是把文件名分成四部分,第一部分是图片的逻辑归属分类(具体是可以体现图标位置),第二部分是图标的内容的类型(具体为控件类型等),第三部分是图片的表现内容(具体可为功能、所代表意义),第四部分是表示图片表现的状态。最好不要超过四部分,根据情况可做删减。

二、编码规范

2.1 Init方法规范

Objective-C有designated Initializers和secondary Initializers的概念。designated Initializers叫做指定初始化方法或”全能初始化方法“。designated Initializers方法是指类中为对象提供必要信息以便其能完成工作的初始化方法。

【必须】所有secondary 初始化方法都应该调用designated 初始化方法。

【必须】所有子类的designated初始化方法都要调用父类的designated初始化方法。使这种调用关系沿着类的继承体系形成一条链。

【必须】禁止子类的designated初始化方法调用父类的secondary初始化方法。否则容易陷入方法调用死循环。如下:

【必须】另外禁止在init方法中使用self.xxx的方式访问属性。如果存在继承的情况下,很有可能导致崩溃。

2.2 dealloc规范

【必须】不要忘记在dealloc方法中移除通知和KVO。

【必须】在dealloc方法中,禁止将self作为参数传递出去,如果self被retain住,到下个runloop周期再释放,则会造成多次释放crash。如下:

【必须】和init方法一样,禁止在dealloc方法中使用self.xxx的方式访问属性。如果存在继承的情况下,很有可能导致崩溃。

2.3 Block规范

【必须】block的声明copy
【必须】调用block时需要对block判空。
【必须】注意block潜在的引用循环。

2.4 delegate规范

【必须】delegate的声明weak

2.5 代码缩进规范

不要在工程里使用默认 Tab 键,使用空格来进行缩进。在 Xcode > Preferences > Text Editing 将 Tab 和自动缩进都设置为 4 个空格

2.6 空格规范

【必须】方法类型(-/ +符号)后应有一个空格;
【必须】参数:符号前后不要有空格
【必须】运算符前后留空格;
【必须】方法大括号左边留空格,不要另起一行;

2.7 UI规范

【必须】如果想要获取window,不要使用view.window获取。请使用[[UIApplication sharedApplication] keyWindow]

【必须】在使用到 UIScrollView,UITableView,UICollectionView 的 Class 中,需要在 dealloc 方法里手动的把对应的 delegate, dataSouce 置为 nil。

【建议】当访问一个 CGRectxywidthheight 时,应该使用[CGGeometry 函数],推荐的写法是这样的:

CGRect frame = self.view.frame;
CGFloat x = CGRectGetMinX(frame);
CGFloat y = CGRectGetMinY(frame);
CGFloat width = CGRectGetWidth(frame);
CGFloat height = CGRectGetHeight(frame);

不建议这样的写法:

CGRect frame = self.view.frame;

CGFloat x = frame.origin.x;
CGFloat y = frame.origin.y;
CGFloat width = frame.size.width;
CGFloat height = frame.size.height;

2.8 IO规范

【建议】尽量少用NSUserDefaults。
说明:[[NSUserDefaults standardUserDefaults] synchronize] 会block住当前线程,知道所有的内容都写进磁盘,如果内容过多,重复调用的话会严重影响性能。
【建议】一些经常被使用的文件建议做好缓存。避免重复的IO操作。建议只有在合适的时候再进行持久化操作。

2.9 数组、字典等集合类规范

【必须】不要用一个可能为nil的对象初始化集合对象,否则可能会导致crash。

【必须】同理,对插入到集合对象里面的对象也要进行判空。

【必须】注意在多线程环境下访问可变集合对象的问题,必要时应该加锁保护。不可变集合(比如NSArray)类默认是线程安全的,而可变集合类(比如NSMutableArray)不是线程安全的。
注:自选列表就出现过该问题。

【必须】禁止在多线程环境下直接访问可变集合对象中的元素。应该先对其进行copy,然后访问不可变集合对象内的元素。

// 正确写法
NSArray *array = [self.curveArray copy];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//do something using obj
}]; 

// 错误写法
[self.curveArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    //do something using obj
    // 如果在enumerate过程中,其他线程对allItems这个可变集合进行了变更操作,这里就有可能引发crash
}]; 

【必须】注意使用enumerateObjectsUsingBlock遍历集合对象中的对象时,关键字return的作用域。block中的return代表的是使当前的block返回,而非使当前的整个函数体返回。

说明:其实block相当于一个匿名函数,在block中使用return返回,仅是让当前这个匿名函数返回。

【建议】如果使用NSMutableDictionary作为缓存,建议使用NSCache代替。

【建议】具有可变副本(例如NSString、NSArray、NSDictionary)的属性声明应该选择copy而不是strong;

2.9 分支语句规范

【建议】if条件判断语句后面必须要加大括号{}。不然随着业务的发展和代码迭代,极有可能引起逻辑问题。

【必须】条件表达式多于3个必须用参数抽取成多个有意义的bool变量。

【建议】遵循gold path法则,不要把真正的逻辑写道括号内,避免多层嵌套。

// 不建议
- (void)someFuncWith:(NSString *)parameter {
    if (parameter) {
        // do something
        [self doSomething];
    }
}

// 建议
- (void)someFuncWith:(NSString *)parameter {
    if (!parameter) {
        return;
    }
    // do something
    [self doSomething];
}

【必须】使用switch...case...语句的时候,不要丢掉default:。除非switch枚举。

【必须】switch...case...语句的每个case都要添加break关键字,避免出现fall-through。

2.10 多线程规范

【必须】禁止使用GCD的dispatch_get_current_queue()函数获取当前线程信息。
【必须】对剪贴板的读取必须要放在异步线程处理,最新Mac和iOS里的剪贴板共享功能会导致有可能需要读取大量的内容,导致读取线程被长时间阻塞。

【必须】禁止在非主线程中进行UI元素的操作。

【必须】在主线程中禁止进行同步网络资源读取,使用NSURLSession进行异步获取。

【必须】如果需要进行大文件或者多文件的IO操作,禁止主线程使用,必须进行异步处理。

2.11 内存管理规范

【建议】函数体提前return时,要注意是否有对象没有被释放掉(常见于CF对象),避免造成内存泄露。

【建议】请慎重使用单例,避免产生不必要的常驻内存。

内存泄漏问题以及解决方案

(1) 控制器VC中代理的声明出错
代理的声明使用weak关键字,如果用了retain、strong强引用声明,有可能导致内存泄漏。

(2) 控制器VC中使用NSTimer出错

[NSTimer scheduledTimerWithTimeInterval:1.0 
                                 target:self 
                               selector:@selector(todo:) 
                               userInfo:nil 
                                repeats:YES];

NSTimer创建时,关键在于timer对target(self)进行了强引用,对象会进行retain操作。既然是被强引用了就应该使用__weak。并在离开页面的时候停止定时器停止并把定时器置为nil就可以解决问题。否则会导致对象不能释放,内存泄漏!
补充:
如果在非主线程的线程中只是创建一个NSTimer并启动,该NSTimer是不会执行的,除非将NSTimer加入到该线程的NSRunloop中,并启动NSRunloop才行。

(3) 控制器VC中Block使用错误
Block中直接使用成员变量(self.xxx)回造成循环引用,导致拥有该实例的对象不能释放。在ARC下要 __weak
注:Block一般用copy声明,这样会把block从栈区移到堆区。这样,在block中进行回调或反向传值到上个页面时,不会出现对象被释放,内存泄露问题。

(4) 由自定义封装的控件使用错误
在控制器VC中自定义的控件View的使用中传入了当前VC或self,造成循环引用,这种情况下pop返回时,当前页面也不会被释放,dealloc也不会走。只有在离开页面前,把该控件View先置为空nil。则可以。

(5) 避免野指针问题
僵尸对象:内存已经被回收的对象。
野指针:指向僵尸对象的指针,向野指针发送消息会导致崩溃。

(5) 检测内存泄漏、僵尸指针
XCode:scheme设置,Instruments
第三方库:MLeaksFinder+FBRetainCycleDetector定位。

2.12 延迟调用规范

【必须】performSelector:withObject:afterDelay:要在有Runloop的线程里调用,否则调用无法生效。
说明:异步线程默认是没有runloop的,除非手动创建;而主线程是系统会自动创建Runloop的。所以在异步线程调用是请先确保该线程是有Runloop的。

2.13 注释规范

  • 单行注释
    使用 // 注释单行代码,最常见的使用场景是在方法内注释某个属性或某块区域的含义
  • 多行注释
    使用 / 文本 / 的注释格式(快捷键cmd+alt+/)可以对属性和类以及方法进行注释,与//不同的是,该注释方式可以写多行,一般使用在类的头文件,多行介绍当前类的含义

【必须】如果方法、函数、类、属性等需要提供给外界或者他人使用,必须要加注释说明。
【必须】如果你的代码以SDK的形式提供给其他人使用,那么接口的注释是必须的。必须对暴露给外界的所有方法、属性、参数加以注释说明。
【建议】因为方法或属性本身就具有自我描述性,注释应该简明扼要。

2.14 类的设计规范

【建议】尽量减少继承,类的继承关系不要超过3层。可以考虑使用category、protocol来代替继承。

【建议】把一些稳定的、公共的变量或者方法抽取到父类中。子类尽量只维持父类所不具备的特性和功能。

【建议】.h文件中尽量不要声明成员变量。

【建议】.h文件中的属性尽量声明为只读。

【建议】.h文件中只暴露出一些必要的类、公开的方法、只读属性;私有类、私有方法和私有属性以及成员变量,尽量写在.m文件中。

2.15 代码组织规范

#pragma mark - Lifecycle
- (instancetype)init {}

#pragma mark - IBActions
- (IBAction)submitData:(id)sender {}

#pragma mark - Public
- (void)publicMethod {}

#pragma mark - Private
- (void)privateMethod {}

#pragma mark - ......Delegate

#pragma mark - setter&getter

2.16 工程结构规范

【必须】为了避免文件杂乱,物理文件应该保持和 Xcode 项目文件同步。

【建议】合理组织工程的内的文件夹,工程中一般包括但不限于以下几个文件夹category(分类)、util/helper(工具类)、resource(资源)、const(常量)、third(第三方)。