5个导致主线程卡顿较鲜为人知的元凶

本文为nimbledroid CEO杨峻峰投稿,nimbledroid专注Android App的性能分析,比如启动速度、内存和流量的使用等,可点击「阅读原文」查看。

广大安卓开发者都知道主线程是APP用来响应用户交互的线程,因此应该避免执行耗时操作。通常来说,耗时方法导致主线程卡住超过16*N毫秒时,APP界面将会丢掉N帧。让我们把这些耗时长的方法称作卡顿函数(Hung Method)。在本文中,我们首先看一个典型的卡顿函数案例,接着仔细分析5个较鲜为人知的导致主线程卡顿原因。

Hollister中一个卡顿函数的例子

让我们分析一下Hollister v3.1.2中的卡顿函数例子:

5个导致主线程卡顿较鲜为人知的元凶prefetchData() 函数导致主线程卡住了很长时间

你可能已经注意到,AFSDK.prefetchData()函数执行了非常长的时间。而其中主要的执行时间耗费在了Jackson库的 ObjectMappger.readValue() 函数中。

幸运的是,Abercrombie & Fitch 的开发者利用 NimbleDroid 的性能分析工具检查出了这一问题,并在4.0.0版本中进行了修复,显著提升了APP的性能和用户体验。 :)

5个导致主线程卡顿较鲜为人知的元凶

新版启动时间为2.2秒,相较于老版本的5.2秒性能显著提升

让我们仔细分析一下冰柱图:

5个导致主线程卡顿较鲜为人知的元凶

数据解析的代码转移到了 RxCachedThreadScheduler-2 线程执行

显然这个bug的修复并不是特别复杂,开发者仅仅是利用RxJava库,把解析大量配置数据的代码放到了后台线程执行。

另一个类似的例子是Fox News,它在启动过程中解析数据耗费了700毫秒。


开发过程中有很多代码可能会导致主线程卡住,例如在主线程进行网络请求,文件读写,或者数据库访问。而上述这些操作都可以通过安卓开发者选项中的Strict Mode(严格模式)检测出来。但是,还有更多严格模式无法检测出来的卡顿函数,我们需要理解各种可能导致主线程卡住的原因,并且时常检查我们的APP中是否存在这样的问题,并进行修复。下面让我们一起看一下5个较鲜为人知的导致主线程卡顿原因。

1. 在主线程中解析网络返回的数据

尽管Akinator FREE app正确的在后台线程进行了网络请求,但是返回数据的解析却仍在主线程。

5个导致主线程卡顿较鲜为人知的元凶192次对 java.util.Scanner.nextInt 的调用耗费了273毫秒

这里开发者使用 java.util.Scanner 来解析数据,显然是较慢的,192次调用耗费了273毫秒。

2. 进行加解密(编解码)运算

Wiper 2.5是一款免费的消息和电话通讯APP,并且包含音乐和视频分享功能,它在主线程调用bitcoinj 库,导致了超过4秒的卡顿。

5个导致主线程卡顿较鲜为人知的元凶

调用 org.bitcoinj.params.MainNetParams.get 耗费了4060毫秒

再看另一个同样严重的问题,Kaave Falı 1.8.0在主线程调用java-aes-crypto 库来生成秘钥。

5个导致主线程卡顿较鲜为人知的元凶调用 javax.crypto.SecretKeyFactory.generateSecret 耗费1882毫秒

3. 初始化

初始化可能是一件很耗时的操作,把耗时的初始化操作放在后台或者懒惰初始化会更高效。FIFA就在调用Charset.availableCharsets()时耗费了269毫秒,而这个函数的调用发生在它使用的某个SDK中,但是需要注意的是,这一个问题可能和具体机型有关。

5个导致主线程卡顿较鲜为人知的元凶

调用 java.nio.charset.Charset.availableCharsets 耗费了269毫秒

4. 遍历资源文件

Talking James Squirrel在执行AssetManager.list()时耗费了大量的时间。

5个导致主线程卡顿较鲜为人知的元凶调用 android.content.res.AssetManager.list 耗费了378毫秒

另外需要注意的是,资源文件越多,这个问题就会越严重。

5. 线程同步

上面4点中我们提到的卡顿函数都是由于主线程持续运行而导致无法及时和用户响应,而还有一种情况,就是在主线程中进行线程同步,例如调用 Object.wait 或者 Thread.join。

为了验证这一问题,我们测试一下Crittercism SDK。在 Crittercism.initialize() 的执行中,开发者正确的创建了一个后台线程来执行耗时的网络操作,但是 initialize() 函数调用了 Thread.join() 等待后台线程执行完毕,导致调用 Crittercism.initialize() 的线程被阻塞。Hotel Tonight 8.5.0是这个问题的受害者之一,但是受害者远不止这一个。

5个导致主线程卡顿较鲜为人知的元凶

建议:定期检查APP,查看是否存在主线程被卡住的情况。把耗时操作转移到后台线程,持续编写高效的代码。


据说只有帅的人才会点GG,我不信!