iOS专项总结

  一个应用经过多次迭代后告一段落,接下去我们在技术上还可以做些什么呢?答案是提高代码的整体质量。关于这方面,除了我们常喊的 重构,测试也非常重要。

  博主近期给我们的 iOS客户端代码来了一次专项测试。主要从常规的 辅助测试 入手,来了次代码大清理,找到代码中的问题,并一一改掉它们。惊喜的是,这对于提高本人的代码水平有很大的帮助。其实,这套代码的质量本身已经很高了,也非常整洁。而这主要得益于严格的代码规范和pull request机制。

  关于测试,App常关注的往往是一些功能性的,包括单元测试,用monkey在界面上点击看页面表现是否正确等等。 我以前还搭过一个 aspectJ + robotium。(这是Java上的) 然而,测试更应该覆盖代码质量,性能检测等等。
  下面给出一副我理解测试的结构图:

  点击图片可放大缩小。

顺着这幅图,我们可以从静态测试入手。


关于 analyze

在Xcode 中执行product -> analyze工具,出现了粗粗细细的箭头标识表示调用逻辑。

image

这个工具所找出的主要是代码中的逻辑问题,本身能发现的问题很多。实际运用到工程代码上,发现的问题主要分为以下四点:

  • 比如初始化赋值未被使用过。
  • 比如未调用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图片版本等。

image


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工具:

image

iOS内存泄露点比较少,大部分都是系统内部方法,或者一些库里的。我们对此别无办法。实测下来,主要的泄露场景集中在 UIKeyboard 上。在我尝试了各种点击场景后,系统弹框时候最容易泄露。

假如你的代码中真有泄露,那它们可能集中在快速滑动 Listing的时候,加载大批量图片的时候。在我们的应用中,这些大都使用了网上开源的成熟库。

对于 iOS可能泄露的地方,我的建议还是通过已知的代码规范 逆向去寻找。比如 误用 strong类型的delegate; block 中没有使用 weak self等等。


Time Profiler

时间都去哪儿了。这个工具可以找出我们最耗时的操作并定位到代码中。

实际使用过程中,确实发现了一处不合理的耗时循环。

image

(右侧黑色部分是耗时操作,但不一定是错误的代码)

工具只是统计时间的消耗,更重要的是通过对代码的敏感,分析定位出可优化的代码。举个例子,有这么一段代码。

有个循环在判断时候使用了枚举类型,但由于枚举类型获取不到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,让程序尽快地冷启动。


帧率等

如何优雅地显示帧率标签?

image

QuartzCore.framework里,有一个 CADisplayLink 类。系统在渲染每一帧的时候,都会调用一次CADisplayLink中的 selector, 它有点像一个定时器类,默认为一秒调用60次。

帧率大部分时间都稳定在 60fps,然而有两种情况下会导致它不能以每秒60次的频率调用回调方法。

  1. CPU忙于其他计算,无暇执行屏幕绘制动作。
  2. 执行回调方法所用时间大于重绘每帧的间隔时间。

我们可以利用这个类的特性,写出如下代码来实时显示一个 屏幕帧率 的标签。

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过快时,都会往下掉得很厉害。而这便是性能瓶颈所在的关键点。