微信号:bornmobile

介绍:专注于分享移动开发前沿和一线技术.

你知道AI可以高效测试移动应用吗?

2017-10-11 18:07 杨峻峰
作者|杨峻峰
编辑|覃云
作者简介:杨峻峰教授于清华大学获得计算机学士学位,于斯坦福大学获得计算机博士学位,在微软短暂工作一年后加盟哥伦比亚大学担任教授,他的创新成果获得多项美国最杰出的科研奖(NSF Career Award,Sloan Research Fellowship,和 AFOSR YIP Award),也被工业界广为采用(包括 Microsoft,VMWare,Coverity,和开源操作系统 Linux)。本篇文章为杨峻峰教授在 QCon 北京.2017 大会上的演讲内容。
   前言   

我是在大学的时候开始写程序,那时候觉得写程序是一个非常有意思的事,写几行代码就能干好多事。但那时候我特别不喜欢搞测试,或者是对测试脚本失败进行分析,不喜欢 debugging,不喜欢测试。后来我去读博士的时候就想能不能做一些比较先进的技术,能够帮助我们进行自动的测试、分析和 debugging。

在斯坦福读博士期间,就一直做程序员工具这方面研究,后来又去了微软工作一年,帮助微软的程序员搭建比较稳定的分布式系统,再后来我去哥大当教授了,也是研究这方面的东西,这些年来一直做开发者工具研究。

可能很多人都用安卓,操作系统实际上是 Linux,我们当年做了很多研究,能帮助 Linux 内核的开发者找出很多 bug。我在哥大教书之后,我的很多学生毕业之后去找工作,做移动开发,有安卓手机也有苹果手机上的应用开发,他们回来跟我说,应用开发是比较难的一件事,平台变化很快,设备很多,同时工具也不是很完整,所以后来我们就做了一些研究,怎么样能够帮助移动开发者提高生产力,帮助降低 bug?

后来我们这个研究成果获得了美国自然科学基金的一些奖,同时也得到了哥伦比亚大学的支持,我们就出来做了一个公司 NimbleDroid,公司是在纽约,我们还算是比较幸运的,拿到了 IDG 和一些其他的 VC 投资,现在我们有一些很大的客户,大概一半客户是财富 500 强的客户。

我也想再提一下,因为我们公司现在实际上是在海外运营,主要分析的都是海外应用,很多的经验可能不一定能在国内市场直接地借鉴或者全部借鉴。大家要想一想什么东西能够在实际的测试中使用,什么东西不能使用,要用我们的服务还需要 vpn,我们 30% 的用户都是中国用户,大家想出海的话,我们在这方面肯定是会提供帮助的。

现在应用开发实际上是一个很累的活,因为要快工出细活,原来都说要慢工出细活,就是做事要比较认真,出来的东西才质量好。但实际上现在的应用开发,product manager 或者是公司的管理层都希望能够很快把应用开发出来,同时也要保证质量好。

应用的质量实际上是很重要的,如果应用有崩溃、内存泄露、比较卡顿的话,用户用起来觉得不好,就把这个应用卸载了,或者用户给你写差评,这些会导致应用的安装率降低,公司的收入也会降低,所以应用质量是很重要的。

开发的速度其实也是很重要的,因为大家想一想,用户的需求和市场竞争都是日息万变的,屏幕上显示的这个应用是海外前几年非常流行的一个游戏,这个游戏出来之后没过多久就出来了十几个一样的游戏。如果开发比较慢的话,那别人可能很快就超过你了,所以开发速度也是很关键的。应用的质量很重要,开发速度也很关键,那我们到底应该要怎么做?

连续整合、连续提交

连续整合、连续提交实际上是一个很好的技术,就是说每次带来变化的时候 check in 到 repository 里,然后会有一个 CI 连续整合的服务器把代码 check out 出来,跑一些测试之后结果可以马上反馈给开发员。

那这有什么好处呢?第一个就是如果 CI 做了很好的测试集合的话,可以用这个测试来查一下每个版本提交有没有问题,可以帮你提高应用的价值。同时,如果你哪个地方写错了,提交代码里有 bug,可以保证你很快 fix 这个 bug,你不需要写很多代码,所以可以帮助提高应用的质量。同时对开发者来说,可以马上看到结果,可以提高生产率,不需要等一个月之后才发现代码哪有错,再想想当时干了什么,想了什么,才写出了有错的代码。而且如果把这些有问题的代码早早地找到了,那等最后要测试的时候时间就短了,提高开发速度,提高把应用放到市场上的速度。

但是连续整合实际上对测试的要求很高,每个版本出来之后请公司的测试人员帮你手工测试,实际上是不适合与 CI 整合在一起的,因为手工测试一般都比较慢,而且要是有很多的场景要测试,要请很多手工测试人员,这可能也会比较贵。假设你有一个应用,做一个变化都想请手工测试人员查这个变化,而且要查在十个不同的设备上的变化,这就花很多时间。

自动测试

自动测试听起来是很好的主意,写了脚本,每次版本提交的时候就跑这个脚本,跑完之后就知道提交的代码到底能不能通过测试。但实际上使用自动测试的不是很多,主要原因是现在自动测试还需要很多程序员的注意力,这是我们从 HackerNews 抓的一个评论,这个人在很多公司都工作过,但无论在哪个公司都没有把自动 UI 测试搞成功,实际上这是一个非常大的痛点。

为什么很难做?实际上自动测试有好几步,第一步要写脚本,这个脚本是一个非常低层的脚本,然后会把这个脚本跑在手机上或者某个云上,如果某个脚本失败了,要人去检查到底为什么失败,大部分时候你会发现失败是这个脚本的问题,或者说测试的置有问题,而不是真正的代码里有 bug。而且以后每次代码更新的时候,或者每次 UI 更新的时候,可能脚本还需要更新一下,不然脚本还会失败。

看一个具体的例子,比如我们要做一个 shopping app,要测试这个应用,那我们怎么做呢?我们都知道这是一个用来买东西的应用,那肯定要测试买东西的几个场景,比如说把要买的东西加在购物筐里,然后再看看购物筐里是不是还有这个东西。我们是有背景的,我们知道东西是应该怎么运作的。

然后再看界面,你知道图片上面实际上是一个裙子,底下写的多少钱,如果今天有折扣,比如今天买了 100 元,它会给我返 20 元,对话框出来了,人知道这个对话框实际上和要测试的东西是没什么关系的,就把它跳过了。人来测试都是很简单的,但实际上要是用来做自动化的话,要写脚本来处理一些东西,实际上是很难的,比如对话框出来的话,脚本就会失败,这是其中一个现在自动化测试不好的原因。

还有一个原因就是现在自动化测试对结果是不做分析的,失败就失败了,人再去看怎么回事。它为什么不做分析?比如应用界面原来写的文字的购物筐,如果更新变成一个图形的购物筐,原来的测试脚本只认购物筐的字,而没有看图片,那测试脚本就会失败。

而且程序运行的时候会有各种各样的问题,比如卡顿或者内存泄漏,这些东西现有的自动化测试系统是不帮你查的,但如果程序用了很长时间,内存用太多了崩溃了,那你找这个原因要花很多时间。还有自动化测试的可能拿设备云或者几台机器跑跑测试脚本,可能和后台的连接有问题了会导致测试脚本失败,失败之后现有的测试系统需要人去查到底是为什么,而不会帮你做分析告诉你实际上脚本失败是因为 USB 线坏了。

 AI 测试 

我们现在做的东西就是用 AI 来解决测试的问题,如果我们拿 advanced 的程序分析,比如可以看程序做了什么,可以对代码进行理解,同时我们拿非常高级的 AI 技术,看一下屏幕上显示的是什么,放在一起可以让测试变得非常自动化。

刚才讲完了我们很大的一个梦想,实际上我们现在在朝着梦想努力的第一步,做一个系统,能够帮助自动测试每一次代码提交。

这个系统有几部分,程序员不需要写脚本,我们能够看应用上面是什么东西,可以爬这个应用,就像爬虫一样,通过这个能发现 app 到底能够做什么事,这是我们第一步自动化的地方。

之后爬出每个场景放到我们的设备云上来跑,之后我们可以帮你做结果的分析,比如说可以帮你量非常精确的性能,用户把一个东西加到购物筐里花了多少时间,同时我们也帮你查崩溃、内存泄漏或者有一些卡顿的东西可以帮你自动查。之后这个结果放到我们的云里,这样的话所有团队成员都可以看测试结果是怎么样的。

这就是我们系统的架构,实际上团队的成员,比如开发人员、测试人员、经理都可以用这个服务,把你的应用发到云里,我们进行分析进行测试,把结果存到我们的数据库里,同时把结果显示给团队的全部用户。

服务可以直接到网页手动上载或者可以和 CI 结合到一起上载,每上载一个应用大概过 15 到 20 分钟,你会收到一个结果。第一页是应用相关的信息,再往下走,你会看到有很多自动发现的用例,比如应用启动的时间,每个用例发现了之后,我们都会进行各种分析,比如里面有多少 bug,性能怎么样,用了多少内存,用了多少网。

我们会更加细致地分析每个用例,比如有多少卡顿,比如说 1 月 31 号的版本,启动时间是 5 秒,现在变成了 5.5 秒,变慢了,再往下走,你会发现我们告诉你应用里有什么问题,卡顿现象都有哪些,比如说其中的一个卡顿现象,会给你显示一下应用在什么地方花的时间,比如每个函数调用的时间记录下来,同时把 call stack 记录下来,都放在图上,看这个图就可以知道应用到底在哪里花的时间。

我们还会显示一下应用的网络分析,因为有的应用要在网上下载很多数据,或者后台太慢,导致这个应用会变慢。除了这个卡顿,我们还可以帮助程序员找内存泄露的现象,因为对某些应用来说,如果不停地泄露内存的话,随着实际使用时间的增长,最后应用可能就会没有内存可以使用,这样它会变得很慢或崩溃掉,这个崩溃是很难查的,那我们对每一个用例都查一下到底哪个地方有内存泄漏,同时会告诉开发者内存是怎么泄露的。

开发者还是比较喜欢我们的产品的,放了一些开发者给我们的反馈。

我们也把结果放到了国外的一些开发者大会上来讲,同时美国自然科学基金给了我们一些研究上面的奖励。

我们实际上用这个系统分析了非常多的应用,下载量都是很大的,几亿几十亿用户的应用。

App 启动时间

我们现在要讲一个具体的问题,应用启动时间的问题,这个问题是比较重要的,因为用户用你的应用之前先要启动你的应用,如果启动的时间长,那用户可能就不用你的应用了,所以这个对用户的使用率是很关键的。

用户启动你的应用大概有三种不同的方式,一个叫冷启动,就是用户一段时间没有用你的应用了,操作系统已经把这个应用杀掉了,用户再启动你的应用,实际上是冷启动。

还有另一种情况是第一次启动,是应用刚刚装完之后第一次启动,这时候操作系统,或者应用要做很多初始化,是比较慢的,但这个启动出现的场景是不多的。

还有一个 warm start,就是用户正在用,然后后台突然有通知,然后用户再返回到你的应用,因为应用的很多数据都放在内存里,这个启动是非常快的,所以我们主要关心的是应用的冷启动。

很多非常流行的应用,我们研究了一下它们的启动时间是多少,结果发现 30~40% 的应用,启动时间都是小于 2 秒的,开发者实际上对应用的启动时间是非常重视的,因为启动慢会导致使用应用的用户变少,这对公司业务来讲是不好的。


影响启动时间的五大因素


那应用启动慢到底有什么原因呢?我们也分析一下研究的结果,发现主要有五个原因。应用用了很多 reflection、dependency injection,在 main thread 里做了很多的事,有的应用用了 Java 的方式来访问资源,还有一个原因就是第三方的 SDK 用了比较慢的东西,导致你的应用跟着变慢了。

  Reflection

从第一个原因看起,我们就查一下 reflection 在安卓里面的性能,我们写了一个 micro-benchmark,是一个很小的程序,跑很多遍,然后看时间是多少来看 reflection 的性能。

这个结果显示,从现在比较流行的安卓版本和设备来看,实际上 reflection 的性能是非常有意思的,有 reflection 比没有 reflection 要慢 4 到 20 倍,发现 ART 让 reflection 性能变慢了许多,所以这个就表明 reflection 实际上在安卓中实现是很慢的。

我们的用户有纽约时报,开发人员发现很多问题,后来他们写了博文,把纽约时报应用从原来启动大概 4-5 秒,最后变成 1. 几秒这么一个过程,其中他们就讲到 reflection 问题。

他们怎么修这个问题的呢?他们首先没有用 reflective adapters,他们用的是 custom type adapters,导致他们花在 data parsing 的时间直线下降。

  Dependency injection

下面再看 dependency injection,实际上就是软件设计的 pattern,这有两种不同的方法,一个叫 dynamic,就是在程序运行的时候做,还有一个叫 static injection 就是在程序编译、build 的时候做。

我们发现 Rogo Guice 是 dynamic,是非常非常慢的,它可能比 static injection 要慢 5 倍或者更多,举了几个例子,比如 skype、American Express、Groupon 动态转成静态的之前之后加速比,有些应用会有 0.8 秒的加速。

所以尽量用 Dagger,而且尽量用 Dagger 2,因为它是纯静态的。

  Too much work in main thread

Main thread 干了很多的活,导致应用卡顿比较多。

看一看都有什么原因,大家都知道 UI thread 是处理 UI 的,用户点击了屏幕或者要更新屏幕都是用 UI thread 来做,如果 UI thread 要干很多事,程序响应就会慢,就会出现卡顿现象。那你可以用 Android’s Strict Mode 来查这个 UI thread 是不是连了网络或者是后面的数据库。

我们还看到很多其他的问题是 UI thread 抓不到的,比如 JSON 有很多数据过来,然后 parse 数据的时候可能会导致 UI thread 变慢,实际上不访问到网络也不访问 storage。我们还看到过有的应用是进行一些加密解密,这本来都是很慢的操作,这些操作也会导致卡顿现象。

这也是一个很有趣的应用,这个应用实际上是一个外国的衣服品牌。这个应用启动的时候花了 2.6 秒时间 parse 这三个 JSON 文件,这三个文件实际上包含了全美每一个店的地址和各种信息,加起来是很多的数据,然后 parse 的时间是很长的。

我们有一次演讲的时候也讲了这个问题,开发人员知道之后就回去把它的应用加速了,后来又上我们的服务查一下到底变了多快,你会发现通过优化,它的应用从原来五点几秒的启动时间变成了两点几秒,优化还是很快的。

对 UI thread 来说,由于它和用户打交道,它用来更新 UI,或者是处理用户的点击屏幕,所以最好不要在 UI thread 里干很多事,要把 UI thread 应该干的事情放到后台去做。

  ClassLoader.getResourceAsStream() and the like

下面再讲这个问题也是很有意思的一个问题,这个叫 ClassLoader.getResourceAsStream(),如果大家以前是写 Java 的,后来写了安卓就知道是用来访问 Java 文件里 property 的一个方法,比如说这段代码写在 Java 里跑起来很快,但如果写在安卓里有一个 property,然后最后你用 ClassLoader.getResourceAsStream() 去访问这个 property,就加了这么一句话,会让你的应用启动慢 1.7 秒,这是为什么呢?这是因为 getResourceAsStream() 在安卓里是非常慢的。

首先调这个函数的时候,安卓要把你的 APK 文件打开,然后把里边所有的 resource 都要 index 一遍,而且 validate 你的 APK 是对的,加起来实际上 load 三次,然后要至少读两次,导致有很大的 overhead,所以大家千万不要用 getResourceAsStream() 函数。

  第三方 SDKs

刚才讲的这些问题都是大家可以避免的,但是有时候你用第三方的 SDK 里边有问题的话你就避免不了了。这些 SDK 是比较头疼的问题,因为 SDK 不是你写的这个代码,你要是想用的话,得想办法把比较慢的坑绕开。

那我们现在就来总结一下,对开发者来说哪些 SDK 是比较慢的。这个图实际上就显示了十个 SDK,其中有 TapJoy,快速生成程序的平台。还有一个东西叫 Joda-Time,这是一个非常流行的一个时间库。

Joda-Time 这么慢是因为它有 Time zone 要跨时区带来很大的 overhead。如果写程序非常简单,就在静态初始化一个 DateTime(), 就会导致程序启动至少慢400 毫秒。

对这个小程序来说,实际上问题跟刚才 getResourceAsStream() 是一样的,在生成第一个 DateTime object 的时候会用 getResourceAsStream() 从它的文件里读时区的数据,这个导致它花了很多时间,所以你应该用自己的 DateTimeZoneProvider 或者用 date4j。

除了 Joda-Time,还有好多 SDK 也调用了 getResource(),其实这个问题还蛮普遍的。

对最流行的应用来说,大概有 11% 应用被影响了。

    总结    

今天给大家分享了我们美好的梦想,就是将来可以用人工智能来帮我们做测试,用程序分析来帮我们分析为什么测试会失败。然后也讲了一些我们现有的系统,它有一个爬虫可以发现 app 有什么用例,然后对每个用例可以进行分析,看它的性能是什么样的,有没有内存泄露、性能问题、卡顿现象。后来又给大家讲了一下,程序启动慢有哪些原因,比如 reflection,dependency injection,UI thread 不要做太多工作,getResourceAsStream(), 同时使用专门为安卓设计的 SDK。

活动推荐:

Google如何用AI造聊天机器人?Pinterest如何用机器学习获得两亿活跃用户?10月QCon上海站,还有来自Uber、Paypal、LinkedIn、Airbnb等顶尖技术专家前来分享前沿实践经验。

QCon报名即将结束,识别下方二维码或点击【阅读原文】与100+国内外技术大咖零距离,如有问题欢迎联系票务经理Hanna ,电话:15110019061,微信:qcon-0410

 
移动开发前线 更多文章 苹果在GitHub上正式开源iOS内核源码 对话式UI解读 你知道怎么用LLDB调试Swift吗? Swift 4正式发布,新功能概览 Kotlin OR Java?快来站队
猜您喜欢 携程Android App插件化和动态加载实践 开源组件:Universal Image Loader 【独家实录】刘海一:医院大数据体系的规划实践 Laracon:Laravel 5.3 总结 《Spring 5 官方文档》24. 使用Spring提供远程服务和WEB服务