一、前言
车同轨,书同文,在同一个公司的开发团队里面,统一使用一套编程规范,共同约定代码风格,这既是高效合作的基础,也是深度创新的开始。
《iOS开发编程规范》是公司内部的各 iOS 开发团队的集体智慧、结晶和经验总结, 从而系统地整理成册,以指导公司团队内部 iOS 开发者统一开发团队内部的编码风格,更加高效、高质量地进行 App 开发,呈现给用户体验好、 性能优、稳定性佳、安全性高的产品。
《iOS开发编程规范》作为公司开发规约重要的一环,我们的目标是:
- 防患未然,提升质量意识,降低故障率和维护成本;
- 标准统一,提升协作效率;
- 追求卓越的工匠精神,打磨精品代码。
本手册以开发者为中心视角分为 Objective-C 语言规范、 iOS 资源文件命名与使用、iOS 基本组件、UI 与布局、进程、线程与消息通信、 文件与数据库,Bitmap、Drawable 与动画、安全,其他等九大部分,根据约束力强弱(公司开发套件的搭建依赖于公司内部的团队建设沉淀,暂不铺展开来讨论), 规约依次分为强制、推荐、参考三大类:
【强制】 必须遵守,违反本约定或将会引起严重的后果;
【推荐】 尽量遵守,长期遵守有助于系统稳定性和合作效率的提升;
【参考】 充分理解,技术意识的引导,是个人学习、团队沟通、项目合作的方向。
二、设计原则
原则一:关注代码的可读性-----代码应简洁易懂,逻辑清晰
无论软件是正在实施产品功能迭代,还是在维护阶段,良好的代码可读性都有助于 开发者 快速上手。
因此,无论是对外暴露的声明文件,还是内部的实现文件,在成员属性、成员函数、变量、逻辑分支等方面 都应 以良好的可读性为基础:
- 1. 不要过分追求编码技巧,降低程序的可读性
- a.对外暴露API的设计,要尽可能贴近系统框架API的特点,以便于 其它开发组团队的同事 在没有阅读API说明的情况下也不会无从下手;
- b.关于
泛型编程、链式编程等若干 高级 编程技巧,在编写工程的业务代码的时候,不建议使用;- 在较大的项目里面,一个开发组一般会有若干个iOS开发者,其中成员架构
初、中、高、资深、架构 配比,肯定是越高级比例越低的 - 因此,若是对API的设计降维,贴近系统API的特点、可读性强,也可以让负责编码业务功能的组员快速上手,推进团队的敏捷开发
- 在写业务代码的时候,要适当降维编程技巧,便于团队其它成员快速上手,推进团队的敏捷开发
- 在较大的项目里面,一个开发组一般会有若干个iOS开发者,其中成员架构
- 2.
简洁的代码可以让bug无处藏身。要写出明显没有bug的代码,而不是没有明显bug的代码- a.重复的代码块,要封装成公共的接口;
- b.逻辑分支 的嵌套 至多不能超过 三层;
- c.成员函数的命名、成员属性的命名,在符合团队统一规范的基础上,要
强调见名知意,不要过分追求简短 背离了 可读性强 的要求
原则二:不要过度设计-----先保证程序的正确性,防止过度工程
过度工程(over-engineering):在正确可用的代码写出之前就过度地考虑扩展,重用的问题,使得工程过度复杂。
我们正常的软件工程开发流程,是会经历:「需求调研」→「需求确认」→「技术方案(概设、详设、类关系设计等)」→「项目实施」→「测试迭代」→「上线维护」等阶段的。
我们的技术方案的设计,应该首先严格围绕我们当前的需求,不能过渡设计,过渡超越项目的 时间、人力、开发资金 等规划成本。
我们可以适当在原有需求的基础上,增加其扩展的可能性,而不必过渡划分成极小的颗粒度去实施
或脱离原有的需求设计,即使是同一个方向的需求,也不能过渡超越 本次项目的需要用到的功能复杂度,这都会拉长 项目实施、维护的成本;
1. 先把眼前的问题解决掉,解决好,再考虑将来的扩展问题2. 先写出可用的代码,反复推敲,再考虑是否需要重用的问题3. 先写出可用,简单,明显没有bug的代码,再考虑测试的问题
原则三:关注模块的插件化、公共化-----面向变化编程,而不是面向需求编程
- 1. 需求是暂时的,只有变化才是永恒的
- 一个应用工程,它可能在不断地迭代演化。一些通用的模块,应该抽象出来形成通用构件,以供未来在相似的场景加以复用;
- 比如 我们目前的需求 是正在写一个积分系统,后期的需求又要写一个红包系统,再后期的需求可能又要写 券系统......
- 几个业务场景的模块之间,有很一定的相似性。我们就可以抽象封装出公用部分,供多个模块共用;
- 经历了n个项目实施经验之后,就会积累了一定的轮子,要适当 将各种轮子进行分层管理设计,演进出一个开发套件,形成团队内部的开发套件集;
- 若是我们面向需求开发,而非基于
沉淀出一套公共组件,依赖公共组件去编写模块,那业务特性相似的部分我们一个团队可能就会产生多种编码风格 - 在项目体量上去之后,风格不一的新旧代码,将会导致编码工作量、维护工作量的增加,而代码质量可能却很难保持在一个相对可观的水平
- 若是我们面向需求开发,而非基于
- 一个应用工程,它可能在不断地迭代演化。一些通用的模块,应该抽象出来形成通用构件,以供未来在相似的场景加以复用;
- 2.每次迭代不能仅仅为了当前的需求,写出扩展性强,易修改的程序才是负责任的做法,对自己负责,对公司负责
- 依据一定的 分层 设计 架构的设计模式,分离不同模块的职责。进行解耦
- 比如: MVVM 设计模式、就是将程序不同职责的代码 分到不同模块去管理;
- 将公共性、通用性的代码块 抽象出来下沉到一个 Base工具 或者 基类。遵循组件化开发的习惯,将公共部分划成组件管理。
- 比如: 阿里巴巴开发团队开源了一个组件化框架BeeHive广受开发者追捧
- 其中的组件化架构设计中的模块化 热拔插思想,就是 基于面向 动态化 模块拆装的编程。该框架 受启发于 SpringCloud 的微服务架构思想。
- 依据一定的 分层 设计 架构的设计模式,分离不同模块的职责。进行解耦
三、概述
这篇文章 既有 团队实施经验心得,也参考了《Effective Objective—C》等书籍 整个文章篇幅总共包括六个章节:
- 一、前言
介绍 写这篇文章的一个初心与契机;- 二、概述 用概括性的语言,向读者介绍每个章节各自的关注点是什么
- 三、分述 这是篇幅最大的一个章节,内部同样也分为两个篇章:通用规范、iOS规范。
在两个小章节中分别有具体的关注点,如置顶的附图。- 四、总结 在这个部分会有总结;同时也会指明本篇文章尚未解决的问题;最后还会引入参考的链接;
- 五、文章推荐
- 六、相关阅读
四、分述
通用规范
1.)运算符
1.1 ) 一元运算符
一元运算符与变量之间没有空格
!bValue
~iValue
++iCount
*strSource
&fSum
1.2 ) 二元运算符(又称双目运算符)
二元运算符与变量之间必须有空格
fWidth = 5 + 5;
fLength = fWidth * 2;
fHeight = fWidth + fLength;
for(int i = 0; i < 10; i++)
1.3 ) 三元运算符(又称三目运算符)
推荐使用三元运算符 适当代替 if-else 的条件分支运算. 它能使代码更加简洁清晰。 推荐:
result = (a > b) ? (x) : (y);
不推荐:
result = (a > b) ?( x = c > d ? c : d) : (y);
当三元运算符的第二个参数(if 分支)返回和条件语句中已经检查的对象一样的对象的时候,下面的表达方式更灵巧:
推荐:
result = object ? : [self createObject];
不推荐:
result = object ? object : [self createObject];
1.4 ) 多个运算符 同在一个运算中
多个不同的运算符 同时存在时 【应该使用括号来明确优先级】
-
在多个
不同的运算符同时存在的时候应该合理使用括号,不要盲目依赖操作符优先级。- 因为有的时候不能保证阅读你代码的人就一定能了解你写的算式里面所有操作符的优先级。
-
例如:来看一下这个算式:2 << 2 + 1 * 3 - 4
- 这里的
<<是移位操作直观上却很容易认为它的优先级很高,所以就把这个算式误认为:(2 << 2) + 1 * 3 - 4 - 但事实上,它的优先级是比加减法还要低的,所以该算式应该等同于:2 << 2 + 1 * 3 - 4
- 所以在以后写这种复杂一点的算式的时候,尽量多加一点括号,避免让其他人误解(甚至是自己)
- 这里的
2.) 变量&&常量
- 一个变量有且只有一个功能,尽量不要把一个变量用作多种用途
- 变量在使用前应初始化,防止未初始化的变量被引用
- 局部变量应该尽量接近使用它的地方
- 全局变量、成员变量、静态变量、常量 可以分别采用 【g_、m_、s_、c_】 打头的命名
- 命名要遵循
见名知意的意图,可以采用驼峰标识的写法 - 编写一个函数/方法 等实现过程的时候,要适当备注当前步骤的这些代码段是 做什么的
推荐这样写:
func someFunction() {
let index = ...;
//Do something With index
...
...
let count = ...;
//Do something With count
}
不推荐这样写:
func someFunction() {
let index = ...;
let count = ...;
//Do something With index
...
...
//Do something With count
}
3.) 语句
- 条件判断语句执行主体不管是 if 里面还是 else 里面都
必须要使用大括号包裹- 即使代码是一行的情况
- if-elseif-else中,
elseif不另起一行,与if的反括号在同一行 - if-else中,
else不另起一行,与if的反括号在同一行 - switch-case中,case后的代码多余一行,则需要{}包裹,建议所有case和default后的代码块均用{}包裹
- 在条件分支语句,紧跟括号
{后边要 适当补充该步骤的 注释文档说明 优点: 减少代码冗余行数,代码更加紧凑,结构清晰。
条件分支语句-if
1. 穷举所有条件分支:必须列出所有分支(穷举所有的情况),而且每个分支都必须给出明确的结果。
推荐这样写:
var hintStr;
if (count < 3) {//注释1
hintStr = "Good";
} else {//注释2
hintStr = "";
}
不推荐这样写:
var hintStr;
if (count < 3) {
hintStr = "Good";
}
2. 控制条件分支的个数:不要使用过多的分支,要善于使用return来提前返回错误的情况
推荐这样写:
- (void)someMethod {
if (!goodCondition) {
return;
}
//Do something
}
不推荐这样写:
- (void)someMethod {
if (goodCondition) {
//Do something
}
}
比较典型的例子可以参考JSONModel这个库中的源码的代码段:
-(id)initWithDictionary:(NSDictionary*)dict error:(NSError)err{
//方法1. 参数为nil
if (!dict) {
if (err) *err = [JSONModelError errorInputIsNil];
return nil;
}
//方法2. 参数不是nil,但也不是字典
if (![dict isKindOfClass:[NSDictionary class]]) {
if (err) *err = [JSONModelError errorInvalidDataWithMessage:@"Attempt to initialize JSONModel object using initWithDictionary:error: but the dictionary parameter was not an 'NSDictionary'."];
return nil;
}
//方法3. 初始化
self = [self init];
if (!self) {
//初始化失败
if (err) *err = [JSONModelError errorModelIsInvalid];
return nil;
}
//方法4. 检查用户定义的模型里的属性集合是否大于传入的字典里的key集合(如果大于,则返回NO)
if (![self __doesDictionary:dict matchModelWithKeyMapper:self.__keyMapper error:err]) {
return nil;
}
//方法5. 核心方法:字典的key与模型的属性的映射
if (![self __importDictionary:dict withKeyMapper:self.__keyMapper validation:YES error:err]) {
return nil;
}
//方法6. 可以重写[self validate:err]方法并返回NO,让用户自定义错误并阻拦model的返回
if (![self validate:err]) {
return nil;
}
//方法7. 终于通过了!成功返回model
return self;
}
可以看到,在这里,首先判断出各种错误的情况然后提前返回,把最正确的情况放到最后返回。
3.用BOOL来降低条件表达式的复杂度: 条件表达式如果很长,则需要将他们提取出来赋给一个BOOL值
推荐这样写:
let nameContainsSwift = sessionName.hasPrefix("Swift")
let isCurrentYear = sessionDateCompontents.year == 2014
let isSwiftSession = nameContainsSwift && isCurrentYear
if (isSwiftSession) {
// Do something
}
不推荐这样写:
if ( sessionName.hasPrefix("Swift") && (sessionDateCompontents.year == 2014) ) {
// Do something
}
4. 变量在左,常量在右:条件语句的判断应该是变量在左,常量在右
推荐这样写:
if ( count == 6) {
}
或者
if ( object == nil) {
}
或者
if ( !object ) {
}
不推荐这样写:
if ( 6 == count) {
}
或者
if ( nil == object ) {
}
5. 用花括号包裹代码块:每个分支的实现代码都必须被大括号包围
推荐这样写:
if (!error) {
return success;
}
不推荐这样写:
if (!error)
return success;
或者
if (!error) return success;
6. 多个条件语句换行:条件过多,过长的时候应该换行
推荐这样写:
if (condition1() &&
condition2() &&
condition3() &&
condition4()) {
// Do something
}
不推荐这样写:
if (condition1() && condition2() && condition3() && condition4()) {
// Do something
}
条件分支语句-switch
1. 每个分支都必须用大括号括起来
推荐这样写:
switch (integer) {
case 1: {
// ...
}break;
case 2: {
// ...
}break;
case 3: {
// ...
} break;
default:{
// ...
} break;
}
2.删除default分支,穷举所有枚举:使用枚举类型时,不能有default分支, 除了使用枚举类型以外,都必须有default分支
HPLeftMenuTopItemType menuType = HPLeftMenuTopItemMain;
switch (menuType) {
case HPLeftMenuTopItemMain: {
// ...
}break;
case HPLeftMenuTopItemShows: {
// ...
}break;
case HPLeftMenuTopItemSchedule: {
// ...
}break;
}
在switch语句使用枚举类型的时候,如果使用了default分支,在将来就无法通过编译器来检查新增的枚举类型了。
循环语句
循环语句 - for
1. 不可在for循环内修改循环变量,防止for循环失去控制。
for (int index = 0; index < 10; index++){
...
logicToChange(index)
}
2. 避免使用continue和break。
continue和break所描述的是什么时候不做什么,所以为了读懂二者所在的代码,我们需要在思维上,认识到这跟进入循环语句的条件取反。
其实最好不要让这两个东西出现,因为我们的代码只要体现出“什么时候做什么”就好了,而且通过适当的方法,是可以将这两个东西消灭掉的:
2.1 如果出现了continue,只需要把continue的条件取反即可
var filteredProducts = Array<String>()
for level in products {
if level.hasPrefix("bad") {
continue
}
filteredProducts.append(level)
}
我们可以看到,通过判断字符串里是否含有“bad”这个prefix来过滤掉一些值。其实我们是可以通过取反,来避免使用continue的:
for level in products {
if !level.hasPrefix("bad") {
filteredProducts.append(level)
}
}
循环语句 - while
1. 消除while里的break:将break的条件取反,并合并到主循环里
在while里的block其实就相当于“不存在”,既然是不存在的东西就完全可以在最开始的条件语句中将其排除。
while里的break:
while (condition1) {
...
if (condition2) {
break;
}
}
取反并合并到主条件:
while (condition1 && !condition2) {
...
}
2. 在有返回值的方法里消除break:将break转换为return立即返回
有些朋友喜欢这样做:在有返回值的方法里break之后,再返回某个值。其实完全可以在break的那一行直接返回。
func hasBadProductIn(products: Array<String>) -> Bool {
var result = false
for level in products {
if level.hasPrefix("bad") {
result = true
}
}
return result
}
遇到错误条件直接返回:
func hasBadProductIn(products: Array<String>) -> Bool {
for level in products {
if level.hasPrefix("bad") {
return true
}
}
return false
}
这样写的话不用特意声明一个变量来特意保存需要返回的值,看起来非常简洁,可读性高。
4.) 函数/方法
1.良好的可读性: 一个 「函数/方法 」的长度必须限制在50行以内
- 通常来说,在阅读一个函数的时候,如果视需要跨过很长的垂直距离会非常影响代码的阅读体验。
- 如果需要来回滚动眼球或代码才能看全一个方法,就会很影响思维的连贯性,对阅读代码的速度造成比较大的影响。
- 最好的情况是在不滚动眼球或代码的情况下一眼就能将该方法的全部代码映入眼帘。
2.有效性进行检查: 对输入参数的正确性和有效性进行检查,参数错误立即返回
推荐这样写:
void function(param1,param2) {
if(param1 is unavailable){
return;
}
if(param2 is unavailable){
return;
}
//Do some right thing
}
3.重用代码块封装: 如果在不同的函数内部有相同的功能,应该把相同的功能抽取出来单独作为另一个函数
原来的调用:
void logic() {
a();
b();
if (logic1 condition) {
c();
} else {
d();
}
}
将a,b函数抽取出来作为单独的函数
void basicConfig() {
a();
b();
}
void logic1() {
basicConfig();
c();
}
void logic2() {
basicConfig();
d();
}
4. 拆分复杂实现代码块步骤封装:将函数内部比较复杂的逻辑提取出来作为单独的函数
一个函数内的不清晰(逻辑判断比较多,行数较多)的那片代码,往往可以被提取出去,构成一个新的函数,然后在原来的地方调用它这样你就可以使用有意义的函数名来代替注释,增加程序的可读性。
举一个发送邮件的例子:
openEmailSite();
login();
writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);
send();
中间的部分稍微长一些,我们可以将它们提取出来:
void writeEmail(title, content,receiver,attachment) {
writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);
}
然后再看一下原来的代码:
openEmailSite();
login();
writeEmail(title, content,receiver,attachment)
send();
包裹方法体的括号
- 不要使用 tab 键来进行缩进, 应该使用 4 个空格来代替。
- 方法的大括号和其他的大括号(if/else/switch/while 等等)始终和声明在同一行开始,在新的一行结束。
- 方法之间应该正好空一行,这有助于视觉清晰度和代码组织性。在方法中的功能块之间应该使用空白分开,但往往可能应该创建一个新的方法。
if (user.isHappy) {//描述1
// Do something
} else { //描述2
// Do something else
}
5.单一原则: 一个「函数/方法 」只做一件事
每个函数的职责都应该划分的很明确(就像类一样)。
推荐这样写:
dataConfiguration()
viewConfiguration()
不推荐这样写:
void dataConfiguration() {
...
viewConfiguration()
}
6.返回值: 对于有返回值的函数(方法),每一个分支都必须有返回值
推荐这样写:
int function() {
if(condition1){
return count1
}else if(condition2){
return count2
}else{
return defaultCount
}
}
不推荐这样写:
int function() {
if(condition1){
return count1
}else if(condition2){
return count2
}
}
7. 使用形参:避免使用全局变量,类成员(class member)来传递信息,尽量使用局部变量和参数。
在一个类里面,经常会有传递某些变量的情况。而如果需要传递的变量是某个全局变量或者属性的时候,有些朋友不喜欢将它们作为参数,而是在方法内部就直接访问了:
class A {
var x;
func updateX() {
...
x = ...;
}
func printX() {
updateX();
print(x);
}
}
我们可以看到,在printX方法里面,updateX和print方法之间并没有值的传递,乍一看我们可能不知道x从哪里来的,导致程序的可读性降低了。
而如果你使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个类,而且更加容易理解,不易出错:
func updateX() -> String
{
x = ...;
return x;
}
func printX()
{
String x = updateX();
print(x);
}
8.带返回值的 方法接口设计 与 调用 判断
- 我们对一个方法的调用 需要有运算结果 判断依据的,可以通过设计返回值的有无来判断 执行的结果;
- 很多系统方法通过 error 返回指针的形式来表示错误, 我们应该针对其返回值判断, 而非错误变量。
推荐:
NSError *error;
if (![self trySomethingWithError:&error]) {
// 处理错误
}
反对:
NSError *error;
[self trySomethingWithError:&error];
if (error) {
// 处理错误
}
一些苹果的 API 在成功的情况下会写一些垃圾值给错误参数(如果非空),所以针对错误变量可能会造成虚假结果(以及接下来的崩溃)
我们也可以通过block作为API的参数,作为API执行结果的回调代替如上API设计,但是
要避免 self的循环引用我们在写 block 回调的时候, 特别是对于实例方法中调用, 很容易导致其循环引用。
我们在项目中已经引入一组强弱引用的宏定义. @weakify(self) 、@strongify(self)
我们在遇到需要修饰 self 的时候直接使用这组宏来进行修饰即可。。
例如:
[HPStartInfo getStartInfoSuccess:^{
@weakify(self)
[HPCheckVersion checkWithPass:^{
@strongify(self);
[self reloadView];
}];
}];
5.) 代码注释
头文件就是文档,需要让使用者快速知道这个类的作用。一个好的方法名可以让使用者快速理解,但大部分时候还是需要相应的注释。 写好格式化注释后,当光标停留在方法名和属性上时,在Xcode右侧的Quick Help栏里会出现注释内容,按住option并单击,也会弹出注释框。
1.必须写注释的三个常见场景
优秀的代码大部分是可以自描述的(API见名知意),我们完全可以用程代码本身来表达它到底在干什么,而不需要注释的辅助。
但并不是说都不需要写注释,有以下三种情况比较适合写注释:
-
- 公共接口(注释要告诉阅读代码的人,当前类能实现什么功能)
-
- 涉及到比较深层专业知识的代码(注释要体现出实现原理和思想)
-
- 容易产生歧义的代码(但是严格来说,容易让人产生歧义的代码是不允许存在的)
除了上述这三种情况,如果别人只能依靠注释才能读懂你的代码的时候,就要反思代码出现了什么问题。
最后,对于注释的内容,相对于“做了什么”,更应该说明“为什么这么做”。
2.单行注释
直接在方法或者属性声明的上一行使用///,后面加注释,同时兼容Xcode和appleDoc。Xcode也支持//!,但是appleDoc不支持。
//HPSearchManagerBase.h
///搜索manager的基类
@interface HPSearchManagerBase : NSObject
///搜索状态
@property (nonatomic, readonly) NSInteger * state;
@end
3.多行注释
多行注释使用:
/**
注释内容
*/
Xcode8提供了快速生成格式化注释的快捷键:option+command+/。如果方法有参数,会自动添加@param关键字,用于描述对应的参数。 Apple提供了官方的headDoc语法,但是很多都已经在Xcode中失效了,而且有些关键字也和appleDoc不兼容。下面几种列举出了在Xcode中仍然有效的一些关键字:
/**
*第一行 写方法简介
*第二行 留白
*第三行开始写API的参数描述等 @brief 方法的简介(appleDoc不支持此关键字)
* @discussion 方法的详细说明
* @code //示例代码(这个在Xcode里常用,但是appleDoc不支持此关键字)
* UIView *view;
* @endcode
* @bug 存在的bug的说明
* @note 需要注意的提示
* @warning 警告
* @since iOS7.0
* @exception 方法会抛出的异常的说明
*
* @attention 注意,从这里开始往下的关键字,appleDoc都不支持
* @author 编写者
* @copyright 版权
* @date 日期
* @invariant 不变量
* @post 后置条件
* @pre 前置条件
* @remarks 备注
* @todo todo text
* @version 版本
*/
- (void)sampleMethod;
在Xcode中,就会显示为这样:
4.枚举注释
如果要给枚举注释,需要在每个枚举值前注释,按照如下格式:
///搜索状态
typedef NS_ENUM(NSInteger,HPSearchState) {
///没有开始搜索
HPSearchStateNotSearch,
///搜索中
HPSearchStateSearching,
///搜索结束
HPSearchStateSearchFinished,
///搜索失败
HPSearchStateSearchFailed
};
5. 几个注释约定
需要注释的内容:
- 尽量为类添加描述,即便只有一句话。
- 标明某些参数和属性的默认值,比如超时time。
- 如果属性是KVO兼容的,即外部可以使用KVO监听此属性,则在属性注释里声明。
- 回调block参数需要说明回调所在的线程,避免让使用者在block里进行多余的线程判断。
- 如果需要的话,说明使用此API需要的前置条件,防止被错误地调用。
- 对使用了method swizzling的API进行统一形式的标注,方便遇到runtime的bug时进行排查。
6.自动化Code Review检查
换行、注释、方法长度、代码重复等这些是通过机器检查出来的问题,是无需通过人来做的。
而且除了审查需求的实现的程度,bug是否无处藏身以外,更应该关注代码的设计。
- 比如类与类之间的耦合程度,设计的可扩展性,复用性,是否可以将某些方法抽出来作为接口等等。
iOS规范
1.) 命名规范
我们应遵守 Apple 的命名约定(The coding guide for cocoa), 其推荐使用长的,描述性强的方法和变量名,使其阅读起来更加清晰易懂。
不能随意使用缩写,导致其他人员阅读代码困难。
1. 大驼峰、小驼峰:变量名必须使用驼峰格式
类名,协议名使用大驼峰:
HomePageViewController.h
<HeaderViewDelegate>
局部变量、成员属性等使用小驼峰:
NSString *personName = @"";
NSUInteger totalCount = 0;
@property(noatomatic,readonly,copy)NSString* userName;
2. 见名知意: 变量的名称必须同时包含功能与类型
UIButton *addBtn //添加按钮
UILabel *nameLbl //名字标签
NSString *addressStr//地址字符串
3. 加入后缀:系统常用类作实例变量声明时加入后缀
| 类型 | 后缀 |
|---|---|
| UIViewController | ViewController |
| UIView | View |
| UILabel | Lbl |
| UIButton | Btn |
| UIImage | Img |
| UIImageView | ImagView |
| NSArray | Array |
| NSMutableArray | MArray |
| NSDictionary | Dict |
| NSMutableDictionary | MDict |
| NSString | Str |
| NSMutableString | MStr |
| NSSet | Set |
| NSMutableSet | MSet |
- 创建类,类名采用大驼峰+后缀
如:
HomePageViewController;
MineViewController;
采用ViewController贴近系统命名风格,易读
- 创建成员变量,变量名使用小驼峰+后缀
如:
nameLbl;
ageLbl;
userNameStr;
userAge
4. 命名头文件
头文件名必须采用一致的前缀开头, 加上其代表相关类名代表的形容词即可- 尽量不要使用缩写,宁愿名字稍微长点儿,也要让其有一目了然的效果。
- 声明相关的类扩展和协议时,必须
将其声明放在主类文件夹里面,这样阅读起来相对方便。 - 如果有
多个功能相似的类,可以考虑将其划分为一个框架,使用 .h 文件进行声明管理。
4. 命名 Category 方法
在写添加类别方法时, 必须采用前缀名_ 后连接对应方法名的方式来进行添加。
这样就有效的避免,覆盖掉系统原有或新增方法, 导致意想不到的问题发生。
前缀可以采用框架统一前缀(例如HP)、项目统一的前缀(例如ICBC)、服务
如:
- (void)JS_methodName
- (void)JSD_methodName
2.) 常量
1. 添加前缀:常量以相关类名作为前缀
推荐这样写:
static const NSTimeInterval kHomePageViewController_FadeOutAnimationDuration = 0.4;
不推荐这样写:
static const NSTimeInterval fadeOutTime = 0.4;
2. 多用类型常量,少预处理指令#define:建议使用类型常量,不建议使用#define预处理命令。前者在编译期,编译器会检查语法
首先比较一下这两种声明常量的区别:
- 预处理命令:简单的文本替换,不包括类型信息,并且可被任意修改。
- 类型常量:包括类型信息,并且可以设置其使用范围,而且不可被修改。
使用预处理虽然能达到替换文本的目的,但是本身还是有局限性的:
- 不具备类型信息。
- 可以被任意修改。
3. 使用extern 对外公开常量:
如果我们需要发送通知,那么就需要在不同的地方拿到通知的“频道”字符串(通知的名称)
那么显然这个字符串是不能被轻易更改,而且可以在不同的地方获取。这个时候就需要定义一个外界可见的字符串常量。
推荐这样写:k模块名_通知描述_NotificationKey
//头文件
extern NSString *const kLoginModule_DidClearCache_NotificationKey;
//实现文件
static NSString * const kLoginModule_DidClearCache_NotificationKey = @"LoginModuleDidClearCacheNotificationKey";
不推荐这样写:
#define CompanyName @"Apple Inc."
#define magicNumber 42
4.枚举常量
在使用策略模式编程的时候,我们常常会用到枚举类型进行类型单一选项选择
要求使用 NS_ENUM() 函数而非 enum() 函数进行声明。
其具有更强的类型检查和代码补全功能。
例如:
typedef NS_ENUM(NSUInteger, MyEnum) {
MyEnumValueA = 0,
MyEnumValueB,
MyEnumValueC,
};
5.可选类型常量
在使用策略模式编程的时候,我们常常会用到可选类型进行叠加类型选择
要求使用 NS_OPTIONS 进行声明。
例如:
typedef NS_OPTIONS(NSUInteger, NYTAdCategory) {
NYTAdCategoryAutos = 1 << 0,
NYTAdCategoryJobs = 1 << 1,
NYTAdCategoryRealState = 1 << 2,
NYTAdCategoryTechnology = 1 << 3
};
3.) 宏
1. 宏名称:宏、常量名都要使用大写字母,用下划线_分割单词
#define k返回值描述_模块描述_职能描述1_职能描述2... XXXXXX
URL字符串:kURLStr_模块描述_职能描述1_职能描述2... @”“ 如:
#define kURLStr_USERINFO_GAIN_QUOTE_LIST @"/v1/quote/list"
#define kURLStr_USERINFO_UPDATE_QUOTE_LIST @"/v1/quote/update"
#define kURLStr_LOGIN_LOGIN @"/v1/user/login”
2. 宏表达式:宏定义中如果包含表达式或变量,表达式和变量必须用小括号括起来。
#define kMINValue_Compare_Between(A, B) ((A)>(B)?(B):(A))
4.)布局函数CGRect
当需要访问 CGRect 中某个成员时, 应该使用 CGGeometry 函数来直接访问,它们的可读性比较高,而且简短。 不推荐 .语法来获取;\
推荐这样写:
CGRect frame = self.view.frame;
CGFloat x = CGRectGetMinX(frame);
CGFloat y = CGRectGetMinY(frame);
CGFloat width = CGRectGetWidth(frame);
CGFloat height = CGRectGetHeight(frame);
CGRect frame = CGRectMake(0.0, 0.0, width, height);
而不是
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;
CGRect frame = (CGRect){ .origin = CGPointZero, .size = frame.size };
5.)代码块Block
要点: (内存管理相关)
- block 是在栈上创建的
- block 可以复制到堆上
- Block会捕获栈上的变量(或指针),将其复制为自己私有的const(变量)。
- (如果在Block中修改Block块外的)栈上的变量和指针,那么这些变量和指针必须用
__block关键字申明(译者注:否则就会跟上面的情况一样只是捕获他们的瞬时值)。 如果 block 没有在其他地方被保持,那么它会随着栈生存并且当栈帧(stack frame)返回的时候消失。仅存在于栈上时,block对 对象 访问的内存管理和生命周期没有任何影响。
如果 block 需要在栈帧返回的时候存在,它们需要明确地被复制到堆上,这样,block 会像其他 Cocoa 对象一样增加引用计数。当它们被复制的时候,它会带着它们的捕获作用域一起,retain 他们所有引用的对象。
如果一个 block引用了一个栈变量或指针,那么这个block初始化的时候会拥有这个变量或指针的const副本,所以(被捕获之后再在栈中改变这个变量或指针的值)是不起作用的。(译者注:所以这时候我们在block中对这种变量进行赋值会编译报错:Variable is not assignable(missing __block type specifier),因为他们是副本而且是const的.具体见下面的例程)。
当一个 block 被复制后,__block 声明的栈变量的引用被复制到了堆里,复制完成之后,无论是栈上的block还是刚刚产生在堆上的block(栈上block的副本)都会引用该变量在堆上的副本。
CGFloat blockInt = 10;
void (^playblock)(void) = ^{
NSLog(@"blockInt = %zd", blockInt);
};
blockInt++;
playblock();
...
// 结果为:blockInt = 10
最重要的事情是 __block 声明的变量和指针在 block 里面是作为显示操作真实值/对象的结构来对待的。
block 在 Objective-C 的 runtime(运行时) 里面被当作一等公民对待:他们有一个 isa 指针,一个类也是用 isa 指针在Objective-C 运行时来访问方法和存储数据的。在非 ARC 环境肯定会把它搞得很糟糕,并且悬挂指针会导致 crash。__block 仅仅对 block 内的变量起作用,它只是简单地告诉 block:
嗨,这个指针或者原始的类型依赖它们在的栈。请用一个栈上的新变量来引用它。我是说,请对它进行双重解引用,不要 retain 它。 谢谢,哥们。
如果在定义之后但是 block 没有被调用前,对象被释放了,那么 block 的执行会导致 crash。 __block 变量不会在 block 中被持有,最后... 指针、引用、解引用以及引用计数变得一团糟。
为常用的Block类型创建typedef
如果我们需要重复创建某种block(相同参数,返回值)的变量,我们就可以通过typedef来给某一种块定义属于它自己的新类型:
- 1. 无返回值 无形参:
typedef void(^代码块名称Block)(void) - 2. 有返回值 无形参:
typedef 返回值类型(^代码块名称Block)(void) - 3. 无返回值 有形参:
typedef void(^代码块名称Block)(形参类型1 形参名称1,形参类型2,形参名称2.....) - 4. 有返回值 有形参:
typedef 返回值类型(^代码块名称Block)(形参类型1 形参名称1,形参类型2,形参名称2.....)例如:
int (^variableName)(BOOL flag, int value) =^(BOOL flag, int value)
{
// Implementation
return someInt;
}
这个Block有一个bool参数和一个int参数,并返回int类型。我们可以给它定义类型:
typedef int(^HPDoSomeBlock)(BOOL flag, int value); (HPDoSome需要根据项目当前使用Block的需要来写)
再次定义的时候,就可以通过简单的赋值来实现:
HPDoSomeBlock block = ^(BOOL flag, int value){
// Implementation
};
定义作为参数的Block:
- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;
这里的Block有一个NSData参数,一个NSError参数并没有返回值
typedef void(^HPCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(HPCompletionHandler)completion;”
通过typedef定义Block签名的好处是:如果要某种块增加参数,那么只修改定义签名的那行代码即可。
避免 self 的循环引用
当使用代码块和异步分发的时候,要注意避免引用循环。 总是使用 weak 来引用对象,避免引用循环。(这里更为优雅的方式是采用影子变量@weakify/@strongify 这里有更为详细的说明) 此外,把持有 block 的属性设置为 nil (比如 self.completionBlock = nil) 是一个好的实践。
它会打破 block 捕获的作用域带来的引用循环。
单个语句,推荐这样写:
__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf doSomethingWithData:data];
}];
不推荐这样写:
[self executeBlock:^(NSData *data, NSError *error) {
[self doSomethingWithData:data];
}];
多个语句的例子,推荐这样写:
__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomethingWithData:data];
[strongSelf doSomethingWithData:data];
}
}];
不推荐这样写:
__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
[weakSelf doSomethingWithData:data];
[weakSelf doSomethingWithData:data];
}];
建议把这两行代码作为 snippet codes 加到 Xcode 里面并且总是这样使用它们。
__weak __typeof(self)weakSelf = self;
__strong __typeof(weakSelf)strongSelf = weakSelf;
在ARC条件中,如果尝试用 -> 符号访问一个实例变量,编译器会给出非常清晰的错误信息:
Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first. (对一个 __weak 指针的解引用不允许的,因为可能在竞态条件里面变成 null, 所以先把他定义成 strong 的属性)
可以用下面的代码展示
__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
id localVal = weakSelf->someIVar;
};
6.)集合类泛型定义
建议在定义NSArray和NSDictionary时使用泛型,可以保证程序的安全性:
NSArray<NSString *> *testArr = [NSArray arrayWithObjects:@"Hello", @"world", nil];
NSDictionary<NSString *, NSNumber *> *dic = @{@"key":@(1), @"age":@(10)};
7.)多用字面量语法,少用与之等价的语法
尽量使用字面量值来创建 NSString , NSDictionary , NSArray , NSNumber 这些不可变对象:
推荐这样写:
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingZIPCode = @10018;
不推荐这样写:
NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill" ];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
NSNumber *buildingZIPCode = [NSNumber numberWithInteger:10018];
功能一致,但是使用字面量能令代码更为整洁。
并且对于创建数组,字典使用字面量语法创建,当误传 nil 时能更早的发现错误.
8.)成员变量(属性)
命名属性和实例变量
一般属性一致采用 @property 进行声明, 特殊的数据类型可以使用实例变量声明。
@property (nonatomic, copy) NSString *name;
声明实例变量时必须采用 _ 下划线作为变量名前缀。
添加 @property 相应代码段, 提高编码速度
1. 属性的命名使用小驼峰
推荐这样写:
@property (nonatomic, readwrite, strong) UIButton *confirmButton;
2. 属性的关键字推荐按照 原子性,读写,内存管理的顺序排列
属性关键字首个应该是 原子性,到内存管理关键词。如果需要写读写关键字的话, 其排在第二位.如:
@property (nonatomic, copy) NSString *signature;
@property (nonatomic, readwrite, copy) NSString *nameStr;
@property (nonatomic, readonly, copy) NSString *genderStr;
@property (nonatomic, readwrite, strong) UIView *headerView;
3. Block属性应该使用copy关键字
推荐这样写:
typedef void (^ErrorCodeBlock) (id errorCode,NSString *message);
@property (nonatomic, readwrite, copy) ErrorCodeBlock errorBlock;
4. 形容词性的BOOL属性的getter应该加上is前缀
推荐这样写:
@property (assign, getter=isEditable) BOOL editable;
5. 使用getter方法做懒加载
实例化一个对象是需要耗费资源的,如果这个对象里的某个属性的实例化要调用很多配置和计算,就需要懒加载它,在使用它的前一刻对它进行实例化:
- (NSDateFormatter *)dateFormatter
{
if (!_dateFormatter) {
_dateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[_dateFormatter setLocale:enUSPOSIXLocale];
[_dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"];
}
return _dateFormatter;
}
但是也有对这种做法的争议:getter方法可能会产生某些副作用,例如如果它修改了全局变量,可能会产生难以排查的错误。
6. 点语法
在访问属性或者修改的过程中,应该全程使用 . 语法来进行。 能提高阅读性同时, 还能减少代码长度。但是又不能滥用
除了init和dealloc方法,建议都使用点语法访问属性
使用点语法的好处:
setter:
- setter会遵守内存管理语义(strong, copy, weak)
- 通过在内部设置断点,有助于调试bug
- 可以过滤一些外部传入的值
- 捕捉KVO通知
getter:
- 允许子类化
- 通过在内部设置断点,有助于调试bug
- 实现懒加载(lazy initialization)
注意:
- 懒加载的属性,必须通过点语法来读取数据。
- 因为懒加载是通过重写getter方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。
- 在init和dealloc方法里面使用点语法的后果是:
- 因为没有绕过setter和getter,在setter和getter里面可能会有很多其他的操作
- 而且如果它的子类重载了它的setter和getter方法,那么就可能导致该子类调用其他的方法。
7. 提防滥用点语法:不要滥用点语法,要区分好方法调用和属性访问
推荐这样写:
view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate;
不推荐这样写:
[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate;
8. 尽量使用不可变对象
建议尽量把对外公布出来的属性设置为只读,在实现文件内部设为读写。具体做法是:
- 在头文件中,设置对象属性为
readonly。 - 在实现文件中设置为
readwrite。
这样一来,在外部就只能读取该数据,而不能修改它,使得这个类的实例所持有的数据更加安全。而且,对于集合类的对象,更应该仔细考虑是否可以将其设为可变的。
如果在公开部分只能设置其为只读属性,那么就在非公开部分存储一个可变型。所以当在外部获取这个属性时,获取的只是内部可变型的一个不可变版本,例如:
在公共API中:
@interface HPPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends //向外公开的不可变集合
- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
@end
在这里,我们将friends属性设置为不可变的set。然后,提供了来增加和删除这个set里的元素的公共接口。
在实现文件里:
@interface HPPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation HPPerson {
NSMutableSet *_internalFriends; //实现文件里的可变集合
}
- (NSSet*)friends
{
return [_internalFriends copy]; //get方法返回的永远是可变set的不可变型
}
- (void)addFriend:(HPPerson*)person
{
[_internalFriends addObject:person]; //在外部增加集合元素的操作
//do something when add element
}
- (void)removeFriend:(HPPerson*)person
{
[_internalFriends removeObject:person]; //在外部移除元素的操作
//do something when remove element
}
- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName
{
if ((self = [super init])) {
_firstName = firstName;
_lastName = lastName;
_internalFriends = [NSMutableSet new];
}
return self;
}
我们可以看到,在实现文件里,保存一个可变set来记录外部的增删操作。
这里最重要的代码是:
- (NSSet*)friends
{
return [_internalFriends copy];
}
这个是friends属性的获取方法:它将当前保存的可变set复制了一不可变的set并返回。因此,外部读取到的set都将是不可变的版本。
9.) 方法
1. 禁用and&&签名要与对应的参数名保持高度一致:方法名中不应使用and,而且签名要与对应的参数名保持高度一致
推荐这样写:
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
不推荐这样写:
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height;
2. 参数列表要换行对齐:方法实现时,如果参数过长,则令每个参数占用一行,以冒号对齐。
- (void)doSomethingWith:(NSString *)theFoo
rect:(CGRect)theRect
interval:(CGFloat)theInterval
{
//Implementation
}
3. 私有方法、私有属性 应该在实现文件中申明
私有方法
@interface ViewController ()
- (void)basicConfiguration;
@end
@implementation ViewController
- (void)basicConfiguration{
//Do some basic configuration
}
@end
私有属性
要求私有属性必须在类 extension 下使用 @peoperty 声明.
@interface HPDataService ()
@property (nonatomic, strong) NSDate *lastRefreshTokenTimeDate;
@property (nonatomic, assign) HPRefreshTokenStatus lastRefreshTokenStatus;
@property (nonatomic, copy) NSString *encodeVersionStr; //编码后的App版本号,6位数字,如3.0.1为030001
@end
4. 方法名用小写字母开头的单词组合而成
- (NSString *)descriptionWithLocale:(id)locale;
5. 根据功能操作的描述写方法名前缀
- 刷新视图的方法名要以
refresh为首。 - 更新数据的方法名要以
update为首。
推荐这样写:
- (void)refreshHeaderViewWithCount:(NSUInteger)count;
- (void)updateDataSourceWithViewModel:(ViewModel*)viewModel;
10.) 面向协议编程
如果某些功能(方法)具备可复用性,我们就需要将它们抽取出来放入一个抽象接口文件中(在iOS中,抽象接口即协议),让不同类型的对象遵循这个协议,从而拥有相同的功能。
因为协议是不依赖于某个对象的,所以通过协议,我们可以解开两个对象之间的耦合。如何理解呢?我们来看一下下面这个例子:
现在有一个需求:在一个UITableViewController里面拉取feed并展示出来。
方案1-代理模式
定义一个拉取feed的类ZOCFeedParser,这个类有一些代理方法实现feed相关功能:
@protocol HPFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(HPFeedParser *)parser;
- (void)feedParser:(HPFeedParser *)parser didParseFeedInfo:(HPFeedInfoDTO *)info;
- (void)feedParser:(HPFeedParser *)parser didParseFeedItem:(HPFeedInfoDTO *)item;
- (void)feedParserDidFinish:(HPFeedParser *)parser;
- (void)feedParser:(HPFeedParser *)parser didFailWithError:(NSError *)error;
@end
@interface HPFeedParser : NSObject
@property (nonatomic, weak) id <HPFeedParserDelegate> delegate;
@property (nonatomic, strong) NSURL *url;
- (id)initWithURL:(NSURL *)url;
- (BOOL)start;
- (void)stop;
@end
然后在HPTableViewController里面传入HPFeedParser,并遵循其代理方法,实现feed的拉取功能。
@interface HPTableViewController : UITableViewController<HPFeedParserDelegate>
- (instancetype)initWithFeedParser:(HPFeedParser *)feedParser;
@end
具体应用:
NSURL *feedURL = [NSURL URLWithString:@"http://bbc.co.uk/feed.rss"];
HPFeedParser *feedParser = [[HPFeedParser alloc] initWithURL:feedURL];
HPTableViewController *tableViewController = [[HPTableViewController alloc] initWithFeedParser:feedParser];
feedParser.delegate = tableViewController;
OK,现在我们实现了需求:在HPTableViewController里面存放了一个HPFeedParser对象来处理feed的拉取功能。
但这里有一个严重的耦合问题:HPTableViewController只能通过HPFeedParser对象来处理feed的拉取功能。 于是我们重新审视一下这个需求:其实我们实际上只需要HPTableViewController拉取feed就可以了,而具体是由哪个对象来拉取,HPTableViewController并不需要关心。
也就是说,我们需要提供给HPTableViewController的是一个更范型的对象,这个对象具备了拉取feed的功能就好了,而不应该仅仅局限于某个具体的对象(HPFeedParser)。所以,刚才的设计需要重新做一次修改:
方案2-通过协议优先规划API、定义公共接口
首先需要在一个接口文件HPFeedParserProtocol.h里面定义抽象的,具有拉取feed功能的协议:
@protocol HPFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(id<HPFeedParserProtocol>)parser;
- (void)feedParser:(id<HPFeedParserProtocol>)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info;
- (void)feedParser:(id<HPFeedParserProtocol>)parser didParseFeedItem:(ZOCFeedItemDTO *)item;
- (void)feedParserDidFinish:(id<HPFeedParserProtocol>)parser;
- (void)feedParser:(id<HPFeedParserProtocol>)parser didFailWithError:(NSError *)error;
@end
@protocol HPFeedParserProtocol <NSObject>
@property (nonatomic, weak) id <HPFeedParserDelegate> delegate;
@property (nonatomic, strong) NSURL *url;
- (BOOL)start;
- (void)stop;
@end
而原来的HPFeedParser仅仅是需要遵循上面这个协议就具备了拉取feed的功能:
@interface HPFeedParser : NSObject <HPFeedParserProtocol>
- (id)initWithURL:(NSURL *)url;//仅仅需要通过传入url即可,其他事情都交给ZOCFeedParserProtocol
@end
而且,HPTableViewController也不直接依赖于HPFeedParser对象,我们只需要传给它一个遵循<HPFeedParserProtocol>的对象即可。
@interface HPTableViewController : UITableViewController <HPFeedParserDelegate>
- (instancetype)initWithFeedParser:(id<HPFeedParserProtocol>)feedParser;
@end
这样一来,HPTableViewController和HPFeedParser之间就没有直接的关系了。以后,如果我们想:
- 给这个feed拉取器增加新的功能:仅需要修改
HPFeedParserProtocol.h文件。 - 更换一个feed拉取器实例:创建一个新类型来遵循
HPFeedParserProtocol.h即可。
11.) 委托模式
1. 区分一个类的不同代理:要区分好代理和数据源的区别
在iOS开发中的委托模式包含了delegate(代理)和datasource(数据源)。虽然二者同属于委托模式,但是这两者是有区别的。这个区别就是二者的信息流方向是不同的:
- delegate :事件发生的时候,委托者需要通知代理。(信息流从委托者到代理)
- datasource:委托者需要从数据源拉取数据。(信息流从数据源到委托者)
然而包括苹果也没有做好榜样,将它们彻底的区分开。就拿UITableView来说,在它的delegate方法中有一个方法:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
这个方法正确地体现了代理的作用:委托者(tableview)告诉代理(控制器)“我的某个cell被点击了”。但是,UITableViewDelegate的方法列表里还有这个方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
该方法的作用是 由控制器来告诉tabievlew的行高,也就是说,它的信息流是从控制器(数据源)到委托者(tableview)的。准确来讲,它应该是一个数据源方法,而不是代理方法。
在UITableViewDataSource中,就有标准的数据源方法:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
这个方法的作用就是让tableview向控制器拉取一个section数量的数据。
所以,在我们设计一个视图控件的代理和数据源时,一定要区分好二者的区别,合理地划分哪些方法属于代理方法,哪些方法属于数据源方法。
2. 代理方法的第一个参数必须为委托者
代理方法必须以委托者作为第一个参数(参考UITableViewDelegate)的方法。其目的是为了区分不同委托着的实例。因为同一个控制器是可以作为多个tableview的代理的。若要区分到底是哪个tableview的cell被点击了,就需要在方法中做个区分
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
3.调用委托者 实现代理 方法 之前 进行 实现检查:向代理发送消息时需要判断其是否实现该方法
最后,在委托着向代理发送消息的时候,需要判断委托着是否实现了这个代理方法:
SEL dlgSelector = @selector(signUpViewControllerDidPressSignUpButton:);
if ([self.delegate respondsToSelector:dlgSelector]) {
[self.delegate signUpViewControllerDidPressSignUpButton:self];
}
4. 遵循代理过多的时候,换行对齐显示
@interface ShopViewController () <UIGestureRecognizerDelegate,
HXSClickEventDelegate,
UITableViewDelegate,
UITableViewDataSource>
5. 代理的方法需要明确optional和required
代理方法在默认情况下都是必须执行的,然而在设计一组代理方法的时候,有些方法可以不是必须执行(是因为存在默认配置),这些方法就需要使用@optional关键字来修饰:
@protocol HPServiceDelegate <NSObject>
@optional
- (void)generalService:(HPGeneralService *)service didRetrieveEntries:(NSArray *)entries;
@end
12.)类
1.类名-父类、子类: 类的名称应该以三个大写字母为前缀;创建子类的时候,应该把代表子类特点的部分放在前缀和父类名的中间
推荐这样写:
//父类
HPSalesListViewController
//子类
HPDaySalesListViewController
HPMonthSalesListViewController
2. initializer && dealloc
推荐:
- 将 dealloc 方法放在实现文件的最前面
- 将init方法放在dealloc方法后面。如果有多个初始化方法,应该将指定初始化方法放在最前面,其他初始化方法放在其后。
dealloc方法禁用点语法:dealloc方法里面应该直接访问实例变量,不应该用点语法访问
2.2 init方法的写法:
- init方法返回类型必须是instancetype,不能是id。
- 必须先实现[super init]。
- (instancetype)init {
self = [super init]; // call the designated initializer
if (self) {
// Custom initialization
}
return self;
}
指定初始化方法
指定初始化方法(designated initializer)是提供所有的(最多的)参数的初始化方法,间接初始化方法(secondary initializer)有一个或部分参数的初始化方法。
注意事项1:间接初始化方法必须调用指定初始化方法。
@implementation HPEvent
//指定初始化方法
- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date
location:(CLLocation *)location
{
self = [super init];
if (self) {
_title = title;
_date = date;
_location = location;
}
return self;
}
//间接初始化方法
- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date
{
return [self initWithTitle:title date:date location:nil];
}
//间接初始化方法
- (instancetype)initWithTitle:(NSString *)title
{
return [self initWithTitle:title date:[NSDate date] location:nil];
}
@end
注意事项2:如果直接父类有指定初始化方法,则必须调用其指定初始化方法
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
}
return self;
}
注意事项3:如果想在当前类自定义一个新的全能初始化方法,则需要如下几个步骤
- 定义新的指定初始化方法,并确保调用了直接父类的初始化方法。
- 重载直接父类的初始化方法,在内部调用新定义的指定初始化方法。
- 为新的指定初始化方法写文档。
看一个标准的例子:
@implementation HPNewsViewController
//新的指定初始化方法
- (id)initWithNews:(HPNews *)news {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_news = news;
}
return self;
}
// 重载父类的初始化方法
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
return [self initWithNews:nil];
}
@end
在这里,重载父类的初始化方法并在内部调用新定义的指定初始化方法的原因是你不能确定调用者调用的就一定是你定义的这个新的指定初始化方法,而不是原来从父类继承来的指定初始化方法。
假设你没有重载父类的指定初始化方法,而调用者却恰恰调用了父类的初始化方法。那么调用者可能永远都调用不到你自己定义的新指定初始化方法了。
而如果你成功定义了一个新的指定初始化方法并能保证调用者一定能调用它,你最好要在文档中明确写出哪一个才是你定义的新初始化方法。或者你也可以使用编译器指令__attribute__((objc_designated_initializer))来标记它。
3. 所有返回类对象和实例对象的方法都应该使用instancetype
将instancetype关键字作为返回值的时候,可以让编译器进行类型检查,同时适用于子类的检查,这样就保证了返回类型的正确性(一定为当前的类对象或实例对象)
推荐这样写:
@interface HPPerson
+ (instancetype)personWithName:(NSString *)name;
@end
不推荐这样写:
@interface HPPerson
+ (id)personWithName:(NSString *)name;
@end
4. 在类的头文件中尽量少引用其他头文件
有时,类A需要将类B的实例变量作为它公共API的属性。这个时候,我们不应该引入类B的头文件,而应该使用向前声明(forward declaring)使用class关键字,并且在A的实现文件引用B的头文件。
// HPPerson.h
#import <Foundation/Foundation.h>
@class HPEmployer;
@interface HPPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) HPEmployer *employer;//将HPEmployer作为属性
@end
// HPPerson.m
#import "HPEmployer.h"
这样做有什么优点呢:
- 不在A的头文件中引入B的头文件,就不会一并引入B的全部内容,这样就减少了编译时间。
- 可以避免循环引用:因为如果两个类在自己的头文件中都引入了对方的头文件,那么就会导致其中一个类无法被正确编译。
但是个别的时候,必须在头文件中引入其他类的头文件:
主要有两种情况:
- 该类继承于某个类,则应该引入父类的头文件。
- 该类遵从某个协议,则应该引入该协议的头文件。而且最好将协议单独放在一个头文件中。
文件引入方式
- 在 .h 文件中尽量使用 @class 声明文件, 直到 .m 文件中真正需要的时候在使用 @improt 进行引用, 能有效的防止其它类引入此类时, 一并也引入了
- @improt 文件顺序: 可以先写引入系统文件, 依次到 Public.h 最后才到我们自己编写的文件;
- 检查文件,避免引入到没有使用到的文件,发现应及时清除;
- 当需要引入不同类型头文件的时候,应当按照一定的顺序 归组 引入;
- 首先引入系统头文件
- 其次引入非系统库的库文件
- 库头文件中 仍需 同一类型归组
- 引入 项目 源码 头文件
5. 类的API应该分组布局
#pragma mark - Life Cycle Methods
- (instancetype)init
- (void)dealloc
- (void)viewWillAppear:(BOOL)animated
- (void)viewDidAppear:(BOOL)animated
- (void)viewWillDisappear:(BOOL)animated
- (void)viewDidDisappear:(BOOL)animated
#pragma mark - Override Methods
#pragma mark - Intial Methods
#pragma mark - Network Methods
#pragma mark - Target Methods
#pragma mark - Public Methods
#pragma mark - Private Methods
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - Lazy Loads
#pragma mark - NSCopying
#pragma mark - NSObject Methods
13.)分类
1. 分类添加的方法需要添加前缀和下划线
推荐这样写:
@interface NSDate (HPTimeExtensions)
- (NSString *)hp_timeAgoShort;
@end
不推荐这样写:
@interface NSDate (HPTimeExtensions)
- (NSString *)timeAgoShort;
@end
2. 把类的方法实现代码分散到便于管理的多个分类中
一个类可能会有很多公共方法,而且这些方法往往可以用某种特有的逻辑来分组。我们可以利用Objecctive-C的分类机制,将类的这些方法按一定的逻辑划入几个分区中。
举个例子:
先看一个没有使用无分类的类:
#import <Foundation/Foundation.h>
@interface HPPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;
- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;
/* Friendship methods */
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(HPPerson*)person;
- (BOOL)isFriendsWith:(HPPerson*)person;
/* Work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;
/* Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
分类之后:
#import <Foundation/Foundation.h>
@interface HPPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;
- (id)initWithFirstName:(NSString*)firstName
andLastName:(NSString*)lastName;
@end
@interface HPPerson (Friendship)
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;
@end
@interface HPPerson (Work)
- (void)performDaysWork;
- (void)takeVacationFromWork;
@end
@interface HPPerson (Play)
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
其中,FriendShip分类的实现代码可以这么写:
// HPPerson+Friendship.h
#import "HPPerson.h"
@interface HPPerson (Friendship)
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;
@end
// HPPerson+Friendship.m
#import "HPPerson+Friendship.h"
@implementation HPPerson (Friendship)
- (void)addFriend:(HPPerson*)person {
/* ... */
}
- (void)removeFriend:(HPPerson*)person {
/* ... */
}
- (BOOL)isFriendsWith:(HPPerson*)person {
/* ... */
}
@end
注意:在新建分类文件时,一定要引入被分类的类文件。
通过分类机制,可以把类代码分成很多个易于管理的功能区,同时也便于调试。因为分类的方法名称会包含分类的名称,可以马上看到该方法属于哪个分类中。
利用这一点,我们可以创建名为Private的分类,将所有私有方法都放在该类里。这样一来,我们就可以根据private一词的出现位置来判断调用的合理性,这也是一种编写“自我描述式代码(self-documenting)”的办法。
14.) 单例
1. 单例不能作为容器对象来使用
单例对象不应该暴露出任何属性,也就是说它不能作为让外部存放对象的容器。它应该是一个处理某些特定任务的工具,比如在iOS中的GPS和加速度传感器。我们只能从他们那里得到一些特定的数据。
2. 使用dispatch_once来生成单例
推荐这样写:
static id sharedInstance = nil;
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
不推荐这样写:
+ (instancetype)sharedInstance {
static id sharedInstance;
@synchronized(self) {
if (sharedInstance == nil) {
sharedInstance = [[MyClass alloc] init];
}
}
return sharedInstance;
}
15.) 方法文档
一个函数(方法)必须有一个字符串文档来解释,除非它:
- 非公开,私有函数。
- 很短。
- 显而易见。
而其余的,包括公开接口,重要的方法,分类,以及协议,都应该伴随文档(注释):
- 以/* 开始:
- 第二行识总结性的语句
- 第三行永远是空行
- 第四行开始在与第二行开头对齐的位置写剩下的注释。
建议这样写:
/*
This comment serves to demonstrate the format of a doc string.
Note that the summary line is always at most one line long, and after the opening block comment,and each line of text is preceded by a single space.
*/
看一个指定初始化方法的注释:
/ *
* Designated initializer.
*
* @param store The store for CRUD operations.
* @param searchService The search service used to query the store.
* @return A HPCRUDOperationsStore object.
*/
- (instancetype)initWithOperationsStore:(id<HPGenericStoreProtocol>)store searchService:(id<HPGenericSearchServiceProtocol>)searchService;
16.)用队列代替同步锁:多用队列,少用同步锁来避免资源抢夺
多个线程执行同一份代码时,很可能会造成数据不同步。建议使用GCD来为代码加锁的方式解决这个问题。
方案一:使用串行同步队列来将读写操作都安排到同一个队列里:
_syncQueue = dispatch_queue_create("com.effectiveObjectiveC.syncQueue", NULL);
//读取字符串
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
//设置字符串
- (void)setSomeString:(NSString*)someString {
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
这样一来,读写操作都在串行队列进行,就不容易出错。
但是,还有一种方法可以让性能更高:
方案二:将写操作放入栅栏快中,让他们单独执行;将读取操作并发执行。
_syncQueue = dispatch_queue_create("com.custom.queue", DISPATCH_QUEUE_CONCURRENT);
//读取字符串
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
//设置字符串
- (void)setSomeString:(NSString*)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
显然,数据的正确性主要取决于写入操作,那么只要保证写入时,线程是安全的,那么即便读取操作是并发的,也可以保证数据是同步的。
这里的
dispatch_barrier_async方法使得操作放在了同步队列里“有序进行”,保证了写入操作的任务是在串行队列里。
17.)实现description方法打印自定义对象信息
在打印我们自己定义的类的实例对象时,在控制台输出的结果往往是这样的:
object = <HPPerson: 0x7fd9a1600600>
这里只包含了类名和内存地址,它的信息显然是不具体的,远达不到调试的要求。
但是!如果在我们自己定义的类覆写description方法,我们就可以在打印这个类的实例时输出我们想要的信息。
例如:
- (NSString*)description {
return [NSString stringWithFormat:@"<%@: %p, %@ %@>", [self class], self, firstName, lastName];
}
在这里,显示了内存地址,还有该类的所有属性。
而且,如果我们将这些属性值放在字典里打印,则更具有可读性:
- (NSString*)description {
return [NSString stringWithFormat:@"<%@: %p, %@>",[self class],self, @{@"title":_title,
@"latitude":@(_latitude),
@"longitude":@(_longitude)}
];
}
输出结果:
location = <HPLocation: 0x7f98f2e01d20, {
latitude = "51.506";
longitude = 0;
title = London;
}>
我们可以看到,通过重写
description方法可以让我们更加了解对象的情况,便于后期的调试,节省开发时间。
18.)几个OC系统类
1.NSArray&&NSMutableArray
- addObject之前要非空判断。
- 取下标的时候要判断是否越界。
- 取第一个元素或最后一个元素的时候使用firtstObject和lastObject
- 多用字面量语法代替其它写法;
若是元素很长需要适当换行:
NSArray *array = @[
@"This is how we do, yeah, chilling, laid back",
@"Straight stuntin’ yeah we do it like that",
@"This is how we do, do do do do, this is how we do",
];
2.NSCache
1. 构建缓存时选用NSCache 而非NSDictionary
如果我们缓存使用得当,那么应用程序的响应速度就会提高。只有那种“重新计算起来很费事的数据,才值得放入缓存”,比如那些需要从网络获取或从磁盘读取的数据。
在构建缓存的时候很多人习惯用NSDictionary或者NSMutableDictionary,但是作者建议大家使用NSCache,它作为管理缓存的类,有很多特点要优于字典,因为它本来就是为了管理缓存而设计的。
2. NSCache优于NSDictionary的几点:
-
当系统资源将要耗尽时,NSCache具备自动删减缓冲的功能。并且还会先删减“最久未使用”的对象。
-
NSCache不拷贝键,而是保留键。因为并不是所有的键都遵从拷贝协议(字典的键是必须要支持拷贝协议的,有局限性)。
-
NSCache是线程安全的:不编写加锁代码的前提下,多个线程可以同时访问NSCache。
-
多用字面量语法代替其它写法;
采用显示清晰的写法
NSDictionary *dict = @{
@"key1" : @"highway",
@"key2" : @"heart",
};
3.NSNotification
1. 通知的名称
建议将通知的名字作为常量,保存在一个专门的类中:
// Const.h
extern NSString * const kHPFoo_DidBecomeBar_NotificationKey
// Const.m
NSString * const kHPFoo_DidBecomeBar_NotificationKey = @"FooDidBecomeBarNotification";
2. 通知的移除
通知必须要在对象销毁之前移除掉。
19.)统一使用内容控制器模板
- 使用#pragma给代码 分层级归类 标注
- 针对大多数没有特别复杂逻辑的控制器实现文件, 我们采用制定好的一套 @implementation 实现模板.
使用#pragma给代码 分层级归类 标注:
#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 - Protocol conformance
#pragma mark - UITextFieldDelegate
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone {}
#pragma mark - NSObject
- (NSString *)description {}
采用制定好的一套 @implementation 实现模板
#import "HPDemoViewController.h"
@interface HPDemoViewController ()
@end
@implementation HPDemoViewController
#pragma mark - 1.Life Cycle
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//1.绑定viewModel
[self bindViewModel];
//2.设置view
[self setupUI];
//3.请求数据
[self setupData];
//4.设置通知
[self setupNotification];
}
#pragma mark - 2.Setting View and Style
- (void)setupUI {
//1.设置导航栏
[self setupNavBar];
}
- (void)bindViewModel {
}
- (void)setupNavBar {
}
- (void)setupNotification {
}
#pragma mark - 3.Request Data
- (void)setupData {
}
20.)写一个合格的@interface头文件
前面的篇幅,大多是零散的 规范性要求;我们在开发过程中,会规划设计一个模块,乃至细致到模块内部的类。于是乎,编写一个合格的 @interface头文件 也是有助于我们团队开发过程中,更好的开展工作的。
总结一些interface声明时的规范,相关宏的介绍,定义方法时有用的修饰符,编写注释的规范,最终写出一个合格的头文件。
-
20.1 读写权限
-
1、 实例变量的@public,@protected,@private关键字
-
- 读写权限
.h文件里的声明是用于暴露给外部的接口,而类内部的私有方法、私有属性和实例变量,应该放到.m文件的interface extension里。
-
- 实例变量的@public,@protected,@private关键字
这3个关键字用于修饰实例变量,不能用于修饰属性。当错误地使用了实例变量时,Xcode会报错提示。
关键字 说明 @private 作用范围只能在自身类 @protected 作用范围在自身类和继承自己的子类,什么都不写,默认是此属性。 @public 作用范围最大,在任何地方。 示例代码:
//HPSearchManager.h @interface HPSearchManager : NSObject { @public NSInteger *state; @public NSInteger *timeout; @protected id *searchAPI; @private id _privateIvar; } @end由于会暴露私有变量,并且没有@property的一些高级关键字,很少在头文件里声明实例变量。优先使用@property。
-
- 属性的readonly,readwrite关键字 头文件中的属性是用于描述这个对象的一系列特性集合。 声明@property时,在.h里使用readonly,让外部只有读的权限,在.m里使用readwrite,使内部拥有读写权限。
示例代码:
//HPSearchManager.h @interface HPSearchManager : NSObject @property (nonatomic, readonly) NSInteger * state; @end//HPSearchManager.m @interface SearchManager : NSObject @property (nonatomic, readwrite) NSInteger * state; @end -
- 属性的readonly,readwrite关键字
-
-
-
20.2 前向声明
当在@interface的接口里用到了其他类,不要在.h里直接导入类的头文件,这样会让使用此头文件的地方也导入这些不必要的其他头文件。
正确的做法是使用关键字@class进行前向声明。当然,如果是继承了父类,还是需要import父类的头文件。 示例代码://HPSearchManager.h #import "HPSearchManagerBase.h"//导入父类的头文件 @class HPLocationModel;//前向声明LocationModel类 typedef void(^HPLocationSearchCompletionHandler)(LocationModel *location, NSError *error); @interface HPLocationSearchManager : SearchManagerBase - (void)searchLocationWithKeyword:(NSString *)keyword completionHandler:(LocationSearchCompletionHandler)completionHandler; @end ```
使用@class会告诉编译器有这么一个类存在,但是现在并不关心这个类的具体实现,等到调用者在.m里使用的时候再import这个类即可。使用@class和@protocol分别声明一个类和一个protocol。 使用前向引用的原因有两个:
-
提升编译效率
- 如果import了
HPLocationModel.h,那么当HPLocationModel.h的内容发生变化时,所有import了HPLocationModel.h的地方都需要重新编译。 - 如果.m引用了
HPSearchManager.h,但是并没有使用HPLocationModel,就会增加不必要的编译,降低开发效率。
- 如果import了
-
解决交叉引用的问题
- 如果类A的头文件import了B,类B的头文件import了A,这样在编译时会报错:“can not find interface declaration”,这是因为Objective-C不允许交叉引用。
-
20.3 只暴露必要的接口和实现
- 1、 不要暴露任何只在类内部使用的私有方法
- 头文件里只声明那些给外部使用的公开方法,并且在设计时需要考虑到可测试性,遵循单一职责。
- 私有方法只定义在类内部,并且为了进行区别,建议在私有方法前加上前缀,例如
-(void)p_myPrivateMethod - 由于Apple在它的编码规范里声明了,Apple公司拥有下划线的方法前缀,就像它拥有
NS,UI这些类名前缀一样,因此不建议我们的私有方法直接使用下划线作为前缀。否则,当我们在继承Cocoa Touch的类时,有可能会覆盖父类的私有方法,造成难以调试的错误。 - 错误的示例代码:
//HPSearchManager.h @interface HPSearchManager : NSObject<NSCoding, UITableViewDelegate> @property (nonatomic, readonly) NSInteger * state; @end
- 2、 不要在头文件里声明类内部遵循的protocol
UITableViewDelegate是类内部使用时遵循的protocol,没有必要暴露给外部,因此应该放到.m文件里。 而NSCoding则描述了类的特性,用于告诉外部本类可以使用归档,因此应该放在头文件里。
- 1、 不要暴露任何只在类内部使用的私有方法
-
20.4 nullability说明
在声明时,可以使用下列关键字描述对象是否可以为nil。
| 关键字 | 说明 |
|---|---|
| nullable | 可空,用于描述objc对象 |
| nonnull | 不可空,用于描述objc对象 |
| null_unspecified | 不确定,用于描述objc对象 |
| null_resettable | set可空,get不为空。仅用于property |
| _Nullable | 可空,用于描述C指针和block |
| _Nonnull | 不可空,用于描述C指针和block |
| _Null_unspecified | 不确定,用于描述C指针和block |
示例代码:
//HPSearchManager.h
#import "HPSearchManagerBase.h"
@class HPLocationModel;
typedef void(^HPLocationSearchCompletionHandler)(HPLocationModel *_Nullable location, NSError *_Nullable error);
@interface HPLocationSearchManager : HPSearchManagerBase
- (void)searchLocationWithKeyword:(nonnull NSString *)keyword completionHandler:(LocationSearchCompletionHandler _Nonnull)completionHandler;
@end
如果向一个使用nonnull修饰的值赋空,编译器会给出警告。 在开发时,大部分时候使用的都是nonnull,因此Apple提供了一对宏NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END来进行快速修饰,写在两个宏之间的属性、方法,均会使用nonnull修饰。 示例代码:
//HPLocationSearchManager.h
#import "HPSearchManagerBase.h"
@class HPLocationModel;
NS_ASSUME_NONNULL_BEGIN
typedef void(^HPLocationSearchCompletionHandler)(HPLocationModel *_Nullable location, NSError *_Nullable error);
@interface LocationSearchManager : SearchManagerBase
- (void)searchLocationWithKeyword:(NSString *)keyword completionHandler:(LocationSearchCompletionHandler)completionHandler;
@end
NS_ASSUME_NONNULL_END
-
20.5 定义枚举
关于NS_ENUM和NS_OPTIONS的区别,参考这里。 简单来说,NS_OPTIONS提供了按位掩码的功能。
- 1、 NS_ENUM 示例代码:
typedef NS_ENUM(NSInteger,HPSearchState) {
HPSearchStateNotSearch,
HPSearchStateSearching,
HPSearchStateSearchFinished,
HPSearchStateSearchFailed
};
- 2、 NS_OPTIONS
示例代码,参考
NSKeyValueObserving.h:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew,
NSKeyValueObservingOptionOld,
NSKeyValueObservingOptionInitial,
NSKeyValueObservingOptionPrior
};
在使用时就可以用|组合多个option:
[_webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL];
- 3、 字符串枚举
当使用字典作为参数传递,或者作为返回值时,往往难以直接提供字典的key,现在使用字符串枚举即可解决这个问题。 示例代码,参考NSKeyValueObserving.h:
//使用NS_STRING_ENUM宏,定义了一个枚举类型
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey;
//使用泛型,声明了change参数用到的key,是在NSKeyValueChangeKey的枚举范围中
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
-
20.6 使用extern向外部提供只读常量
这不关@interface的事,但是和头文件有关,就放在一起说明了。
//HPSearchManager.h
extern NSString *const HPSearchErrorDomain;
extern NSInteger HPSearchDefaultTimeout;
@interface HPSearchManager : NSObject
@end
//HPSearchManager.m
NSString *const HPSearchErrorDomain = @"HPSearchErrorDomain";
const NSInteger HPSearchDefaultTimeout = 20;
@interface HPSearchManager()
@end
-
20.7 向子类和category提供父类的私有属性
由于类的头文件只存放那些暴露给外部的属性和方法,在遇到这些情况时,会遇到障碍:
- 在子类里或者category里,想要使用父类定义在.m里的私有属性。
- 在类的头文件里属性是readonly,但是在子类或者category里,需要readwrite权限。 由于这些属性并没有暴露在头文件里,因此需要另外建立一个私有头文件,用来存放这些需要暴露给子类和category的属性。 可以参考Apple官方的
UIGestureRecognizerSubclass.h。 示例代码:
//HPSearchManager.h
@interface HPSearchManager : NSObject
///外部访问,只有读权限
@property (nonatomic, readonly) HPSearchState state;
@end
//HPSearchManager.m
@interface HPSearchManager()
///内部使用,有读写权限
@property (nonatomic, assign) HPSearchState state;
///只在内部使用的私有属性
@property (nonatomic, strong) id searchAPI;
@end
///暴露给子类和category的私有属性和私有方法
//HPSearchManagerInternal.h
///限制使用此头文件,防止被别的类误用
#ifdef SEARCHMANAGER_PROTECTED_ACCESS
#import "HPSearchManager.h"
@interface HPSearchManager()
///在internal.h里,重新声明为readwrite权限
@property (nonatomic, readwrite, assign) SearchState state;
///暴露私有属性
@property (nonatomic, strong) id searchAPI;
///暴露私有方法
- (void)p_privateMethod;
@end
#else
#error Only be included by SearchManager's subclass or category!
#endif
///category的实现文件
//HPSearchManager+Category.m
///声明私有头文件的使用权限
#define SEARCHMANAGER_PROTECTED_ACCESS
///导入私有头文件
#import "HPSearchManagerInternal.h"
@implementation HPSearchManager(Category)
- (void)categoryMethod {
//拥有了读写权限
self.state = SearchStateSearching;
//可以访问私有属性
[self.searchAPI startSearch];
//可以使用私有方法
[self p_privateMethod];
}
@end
HPSearchManagerInternal.h其实也是公开的,其他类也能够导入并使用,只能在开发时进行约定。如果想要限制其他类导入,并且提示错误,Internal.h可以使用如下方式:
#ifdef MYCLASS_PROTECTED_ACCESS
//声明部分
#else
#error Only be included by MYCLASS's subclass or category!
#endif
这样在别的类内意外地导入了Internal.h时就会产生编译警告,并且无法直接使用。缺点是需要在所有使用到Internal.h的地方都#define MYCLASS_PROTECTED_ACCESS。
-
20.8 标明designated initializer
指定初始化方法,即接收参数最多的那个初始化方法,其他初始化方法调用它即可,这样设计的目的是为了保证所有初始化方法都正确地初始化实例变量。 在方法后面加上NS_DESIGNATED_INITIALIZER宏即可。这样,当你子类化这个类时,在子类的初始化方法里如果没有正确地调用父类的designated initializer,编译器就会给出警告。 实例代码:
@interface WKWebView : UIView
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
@end
21.)其他
1.Xcode工程目录管理
如附图:
- Xcode工程文件的
物理路径要和逻辑路径保持一致,物理文件和逻辑文件夹的必须一致; - (如第一个附图)推荐按层级管理工程目录
- 对外暴露AppDelegate .h .m 在工程目录的主目录,如附图中的标注1;
- 在Applications文件夹内放
应用工程源文件,若是单应用工程,可以直接分划分应用内部的结构如附图中的标注2,若是多应用就得隔离划分 公共模块 和 各应用的 自己的源文件; - 应用内部的模块 用Section节点文件夹 来管理,如附图中的标注3;
- Section文件夹内部的单模块,按照该模块的设计模式、架构、框架去划分。如附图中的标注4;
2.忽略没有使用变量的编译警告
对于某些暂时不用,以后可能用到的临时变量,为了避免警告,我们可以使用如下方法将这个警告消除:
- (NSInteger)giveMeFive {
NSString *foo;
#pragma unused (foo)
return 5;
}
3. 手动标明警告和错误
手动明确一个错误:
- (NSInteger)divide:(NSInteger)dividend by:(NSInteger)divisor {
#error Whoa, buddy, you need to check for zero here!
return (dividend / divisor);
}
手动明确一个警告:
- (float)divide:(float)dividend by:(float)divisor {
#warning Dude, don't compare floating point numbers like this!
if (divisor != 0.0) {
return (dividend / divisor);
} else {
return NAN;
}
}
4. 推荐 开发团队 统一 Xcode IDE版本
- Xcode 版本更新迭代很快, 建议定时到 App Store 进行更新到最新版;
- 同一个团队统一 Xcode版本 可以避免 由于官方对IDE更得,导致的 编译配置 差异 引起的适配问题;
5. 对于三方依赖包、团队私有库 的管理,统一使用 Cocoapods
- 依赖包管理工具有好几种,但是Cocoapods是最流行的,建议统一用Cocoapods管理;
- 使用 Cocoapods 统一对第三库进行管理, 建议定时更新到最新版本, 保持团队内部的版本一致;
- 关于引用三方库原则: github 上的项目必须要求 200 Star 以上;
- 对于 具备 ‘内网开发/拥有团队开发套件’ 条件的, 建议适当 将“三方库、二方库” ,要在建立 “三方库、二方库”的本地维护版本 添加到pod私有库去管理;
6. 添加 TODO.md
- 当在开发某个功能中遇到一些疑难杂症的问题, 但是又由于时间紧急, 来不及把细节做到完美,要进行一定的备忘录备忘;
- 如果在不影响当前功能使用的情况下, 为了防止在后面的开发中遗忘掉, 我们必须要在相应的地方添加注释 然后以 ==TODO:== 的格式, 接相关问题描述添加进来, 待后面有空闲时间之后回来进行修复完善;
- 备忘日志可以统一归档到
TODO.md文档中,冗余到工程根目录文件夹
7. 防止重复造轮子
- 在着手开发新需求之前,应先观察一下项目中,是否有已经封装好的相关功能类
- 如果已经有了,最好在当前类上添加 Catogory 进行扩展
- 如果没有,才添加新的功能类;
- 项目中的基础功能类, 以及对应封装的工具一致保存在 Class ---> Public。
8.关于缩写
Objective-C 本身就是让阅读者读起来就像在阅读句子一样的一门语言, 变量, 常量 特别是方法名相对其他语言来说会相对长一点,主要用意就是为了表达更加清晰明了, 尽量不让阅读的人产生歧义,并且基本能达到光看代码名字就知道其用意。
所以我们也应该严格遵守语言的设计风格, 和大家保持一致, 能有效的提高我们协同开发的效率同时,也能使我们能编写出更容易让大家接受的代码。
我们可以适当进行使用常用的缩写,但不能滥用缩写:
参考文档: Cocoa guide 可接受的缩略语和首字母缩略词
不推荐大家使用缩写,宁愿命名长一点,也一定要保证语义表达清晰的原则。
| Abbreviation | Meaning and comments |
|---|---|
| alloc | Allocate |
| alt | Alternate. |
| app | Application. For example, NSApp the global application object. However,“application” is spelled out in delegate methods, notifications, and so on. |
| calc | Calculate. |
| dealloc | Deallocate. |
| func | Function. |
| horiz | Horizontal. |
| info | Information. |
| init | Initialize (for methods that initialize new objects). |
| int | Integer (in the context of a C int—for an NSInteger value, use integer). |
| max | Maximum. |
| min | Minimum. |
| msg | Message. |
| nib | Interface Builder archive. |
| pboard | Pasteboard (but only in constants). |
| rect | Rectangle. |
| Rep | Representation (used in class name such as NSBitmapImageRep). |
| temp | Temporary. |
| vert | Vertical. |
9.iOS 资源文件命名与使用
9.1 【推荐】对于单一 资源文件命名需带前缀 ,譬如:模块名称|前缀/Scene描述|前缀 等类型的前缀。格式可为:前缀_具体用途描述_具体文件名 / 前缀_具体文件名
以什么样的前缀,可以根据该资源文件服务的范围和对象来决定:
- 模块名称|前缀: 若是该资源文件 只为某个模块服务,可命名为:module_xxx (如:业务模块Home_GoodsList_MockData、功能模块WebRTC_Default_CameraConfig);
- Scene描述|前缀:
- 若是该资源文件 只服务于某个场景,不限于某个模块,可命名为:scence_xxx(如:SocialShare_PlatformIcon);
- 若是该资源文件 服务于某个模块的该类型场景,可命名为:module_scence_xxx(如:ShoppingMall_GoodsDisplay_DefaultIcon);
9.2 【推荐】对于多个 资源文件 需要 归类到同一个文件夹目录,甚至是创建一个Bundle来存放
- 文件夹目录/bundle 需要以
(功能模块或业务模块)模块名/场景描述命名; - 文件夹/bundle 内部的 资源文件,可以直接使用 一个简明的描述;
五、总结
通过前面的介绍,我们基本知道了:
- 编码规范中的通用规范
- iOS编码规范
本篇文章,没有解决的问题如下:
-
1.提及了
组件化,没有深入探索相关的方法论和实践 -
2.提及了
《EffectiveObjective-C》的部分方法论,没有深入讨论原理 -
3.提及了 自动化 CodeReview 没有进一步 去讨论如何
自动化CodeReview检查 -
4.目前只是关注到了常见的点,还有进一步扩展的空间
-
5.本文提及到了
Block和GCD相关的使用规约,没有深入探讨其原理特性; -
6.文章内部 关于 工程文件 管理部分 提及了
多应用,本篇文章没有展开讨论相关要点; -
7.本篇文章提及了
《软件工程》相关的要点,但没有展开来介绍; -
8.本篇文章提及了
设计模式,但没有展开来系统介绍; -
...... 本篇幅文章提及到但没有深入讨论的诸多要点,欢迎广大朋友关注我的博客,后期排计划输出,进一步展开说明诸多要点;
-
参考: