cover_image

像少年般飞驰——QQ阅读Android版性能优化实践

飞蛾 阅文技术
2017年12月25日 09:50



135 editor

图片

随着产品的快速迭代,功能的不断增加,我们很容易就忽视了针对App的性能优化。作为一名普通的开发同学,为了保证开发和测试效率,大部分时间使用的都是最新的、最高端、速度最快的手机。无形中,我们就忽略了低端用户的产品体验。考虑到我们的产品还有一部分使用较低端手机的用户,近期我们针对低端机对QQ阅读Android版本进行了一系列性能优化。

图片

图片

QQ阅读启动页流程

闪屏页(SplashScreen),一般是应用启动后最先被看到的页面,可以说是给用户第一印象的页面。从点击桌面图标到看到闪屏的时间间隔则起到了至关重要的作用。响应时间越长,给用户的第一印象越糟糕;反之,秒开App无疑会给用户的第一印象大大加分。因此我们先从App的启动速度优化说起。


1
App启动速度优化


首先放一张优化前后的对比图

图片

从截图可以看出,从点击APP图标到闪屏页面,对于低端机型效果还是非常明显的如:

  • 三星gt-i9300:配置略低,启动速度可以提升260%左右;

  • 红米note:配置一般,启动速度可以提升140%;

  • 对于中端机型也有一定效果,如小米5c:配置较高,启动速度可以提升64%;

  • 而对于小米6这类高端机型,因为本身配置的多核cpu、gpu运算能力很高,所以优化效果并不是特别明显。


1.1
计算Activity启动时间的几种方式


a、最为精准的adb指令

借助adb指令中的am指令中的am start指令来启动并打印Activity的启动时间。 
首先我们看下am start指令的用法:

图片

图片

这里我们可以直接使用am start -n{package}/{package}.{activity}来启动. 
例如:这里我们启动一个测试的MainActivity,adb shell am start -W com.test/com.test.MainActivity

图片

返回结果中的三个时间表示的含义

  • ThisTime:该activity启动耗时

  • TotalTime:应用自身启动耗时=ThisTime+应用application等资源启动时间

  • WaitTime:系统启动应用耗时=TotalTime+系统资源启动时间



b、更为灵活的TimingLogger

TimingLogger是安卓官方提供的一个轻量级打印时间的工具类。这个类的注释中也介绍了用法
图片

TimingLogger相对于adb命令的方式,无疑更加灵活。


1.2
App启动速度分析


低端手机的App启动瓶颈在哪里呢?表面上无非三点。

  • CPU和GPU运算能力差

  • 运行内存低

  • IO性能差

看起来我们把启动慢的因素全怪手机硬件了(似乎看到了手机扬起帅气的小脸,说了句:“怪我喽?”),那大环境无法改变的情况下,我们还是要从自身来寻找原因,缓解手机性能差带来的App启动速度慢问题。


优化思路 

安卓App的初始化逻辑大家都是轻车熟路,这里就不过多解释了。

  • 在Application的构造器方法、attachBaseContext()、onCreate()等关键方法中尽量减少调用耗时方法,将非必要数据放到非UI线程(工作线程)中初始化

  • 延迟创建非必要的私有进程和全局进程

  • 对于SplashActivity(闪屏Activity)的UI尽量减少布局层次,onCreate、onStart、onResume方法和其他UI线程调用的方法中尽量不要做耗时操作

  • 减少SplashActivity占用的内存,尽可能减少gc次数

  • 初始化期间减少SharedPreferences的读写


1.3
QQ阅读做了哪些优化


aApplication

快速的迭代中,QQ阅读加入不少业务模块,例如:游戏、直播、订阅、推送等等,不少都需要在Application中做初始化操作,或者需要启动独立进程。我们仔细分析Application中的不同模块业务逻辑,必要的加载时机,以及各个模块的耗时情况,对于Application采取了如下的优化措施。

  • 将Application中的业务代码模块化,各个进程只加载必须的业务模块,减少Application启动的耗时和浪费系统资源

  • 延迟启动和初始化非必须立即使用的的私有进程,防止抢占系统资源

  • 使用UI线程(主进程)和非UI线程(工作线程)分别延迟加载一些非必须立即使用的业务模块


b、闪屏Activity

对于闪屏页,也存在一些业务的初始化操作,对于这部分,和Application中类似,使用非UI线程(工作线程)加载耗时操作、延迟加载等等。

除了这些,闪屏页还会加载一个运营模块,大部分情况会是一张大图。因此这里会迎来第一个内存高峰,对于低端机型也可能会产生多次gc,导致卡顿。

很久以前我们针对闪屏做过防止OOM的处理,其中包括:

  • 使用相对较低的画质模式RGB_565来读取图片,以减少单位像素Bitmap使用的内存大小

  • 根据当前可用内存,使用动态采样,进一步降低画质,防止OOM

除了这些,我们再次进行了代码重构,减少了Bitmap的创建和decode和缩放次数,进一步减少了内存占用。

经过了上面的一系列优化,才有了文章开始的优化效果。


2
阅读页启动优化


阅读页作为QQ阅读最重要且最常用的页面,对其的性能要求一直是我们关注的重点,就阅读页启动时间这一项来说,用户当然是希望从点击一本书的书封开始到完全显示出文字内容所需的时间越短越好。在硬件配置较高,安卓版本较新的高端手机上,阅读页的打开时间似乎可以满足用户的心里需求,但是在配置较低,安卓版本比较陈旧的手机上,阅读页的打开速度就不那么理想了。于是我们本次针对低端机的阅读页启动速度进行一些优化。


2.1
优化前阅读页概况


其实随着版本迭代的次数不断增多,阅读页的一些最初的设计可能已经不太适用,但是由于阅读页承担的功能越来越多,相互之间的逻辑和依赖非常复杂,代码量也很大,要完全重构是非常困难的,我们只好先根据启动流程去梳理一下哪些可以作为优化点,先进行一些动作较小但可行性高的优化。


a、优化前启动流程

使用前文提到过的android系统提供的TimingLogger工具类,在阅读页启动的各个重要位置打上时间戳,结合代码可以得出整个阅读页目前主要的启动流程及各部分耗时情况。如下表。

注:

1.由于所有时间均上下浮动不固定,以下数据均为多次采样后取的接近平均值的一组

2.测试手机型号ZTEGT718C Android 4.4.2 API19。

3.书籍尾页是一本书翻到正文的最后一页之后再翻一页会出现的一页特殊的页面,上面会有一些推荐书籍,活动入口等。


主线程

耗时占总比



onCreate

9.4%

     |-initView

8.8%

     |-post->书籍尾页初始化


onResume

13.3%

系统间隔

11.3%

onWindowFocusChanged

0.3%

     |-sendMessage->doFocusChanged


系统间隔

14.3%

书籍尾页初始化

9.2%

doFocusChanged

42.2%

     |-initData

32%

     |-文字排版

10%

文字显示在屏幕上

*其中蓝色部分为了特别标明:

1.书籍尾页初始化是由onCreate中post执行

2.doFocusChanged是由onWindowFocusChanged中发送message执行


b、耗时分析及优化方法

根据上表分析,可以很明显得出目前阅读页启动过程中存在的主要问题:

  • 总体来看,目前阅读页启动是一个Activity的标准启动流程,其中所有数据和view的初始化都在一个主线程串行流程中完成

  • onWindowFocusChanged执行后到最终的文字显示出来还有很大一段时间用来初始化数据,也即用户会先看见空白页面,要等一段时间才能看到文字

  • initView耗时较高,该方法主要是是用来初始化阅读页的基础view

  • initData耗时非常高,其中包括获取阅读页所需的书籍文本数据及设置相关的view

  • 书籍尾页初始化耗时高,虽然是由oncreate中post执行,但仍然会占用主线程的时间


于是我们就可以针对上面这些问题,结合代码提出一些优化方法:

  • 将串行结构改为并行结构,将initData中的数据和view初始化剥离开,数据部分放在子线程优先初始化,数据准备完毕再将交由主线程绘制到view上。这样可以利用多线程并行节省掉数据初始化的时间,在onWindowFocusChanged之后很快看到文字

  • initView方法中有一些view因为需求或逻辑变更已经不需要在oncreate中初始化,可以延迟至文字排版之后初始化

  • 根据需求和交互,书籍尾页不需要在用户一打开阅读页时就立刻初始化,所以可以在全部阅读页初始化流程结束后发一个message来延迟初始化

  • 优化代码逻辑,收敛方法出入口,合并重复执行的代码,使得代码既简洁又高效

  • 对布局较复杂或层级较深的view进行布局优化(本期未实施,移至后续版本实施)


2.3
优化后阅读页概况


a、优化后的启动及耗时

主线程

子线程

耗时占总比

onCreate


19.1%

    |-initView


17.6%


initData


onStart


0.3%

onResume


12.6%

    |-onShow


11.8%

系统间隔


0.5%


    |-初始化书籍数据

42.4%


    |-sendMessage ->initDataFinished


系统间隔


10.6%

onWindowFocusChanged


1%

    |-doFocusChanged


0

系统间隔


0.3%

initDataFinished


13.2%

    |-文字排版


12.9%

    |-initRest


0.2%

    |-post->书籍尾页初始化



文字显示在屏幕上

总体相比优化前时间已缩短52%


之后为用户不关心的时间



系统间隔



书籍尾页初始化






优化效果:最终使得从Activity启动到文字显示的整体时间缩短52%,速度提升107%


b、优化后总结

由上表可见,使用子线程可以使得数据初始化与主线程的正常启动并行,相比串行会大大节省时间,逻辑优化后也会使得初始化正常有序,可以节省一些系统调度和资源抢占的等待时间。并且书籍尾页初始化移至最后可以使其不占阅读页启动的时间,在阅读页正常启动之后再去初始化。

最终使得从Activity启动到文字显示的整体时间缩短52%,速度提升107%。


3
图片缓存优化


在低端手机上除了启动速度以外,还有一个显著的性能问题就是内存消耗问题。我们的app由于加载图片较多,所需要的内存比较高,高端手机的内存一般比较充足,可以满足我们的需要,但是低端手机配置不高,经常因为我们申请内存时抛出OutOfMemoryException(OOM),所以我们需要想一些办法来缓解一些内存的消耗,减少低端机的OOM发生。


3.1
使用Glide统一加载本地图片优化内存


  • 系统启动Acitivty时,在对布局进行初始化时,布局中的Background Drawable需要申请为图片分配内存,系统不会立即GC来使用哪些可回收的内存,仍然会分配可用剩余内存,从而导致内存峰值不断增高,由于是在整个App可使用的最大内存数范围内,由系统管理整个内存的分配与回收,使用简单的缓存机制,遇到内存不足时会导致应用的崩溃

  • 解决思路是,使用Glide统一管理加载本地图片的请求,使用Glide的固定大小的内存缓存池,通过Glide高效的缓存调度来回收和重新分配内存,所有请求都在内存缓存池内分配,使应用总的内存峰值不增加,而Glide缓存池的大小比系统的可用内存数小的多,从而避免因内存不足而导致整个应用崩溃

图片

图片


实现方案:

根据需求,对在布局内加载的资源按照其(width)*(height)*(bit-depth)乘积进行排序,优先替换大内存图片

优化后数据对比:

手机:华为P7-L00,操作路径:启动->闪屏页->主页->侧边栏

主页侧边栏背景图600x1280文件大小50.48K

Context加载drawable,decode bitmap默认RGBA_8888,消耗内存约为600*1280*4=2.92Mb


图片


3.2
largeHeap与OOM


如果一个应用在其manifest文件中加入android:largeHeap=“true”标记,则会请求系统为Dalvik虚拟机分配比标准更大的堆内存空间,通常按照Google标准,一个应用最大可使用的堆内存为192Mb,largeHeap设为true后,可以申请系统给与最大512Mb的堆内存空间,各个手机厂商又对ROM系统进行了不同的设置

使用ActivityManager.getMemoryClass函数可以查看系统可以提供的标准的最大内存数量,ActivityManager.getLargeMemoryClass函数可以查看加入largeHeap标签后系统可以提供的最大内存数量

开启了largeHeap设置后,意味着应用有了更多可以使用的内存空间,在对内存使用较多的场景如视频、图片处理等应用有很大的帮助。但我们应谨慎的开启使用此标签,应用在内存不足时抛出OOM错误意味着系统GC过后仍有大量内存被占用无法释放,此时应从查找泄露、已有内存是否被合理的分配使用的角度进行检查,如果简单的开启largeHeap功能,则只能延后OOM发生的时间,并不能在根本上解决内存不足发生OOM的问题。


图片


4
SharePreference拆分优化


在我们程序中,广泛使用了SharePreference(SP)来存储一些配置值,但是随着配置项越来越多,读写SP越来越频繁,SP相关的性能问题也暴露了出来:

  • SP字段存储过于集中,绝大部分字段存储在同一个config文件中。

  • config文件过大,内存占用大。

  • 目前绝大部分使用commit,不必要耗时。


4.1
优化方案


针对以上问题我们也对SP进行了一些优化:

  • 根据业务拆分config文件,每个独立的业务模块使用单独的config类及sp文件,后续使用也依照这个规则创建新config类和使用新的SP文件。

  • 迁移旧的数据。

  • 提供方法控制执行apply或commit操作。apply异步操作文件。


4.2
优化效果


a、读写操作

  • 写操作,取样10个字段进行写操作,统计操作时间,单位ms。操作耗时由8.46ms提升至0.31ms。

图片

  • 读操作,取样10个字段进行读取操作,统计操作时间,单位ms,操作耗时如下。下表中业务1CONFIG和业务2CONFIG是由原config文件拆分生成。

图片

由上面的结果可见,执行时间上,由于写文件操作改为异步,写操作提升明显。读操作有很小的提升。


b、文件大小

将原来的一个大文件文件拆为若干个各司其职的小文件,不仅总体文件大小有所降低,而且逻辑更加清晰,单独的小文件相比一个大文件来说读写也更加高效。


5
总结


以上是我们本次针对低端手机对QQ阅读的性能所做的一些优化,在硬件配置低这个无法避免的客观条件下,尽可能找到了一些办法来对目前的代码进行改变,使得低端手机用户也可以从应用启动、阅读页启动、程序稳定性等多个方面提升使用感受。而这次的优化也只是打响了我们对程序性能优化的第一枪,可能还存在一些瑕疵或者遗留了一些未解决的问题,这些我们还将在后续的版本里继续跟踪,找到最优的方法来处理,不断提升QQ阅读的性能指数。


编者按:

图片

飞蛾

目前就职于阅文集团内容中心,主要负责分布式文件系统的研发工作。关注分布式系统,数据存储,网络编程,linux系统,机器学习等相关领域。


本文由QQ阅读android终端技术团队“悦来悦好”、“查理不会冲浪”、“。ZQN”、“chyli”、“大耳朵隗”、“涛涛涛”等同学参与创作。




继续滑动看下一个
阅文技术
向上滑动看下一个