【前言】
一份合格的代码不应只满足于实现功能, 更应该遵循良好的规范. 遵循良好的代码规范有利于:
- 统一标准, 提升多人协作效率;
- 方便新人快速上手, 在项目组人员发生变动时保证项目进度;
- 提升程序稳定性, 减少代码隐患, 降低故障率;
- 增强可扩展性, 大幅提高维护效率;
这也是我整理这份文档的初衷,技术有限,抛砖引玉希望大家多多指导。
【目录】
- 命名规范
- 注释规范
- UI/布局规范
- 编码规范
- 资源文件规范
- 版本规范
- 其他规范
- 系统设计
- 开发流程
- 持续集成
- 调试技巧
【命名规范】
不允许出现中文命名方式+采用驼峰命名法。
项目命名
- 项目名都遵循大驼峰命名。例如:
AliTaoBaoProject
Bundle Identifier 命名
- 采用反域名命名规范,全部采用小写字母,以域名后缀+公司顶级域名+应用名形式命名。例如:
com.ali.taobao
Class类名
- 类的命名都遵循大驼峰命名。一般是:
前缀 + 功能 + 类型
。例如:WN + Login + ViewController
,在实际开发中,一般都会给工程中所有的类加上属于本工程的前缀。 - 常用控件类命名类型对照表(下表中前缀为:WN,如果用到下表中没有列举出来,请去掉UI首字母,遵循实际规则即可。)
控件名 | 类型 | 示例 |
---|---|---|
UIViewController | ViewController | WNBaseViewController |
UView | ViewController | WNBaseView |
UITableView | TableView | WNOrderTableView |
UITableViewCell | TableViewCell | WNOrderListCell |
UIButton | Button | WNSuccessButton |
UILabel | Label | WNSuccessLabel |
UIImageView | ImageView | WNGoodsImgView |
UITextField | TextField | WNNameTextField |
UITextView | TextView | WNSuggestTextView |
对象命名
- 用小驼峰方式。例如:
UIViewController * userVc = [[UIViewController alloc] init ];
常量
宏:小写k+大驼峰 即为:
#define kUserGaoDeKey @“gaodeKey”
全局常量:工程前+缀全大写,下划线隔开 即为:
extern const NSString WN_USER_AGE_KEY
Resource文件
- 全部小写,采用下划线命名法,加前缀区分。所有的资源文件都需要加上工程前缀(小写形式)。
- 命名模式:可加后缀_small表示小图,_big表示大图,逻辑名称可由多个单词加下划线组成,采用以下规则:
用途_模块名_逻辑名称
用途_模块名_颜色
用途_逻辑名称
用途_颜色
例如:img_home_banner
版本号命名规范
- 采用A.B.C 三位数字命名,比如:1.0.2,当有版本更新的时候,依据下面的情况来确定版本号规范
版本号 | 说明 | 示例 |
---|---|---|
A.b.c | 属于重大更新内容 | 1.0.0 -> 2.0.0 |
a.B.c | 属于小部分更新内容 | 1.0.2 -> 1.2.2 |
a.b.C | 属于补丁更新内容 | 1.0.2 -> 1.0.4 |
函数命名
OC的命名方法通常比较长,是为了让程序有更好的可读性,目的是为了可以当成一个句子的形式朗读出来,达到见名知意的效果
方法一般以小写字母打头,每一个后续的单词首字母大写,方法名中不应该有标点符号(包括下划线),有两个例外:
可以用一些通用的大写字母缩写打头方法,比如PDF,TIFF等。
可以用带下划线的前缀来命名私有方法或者类别中的方法。
- 如果方法表示让对象执行一个动作,使用动词打头来命名,注意不要使用do,does这种多余的关键字,动词本身的暗示就足够了:
//动词打头的方法表示让对象执行一个动作
- (void)invokeWithTarget:(id)target;
- (void)selectTabViewItem:(NSTabViewItem *)tabViewItem;
- 如果方法是为了获取对象的一个属性值,直接用属性名称来命名这个方法,注意不要添加get或者其他的动词前缀:
//正确,使用属性名来命名方法
- (NSSize)cellSize;
//错误,添加了多余的动词前缀
- (NSSize)getCellSize;
- 对于有多个参数的方法,务必在每一个参数前都添加关键词,关键词应当清晰说明参数的作用:
//正确,保证每个参数都有关键词修饰
- (void)cycleScrollView:(SDCycleScrollView *)cycleScrollView didSelectItemAtIndex:(NSInteger)index
//错误,遗漏关键词
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
//正确
- (id)viewWithTag:(NSInteger)Tag;
//错误,关键词的作用不清晰
- (id)taggedView:(int)Tag;
- 不要用and来连接两个参数,通常and用来表示方法执行了两个相对独立的操作(从设计上来说,这时候应该拆分成两个独立的方法):
//错误,不要使用and来连接参数
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
//正确,使用and来表示两个相对独立的操作
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;
通知命名
- 通知常用于在模块间传递消息,所以通知要尽可能地表示出发生的事件,通知的命名方式是:
code[触发通知的类名] + [ Did | Will ] + [动作] + Notification
举个栗子
NSApplicationDidBecomeActiveNotification
NSWindowDidMiniaturizeNotification
命名规范总结
清晰
- 命名应该尽可能的清晰和简洁,但在Objective-C中,清晰比简洁更重要
//清晰
insertObject:atIndex:
//不清晰,insert的对象类型和at的位置属性没有说明
insert:at:
- 不要使用单词的简写,拼写出完整的单词
//清晰
destinationSelection:setBackgroundColor:
//不清晰,不要使用简写
destSel:setBkgdColor:
- 命名方法或者函数时要避免歧义
//有歧义,是返回sendPort还是send一个Port?
sendPort
//有歧义,是返回一个名字属性的值还是display一个name的动作?
displayName
命名统一使用驼峰命名法;只采纳有广为人知含义的缩写,比如
info
、msg
、UI
、HTTP
这类。自造的缩写不被认可。总体的命名原则是清晰和一致,避免歧义。类名需要结合项目名称来命名,确保整个项目中的自定义类的名称开头是统一的,同样要确保类名需要大写字母开头。
类名命名需结合功能或者模块,并且尾部要带上该类的类型,比如
UIViewController的子类命名为JasonIndexViewController。
UIViewController 后缀添加“Controller”
UIView 后缀添加“View”
UIButton 后缀添加“Button"或者"Btn"
UILabel 后缀添加“Label"
一致性
- 整个工程的命名风格要保持一致性,最好和苹果SDK的代码保持统一。不同类中完成相似功能的方法应该叫一样的名字,比如我们总是用count来返回集合的个数,不能在A类中使用count而在B类中使用getNumber。
【注释】
文件注释
/*******************************************************************************
Copyright (C), 2011-2013, Andrew Min Chang
File name: AMCCommonLib.h
Author: Andrew Chang (Zhang Min)
E-mail: LaplaceZhang@126.com
Description:
This file provide some covenient tool in calling library tools. One can easily include
library headers he wants by declaring the corresponding macros.
I hope this file is not only a header, but also a useful Linux library note.
History:
2012-??-??: On about come date around middle of Year 2012, file created as "commonLib.h"
2012-10-10: Change file name as "AMCCommonLib.h"
2012-12-04: Add UDP support in AMC socket library
2013-01-07: Add basic data type such as "sint8_t"
2013-01-18: Add CFG_LIB_STR_NUM.
2013-01-22: Add CFG_LIB_TIMER.
2013-01-22: Remove CFG_LIB_DATA_TYPE because there is already AMCDataTypes.h
Copyright information:
This file was intended to be under GPL protocol. However, I may use this library
in my work as I am an employee. And my company may require me to keep it secret.
Therefore, this file is neither open source nor under GPL control.
********************************************************************************/
/*************************************************************
* Copyright (c) xxx科技有限公司
* All rights reserved.
*
* 文件名称: xxx
* 文件标识: xxx
* 摘要说明: xxx
*
* 当前版本: 1.0.0
* 作 者: CPX
* 更新日期:
* 整理修改:
*
***************************************************************/
文件注释的格式通常不作要求,能清晰易读就可以了,但在整个工程中风格要统一。
代码注释
- 方法、函数、类、协议、类别的定义都需要注释,推荐采用Apple的标准注释风格,好处是可以在引用的地方alt+command+/点击自动弹出注释,非常方便
/**
* Get the COPY of cloud device with a given mac address.
*
* @param macAddress Mac address of the device.
*
* @return Instance of IPCCloudDevice.
*/
-(IPCCloudDevice *)getCloudDeviceWithMac:(NSString *)macAddress;
- 协议、委托的注释要明确说明其被触发的条件
// Individual rows can opt out of having the -editing property set for them. If not implemented, all rows are assumed to be editable.
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath;
【UI布局规范】
- 建议项目统一使用Masonry和xib结合的方式布局。不允许出现直接设置frame的情况。如果是纯代码的项目,不允许出现xib和拉约束的情况。不允许纯storyboard开发。
- 提取方法,去除重复代码。对于必要的工具类抽取也很重要,这在以后的项目中是可以重用的。
- 尽可能的使用局部变量
- 尽量减少对变量的重复计算
- 尽量在合适的场合使用单例。使用单例可以减轻加载的负担,缩短加载的时间,提高加载效率。注意:并不是所有的地方都适用于单例
【编码规范】
函数的书写
方法长度建议不超过80行,如果方法太长可以考虑抽取其中一部分
方法(-、+)和返回值前面的左括号间隔一个空格,方法参数直接间隔一个空格。每个方法结束后需间隔一行来书写新方法
- (void)applicationDidEnterBackground:(UIApplication *)application
- (void)applicationWillResignActive:(UIApplication *)application
如果一个函数有特别多的参数或者名称特别长,将其按照:来对齐分行显示
-(id)initWithModel:(IPCModle)model
ConnectType:(IPCConnectType)connectType
Resolution:(IPCResolution)resolution
AuthName:(NSString *)authName
Password:(NSString *)password
MAC:(NSString *)mac
AzIp:(NSString *)az_ip
AzDns:(NSString *)az_dns
Token:(NSString *)token
Email:(NSString *)email
Delegate:(id<IPCConnectHandlerDelegate>)delegate;
使用函数的调用
函数调用的格式和书写的差不多,可以按照函数的长短选择写在一行或者分成多行
//写在一行
[myObject doFooWith:arg1 name:arg2 error:arg3];
//分行写,按照 : 对齐
[myObject doFooWith:arg1
name:arg2
error:arg3];
//第一段名称过短的话后续可以进行缩进
[myObj short:arg1
longKeyword:arg2
evenLongerKeyword:arg3
error:arg4];
使用#pragma mark 来分类方法,参考以下结构,通常将不常用的函数方法写在底部,这里强制要求懒加载必须写在底部
#pragma mark – Life Cycle
#pragma mark - Events
#pragma mark – Private Methods
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - Custom Delegates
#pragma mark – Getters and Setters
枚举的定义参考系统定义枚举方式
typedef NS_ENUM(NSUInteger, UISearchBarStyle) {
UISearchBarStyleDefault, // currently UISearchBarStyleProminent
UISearchBarStyleProminent, // used my Mail, Messages and Contacts
UISearchBarStyleMinimal // used by Calendar, Notes and Music
}
if和case语句,不论if或者else下有一个还是多个语句,都必须带上大括号。同样case语句也是如此。
//正确
if (!error) {
return success;
}
//错误
if (!error)
return success;
或
if (!error) return success;
布尔值推荐写法
if (someObject) {
//...
}
if (![anotherObject boolValue]) {
//...
}
当需要提高代码的清晰性和简洁性时,三元操作符才会使用。
//推荐写法
NSInteger value = 5;
result = (value != 0) ? x : y;
BOOL isHorizontal = YES;
result = isHorizontal ? x : y;
//不推荐写法
result = a > b ? x = c > d ? c : d : y;
CGRect函数
//推荐写法
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;
当使用条件语句编码时,不要嵌套if语句,多个返回语句也是OK
//推荐写法
- (void)someMethod {
if (![someOther boolValue]) {
return;
}
//Do something important
}
//不推荐写法
- (void)someMethod {
if ([someOther boolValue]) {
//Do something important
}
}
单例模式
//单例对象应该使用线程安全模式来创建共享实例
+ (instancetype)sharedInstance {
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
闭包
block
的右括号"}"应该和调用block
那行代码的第一个非空字符对齐
block
内的代码采用一个tab
(四个空格的距离)的缩进
如果block
过于庞大,应该单独声明一个变量来使用
//分行书写的block,内部使用一个tab的缩进
[operation setCompletionBlock:^{
[self.delegate newDataAvailable];
}];
//使用C语言API调用的block遵循同样的书写规则
dispatch_async(_fileIOQueue, ^{
NSString* path = [self sessionFilePath];
if (path) {
// ...
}
});
//庞大的block应该单独定义成变量使用
void (^largeBlock)(void) = ^{
// ...
};
[_operationQueue addOperationWithBlock:largeBlock];
//在一个调用中使用多个block,通过进行一个tab缩进
[myObject doSomethingWith:arg1
firstBlock:^(Foo *a) {
// ...
}
secondBlock:^(Bar *b) {
// ...
}
];
【系统设计】
- 推荐使用Swift语言;
- 推荐使用MVVM架构;
- 推荐采用模块分类方式替代文件类别方式, 方便快速查找模块相关内容;
隐私协议
- 根据法律法规,目前的APP都需要加入《隐私政策》的选项以供用户体验,所以在APP第一步就是添加该功能。
// 强制退出App
- (void)exitApplication {
UIWindow * window = [[[UIApplication sharedApplication] delegate] window];
[UIView animateWithDuration:0.2f animations:^{
window.alpha = 0;
} completion:^(BOOL finished) {
exit(0);
}];
}
版本控制更新
- 程序在发布初始1.0版本的时候建议加上版本控制。
- 分为强制更新和选择更新,由管理后台进行控制。
- 具体功能开发根据自己情况来制定。
Crash搜集
1:注册
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSSetUncaughtExceptionHandler(handleExceptionAndTalk);
return YES;
}
2:实现handleExceptionAndTalk方法
- (void)handleExceptionAndTalk(NSException *exception){
NSString *content = [NSString stringWithFormat:@"========异常错误报告========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@",name,reason,[callStack componentsJoinedByString:@"\n"]];
//保存异常信息
NSMutableDictionary *info = [NSMutableDictionary dictionary];
info[@"name"] = [exception name]; // 异常名字
info[@"reason"] = [exception reason]; // 异常描述(报错理由)
info[@"callStackSymbols"] = [exception callStackSymbols]; // 调用栈信息(错误来源于哪个方法)
//写入沙盒
NSString *path =[NSHomeDirectory() stringByAppendingString:@"/crash.plist"];
[info writeToFile:path atomically:YES];
// 把异常崩溃信息发送至开发者邮件
NSMutableString *mailUrl = [NSMutableString string];
[mailUrl appendString:@"mailto:test@qq.com"];
[mailUrl appendString:@"?subject=程序异常崩溃,请配合发送异常报告,谢谢合作!"];
[mailUrl appendFormat:@"&body=%@", content];
// 打开地址
NSString *mailPath = [mailUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:mailPath]];
}
当用户使用软件过程中程序崩溃,我们可以及时捕获,并得到日志进行错误分析。
我们这里也可以根据自己业务逻辑来进行相关处理,也可以使用第三方工具来实现,例如:Bugly
。
CocoaPods管理第三方库
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, ‘9.0’
use_frameworks!
target 'AliTaoBao' do
pod 'MJRefresh' , '3.1.15.7'
pod 'MJExtension', '3.0.15.1'
pod 'SDWebImage' , '4.4.2'
pod 'AFNetworking' , '3.2.1'
pod 'MBProgressHUD', '0.9.2'
pod 'SDCycleScrollView' , '1.75'
pod 'IQKeyboardManager' , '6.2.0'
pod 'Masonry' , '1.1.0'
pod 'Bugly' , '2.5.0'
end
pod update
pod update --verbose --no-repo-update
网络请求
class func tokenRequest(isShow: Bool = true,
method: NetMethod = .POST,
url: String,
parameter: [String: AnyObject]? = nil ,
finished: @escaping (_ result: [String: AnyObject]?, _ issuccess: Bool, _ msg: String?) -> ()) {
guard let token = UserModel.shared.token else { return }
var para = parameter
if para == nil {
para = [String : AnyObject]()
}
para!["uid"] = UserModel.shared.id as AnyObject
para!["token"] = token as AnyObject
isShow ? SVProgressHUD.show() : ()
let httpMethod: HTTPMethod = method == .GET ? .get : .post
Alamofire.request(url, method: httpMethod, parameters: para).responseJSON { (response) in
SVProgressHUD.dismiss()
switch response.result {
case .success(let data):
if let data = data as? [String: AnyObject],
let status = data["status"] as? Int{
if status == 200 {
finished(data, true, nil)
} else if status == 16 {
keyWindow?.rootViewController = BaseNavigationController(rootViewController: LoginController())
finished(nil, false, data["error_desc"] as? String)
} else {
finished(nil, false, data["error_desc"] as? String)
}
}
case .failure(let error):
print(response)
finished(nil, false, "网络请求超时")
}
}
}
这里只是对Alamofire
做个简单的处理。大家根据公司的流程来对网络请求框架进行自我封装,可以加上请求头
,数据加密
,数据压缩
,请求失败处理
,域名更换
,循环请求
等处理。
安全设计
- 网络请求的参数和返回结果进行加密。比如:
Base64加密
,Zlib压缩
等。 - 引入Token验证机制。
- 禁止把敏感信息打印到Log中。
- 在应用发布时必须确保Release模式。
- Http请求都带上MAC,进行双向安全校验。
- 针对用户密码等涉及的重要信息进行
加密传输
。 - 其他的持续补充ing。
图片/动画
TODO
文件/数据库
TODO
埋点设计
TODO
- 其他的持续补充ing。
【开发流程】
【持续集成】
-
Xcodebuild
编译打包。 -
Fastlane
自动化打包上传发布平台。 -
Jenkins
持续集成配置。 - 还有很多工具,网上也有相关教程,目前我自己使用的是
Fastlane
。
【调试技巧】
NSLog
日常的开发过程中最常见的Debug方式就是打Log。
#if DEBUG
#define NSLog(...) NSLog(__VA_ARGS__)
#define debugMethod() NSLog(@"%s",__func__)、
#else
#define NSLog(...)
#define debugMethod()
#endi
另外在使用NSLog的时候应当注意,release版本中应该要去掉NSLog。
EXC_BAD_ACCESS
- 1、开启僵尸对象
开启Zombie模式之后会导致内存上升,因为所以已经被释放(引用计数为0)的对象被僵尸对象取代,并未真的释放掉。这个时候再给僵尸对象发送消息,就会抛出异常,并打印出异常信息,你可以轻松的找到错误代码位置,结束Zombies时会释放。它的主要功能是检测野指针调用。
使用方法:
“Edit Scheme…” —> “Run” —> “Diagnostics” —> “Zombie Objects”
打开”Edit Scheme…”窗口:
- 2、Address Sanitizer
在Xcode7之后新增了AddressSanitizer工具,为我们调试EXC_BAD_ACCESS错误提供了便利。当程序创建变量分配一段内存时,将此内存后面的一段内存也冻结住,标识为中毒内存。程序访问到中毒内存时(访问越界),立即中断程序,抛出异常并打印异常信息。你可以根据中断位置及输出的Log信息来解决错误。当然,如果变量已经释放了,它所占用的内存也会被标识为中毒内存,这个时候访问这片内存空间同样会抛出异常。
使用方法:
“Edit Scheme…” —> “Run” —> “Diagnostics” —> “Zombie Objects”
开启AddressSanitizer之后,在调试程序的过程中,如果有遇到EXC_BAD_ACCESS错误,程序则会自动终端,抛出异常。
- 其他的持续补充ing。