“一本正经地胡说八道” 用日语怎么说?大概是「真面目にふざけている」吧。这篇日志大概就是这么一个意思?
一直以来都想对 Android APP 开发的性能调优做一下总结,其实性能调优涉及到多方面的工作,每次有一些心得我都会记录下来,零零散散记录了很多,最近发现许多地方重复了,感觉还是得做一下整理的,知识就是这么牢固起来的。
“APP 卡顿” 是一个问题,我们既需要知道怎么查找出哪里造成卡顿,也需要掌握规避这些卡顿的技巧,所以这个话题可以分为 “如何定位 APP 中的性能问题” 和 “提高性能需要注意哪些点” 这两部分,后续在陆续对这两点展开讨论吧,今天先从整体分析下问题存在的原因。
开始正题之前先让我吐一吐苦水吧。
我个人喜欢日语,所以学了很久的日语了,同样我也是因为喜欢 Android,才开始跳进了 Android 开发这个坑。不过非常遗憾的是,就和 “当初我在日语班里,周围大部分人只是因为日语专业比较轻松才选了它” 一样,我周围的 Android 研发同事大部分用的是 IOS 手机,甚至有人问过我 “我说你工资也不至于那么低吧,怎么每天都拿着安卓手机”。产品则是每次都说 “你这个交互和 IOS 的不一样,我需求文档里写得清清楚楚,要保持一致的用户体验”。设计的话,我从来就没有遇见过拿安卓手机的。
我尝试说过 Android 不比 IOS 差,但是没人站在我这边。
自从 Android 系统诞生以来一直都有一个疑问,“为什么 Android 手机这么卡?”,无论 Android 设备的硬件再怎么升级,版本再怎么迭代,“Android 比 IOS 卡顿” 似乎成为一个板上钉钉的事实。Android 手机真的卡么?
至少在 Android Kitkat 之前,许多 Android 开发者都会选择回避这个问题;Kitkat 之后,有一部分开发者已经有底气回答这个问题了;随着 Android Lollipop 以及 Marshmallow 先后的出现,我觉得 Android 开发者都可以自信地回答说 “Android 不卡” 了。
在一个公司内部分享会中,国内 Android 领域的大神罗升阳在分享他对 ART 模式的研究中说到,继 Kitkat 采用了 ART 模式后,Lollipop 中,除了 UI 线程之外又提供了一条专门用于执行动画的线程,这样一来 UI 线程就可以专注于展示数据和响应用户的输入,这让 Android 的流畅度又上了一个档次。IOS 是闭源的所以我们无从得知,但是有人分析说 IOS 之所以这么流畅很大一个原因就是它大概也采用了类似动画线程的机制。老罗也相信 “Android 系统不会比 IOS 卡,甚至已经比它还流畅”。
这可不是随便说说,我的 Nexus5 升级到 Lollipop 之后我觉得它已经可以匹敌同事的 IPhone6 了,前不久升级到 Marshmallow 内测版,我更是觉得它已经拉开 IPhone6 一个档次了。
那为什么许多人觉得 Android 用起来还是比 IOS 卡?注意老罗说 “Android 系统” 比 IOS 流畅,而不是 “Android 应用”,言下之意就是 Android 系统本身不卡,卡的是设备开发商开发的 ROM 以及开发者开发的第三方 APP。我个人觉得 Android 卡顿问题大致有以下的原因。
客观原因
Android 设备系统升级速度慢
现在 Lollipop 甚至 Kitkat 的普及率还不算很高,更别谈 Marshmallow 了,这是 Android 碎片化的问题决定的,具体原因有兴趣的可以自己 Google,网上一堆比我在这里吹的靠谱多的分析。
当然这里也有很大一部分是设备开发商的锅,Google 开源的 AOSP 项目只能兼容 Nexus 系列手机的硬件,如果第三方设备开发商需要使用 AOSP 的话,最起码也要把 AOSP 里面的驱动部分改成能兼容自己的设备的,此外,他们还喜欢把系统 UI 风格做成自己家的,这起码也要自己写一个 Launcher 应用和自定义主题(这也是许多 Android ROM 被吐槽成换皮肤的原因);此外有一些特色功能,比如指纹设备,Android Marshmallow 之前 AOSP 并没有这个功能,所以开发商就得自己出解决方案了。这一系列的工作,造成了开发商无法在 Google 发布 Android 的新版本之后迅速升级自家 Android 设备的系统。
Android APP 需要做过多旧版本的兼容
Android4.0 版本相比之前的版本性能上优化了许多,无奈我接触的 Android 产品都要求最低支持 Android2.3,有一款 SDK 甚至要求支持到 Android1.6 而且不能使用 Support 库(今年可是 2015 年!你能想象一个手动写 Thread 去控制一个复杂的属性动画的效果有多糟糕吗?),所以 Android 开发者需要做一大堆向下兼容的工作,有时候为了保证在一些奇葩机型的兼容性,选择了保守的实现方式而不采用 Android 的新特性。向下兼容的逻辑使得 APP 即使在高级版本的 Android 系统上也要跑一堆没用的判断逻辑,如果这些逻辑出现在循环体内则更糟糕;采用保守的实现方式,使得 APP 无法发挥新版本 Android 的性能,即使用户手机升级了 Android 系统版本也享♂受新版本带来的体验。此外,即使许多新的 APP 产品都选择最低支持 Android4.0,这使得许多新特性都不用 Support 库支持就能直接使用,但是许多手机设备还是无法升级到 Android4.4 版本的系统,即使有,很多 ROM 还是把 ART 模式给严格了,无法体验其脱胎换骨版的顺畅。
大量采用 “黑科技”
最近支付宝被 Google Play 下架了,原因就是其 “从 Play 市场以外的服务器下载可执行代码”,意思就是它使用了动态加载技术。Android 的动态加载也不是新鲜的事物了,我的项目中也采用过,简单来说原理就是 Android APP 采用 “APK 空壳 + 可执行代码” 的开发方式,APK 只是一个空壳,用户安装过一次后就不用重新安装了,如果需要升级 APP,APK 空壳会从服务器下载新的可执行代码,更换本地的即可完成升级(具体实现方案现在网上一堆教程,也可以参考我 Github 上的相关项目)。采用这种开发方式,开发者可以迅速完成用户安装好的 APP 的升级,在某种意义上提高了用户的体验,但是开发者的开发方式也会变得比较 “绕”,开发成本增加了不少,为了保证兼容性也会放弃使用 Android 的一些无法在动态加载框架上使用的功能,所以体验往往比不上 “正统” 的 Android 开发方式。
常驻内存、全家桶以及互相唤醒
缺少了 Google Play 的约束,一些 Android APP 就变得肆意妄为了,这一点在 BAT 系中显得格外明显。常驻内存就接受到服务器的推送信息的成功率就比较高了,用户明明关了一些 APP,但是它们就不想退出,就算我们手动关闭了它们,一旦重新启动、网络变化等,它们就又重新启动了,占着本来就珍贵的手机内存,很快就不够用了,这也是 Android 卡顿的一大原因。“百度全家桶” 你怕不怕?,一旦安装了百度家族启动的一个 APP,就会偷偷帮你安装上全家族的 APP,就算是我这种做 Android 研发的都经常中招,不用说普通的用户了。更可恨的是这些 APP 还会互相唤醒,Andriod Lollipop 之后你可以彻底关闭一个 APP,除非你手动启动它否则它无法自启动,但是家族 APP 之间互相唤醒使得这成为了可能,“绿色守护” 等后台清理神器在 “互相唤醒” 大法之下也没辙了。
H5 内容、硬生生把 Android 应用做成 IOS 应用等等
Android 原生与 H5 界面交互的框架已经很成熟了,许多中大型的 APP 的有 H5 的界面,特别是淘宝、天猫、京东这种购物类的,这些 APP 有大量和时间相关的活动,在节日之前开发一个新版本的 APP 再发布到应用市场显得太蠢了,H5 网页这种随时可以更新在线内容的技术最适合这种活动了。然而 H5 界面的性能在还是肛不过原生界面,特别是各种黑科技附身的 Lollipop 之后的时代,特别是当一个界面有 H5 的东西也有原生的东西那就更加糟糕了。此外,国内设计师都喜欢把 Android 的美术图设计得和 IOS 一样,这里不讨论这样做的原因,然而结果就是 Android 的开发需要使用大量的自定义 View,这样一来,许多系统自带控件的加速效果(特别是当用到系统公共资源的时候)就没有什么卵用了,而且往往这些自定义 View 都有或多或少的 Bug,可能设计得不合理,也可能有内存泄露,这些都会影响 APP 的性能。
主观原因
上面说的客观原因除了由于 Android 的碎片化问题之外,其实有很大部分是开发者有意为之,但是我相信这并不是 Android 开发的责任,我们是都是清白的,都是无奈的,错的不是我们,是世界啊!╮( ̄▽ ̄”)╭。也不是产品经理的锅,而是大环境造就的。国内互联网的竞争堪比电商,不这么做,不抢占用户的手机的话根本就无法生存,就算百度不去做,阿里也会去…… 所以勉强可以把这些归类为客观因素。但是瞎 BB 了这么多,都不是我想说的主要内容,我想许多人都不感兴趣,我只是一时来兴致了敲了这么多字,感觉如果不贴出来的话不就亏了嘛。 ………… …… …
既然你都看到这了,顺便把主观原因部分也看完吧_(-ω-`)⌒)。
主观原因主要是由于开发过程中的过错导致的,总体上来说大致有以下几方面的原因
没做好异步
APP 要流畅的话就好让它保持高度相应,CPU 要能及时响应 UI 线程的操作。不过不可能把所有非 UI 的工作统统扔到异步任务里面去,有时候一些工作直接在 UI 线程搞会非常方便,而且太多后台线程也会造成大量的性能开销。这是一个矛盾,这时候就需要成熟的异步任务框架咯,如果这个框架没写好的话,就可能导致后台任务混乱,产生多余的线程开销,UI 线程得不到及时的响应,更甚,如果有异步任务造成内存泄露,内存不够用很快就卡顿了,甚至直接 OOM 嗝屁了。
内存泄露以及 GC
内存是非常宝贵的,再多也不嫌多。所以当一个对象离开他的作用域后,我们一定立刻回收它占有的内存,最理想的状态是对每一个对象都能做到这样,如果有一个对象做不到,就说明他泄露了。Android 中,Activity 对象以及 Bitmap 的像素数据往往占用非常大的内存,如果这两者发生泄漏会导致可用内存急剧减少,那卡顿则就无法避免。此外,即使没有严重的内存泄露,但是频繁创建对象和回收对象的话,会引发虚拟机频繁的 GC,GC 占用比较大的资源开销,同样也可能会导致卡顿。总的来说,开发过程中要对内存的使用保持敏感,知根知底。
没做好界面的布局优化
设计师给出的同一张美术图可以有多张的布局实现,不同方案之间的性能可能相差很远。ListView 等列表控件的 Adapter 也有许多优化点,许多人容易疏忽。
没做好代码优化
这就不只是 Android 领域的问题啦,某些特定的业务应该采用最优算法,把时间复杂度降到最低,尽量避免指数级别的时间复杂度,尽量以空间换时间,必要的时候使用 Native 库来提高算法。
其他
一开始我也提到了,其实性能调优涉及到多方面的工作,比如一些静态的网络资源要做好缓存不要重复请求;频繁数据库操作的话最好使用异步任务,SQLite 默认实在 UI 线程直接操作数据库的;使用反射也会比较性能,不过反射有时候确实挺方便的,特别是项目庞大的时候,这个看取舍;过度使用 “设计模式” 也会有额外的开销,设计意味着更多接口和多态,业务跑起来需要额外的空间和时间;尽量不要开多进程,进程之间的通讯比同一进程之间的互调消耗的性能非常多,一般项目只要一个进程就够了,有推送的可以多一个推送进程;此外,不限制与 Android 客户端开发,H5、服务器的优化也能提高 APP 的性能。
性能问题的排查方法
这个在后面的具体分析中,再结合实际遇到的问题一起介绍吧。
关于性能优化的技术点,欢迎大家到这个日志里补充:[收集向] Android 性能调优的技术点