iOS专项总结
一个应用经过多次迭代后告一段落,接下去我们在技术上还可以做些什么呢?答案是提高代码的整体质量。关于这方面,除了我们常喊的 重构
,测试也非常重要。
博主近期给我们的 iOS客户端代码来了一次专项测试
。主要从常规的 辅助测试
入手,来了次代码大清理,找到代码中的问题,并一一改掉它们。惊喜的是,这对于提高本人的代码水平有很大的帮助。其实,这套代码的质量本身已经很高了,也非常整洁。而这主要得益于严格的代码规范和pull request机制。
关于测试,App常关注的往往是一些功能性的,包括单元测试,用monkey在界面上点击看页面表现是否正确等等。 我以前还搭过一个 aspectJ + robotium。(这是Java上的) 然而,测试更应该覆盖代码质量,性能检测等等。
下面给出一副我理解测试的结构图:
点击图片可放大缩小。
顺着这幅图,我们可以从静态测试入手。
关于 analyze
在Xcode 中执行product -> analyze工具,出现了粗粗细细的箭头标识表示调用逻辑。
这个工具所找出的主要是代码中的逻辑问题,本身能发现的问题很多。实际运用到工程代码上,发现的问题主要分为以下四点:
- 比如初始化赋值未被使用过。
- 比如未调用super语句。
- 比如从未使用过的变量。
- 比如逻辑上错误的代码。(这点极少)
但是关于资源文件,该工具是扫描不出来的。另外,我们可以在Xcode中配置该工具。
Clang 静态分析器
analyze 的底层其实就是 Clang 静态分析器,包含 ’shallow’ 和 ‘deep’ 深浅两种模式。
深模式比浅模式慢很多,因为多出了跨方法的 控制流分析
以及 数据流分析
。(如前文那副图)
建议:
开启分析器的 全部检查(方法是在 build setting 的 “Static Analyzer” 部分开启所有选项)
在 build setting 里,对 release 的 build 配置开启 “Analyzer during ‘Build’“。(真的,一定要这样做 — 你不会记得手动跑分析器的。)
把 build setting 里的 “Model of Analysis for ‘Analyze’“设为 Shallow(faster)
把 build setting 里的 “Model of Analysis for ‘Build’“设为 Deep
但这两种对于公司这么一个大项目来说都不快,假如在 build setting里 开启 Model of Analysis for ‘Build’ 并设为浅模式,那么每次运行就会整体跑一遍静态分析并不合理。
还有一种设想是在打 ad hoc 包前自动跑一遍 静态分析,是比较合理的也可以在XCode中开启。但是我们编译打包用的是自动化工具,服务器直接从github读取而不是通过XCode,会跳过这个分析。
比较合理的做法是定期 跑一遍 analyzer 工具。(比如每周五)而不是 build , release 以前做这事。(问了下其他同事的意见)
目前静态分析在工程中有 这么几块不改的问题: 首先是网上拷过来的算法。包括加密算法,模糊算法,64位编译算法等等。里面有一些远大于,远小于,与运算等。看不懂的地方不改。
为什么不改呢?因为没看出逻辑问题。(或者看懂了也不敢改,怕算法错误。)
Slender
slender 是一款针对 iOS 图片分析的 Mac 软件。导入工程,可以找到所有未使用到的 resource。(unused 这软件也可以找到多余的资源)
除此以外,它还给出了所有资源文件的建议。包括你缺省的 x3, x2图片版本等。
Faux Pas
意外发现的惊喜。Faux Pas 是一个出色的静态 Error 检测工具。
slender 找到的是未使用的资源,faux 找到的是代码中使用到,但不存在的image。(实测下来往往有很多)
甚至由此 发现了一些弃用的文件夹。
不仅如此,它有一百多个error 和 warning 规则。包括 未定义的 user define runtime attributes(跑在runtime上)。这一点平时靠程序员的肉眼狠难见。曾经你为了图省事直接在 xib 中炫技,加上了一些 key。然而当代码变迁,删掉一个类的时候,根本想不到 到 xib中去删除 对应的部分。
还有一些代码规范上的问题。
比如界面上的文字未本地化;在release 版本依旧有NSLog 输出;NSPhotoLibraryUsageDescription在info.plist中未定义;被定义成strong类型的delegate等等。
和一些配置上的建议。
Warning
你的pods 中还在报 warning 吗?你可以使用下面这种方法屏蔽它们。
在podfile 的开头加上这样一句话:
inhibit_all_warnings!
就能屏蔽 pods 中所有代码上的 warning了。
然后可惜的是,对于编译上的 warning却束手无策。注:iOS中不能阻止 Xcode 报warning 的选项。只能屏蔽某一文件,或某工程的warning。比如我们已屏蔽的pods中 代码的错误。但是屏蔽不了系统在编译过程中 非代码级别的
警告。
并且也不建议屏蔽这种警告,比如由于友盟未支持 7.0 版本所导致的 警告。现在由于 友盟本身未支持,7.0 用户还有许多等等原因,但未来或许能解决。不应忘记这点。
Leaks
苹果集成了一个性能检测工具,叫instruments。使用instrument中的Leaks工具:
iOS内存泄露点比较少,大部分都是系统内部方法,或者一些库里的。我们对此别无办法。实测下来,主要的泄露场景集中在 UIKeyboard 上。在我尝试了各种点击场景后,系统弹框时候最容易泄露。
假如你的代码中真有泄露,那它们可能集中在快速滑动 Listing的时候,加载大批量图片的时候。在我们的应用中,这些大都使用了网上开源的成熟库。
对于 iOS可能泄露的地方,我的建议还是通过已知的代码规范 逆向去寻找。比如 误用 strong类型的delegate; block 中没有使用 weak self等等。
Time Profiler
时间都去哪儿了。这个工具可以找出我们最耗时的操作并定位到代码中。
实际使用过程中,确实发现了一处不合理的耗时循环。
(右侧黑色部分是耗时操作,但不一定是错误的代码)
工具只是统计时间的消耗,更重要的是通过对代码的敏感,分析定位出可优化的代码。举个例子,有这么一段代码。
有个循环在判断时候使用了枚举类型,但由于枚举类型获取不到count,所以直接写死了循环10次。
但其实我们可以在枚举类型的末尾加上TYPE_MAX。这样既拥有了枚举在switch时候的优势,又得到了NSArray获取count的办法。
加载时间
加载时间上,主要看看不同网络下出首页有没有卡死等。公司wifi条件真机上测试大约 1.2s 内加载完毕,而当模拟边缘网络时主线程加载依然迅速,没发现什么问题。
关于启动时间,有两种查看方法。一种是通过上文的时间检测工具,另一种是直接在代码中打log。其中log又有两种打法。
分别说下这两种打法:
CFAbsoluteTime StartTime;
int main(int argc, char **argv) {
StartTime = CFAbsoluteTimeGetCurrent();
// ...
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Launched in %f sec", CFAbsoluteTimeGetCurrent() - StartTime);
});
// ...
}
这是通常的打印方法。然而,从main 开始启动有太多的不可控因素。
iOS App启动过程
- 链接并加载Framework和static lib
- UIKit初始化
- 应用程序callback
- 第一个Core Animation transaction
其中 framework的加载过程我们难以控制,而初始化字体、状态栏、user defaults、main nib等的过程,我们也不需要关心。当然,你可以做的是保持它们尽可能小,没有冗余。
所以,还有一种做法是记录你在 didFinishLaunchingWithOptions
中所耗费的时间。在这里,你可能初始化了许多不必须的第三方库,做了几次网络请求。而这些,我们都可以 lazyLoad,让程序尽快地冷启动。
帧率等
如何优雅地显示帧率标签?
在 QuartzCore.framework
里,有一个 CADisplayLink 类。系统在渲染每一帧的时候,都会调用一次CADisplayLink中的 selector, 它有点像一个定时器类,默认为一秒调用60次。
帧率大部分时间都稳定在 60fps,然而有两种情况下会导致它不能以每秒60次的频率调用回调方法。
- CPU忙于其他计算,无暇执行屏幕绘制动作。
- 执行回调方法所用时间大于重绘每帧的间隔时间。
我们可以利用这个类的特性,写出如下代码来实时显示一个 屏幕帧率 的标签。
CADisplayLink displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
- (void)tick:(CADisplayLink *)displayLink {
//...
self.numberOfFrames++;
NSTimeInterval delta = displayLink.timestamp - self.lastTimestamp;
// Less than one second
if (delta < 1) {
return;
}
CGFloat fps = self.numberOfFrames / delta;
//Update label
....
}
这个fps 的数值,便是每秒帧数。实测当下拉刷新,或加载List过快时,都会往下掉得很厉害。而这便是性能瓶颈所在的关键点。
本文系冰洁原创,遵循 署名-非商业性知识共享进行许可. 转载请在文章开头显眼处注明注明作者和出处 【冰洁】http://www.bingjie.me