阅读 942

iOS developer的良好习惯

前言

隐藏细节,暴露抽象。

作为一名有追求的工程师,我们希望代码能够在版本迭代中逐渐优化而不是劣化;同时也会学习掌握更多的技巧和工具,去更好的设计、实现和组织代码。偶然看到一个apple工程师的分享,于是加上一些自己的经验和感受,做一些总结。

正文

一、代码组织

1、使用group

作为一名iOS工程师,Xcode应该是最熟悉的工具之一。旧版本的Xcode在新建一个目录时,只会作为创建一个引用,不会同时在相同的路径下去创建目录。新版本Xcode创建目录的时候都是以group的形式去创建,会在同级路径下去创建对应的目录。
比如下图,在创建New Group的时候,就会同样在Audio的目录下去创建一个New Group的目录。如果项目的代码是很久以前的Xcode创建的,最好检查一遍目录,使得Xcode的工程文件目录和实际的文件目录结构保持一致;如果项目是新Xcode创建则尽量在Xcode中创建group。

2、拆分大文件

如果项目有使用storyboard,则可以把较大的storyboard文件,通过引用的方式拆分成多个storyboard。这样能提升打开时的速度,也能使得多人协同开发时减少冲突的产生。
但是我经历过的项目都没有使用storyboard,大文件的矛盾更多是产生在.m文件,以一个我们项目中的文件为例:

这个2000行的.m文件并不是一蹴而就,而是随着十几个版本的迭代,逻辑不断增加,慢慢变大的文件。这也是我们常说的历史技术债务。技术债务产生的原因多种多样,可能是最开始的时候没有很好的框架设计,也可能是实现过程中有不规范的现象,又或者是多人协作开发导致的代码膨胀。当发现问题之后,就需要去偿还这个技术债务。

.m文件拆分首先需要把业务的核心逻辑梳理出来,抽象出来该模块的状态信息、关键参数,将外部业务在.m内添加的逻辑改为依赖.m提供的状态,而状态可以通过通知、消息等方式抛出去;
核心但是又内聚的逻辑可以使用xxLogic去封装,然后.m文件直接依赖该xxLogic;也可以将其聚合到.m文件的一个Category之中;
经过这番处理,.m文件能得到极大瘦身,而梳理完内外部依赖之后,后续再新增逻辑也不用去查看.m文件,而是依赖下面的.h文件。

3、重视Xcode的提示

保持Xcode工程设置是最新的,使用Xcode自带的性能优化。当Xcode弹出下面这个框的提示时,如果没有特殊诉求,apple工程师推荐点击Perform Changes按钮。

在编译的过程中,Xcode给出的warning可能在线上运行时就是一个Bug。建议在debug开发阶段,打开Treat Warnings as Errors选项;追本溯源,找到问题的根本原因,解决每一个编译期间的warning。

如果是已知问题,暂无解决方案,为了避免阻塞编译运行,可以使用xcode指令去忽略。(具体的warning类型可以在Xcode的Issue Navigator查看,快捷键是command+5)

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// ingore code
#pragma clang diagnostic pop
复制代码
4、去掉无用的代码

我们有时会提交一部分被注释代码,理由可能是代码现在不需要但下个版本可能会用到,临时注释一下,反正不影响运行。但是设想一下,如果团队里面每个人都有这个习惯,那么项目中是否会存在很多无用的代码?并且这个代码可能永远也不会有用武之地。
所以,果断地删除那些无用代码吧,即使真的有需要用到的时候,也可以通过代码的版本控制工具去找到那些历史代码。

二、代码管理

版本控制系统已经成为开发的必备工具之一。曾经svn也是版本管理的高效工具,Windows系统中的小乌龟(TortoiseSVN)非常好用。但是随着git的出现,svn已经被逐渐淘汰。

1、提交独立

一个复杂功能往往由多个需求点组成,开发过程也可能持续数天时间。可以把需求的提交拆分成多次,尽量使得单次提交独立,Xcode可以看到每一行代码的提交备注信息。 换位思考,我们希望从git的commit信息里面,看到这段代码的缘由。
点一下右边对应信息,选择show commit,还可以看到对应commit的具体内容。

一个人可以记住昨天为什么写这段代码,但很难记住一段数月乃至数年前的代码为何出现。

2、分支管理

为了保持开发阶段的便利,提供alpha分支,作为日常开发的合入分支;为了保证外网代码的可查,提供beta分支,作为版本发布的打包分支;当版本发布之后,还需要打tag记录对应版本,比如说release_1.0.0.10。
日常的需求开发(feature分支)、问题修复(bug分支)都是在非主干分支进行开发,最终再合入alpha分支。合入的要求根据团队实际情况,可以是分支验收完成再合入,也可以合入后统一验收。

3、Code Review

Code Review(代码审查,后面简称CR)是发生在分支合入的情况,是成熟开发团队必不可少的环节。CR有助于团队代码风格的统一,包括函数命名、变量命名、代码组织风格等。同时,CR要求代码具备一定的可读性,也要求单次提交不过包括过多改动。

三、文档

1、必要的注释

好的代码一目了然,能清晰描述逻辑,不需要注释来辅助描述。但是一段特殊逻辑,需要有注释来描述为何存在,以方便在改动之后去回归影响点。
比如说一段经典的dispatch_after 1秒的逻辑,这1秒可能是为了避免某些异常case,也可能是产品侧的需求要求。

2、对外方法的描述

平时的开发过程,除了注意变量和方法的命名要具有含义,对外提供方法的注释可以清晰描述需要的参数。比如说下面的一个方法:

在Xcode中选择对应的方法,按下快捷键option+?就可以看到该方法的描述,以及各个参数的要求。如果方法还没添加描述,则按下option+command+?自动生成待补充的描述。

3、文档积累

随着业务的发展,项目中代码不可避免的会快速膨胀,直接阅读代码会非常吃力。此时就需要有文档来辅助了解各个模块的情况。
文档应当避免对具体逻辑细节的赘述,更是和从整体的设计和考虑的因素出发,描述该模块是如何运行起来。同时在设计的过程,也应该基于之前的技术方案设计。

培养团队的写文档习惯,每个版本前期组织技术方案评审,由负责较为复杂需求的工程师准备一份技术方案的设计文档,可以达到事半功倍的效果。

四、便捷工具

大家提到Xcode的分析工具,第一反应往往是Instrucment中的工具集。但是实际开发中还有一些便捷工具。

1、Network Link Conditioner

模拟弱网络环境,以前是在手机的设置-开发者-Network Link Conditioner可以去设置,现在真机连接之后可以在Xcode中按下command+shift+2,选择对应的设备就可以选择具体的网络环境。

2、 Address Sanitizer

Address Sanitizer是内存错误检测工具,通过malloc/free增加标记实现。 比如说下面这一段代码,buf指针创建了1024内存,再手动释放,然后再去访问buf指针的元素。这段代码编译时正常,在运行时不一定会崩溃 ,有可能就会演化成一个偶现bug,难以定位。 在使用Address Sanitizer工具的时候,运行到130行时就会报错:Use of dealloccated memory

打开方式是在scheme选项中,勾选Address Sanitizer。

3、Thread Sanitizer

Thread Sanitizer是线程错误检测工具,可以检测到一些多线程数据访问的错误,比如说下面的代码。 sTestNum是静态全局变量,创建了多个线程去操作该变量,会触发Data Race

打开方式是在scheme选项中,勾选Thread Sanitizer。

Thread Sanitizer关注的是数据的多线程访问,通过记录内存的访问来实现,并不能定位到多线程的crash问题,比如下面这个crash:

4、Main Thread Checker

Main Thread Checker是多线程操作UI检查工具,UI操作只能在主线程执行,如果在子线操作则会触发警告。

打开方式是在scheme选项中,勾选Main Thread Checker。

5、Debug Gauges

在debug运行程序的时候,Debug Gauge能快捷地查看CPU、Memory、Disk、Network信息。

打开方式是Xcode按下command+7。

五、开发建议

1、最小依赖原则

一段逻辑的运行,往往需要外部的变量输入。有时候为了便捷开发,函数调用时候不会传递参数,而是通过全局变量、self指针等直接去获取需要的数据。但是这样会导致代码逻辑紊乱。在编码的时候,非常建议使用最小依赖原则:尽可能少的使用外部依赖。
以函数为例,一个xx逻辑处理的方法应该只依赖函数参数。这样函数的输入输出是固定的,即使函数放到其他地方,只要保证函数的输入不变,则逻辑的输出是不变的。
同理,除了函数还有view、model等等,尽可能少的去依赖外部数据、外部模块,则该处逻辑更加独立,更容易实现可以直接复用的view、model等等。

2、组件化&模块化

实现功能的时候,应尽可能去除耦合;特定功能组成的库就是组件,写新功能代码尽可能要往组件方向实现;而模块化指的是根据业务形态,把代码按照功能、业务进行聚合,相当于组合了各种组件和业务逻辑的库。
模块化和组件化等一个重要特点就是Pod化,将这些特定、独立的功能代码和业务代码从主工程中剥离,抽象出来业务需要的接口,再重新通过pod依赖引入主工程。在这个过程,不单单是把代码转移到Pod库,还需要做一些业务的解耦和依赖抽象。
好处也是显而易见:
开发上,模块化后各个业务相对独立,能够更加专注自己业务逻辑,即使业务出错影响面也比较可控;
效率上,模块化后可以做二进制组件,加快编译速度;
管理上,组件owner的意识更强,方便添加数据监控;
架构上,强迫面向接口编程,避免大量耦合的胶水代码。

总结

本文部分参考自 WWDC2019,结合一些工作经验,做了更适合自己的阐述。
自己也梳理了接下来一段时间的技术优化方向:
日常业务迭代,通过CR保证新增代码风格统一;
复杂业务需求,需要做技术方案评审,集思广益;
已有历史债务,小模块微整实现,大业务走专项重构,注意人力投入、业务影响和收益评估。

文章分类
iOS
文章标签