Android性能优化

Posted by pxfile Blog on May 5, 2018

Android性能优化

四个方面

可以把用户体验的性能问题主要总结为4个类别:

  • 流畅

  • 稳定

  • 省电、省流量

  • 安装包小

性能问题的主要原因是什么,原因有相同的,也有不同的,但归根到底,不外乎内存使用、代码效率、合适的策略逻辑、代码质量、安装包体积这一类问题,整理归类如下: 性能优化1

:使用时避免出现卡顿,响应速度快,减少用户等待时间,满足用户期望 :减低crash率和ANR率,不要在用户使用过程中崩溃和无响应 :节省流量和耗电,减少用户使用成本,避免使用时导致手机发烫 :安装包小可以降低用户的安装成本

卡顿优化

  • 界面绘制 主要原因是绘制的层级深,页面复杂,刷新不合理,由于这些原因导致卡顿的场景更多出现在UI和启动后的初始界面以及跳转到页面的绘制上。

  • 数据处理 导致卡顿的原因是数据处理量太大,一般分为三种情况,一是数据处理在UI线程,二是数据处理占用CPU高,导致主线程拿不到时间片,三是内存增加导致GC频繁,从而引起卡顿。

性能分析工具

性能问题并不容易复现,也不好定位,但是真的碰到问题还是需要去解决的,那么分析问题和确认问题是否解决,就需要借助相应的的调试工具,比如查看 Layout 层次的 Hierarchy View、Android 系统上带的 GPU Profile 工具和静态代码检查工具 Lint 等,这些工具对性能优化起到非常重要的作用,所以要熟悉,知道在什么场景用什么工具来分析。

1,Profile GPU Rendering

在手机开发者模式下,有一个卡顿检测工具叫做:Profile GPU Rendering,如图:

它的功能特点如下:

  • 一个图形监测工具,能实时反应当前绘制的耗时

  • 横轴表示时间,纵轴表示每一帧的耗时

  • 随着时间推移,从左到右的刷新呈现

  • 提供一个标准的耗时,如果高于标准耗时,就表示当前这一帧丢失

2,TraceView

TraceView 是 Android SDK 自带的工具,用来分析函数调用过程,可以对 Android 的应用程序以及 Framework 层的代码进行性能分析。它是一个图形化的工具,最终会产生一个图表,用于对性能分析进行说明,可以分析到每一个方法的执行时间,其中可以统计出该方法调用次数和递归次数,实际时长等参数维度,使用非常直观,分析性能非常方便。

3,Systrace UI 性能分析

Systrace 是 Android 4.1及以上版本提供的性能数据采样和分析工具,它是通过系统的角度来返回一些信息。它可以帮助开发者收集 Android 关键子系统,如 surfaceflinger、WindowManagerService 等 Framework 部分关键模块、服务、View系统等运行信息,从而帮助开发者更直观地分析系统瓶颈,改进性能。Systrace 的功能包括跟踪系统的 I/O 操作、内核工作队列、CPU 负载等,在 UI 显示性能分析上提供很好的数据,特别是在动画播放不流畅、渲染卡等问题上。

优化建议

1,布局优化

布局是否合理主要影响的是页面测量时间的多少,我们知道一个页面的显示测量和绘制过程都是通过递归来完成的,多叉树遍历的时间与树的高度h有关,其时间复杂度 O(h),如果层级太深,每增加一层则会增加更多的页面显示时间,所以布局的合理性就显得很重要。

那布局优化有哪些方法呢,主要通过减少层级、减少测量和绘制时间、提高复用性三个方面入手。总结如下:

  • 减少层级 合理使用RelativeLayout和LinerLayout,合理使用Merge

  • 提高显示速度 使用 ViewStub,它是一个看不见的、不占布局位置、占用资源非常小的视图对象。

  • 布局复用 可以通过标签提高复用

  • 尽可能少使用wrap_content wrap_content会增加布局Measure时的计算成本,在一直宽高为固定值时,不用wrap_content。

  • 删除控件中无用的属性

2,避免过度绘制

过度绘制是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构中,如果不可见的 UI 也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费了多余的 CPU 以及 GPU 资源。

如何避免过度绘制呢,如下:

  • 布局上的优化 移除 XML 中非必须的背景,移除 Window 默认的背景、按需显示占位背景图片
getWindow().setBackgroundDrawable(null);

或者在theme中添加

android:windowbackground="null";
  • 自定义View优化 使用 canvas.clipRect()来帮助系统识别那些可见的区域,只有在这个区域内才会被绘制。

3,启动优化

通过对启动速度的监控,发现影响启动速度的问题所在,优化启动逻辑,提高应用的启动速度。启动速度主要完成三件事:UI布局,绘制和数据准备,因此启动速度优化就是需要优化这样三个过程:

  • UI布局 应用一般都是有闪屏页,优化闪屏页的UI布局,可以通过Profile GPU Rendering检测丢帧情况

  • 启动加载逻辑优化 可以采用分布加载,异步加载,延期加载策略来提高应用启动速度

  • 数据准备 数据初始化分析,加载数据可以考虑用线程初始化等策略

4,合理的刷新机制

在应用开发过程中,因为数据的变化,需要刷新页面来展示新的数据,但频繁刷新会增加资源开销,并且可能导致卡顿发生,因此,需要一个合理的刷新机制来提高整体的 UI 流畅度。合理的刷新需要注意以下几点:

  • 尽量减少刷新次数

  • 尽量避免后台有高的CPU线程运行

  • 缩小刷新区域

5,其他

在实现动画效果时,需要根据不同场景选择合适的动画框架来实现。有些情况下,可以用硬件加速方式来提供流畅度。

内存优化

内存分析工具

1,Memory Monitor

2,Heap Viewer

3.LeakCanary

LeakCanary 函数库也是一个很好的工具,它可以追踪对象并确保它们不会泄漏。如果内存泄露了 —— 你将收到一个通知告诉你在哪里发生了什么。

常见内存泄漏的场景

资源性对象未关闭。比如Cursor、File文件等,往往都用了一些缓冲,在不使用时,应该及时关闭它们。

注册对象未注销。比如事件注册后未注销,会导致观察者列表中维持着对象的引用。

类的静态变量持有大数据对象。

非静态内部类的静态实例。

Handler临时性内存泄漏。如果Handler是非静态的,容易导致 Activity 或 Service 不会被回收。

容器中的对象没清理造成的内存泄漏。

WebView。WebView 存在着内存泄漏的问题,在应用中只要使用一次 WebView,内存就不会被释放掉。

除此之外,内存泄漏可监控,常见的就是用LeakCanary 第三方库,这是一个检测内存泄漏的开源库,使用非常简单,可以在发生内存泄漏时告警,并且生成 leak tarce 分析泄漏位置,同时可以提供 Dump 文件进行分析。

优化内存空间

没有内存泄漏,并不意味着内存就不需要优化,在移动设备上,由于物理设备的存储空间有限,Android 系统对每个应用进程也都分配了有限的堆内存,因此使用最小内存对象或者资源可以减小内存开销,同时让GC 能更高效地回收不再需要使用的对象,让应用堆内存保持充足的可用内存,使应用更稳定高效地运行。常见做法如下:

节制地使用Service

  • 系统会倾向于保留有Service所在的进程,这使得进程的运行代价很高,因为系统没有办法把Service所占用的RAM空间腾出来让给其他组件,另外Service还不能被Paged out。这减少了系统能够存放到LRU缓存当中的进程数量,它会影响应用之间的切换效率,甚至会导致系统内存使用不稳定,从而无法继续保持住所有目前正在运行的service。

  • 建议使用JobScheduler,而尽量避免使用持久性的Service。还有建议使用IntentService,它会在处理完交代给它的任务之后尽快结束自己。

对象引用。强引用、软引用、弱引用、虚引用四种引用类型,根据业务需求合理使用不同,选择不同的引用类型。

尽量不用枚举

减少不必要的内存开销。注意自动装箱,增加内存复用,比如有效利用系统自带的资源、视图复用、对象池、Bitmap对象的复用。

使用最优的数据类型。比如针对数据类容器结构,可以使用ArrayMap数据结构,避免使用枚举类型,使用缓存Lrucache等等。

  • 而SparseArray就避免掉了基本数据类型转换成对象数据类型的时间。

  • Protocol buffers是Google为序列化数据设计的一种语言无关、平台无关、具有良好扩展性的数据描述语言,与XML类似,但是更加轻量、快速、简单。如果使用protobufs来实现数据的序列化及反序列化,建议在客户端使用nano protobufs,因为通常的protobufs会生成冗余代码,会导致可用内存减少,Apk体积变大,运行速度减慢。

图片内存优化。可以设置位图规格,根据采样因子做压缩,用一些图片缓存方式对图片进行管理等等。

避免内存抖动

垃圾回收通常不会影响应用的表现,但是短时间内多次的垃圾回收会消耗掉界面绘制的时间。系统花费在GC上的时间越多,进行界面绘制或流音频处理的时间就越短。通常内存抖动会导致多次的GC,实践中内存抖动代表了一段时间内分配了临时对象。

避免非静态内部类

谨慎使用第三方库

谨慎使用LargeHeap属性

可以通过在manifest的application标签下添加largeHeap=true的属性来为应用声明一个更大的heap空间(可以通过getLargeMemoryClass()来获取到这个更大的heap size阈值)。然而,声明得到更大Heap阈值的本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用)。不要轻易的因为你需要使用更多的内存而去请求一个大的Heap Size。只有当你清楚的知道哪里会使用大量的内存并且知道为什么这些内存必须被保留时才去使用large heap,使用额外的内存空间会影响系统整体的用户体验,并且会使得每次gc的运行时间更长。在任务切换时,系统的性能会大打折扣。另外, large heap并不一定能够获取到更大的heap。在某些有严格限制的机器上,large heap的大小和通常的heap size是一样的。

谨慎使用多进程

多进程确实是一种可以帮助我们节省和管理内存的高级技巧。如果你要使用它的话一定要谨慎使用,因为绝大多数的应用程序都不应该在多个进程当中运行的,一旦使用不当,它甚至会增加额外的内存而不是帮我们节省内存;同时需要知晓多进程带来的缺点。这个技巧比较适用于那些需要在后台去完成一项独立的任务,和前台的功能是可以完全区分开的场景。

使用try catch进行捕获

对高风险OOM代码块如展示高清大图等进行try catch,在catch块加载非高清的图片并做相应内存回收的处理。注意OOM是OutOfMemoryError,不能使用Exception进行捕获。

内存优化的套路

  1. 解决所有的内存泄漏

    • 集成LeakCanary,可以方便的定位出90%的内存泄漏问题;
    • 通过反复进出可疑界面,观察内存增减的情况,Dump Java Heap获取当前堆栈信息使用MAT进行分析。
    • 内存泄漏的常见情形可参照《Android 内存泄漏分析心得》
  2. 避免内存抖动

    • 避免在循环中创建临时对象;
    • 避免在onDraw中创建Paint、Bitmap对象等。
  3. Bitmap的使用

    • 使用三方库加载图片一般不会出内存问题,但是需要注意图片使用完毕的释放,而不是被动等待释放。
  4. 使用优化过的数据结构

  5. 使用onTrimMemory根据不同的内存状态做相应处理
  6. Library的使用
    • 去掉无用的Library,对生成的Apk进行反编译查看使用到的Library,避免出现无用的Lib仍然被打进Apk;
    • 避免引入巨大的Library;
    • 使用Proguard进行混淆、压缩。

稳定性优化

Android 应用的稳定性定义很宽泛,影响稳定性的原因很多,比如内存使用不合理、代码异常场景考虑不周全、代码逻辑不合理等,都会对应用的稳定性造成影响。其中最常见的两个场景是:Crash 和 ANR,这两个错误将会使得程序无法使用,比较常用的解决方式如下:

提高代码质量。比如开发期间的代码审核,看些代码设计逻辑,业务合理性等。

代码静态扫描工具。常见工具有Android Lint、Findbugs、Checkstyle、PMD等等。

Crash监控。把一些崩溃的信息,异常信息及时地记录下来,以便后续分析解决。

Crash上传机制。在Crash后,尽量先保存日志到本地,然后等下一次网络正常时再上传日志信息。

耗电优化

在 Android5.0 以前,在应用中测试电量消耗比较麻烦,也不准确,5.0 之后专门引入了一个获取设备上电量消耗信息的 API:Battery Historian。Battery Historian 是一款由 Google 提供的 Android 系统电量分析工具,和Systrace 一样,是一款图形化数据分析工具,直观地展示出手机的电量消耗过程,通过输入电量分析文件,显示消耗情况,最后提供一些可供参考电量优化的方法。

adb命令导出电量信息:

adb shell dumpsys batterystats --reset(Android4.1到4.3 adb shell dumpsys batteryinfo)
adb bugreport > bugreport.txt(Android7.0以上 adb bugreport bugreport.zip)

安装Battery Historian后打开:http: //localhost:9999/, 上传bugreport.txt文件开始分析

  • 我们应该尽量减少唤醒屏幕的次数与持续的时间,使用WakeLock来处理唤醒的问题,能够正确执行唤醒操作并根据设定及时关闭操作进入睡眠状态。

  • 某些非必须马上执行的操作,例如上传歌曲,图片处理等,可以等到设备处于充电状态或者电量充足的时候才进行。

  • 触发网络请求的操作,每次都会保持无线信号持续一段时间,我们可以把零散的网络请求打包进行一次操作,避免过多的无线信号引起的电量消耗。

电量优化

Android系统上App的电量消耗主要由cpu、wakelock、数据传输(流量和wifi)、wifi运行、gps、other senior组成,而耗电异常也是由于这几个模块的使用不当。

  1. 在设置-电量里查看App的耗电情况;
  2. 使用Battery Historian进行分析,这是分析里最重要的一步;
  3. 针对分析结果,参照第三章节的优化方式进行优化。

CPU时间片优化

当检测到CPU时间片消耗异常时,需要使用TraceView,获取进程执行信息,定位CPU占用率异常的问题,关于CPU的使用可以参照《Android性能优化(一)之启动加速35% 》一文。

3.2 网络传输

通常情况下,使用3G移动网络传输数据,电量的消耗有三种状态:

Full power: 能量最高的状态,移动网络连接被激活,允许设备以最大的传输速率进行操作。 Low power: 一种中间状态,对电量的消耗差不多是Full power状态下的50%。 Standby: 最低的状态,没有数据连接需要传输,电量消耗最少。

3.2.1 数据压缩

通过数据压缩等方式缩减传输时间,降低电量消耗,此章节可以参考《Android 性能优化(八)之网络优化》

3.2.2 选择更快的传输方式

虽然3G芯片比Wifi芯片耗电低,但Wifi的速率可以让数据在较短时间内完成传输,从而降低电量消耗。

3.2.3 请求集中发送

分析和统计之类的非重要操作,可以在合适状态(电量充足或Wifi状态)下发送。参见3.6节JobScheduler。

3.2.4 无网状态避免网络请求

之前在网络优化的文章里写过,网络请求失败之后的重试机制,但是要注意这个重试是在有网状态下的重试。否则无网状态下重试不会请求成功,只会消耗电量。尤其是与AlarmManager或者WakeLock连用的场景下,耗电量会更多。

3.3 GPS

定位是App中常用的功能,但是定位不能千篇一律,不同的场景以及不同类型的App对定位更加需要个性化的区分。

3.3.1 选择合适的Location Provider

Android系统支持多个Location Provider:

  • GPS_PROVIDER: GPS定位,利用GPS芯片通过卫星获得自己的位置信息。定位精准度高,一般在10米左右,耗电量大;但是在室内,GPS定位基本没用。

  • NETWORK_PROVIDER: 网络定位,利用手机基站和WIFI节点的地址来大致定位位置,这种定位方式取决于服务器,即取决于将基站或WIF节点信息翻译成位置信息的服务器的能力。

  • PASSIVE_PROVIDER: 被动定位,就是用现成的,当其他应用使用定位更新了定位信息,系统会保存下来,该应用接收到消息后直接读取就可以了。比如如果系统中已经安装了百度地图,高德地图(室内可以实现精确定位),你只要使用它们定位过后,再使用这种方法在你的程序肯定是可以拿到比较精确的定位信息。

使用Criteria,设置合适的模式、功耗、海拔、速度等需求,系统会返回合适的Location Provider。

例如你的App只是需要一个粗略的定位那么就不需要使用GPS进行定位,既耗费电量,定位的耗时也久。

3.3.2 及时注销定位监听

在获取到定位之后或者程序处于后台时,注销定位监听,此时监听GPS传感器相当于执行no-op(无操作指令),用户不会有感知但是却耗电。

    public void onPause() {
        super.onPause();
        locationManager.removeListener(locationListener);
    }

    public void onResume(){
        super.onResume();
        locationManager.requestLocationUpdates(locationManager.getBestProvider(criteria, true),6000,100,locationListener);
    }
3.3.3 多模块使用定位尽量复用

多个模块使用定位,尽量复用上一次的结果,而不是都重新走定位的过程,节省电量损耗;例如:在应用启动的时候获取一次定位,保存结果,之后再用到定位的地方都直接去取。

计算优化,避开浮点运算等。

避免 WaleLock 使用不当。

Android为了节省电量,会在用户无操作一段时间之后进入休眠状态。Wake Lock是一种锁的机制,只要有人拿着这个锁,系统就无法进入休眠。一些App为了能在后台持续做事情,就会持有一个WakeLock,那么手机就不会进入休眠状态,App要做的事情能做了,但是也更加耗电。

  • App在前台不要申请WakeLock,此时无需申请,申请的话会计算到应用电量消耗;
  • App在后台由于业务需要必须要申请WakeLock时使用带有超时参数的方法,防止由于忘记或者异常情况下没有释放;
  • App申请使用WakeLock,任务结束之后及时释放,让系统再次进入休眠状态。

如果只是需要屏幕常亮的话,可以使用FLAG_KEEP_SCREEN_ON,无需考虑释放WakeLock的问题。

使用 Job Scheduler。

使用JobScheduler,一些任务通过JobScheduler来触发,例如可推迟的网络请求、下载、GPS等,可以在特定场景:连接Wifi、连接电源等场景触发。既完成了任务,也无需考虑由于一些任务导致的电量消耗。

安装包大小优化

应用安装包大小对应用使用没有影响,但应用的安装包越大,用户下载的门槛越高,特别是在移动网络情况下,用户在下载应用时,对安装包大小的要求更高,因此,减小安装包大小可以让更多用户愿意下载和体验产品。

减少安装包大小的常用方案

代码混淆。使用proGuard 代码混淆器工具,它包括压缩、优化、混淆等功能。

资源优化。比如使用 Android Lint 删除冗余资源,资源文件最少化等。

对于那些没有被引用到的资源,会在编译阶段被排除在APK安装包之外,要实现这个功能,对我们来说仅仅只需要在build.gradle文件中配置shrinkResource为true

图片优化。比如利用 AAPT 工具对 PNG 格式的图片做压缩处理,降低图片色彩位数等。

避免重复功能的库,使用 WebP图片格式等。

插件化。比如功能模块放在服务器上,按需下载,可以减少安装包大小。

App瘦身总结:

1 代码瘦身

  • 移除无用代码、功能;
  • 移除无用的库、避免功能雷同的库;
  • 启用Proguard;
  • 缩减方法数;

2 资源瘦身

  • 移除无用的资源文件;
  • Drawable目录只保留一份资源;
  • 对图片进行压缩;
  • PNG转换JPG;
  • 使用矢量图;
  • 使用WebP;
  • 资源混淆;
  • 资源在线化;

3 So瘦身

  • 在允许的情况下,针对用户机型分布保留特定架构的So;

4 7Zip压缩

使用7Zip对Apk进行极限压缩。

5 其它

  • 类如插件化,将Dex与资源文件放在服务端,需要时下载;但是插件化实施以及与现有项目结合难度不小,也超出本文主题,不细说;
  • 通过在 build.gradle配置include来针对每个CPU架构生成单独的安装包,按照架构上传Apk;但是这个方案在国内应用市场几乎没有采用的,只能在Google Play上使用。
  • 一点经验:对Apk进行瘦身,瘦身So以及资源文件是见效最快的操作。瘦身So以及删除不用的图片、压缩图片之后,Apk会缩减很大的比例;而针对Dex的优化可能作用不会很明显。

应用安装包构成

常用应用安装包的构成,如图所示:

安装包的构成

从图中我们可以看到:

assets文件夹。存放一些配置文件、资源文件,assets不会自动生成对应的 ID,而是通过 AssetManager 类的接口获取。

res。res 是 resource 的缩写,这个目录存放资源文件,会自动生成对应的 ID 并映射到 .R 文件中,访问直接使用资源 ID。

META-INF。保存应用的签名信息,签名信息可以验证 APK 文件的完整性。

AndroidManifest.xml。这个文件用来描述 Android 应用的配置信息,一些组件的注册信息、可使用权限等。

classes.dex。Dalvik 字节码程序,让 Dalvik 虚拟机可执行,一般情况下,Android 应用在打包时通过 Android SDK 中的 dx 工具将 Java 字节码转换为 Dalvik 字节码。

resources.arsc。记录着资源文件和资源 ID 之间的映射关系,用来根据资源 ID 寻找资源。