iOS代码规范

前言

核心原则:代码应该简洁易懂,逻辑清晰

规范级别

  • 【必须】:必须遵守。是不得不遵守的约定,一旦违反极有可能引起严重后果。
  • 【建议】:建议遵守。长期遵守这样的约定,有助于维护系统的稳定和提高合作效率。

参考资料:

命名规范

类命名

  • 【必须】类名(不包括类别和协议名)应该用大写开头的大驼峰命名法。类名中应该包含一个或多个名词来说明这个类(或者类的对象)是做什么的。
  • 【必须】前缀:项目公共模块用项目前缀,其他各个模块用各自模块前缀(OC必须,swift?)

类别命名

  • 【必须】类名+扩展(UIImageView +Web)
  • 【必须】类别的方法应该都使用一个前缀(型如hp_myCategoryMethodOnAString ),以防止Objective-
    C代码在单名空间里冲突。如果代码本来就不考虑共享或在不同的地址空间(address-space),方法命名规则就没必要恪守了。 //前缀在Swift不用,OC建议用

协议(委托/代理)命名

  • 【建议】与类命名相同,此外需添加“Delegate”后缀
  • 【建议】有时候protocol只是声明了一堆相关方法,并不关联class。这种不关联class的protocol使用ing形式以和class区分开来。比如NSLocking而非NSLock。
  • 【建议】如果proctocol不仅声明了一堆相关方法,还关联了某个class。这种关联class的protocol的命名取决于关联的class,然后再后面再加上protocol或delegate用于显示的声明这是一份协议。

方法命名

  • 【必须】首字母小写,之后每个单词首字母都大写
  • 【建议】方法名使用动词短语 举例:- (void)setPostValue:(int)value

Delegate方法命名规范

  • 【建议】以触发消息的对象名开头,省略类名前缀并且首字母小写。
  • 【建议】使用did或will这两个情态动词通知delegate对象某件事已经发生或将要发生。
  • 【建议】虽然我们可以在delegate方法中使用did和will来询问delegate是否可以代替另一个对象做某件事情,但是使用should看起来更加完美。

方法参数命名

  • 【必须】首字母小写,之后每个单词首字母都大写
  • 【建议】具有足够的说明性
  • 【建议】不需要添加类型前缀 举例:- (void)sendUserInfo:(NSDictionary *)userInfo

变量命名

  • 【必须】变量名使用小驼峰法, 使变量名尽量可以推测其用途属性具有描述性。别一心想着少打几个字母,让你的代码可以迅速被理解更加重要。

常量命名

  • 【必须】常量(预定义,枚举,局部常量等)使用小写k开头的驼峰法,比如kInvalidHandle ,kWritePerm
  • 【必须】枚举请使用oc风格,不要使用C语言风格

图片资源文件命名

如果设计人员提供的图片不复合规范,请开发人员让相关人员进行修改再使用。

  • 【建议】模块_类别_功能_状态.png,比如:tab_btn_search_normal.png

Notification命名规范

  • 【建议】notification的命名使用全局的NSString字符串进行标识。命名方式如下:
    [Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification
    例如:NSApplicationDidBecomeActiveNotification
  • 【建议】object通常是指发出notification的对象,如果在发送notification的同时要传递一些额外的信息,请使用userInfo,而不是object。
  • 【建议】notification名常量应该在.h头文件中暴露给外部,而字符串常量真正的赋值是在.m文件中。如下:
.h文件
extern NSString *const WSNetworkReachablityStatusDidChangedNotification;
.m文件
NSString * const WSNetworkReachablityStatusDidChangedNotification = @"WSNetworkReachablityStatusDidChangedNotification";

UI控件命名

  • 【必须】前缀还是后缀?比如:btnTitle 或 titleBtn?
    //titleBtn,控件类型写在最后
  • 【必须】通用缩写包括?(btn/lbl...)
    //长类名可以缩写,短的建议写完整

编码规范

格式规范

  • 【必须】一元运算符与变量之间没有空格,比如:!bValue
  • 【必须】二元运算符与变量之间必须有空格。比如:fHeight = fWidth + fLength;
  • 【必须】多个不同的运算符同时存在时应该使用括号来明确优先级
  • 【必须】留一个空格在 - 或 + 和返回类型之间
  • 【必须】使用Tab空格进行缩进。
  • 【建议】如果参数过多,推荐每个参数各占一行。使用多行的情况下,以参数前的冒号用于对齐。
  • 【建议】左大括号不要另起一行。前面加空格
  • 【建议】不用在冒号前添加空格。
  • 【建议】逗号后面跟一个空格
  • 【建议】左括号后面和右括号前面都不需要加空格
  • 【建议】遵循 xcode 内置缩紧格式,多用格式化工具
  • 【建议】删除多余的空行
  • 【建议】所有方法与方法之间空1行
  • 【建议】所有代码块之间空1行
  • 【建议】block参数放到最后

Init方法规范

  • 【必须】所有secondary 初始化方法都应该调用designated 初始化方法。
  • 【必须】所有子类的designated初始化方法都要调用父类的designated初始化方法。使这种调用关系沿着类的继承体系形成一条链。
  • 【必须】如果子类的designated初始化方法与超类的designated初始化方法不同,则子类应该覆写超类的designated初始化方法。(因为开发者很有可能直接调用超类的某个designated方法来初始化一个子类对象,这样也是合情合理的,但使用超类的方法初始化子类,可能会导致子类在初始化时缺失一些必要信息)。
  • 【必须】如果超类的某个初始化方法不适用于子类,则子类应该覆写这个超类的方法,并在其中抛出异常。
  • 【必须】禁止子类的designated初始化方法调用父类的secondary初始化方法。否则容易陷入方法调用死循环
  • 【必须】另外禁止在init方法中使用self.xxx的方式访问属性。如果存在继承的情况下,很有可能导致崩溃。参考:为什么不能在init和dealloc函数中使用accessor方法
    //可以尝试使用filnal关键字

dealloc规范

  • 【必须】不要忘记在dealloc方法中移除通知和KVO。
  • 【建议】dealloc 方法应该放在实现文件的最上面,并且刚好在 @synthesize 和 @dynamic 语句的后面。在任何类中,init 都应该直接放在 dealloc 方法的下面。
  • 【必须】在dealloc方法中,禁止将self作为参数传递出去,如果self被retain住,到下个runloop周期再释放,则会造成多次释放crash
  • 【必须】和init方法一样,禁止在dealloc方法中使用self.xxx的方式访问属性。如果存在继承的情况下,很有可能导致崩溃。 参考:为什么不能在init和dealloc函数中使用accessor方法

Notification规范

  • 【建议】post通知时,object通常是指发出notification的对象,如果在发送notification的同时要传递一些额外的信息,请使用userInfo,而不是object。
    //能用模型尽量用模型,提高容错率

  • 【建议】通知名使用extern方式。

IO规范

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

Collection规范

  • 【必须】不要用一个可能为nil的对象初始化集合对象,否则可能会导致crash。
  • 【必须】同理,对插入到集合对象里面的对象也要进行判空。
  • 【必须】注意在多线程环境下访问可变集合对象的问题,必要时应该加锁保护。不可变集合(比如NSArray)类默认是线程安全的,而可变集合类(比如NSMutableArray)不是线程安全的。
  • 【必须】禁止在多线程环境下直接访问可变集合对象中的元素。应该先对其进行copy,然后访问不可变集合对象内的元素。
  • 【必须】注意使用enumerateObjectsUsingBlock遍历集合对象中的对象时,关键字return的作用域。block中的return代表的是使当前的block返回,而非使当前的整个函数体返回。
  • 【必须】禁止返回mutable对象,禁止mutable对象作为入参传递。
  • 【建议】如果使用NSMutableDictionary作为缓存,建议使用NSCache代替。
  • 【建议】集合类使用泛型来指定对象的类型。
    //遍历时可以使用block方式代替for循环

分支语句规范

  • 【建议】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];
}
  • 【建议】对于条件语句的真假,因为 nil 解析为 NO,所以没有必要在条件中与它进行比较。永远不要直接和 YES 和 NO进行比较,因为 YES 被定义为 1,而 BOOL 可以多达 8 位。
// 建议
if (isAwesome)
if (![someObject boolValue])
// 禁止这样做
if ([someObject boolValue] == NO) { }
if (isAwesome == YES) { } 
  • 【必须】使用switch...case...语句的时候,不要丢掉default:。除非switch枚举。
  • 【必须】switch...case...语句的每个case都要添加break关键字,避免出现fall-through。(swift除外)
  • 【必须】条件表达式如果很长,则需要将他们提取出来赋给一个BOOL值。比如:
let nameContainsSwift = sessionName.hasPrefix("Swift")
let isCurrentYear = sessionDateCompontents.year == 2014
let isSwiftSession = nameContainsSwift && isCurrentYear
if (isSwiftSession) { 
   // Do something
}
  • 【建议】条件语句的判断应该是变量在左,常量在右. 比如:if ( count == 6)

懒加载规范

懒加载适合的场景:

  • 一个对象的创建依赖于其他对象。

  • 一个对象在整个app过程中,可能被使用,也可能不被使用。

  • 一个对象的创建需要经过大量的计算或者比较消耗性能。除以上三条之外,请不要使用懒加载。

  • 【建议】懒加载本质上就是延迟初始化某个对象,所以,懒加载仅仅是初始化一个对象,然后对这个对象的属性赋值。懒加载中不应该有其他的不必要的逻辑性的代码,如果有,请把那些逻辑性代码放到合适的地方。

  • 【必须】不要滥用懒加载,只对那些真正需要懒加载的对象采用懒加载。

多线程规范

  • 【必须】禁止使用GCD的dispatch_get_current_queue()函数获取当前线程信息。
  • 【必须】对剪贴板的读取必须要放在异步线程处理,最新Mac和iOS里的剪贴板共享功能会导致有可能需要读取大量的内容,导致读取线程被长时间阻塞。
  • 【必须】仅当必须保证顺序执行时才使用dispatch_sync,否则容易出现死锁,应避免使用,可使用dispatch_async。
  • 【必须】禁止在非主线程中进行UI元素的操作。
  • 【必须】在主线程中禁止进行同步网络资源读取,使用NSURLSession进行异步获取。当然,你可以在子线程同步获取网络资源,但还是上面的那一条建议:避免使用dispatch_sync,尽量使用dispatch_async。因为死锁不一定只发生在主线程。
  • 【必须】如果需要进行大文件或者多文件的IO操作,禁止主线程使用,必须进行异步处理。

内存管理规范

  • 【建议】请慎重使用单例,避免产生不必要的常驻内存。
    说明:我们不仅应该知道单例的特点和优势,也必须要弄明白单例适合的场景。UIApplication、access database 、request network 、access userInfo这类全局仅存在一份的对象或者需要多线程访问的对象,可以使用单例。不要仅仅为了访问方便就使用单例。
  • 【建议】单例初始化方法中尽量保证单一职责,尤其不要进行其他单例的调用。极端情况下,两个单例对象在各自的单例初始化方法中调用,会造成死锁。
  • 【必须】在dealloc方法中,禁止将self作为参数传递出去,如果self被retain住,到下个runloop周期再释放,则会造成多次释放crash。
  • 【建议】除非你清楚的知道自己在做什么。否则不建议将UIView类的对象加入到NSArray、NSDictionary、NSSet中。如有需要可以添加到NSMapTable 和 NSHashTable。因为NSArray、NSDictionary、NSSet会对加入的对象做strong引用(即使你把加入的对象进行了weak)。而NSMapTable、NSHashTable会对加入的对象做weak引用。
    说明:简单的说,NSHashTable相当于weak的NSMutableArray;NSMapTable相当于weak的NSMutableDictionary.

延迟调用规范

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

使用performSelector:withObject:afterDelay:和cancelPreviousPerformRequestsWithTarget组合的时候要小心:
afterDelay会增加引用计数,而cancel会对引用计数减一
如果receiver在引用计数器为1的时候,调用cancel会立即回收receiver。后续再次调用receiver的方法就会crash。所以我们需要使用weakSelf并判空。如下:

__weak typeof(self) weakSelf = self;
[NSObject cancelPreviousPerformRequestsWithTarget:self]; 
if (!weakSelf) {
    // NSLog(@"self dealloc");
    return;
 }
[self doOther];

长度规范

  • 【建议】一个类的代码行数尽量不要超过 500 行
  • 【必须】一个函数的长度必须限制在50行以内,建议20行以内
  • 【建议】代码中的每行文本不要超过 80 个字的长度

其他

  • 【必须】测试的数据或代码要添加警告标记,比如:#warning; 同时加上“test”标签
  • 【建议】用到classname,建议用NSStringFromClass,不要直接写字符串。
  • 【建议】多用字面量语法。比如:NSArray *arr = @[@"1",@"2",@"3"];
  • 【必须】如果方法和类没有使用到,请删除它
  • 【建议】删除未被使用的资源文件
  • 【建议】图片资源文件,强烈建议采用Images.xcassets管理。

注释规范

  • 【必须】如果方法、函数、类、属性等需要提供给外界或者他人使用,必须要加注释说明。
  • 【必须】如果你的代码以SDK的形式提供给其他人使用,那么接口的注释是必须的。必须对暴露给外界的所有方法、属性、参数加以注释说明。
  • 【建议】注释应该说明其作用以及注意事项(如果有)。
  • 【建议】因为方法或属性本身就具有自我描述性,注释应该简明扼要,说明是什么和为什么即可。
  • 【建议】方法注释的时候尽量采用 Xcode 快捷注释。如 cmd + opt + /
  • 【必须】每个类都要有类的注释说明
  • 【建议】容易产生歧义的代码需要些注释
  • 【建议】涉及到比较深层专业知识的代码(注释要体现出实现原理和思想)。

代码设计规范

【建议】面向协议编程

  • 因为协议是不依赖于某个对象的,所以通过协议,我们可以解开两个对象之间的耦合。

类的设计规范

  • 【建议】尽量减少继承,类的继承关系不要超过3层。可以考虑使用category、protocol来代替继承。
  • 【建议】.h文件中尽量不要声明成员变量。
  • 【建议】.h文件中的属性尽量声明为只读。
  • 【建议】.h文件中只暴露出一些必要的类、公开的方法、只读属性;私有类、私有方法和私有属性以及成员变量,尽量写在.m文件中。
  • 【建议】把类的实现代码分散到便于管理的多个分类中
  • 【建议】一个类的代码行数尽量不要超过 500 行

函数

  • 【必须】一个函数的长度必须限制在50行以内,建议20行以内
  • 【必须】一个函数只做一件事(单一原则)

代码组织规范

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

- (void)dealloc {}

- (void)viewDidLoad {}

- (void)viewWillAppear:(BOOL)animated {}

- (void)didReceiveMemoryWarning {}

#pragma mark - Custom Accessors
- (void)setCustomProperty:(id)value {}

- (id)customProperty {}

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

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

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

#pragma mark - UITextFieldDelegate

#pragma mark - UITableViewDataSource

#pragma mark - UITableViewDelegate

#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {}

#pragma mark - NSObject
- (NSString *)description {}

工程结构规范

  • 【必须】为了避免文件杂乱,物理文件应该保持和 Xcode 项目文件同步。Xcode 创建的任何组(group)都必须在文件系统有相应的映射。为了更清晰,代码不仅应该按照类型进行分组,也可以根据业务功能进行分组。
    //必须强制使用实体文件夹

  • 【必须】项目根目录添加README.md文件,内容包含:接口文档地址和简单说明、需求文档地址。

需要重新确定工程结构

参考方案:

1.主目录结构
-ProjectDemo
	 --README.md			//项目说明
    --Features         //模块。包含各个模块的Model,View,Controller,Manager
    --categories            //类目。包含各种类的分类
    --Frameworks        //系统框架。包含导入的系统的框架
    --Helpers           //帮助类。包含网络,数据库,归档,定位等操作类的封装和实现
    --Utilites       //工具类,一些非对象的,而是类方法调用的类
    --Vendors           //第三方库。部分需要修改或者不支持cocoapod的第三方的框架引入
    --Config                //配置。包含宏定义文件,全局配置文件,全局常量文件,颜色配置文件
    --Resources         //资源。包含plist,image,html,bundle,Localizable.strings等
    --AppEntry          //程序入口。包含AppDelegate,main.c,info.plist
-PAHealthTests
-PAHealthUITests
-Products           // 系统自动生成的.app所在文件夹
-Pods                   // 采用 CocoaPods 管理的第三方库。

2.模块目录结构
-- Features         
    ---Base             //MVC的基类或者通用类
        ----Models      //数据模型
        ----Views       //视图
        ----Controllers //控制器
        ----Manager     //store层的数据管理类
    ---Home
        ----Models
        ----Views
        ----Controllers
        ----Manager
    ---UserCenter
        ----Models
        ----Views
        ----Controllers
        ----Manager
    ---UserEntry
        ----Models
        ----Views
        ----Controllers
        ----Manager

    ---Payment
        ----Models
        ----Views
        ----Controllers
        ----Manager
    …

之前的方案:

AppDelegate        AppDelegate.h(.m) 文件,整个应用的入口文件
 
Common                公用的组件
 
Models                 与数据相关的 Model 文件
        index                 index 模块
        …                      其他模块
 
Sections             这个目录下面的文件对应的是 app 的具体单元 / 模块
        index                 index 模块
        …                      其他模块
 
ThirdParty              第三方的类库 /SDK
 
Resources           app 的资源文件
        images             图片资源,为方便管理按 Sections 生成子目录,公共的放 common 目录
			common            	公共的图片资源
			index             	index 模块
			…                 	其他模块,与 Sections 子目录一致
        sounds              声音资源
        other               其他资源       

协作规范

  • 【必须】有文件的改动时(比如:增删文件、修改文件名),先commit之前的改动,再从远程pull,再进行文件改动,改动完成后立马commit,然后pull,最后push。接下来再进行其他修改。这样能尽量避免xcodeproj文件冲突。
    //使用xib,要各自建立独立的SB、或者xib文件,避免在一个文件中修改

Swift相关规范

安全相关规范

  • 【建议】避免对 可选类型 强解包
  • 【建议】避免隐式解析的可选类型
  • 【建议】尽量不要使用 as! 强try。 try!
  • 【建议】能用 let 尽量用 let 而不是 var

编码相关规范

  • 【建议】在创建字典或者 数组的时候。推荐类型写在 冒号后面 不推荐写在 = 号后面进行创建对象,推荐:Let a: [string] = []. 不推荐:let a = string.
  • 【建议】尽早地用guard return 或者 break
  • 【建议】需要时才写上 self
  • 【建议】如果一个变量判断 nil 时,在后续需要使用的时候,采用 if let 或者guard let 形式。
  • 【建议】当遍历一个集合变形为另一个集合时,推荐使用 map,filter ,reduce。
  • 【建议】能不用下标访问数组尽量不用 如 .first .last 。在遍历数组时候采用 for item in items。 尽量不用 for i in 0…10
  • 【建议】使用类型推断。 省略显而易见的类型有助于提高可读性 (swift 进阶书里推荐的)
    //能用进阶写法完成时,尽量使用进阶写法,比如map,reduce

代码设计相关规范

  • 【建议】代使用 extention 区分理协议方法 ,并在顶部 标 // MARK:注释内容
  • 【建议】除非你的设计就是希望某个类被继承使用,否则都应该将它们标记为 final (swift 进阶书里推荐的)
  • 【建议】尽可能地对现有的类型和协议进行扩展,而不是写一些全局函数。这有助于提高可读性,让别人更容易发现你的代码 (swift 进阶书里推荐的)
  • 【建议】在使用switch 语句时候 推荐按照概率由高到低排列

热修复相关(针对需要支持热修复的代码)

  • Struct 结构体不能使用,因为无法桥接成 OC 对象。无法拥有动态属性
  • 声明 Class 需要继承 NSObject,并且对属性和方法进行动态说明,也就是需要添加相应的 @objcdynamic@objcMembers 关键字。
class TestProject: NSObject {

    @objc dynamic fileprivate var name = ""
    
    override init() {
        name = ""
        super.init()
    }
    
    @objc dynamic func testLog() {
        print("原始打印log")
    } 
}

@objcMembers
class TestProject: NSObject {

    fileprivate var name = ""
    
    override init() {
        name = ""
        super.init()
    }
    
    func testLog() {
        print("原始打印log")
    } 
}
  • Enum 枚举尽量少用,需要一些特殊处理,并且枚举中不能有其他方法。即使桥接成OC枚举,JavaScript没办法获取。
@objcpublicenumNVActivityIndicatorType: Int {
   case Blank
   case BallPulse
   case BallGridPulse
   case BallClipRotate
   case SquareSpin
}
  • Protocol 协议需要在相应的地方添加 @objc 关键字, 并且继承 NSObjectProtocol 协议。
@objc protocol TestDelegate: NSObjectProtocol {
	@objc func TestClick(Str: String)
}
  • 元组类型不能使用。

  • 需要在 JavaScript 调用或者修改的方法都必须具有动态属性,而且方法所用到的参数以及返回的对象都必须具有动态属性。

  • 调用 C 函数 函数很麻烦需要做绑定操作,所以尽量少用,而且不能保证所有的 C 函数 都能绑定调用。尤其是内联函数

  • 常量、枚举、宏、全局变量不要使用,因为 JavaScript 没办法获取。

  • 指针尽量不要使用,对于 SwiftJavaScript 语言来说,指针使用麻烦,容易出错。指针使用方法请看JPMemory使用文档

  • 方法里的代码尽量不能太多,尽量不要超过 30 行。对臃肿代码,尤其是逻辑比较重要的代码进行方法拆分。

  • 项目中对于公用工具类最好具备动态属性,而且如果是纯 Swift 写的就尽量中间封装动态中间类。