近期重构工作的一点收获
更新:关于重构后增加的 bug 数量大家就不用多心了😂 我在团队是担任移动端 Lead 的,如果有重大失误,应该早就被拉出去祭天了吧。重构工作是两个月以前了的,结合这两个月的 issue 列表来看,引入的 bug 不多,最近在统计自己都做了什么工作,所以才把这篇文章分享出来。
以前做个人项目的时候,简历上写过重构了三次,后来在扇贝面试的时候,面试官问三次分别重构了什么,仔细想想那时候的重构并不算重构,第一次是 UI 改版,但是项目结构没什么大的变化,第二次是整体迁移到了 CocoaPods,这次勉强能算重构,第三次仅仅是变量名方法名空行这些地方的风格统一而已。
在现在工作的地方,接手这些项目之后,主要工作做的是重构,而重构工作,本来想写成一行,结果发现挺多,我列个列表吧:
- 删除没用到的第三方库
- 删除不合理的第三方库,使用系统自带的或者自己造轮子
- 删除定义好但是没有用到的变量
- 删除 import 进来但是没有用到的头文件
- 删除更旧项目留下来的用不到的逻辑
- Controller 层不合理的层级结构重构,无用代码清理
- View 层不合理的结构重构
- Service 层冗余的写法重构
- Model 层不合理的写法重构
- 拆开不合理的耦合
- 耦合一个类别的模块
- 修复了多处内存泄露
- 修复了多处循环引用
- 优化编译速度
- 消除项目中的 warning
关于删除代码,在某个项目里,Pods 文件夹那些第三方库的代码删了 9 万多行(那个目录没有被 git ignore 掉),项目里面删除了大约 4 万行,其中大量代码是该项目之前的项目里面留下来的东西,只不过没人清理。在删了 4 万行之后,程序仍然能完整的跑。
接下来是做了部分重构,把一些第三方库删掉,自己造轮子,在这个过程中,累计删除了 1.2 万行代码,增加了 1100 行左右。
整个重构工作下来,编译速度从 2-3 分钟减小到了 40 多秒,warning 从 70 多减少到了 0,第三方库的数量从 51 个减少到了 13 个,安装包从 22.1M 减小到了 3.7M,功能反而比之前还要多。
内存泄露方面,因为没人在意这件事,有一个功能使用一次,就会增加好几百 kb 内存,那部分代码是用 C 写的,所以及时释放内存,并且优化下调用方式,内存泄露的问题就完美解决。
循环引用方面,是因为有人把 Xcode 的 warning 关了,后来打开的时候,发现了四个循环引用 + 几十个 warning,并且测试过程中发现那个页面不断打开退出,程序会 crash。
笼统的就这么多,我再来分享几个具体的点。
避免滥用单例
单例用着确实爽,但是程序退出之前是不会被回收的,如果是整个生命周期基本用不到的模块做成单例,那么只会浪费内存而已。
避免无用的层级
具体是什么意思呢,用网络层举例子,封装 AFN 是一层,API 的后缀字符串放一层,构造请求放一层,OAuth 授权放一层,发普通请求又是一层。增加一个 API,至少要修改 6 个文件。写着也很痛苦,看着也很痛苦啊。
网络层就只设计一层,封装 AFN,发请求的函数也在里面,API 地址直接用字符串写进去,搞那么多层没实际意义,在这么小的一个项目里面。
除此之外,关于项目文件结构,一两个文件的建议不要新建文件夹放进去,这个主要是个人习惯,其实无大碍。
合理设计方法名
不留隐患
- (void)requestAtPathForRouteNamed:(NSString *)routeName object:(id)object parameters:(NSDictionary *)parameters
和
- (void)requestWithMethod:(XXHTTPMethod)method path:(NSString *)path params:(id)params paramsType:(XXParamType)paramsType
之前这样设计的目的是 param 放 form data 类型数据,object 放 json 格式,显然不合理,同一个 API 不应该允许同时存在 form data 和 json,如果采用第一种,新来的同事可能会认为这两个都可以填数据,这是不符合我们期望的。
甚至再极端一点,某天我们需要传文件过去,是不是还得再扩充字段。
如果采用第二种,param 是 id 类型,如果是 json,type 传入 json 枚举类型,如果是二进制,type 传入二进制枚举类型,只留一个字段暴露给开发者更合理。
避免耦合
我们的项目中有一条渐变颜色的线要到处用到,这条线我们放在了 UIImage+XXUtil.h 里面,之前的设计是这样的:
+ (instancetype)xx_navigationBarShadowImage
在 .m 的实现中,还把 UIColor+XXTheme 耦合进去了,并且这个方法已经脱离了类名 Util 的实质,他已经不是一个通用的工具了,重构之后的命名是这样的:
+ (instancetype)xx_gradientImageWithStartColor:(UIColor *)aColor endColor:(UIColor *)bColor andWidth:(CGFloat)width
这样就很符合 Util 这个 category 名字。
避免滥用继承
继承确实很好用,带来的后果就是子类会把父类的方法挨个执行一遍,乍一看没什么,但是如果这个方法很消耗性能呢。
我们这个项目就遇到了,app 经常卡死,用着用着,就 freeze 了,点哪里都没反应。因为所有页面都继承自基类的一个设计,恰好基类里面有一个比较耗时的操作,每个页面都会执行至少三次,就导致了页面假死。
重构后的做法是设计成一个 category,只是给 UIViewController 添加了几个方法,按需调用,不需要在每个页面都调用,于是解决了这个诡异的 bug。
合理选择第三方库
如果有一个功能,迫于各种原因,不得不采用第三方库,至少也要选一个 GitHub 上 star 比较多的吧,其次是看看 issue 列表有没有什么很严重的 bug 没修好,以及兼容性问题,多养成好习惯,慢慢就能筛选出来最合适的库了。
避免滥用第三方库
我们的项目之前有用到 YYText 这个库,就为了一段文字里面加一张图片,活动当天 iOS9 设备出现好几百次 crash,实际上这段代码用 NSAttributedString attributedStringWithAttachment
写一下,七行就够了,七行替代掉一个不稳定的第三方库,还是很划算的。
不知道因为什么原因,可能是更旧的项目里面用了 PSCollectionView,能跑就没去重构,这类库也是属于完全没必要的,系统自带的足够好用,并且更安全。
各司其职
数据的处理,比如字符串进行 UTF8 编码,时间戳转成 YYYY-MM-DD 字符串,这些都放在 Model 层来处理,各司其职,Model 层就是做数据处理的。
结合实际需求
后端把各种 ID 用 long 型来记录,是因为他们要做索引,为了索引速度。而客户端完全没这个需求,直接用 string 就好,还不用担心长度不够的溢出,做展示的时候还不用转类型。
同样的,金额按理说应该用双精度浮点型,因为 float 的精度不够,结合我的开发经验看,金额很少要客户端做加减,直接用 string 即可,需要计算的时候再转换,只转换一次,避免丢失精度。
关于命名规范
苹果的 UIKit 就是最好的例子,写什么组件不知道名字怎么起的时候,就想想苹果有没有类似的组件,去找找灵感。
避免无意义的注释
OC 的方法名本身就很长很清晰了,只是给方法名中间加几个空格,然后作为注释,跟没写一样吧。
不用的代码删掉
Git 的作用就是随时回溯以前版本,代码都是能找到的,把代码注释掉,再写一行类似的,除了增加阅读成本,容易引起歧义,应该没什么用了。一个文件一共一两百行,打开之后发现七八十行代码被注释了,这种感觉相当操蛋,影响阅读。
重构祖传代码真的会有一种活久见的感觉,上面提到的那些是印象比较深刻的,还有一些小问题已经悄悄解决了,希望我写的代码不会让后面的同学也这样认为吧。