Android性能优化

2019/10/8 posted in  Android

Profile GPU Rendering

这是手机开发的一个辅助工具。

功能特点:

  • 它是一个图形检测工具,能实时反应当前绘制的耗时。
  • 横轴表示时间,纵轴表示每一帧的耗时。
  • 随着时间推移,从左到右的刷新呈现。
  • 提供了一个标准的耗时,如果高于标准耗时,表示当前这一帧丢失。

打开Profile GPU Rendering后可以看到实时刷新的彩色图,每一根竖线表示一帧,由多个颜色组成。

  • 每一条柱状图都由4中颜色组成: 红、黄、蓝、紫,这些线对应每一帧在不同阶段的实际耗时。
  • 蓝色代表测量绘制的时间,它代表需要多长时间去创建和更新DisplayList。蓝线很高时,有可能是因为需要重新绘制,或者自定义视图的onDraw函数处理事情太多。
  • 红色代表执行时间,这部分时Android进行2D渲染Display List的时间,为了绘制到屏幕上,Android需要使用OpenGl ES的API接口来绘制Display List,这些API有效地将数据发送到GPU,最终在屏幕上显示出来。当红线非常高时,可能是重新提交了视图导致的。
  • 橙色部分表示处理时间,或者是CPU告诉GPU渲染一帧的地方,是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复。如果橙线很高,意味着GPU太繁忙。
  • 紫色代表资源转移到渲染线程的时间。

Profile GPU Rendering工具能很好地帮助你找到渲染相关的问题,但是要修复这些问题需要结合另一个耗时工具和代码来具体分析,找到性能瓶颈,并优化。

TraceView

TraceView是AndroidSDK自带的工具,用来分析函数调用过程,可以对Android的应用程序及Framework层的代码进行性能分析。它是一个图形化的工具,最终会产生一个图表,用于对性能分析进行说明,可以分析到应用具体每一个方法的执行时间,使用可以非常直观简单,分析性能问题很方便。

使用方法

在使用TraceView分析问题之前需要得到一个*.trace的文件,然后通过TraceView来分析trace文件的信息。

  • 在DDMS中使用(这种方法使用方便,但监控范围不够精确)
    • 单击Start Method Profiling按钮
    • 在应用中操作需要监控的点,比如进入一个Activity或者滑动一个列表,完成后单击Stop Method Profiling按钮
    • 结束会自动跳转到TraceView视图
  • 代码中加入调试语句保存trace文件(要在应用中打开write to external storage权限)
    • 在需要开始监控的地方调用Debug.startMethodTracing()
    • 在需要结束监控的地方调用Debug.stopMethodTracing()
    • 系统会在SD卡中创建.trace文件
    • 使用traceview打开该文件进行分析。

TraceView视图说明

上半部分为时间片面板,下半部分为分析面板。

  • 时间片面板
    • X轴表示时间消耗,单位为毫秒,Y轴表示各个线程,每个线程中的不同方法使用了不同的颜色来表示,颜色占用面积越宽,表示该方法占用CPU时间越长。
    • 时间片面板可以放大/缩小,也可以指定区域放到最大,方便查看具体的过程,一半优先选择放大耗时严重的区域。
  • 分析面板
列名 意义
Name 所有的调用项,展开可以看到有的有Parent和Children子项,指被调用和调用
Inclusive 统计函数本身运行的时间 + 调用子函数运行的时间
incl inclusive时间占总时间的百分比
Exclusive 统计函数本身运行的时间
Excl 执行占总时间的百分比
Calls + Recur Calls/Total 该方法调用次数 + 递归次数
Cpu Time/Call 该方法耗时
Real Time/Call 实际时长(包含了CPU的上下文切换、阻塞、GC等)

使用TraceView查看耗时,主要关注Calls + Recur Calls / Total 和 Cpu Time / Call这两个值,也就是关注调用次数多和耗时久的方法,然后优化这些方法的逻辑和调用次数,减少耗时。

Systrace UI性能分析

Systrace工具可以跟踪、收集、检查定时信息,可以很直观地查看CPU周期消耗的具体时间,显示每个线程和进程的跟踪信息,使用不同颜色来突出问题的严重性,并提供如何解决这些问题的建议。

Systrace的使用方法

  • 在DDMS上使用
    • 打开Android Device Monitor,连接手机并准备需要抓取的界面
    • 单击Systrace按钮进入抓取前的设置,选择需要跟踪的内容
    • 手机上开始操作需要跟踪的过程
    • 到了设定好的时间后,生成Trace文件
    • 使用Chrome打开文件即可分析
  • 使用命令行(使用命令行更灵活,速度更快,并且配置好后再使用能快速得到结果)
    • python systrace.py --time=10 -o mynewtrace.html sched gfx view wm
参数名 意义
-h, --help 帮助信息
-o 保存的文件名
-t N, --time=N 多少秒内的数据,默认为5秒,以当前时间点往后倒N秒时间
-b N, --buf-size=N 单位为千字节,限制数据大小
-k , --ktrace= 追踪特殊的方法
-l, --list-categories 设置需要追踪的标签
-a , --app= 包名
--from-file= 创建报告的来源trace文件
-e , --serial= 设备号

category可取值:

category 解释
gfx Graphic系统的相关信息,包括SerfaceFlinger,VSYNC消息,Texture,RenderThread等;分析卡顿非常依赖这个。
input Input
view View绘制系统的相关信息,比如onMeasure,onLayout等。。
webview WebView
wm Window Manager
am ActivityManager调用的相关信息;用来分析Activity的启动过程比较有效。
sm Sync Manager
audio Audio
video Video
camera Camera
hal Hardware Modules
app Application
res Resource Loading
dalvik 虚拟机相关信息,比如GC停顿等。
rs RenderScript
bionic Bionic C Library
power Power Management
sched CPU调度的信息,非常重要;你能看到CPU在每个时间段在运行什么线程;线程调度情况,比如锁信息。
binder_driver Binder驱动的相关信息,如果你怀疑是Binder IPC的问题,不妨打开这个。
core_services SystemServer中系统核心Service的相关信息,分析特定问题用。
irq IRQ Events
freq CPU Frequency
idle CPU Idle
disk Disk I/O
mmc eMMC commands
load CPU Load
sync Synchronization
workq Kernel Workqueues
memreclaim Kernel Memory Reclaim
regulators Voltage and Current Regulators
  • 应用中获取(通过Trace类来实现,Trace.beginSection()和Trace.endSection()之间的代码工作会一直被追踪)
    • 在Trace被嵌套在另一个Trace中时,endSection()方法只会结束离它最近的一个beginSection()。所以要保证endSection()和beginSection()调用次数匹配。
    • Trace的begin和end必须在同一线程中执行。

分析Systrace报告

获取到的trace.html文件,需要使用Chrome打开。

快捷键 功能
W 放大
S 缩小
A 左移
D 右移

图表中和UI绘制关系最密切的是Alerts和Frame两个数据。

Alerts

Alerts一栏标记了性能有问题的点,单击该点可以查看详细信息,在右边栏还有一个Alerts框,单击可以查看每个类型的Alerts的数量,单击某一个Alert卡伊看到问题的详细描述

Frame

每个应用都有一行专门显示frame,每一帧就显示为一个绿色的圆圈。当显示为黄色或者红色时,它的渲染时间超过了16.6ms。使用W键放大,看看这一阵的渲染过程中系统到底做了什么。

小结

如果想知道UI线程怎么会花费这么多时间的话,就需要用到TraceView来分析具体哪些函数在消耗时间。

Hierarchy Viewer

Hierarchy Viewer是AndroidSDK自带的一款可视化调试工具,用来检查Layout嵌套及绘制时间,以可视化的布局角度直观获取Layout布局设计和各种属性信息,开发者在调试和布局UI界面时可以方便地使用,提高用户的开发效率。

打开Hierarchy Viewer

  • 打开Android Device Monitor,直接打开Hierarchy Viewer。
  • 从AndroidSDK工具包中,通过命令行的方式启动。

使用Hierarchy Viewer查看层级和耗时

  • 1view 表示当前节点下的子View和其本身。1表示只有一个View,就是其自身。
  • Measure、Layout、Draw代表三个阶段的耗时。最后一个框有不同色的三个指示灯,分别对应当前控件在测量、布局以及画视图三个阶段,颜色表示这个控件占用时间的百分比。
    • 绿色: 该控件在该阶段比其他50%的控件的速度要快
    • 黄色: 该控件在该阶段比其他50%的控件的速度要慢
    • 红色: 该控件在该阶段的处理速度最慢

由于一个应用的界面非常多,如果一个个这样分析的话效率非常低,所以通过检查所有页面的层级,并把深度高于N的界面输出,然后通过Hierarchy Viewer工具来仔细分析。

使用Lint扫描前,先配置需要检查的项目,只需要检查Layout层级深度。进入File -> Settings -> Inspections -> Android Lint配置只扫描Layout的层级和View的个数。

  • TooDeepLayout: 表示布局太深,默认层级超过10层会提示该问题,可以自定义环境变量ANDROID_LINT_MAX_DEPTH来修改。布局深度增加会导致内存消耗也随之增加,因此布局尽可能浅而宽。
  • TooManyViews: 表示控件太多,默认超过80个控件会提示该问题。

布局优化方法

优化的目的就是减少层级,让布局扁平化,以提高绘制的时间,提高布局的复用性节省开发和维护成本。

减少层级

层级越少,测试和绘制的时间就越短

  • 合理使用RelativeLayout和LinearLayout
    • 在布局中,RelativeLayout不如LinearLayout快。RelativeLayout会对子View做两次测量,导致性能偏低。
    • LinearLayout在使用weight属性的情况下,也会对自身进行两次测量,但因为没有更多的依赖关系,所以仍会比RelativeLayout的效率高。
    • LinearLayout可能会导致布局过深。布局层次深会增加内存消耗,甚至引出栈溢出等问题。
    • 小结
      • 尽量使用RelativeLayout和LinearLayout。
      • 在布局层级相同的情况下,使用LinearLayout。
      • 用LinearLayout有时会使嵌套层级变多,应该使用RelativeLayout,使界面尽量扁平化。
  • 合理使用Merge
    • 在自定义View中,父元素尽量是FrameLayout或者LinearLayout
    • Merge只能用在布局XML文件的根元素
    • 使用merge来加载一个布局时,必须指定一个ViewGroup作为其父元素,并且要设置加载的attachToRoot参数为true
    • 不能在ViewStub中使用Merge标签

提高显示速度

ViewStub是一个轻量级的View,它是一个看不见的,并且不占布局位置,占用资源非常小的视图对象。可以为ViewStub指定一个布局,加载布局时,只有ViewStub会被初始化,然后当ViewStub被设置为可见时,或是调用了ViewStub.inflate()时,ViewStub所指向的布局会被加载和实例化,然后ViewStub的布局属性都会传给他指向的布局。

注意点:

  • ViewStub只能加载一次,之后ViewStub对象会被置为空。
  • ViewStub只能用来加载一个布局文件,而不是某个具体的View,当然也可以把View写在某个布局文件中。
  • ViewStub不能嵌套Merge标签

    主要使用场景:

  • 在程序运行期间,某个布局在加载后,就不会有变化,除非销毁该页面再重新加载。

  • 想要控制显示和隐藏的是一个布局文件,而非某个View。

布局复用

将代码公用部分提取出来,在使用的时候用添加进去。

小结

影响布局效率

  • 布局的层级越少,加载速度越快
  • 减少同一层级控件的数量,加载速度会变快
  • 一个控件的属性越少,解析越快

优化总结

  • 尽量多使用RelativeLayout或LinearLayout,不要使用绝对布局AbsoluteLayout
  • 将可复用的组件抽取出来并通过标签使用
  • 使用标签加载一些不常用的布局
  • 使用标签减少布局的嵌套层次
  • 尽可能少用wrap_content, wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content
  • 删除控件中的无用属性

过度绘制

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

导致过度绘制的主要原因

  • XML布局 -> 控件有重叠且都有设置背景
  • View自绘 -> View.onDraw里面同一个区域被绘制多次

检测工具

打开手机开发者模式中的Show GPU Overdraw选项,打开后会有不同的颜色区域表示不同的过度绘制次数。

  • 无色: 没有过度绘制,每个像素绘制了1次
  • 蓝色: 每个像素多绘制了一次。
  • 绿色: 每个像素多绘制了两次。
  • 淡红: 每个像素多绘制了3次。一般来说,这个区域不超过屏幕的1/4是可以接受的。
  • 深红: 每个像素多绘制了4次或者更多。严重影响性能,需要优化,避免深红色区域。

如何避免过度绘制

布局上优化

  • 移除XML中非必须的背景,或根据条件设置
  • 移除Window默认的背景
  • 按需显示占位背景图片

在Android自带的一些主题时,activity往往会被设置一个默认的背景,这个背景由DecorView持有,当自定义布局有一个全屏的背景时,比如设置了这个界面的全屏黑色背景,DecorView的背景此时对我们来说是无用的,但是它会产生一次Overdraw。因此没有必要的话,也可以移除。this.getWindow().setBackgroundDrawable(null);这段代码放到setContentView()之后

自定义View优化

虽然自定义View减少了Layout的层级,但在实际绘制时也是会过度绘制的。可以通过canvas.clipRect()来帮助系统识别那些可见区域,这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。

启动优化

借助ADB得到准确的启动时间。

adb shell am start -W [packageName]/[packageName.AppstartActivity]

执行后可以得到三个时间

  • ThisTime: 一般和TotalTime时间一样。
  • TotalTime: 应用的启动时间,包括创建进程 + Application初始化 + Activity初始化到界面显示
  • WaitTime: 一般比TotalTime大些,包括系统影响的耗时

也可以使用代码进行打点获得方法执行,以及生命周期的准确时间。

启动优化方案

  1. UI布局: 优化第一个Activity的布局,检查是否掉帧,以及优化布局结构,避免过度绘制
  2. 启动加载逻辑优化: 采取分步加载、异步加载、延期加载策略

合理的刷新机制

  • 尽量减少刷新次数
  • 尽量避免后台有高CPU线程运行
  • 缩小刷新区域

减少刷新次数

  • 控制刷新频率(避免频繁的刷新)
  • 避免没有必要的刷新

避免后台线程的影响

后台线程虽然不会直接影响主线程,但如果后台线程开销很大,占用CPU过高,导致系统GC频繁和CPU时间片资源紧张,还是可能会导致页面卡顿。比如ListView滚动过程中可以监听onScrollStateChanged事件,在滚动时暂停图片下载线程工作,结束后再开始,可以提高ListView的滚动平滑度。

缩小刷新区域

采用局部刷新

  • 使用invalidate(Rect dirty); invalidate(int left, int top, int right, int bottom);
  • 刷新List中单个Item

提升动画性能

  • 使用属性动画比补间动画重绘次数更少
  • 对可以开启硬件加速的控件,开启硬件加速,会大幅提升动画流畅度。

Memory Monitor

Memory Monitor 是一款使用非常简单的图形化工具,可以很好地监控系统或应用的内存使用情况。

  • 显示可用和已用内存,并且以时间为维度实时反映内存分配和回收情况。
  • 快速判断应用程序的运行缓慢是否是由于过度的内存回收导致。
  • 快速判断应用是否是由于内存不足导致程序崩溃

使用场景

  • 内存分配与释放: 观察是否触发了系统GC之后,迅速释放了一大块内存,判断是否为合理分配内存。
  • 大内存申请与内存抖动: 内存抖动一般在很短时间内发生了多次的内存分配和释放,并且通常在发生严重内存抖动时,也能感觉到应用卡顿。

Allocation Tracker

Memory Monitor可以很直观且实时地监控内存使用情况,还能发现内存问题,但发现内存问题后不能再进一步找到原因。这个时候就需要Allocation Tracker进行更详细的分析, Allocation Tracker可以分配跟踪记录应用程序的内存分配,并列出它们的调用堆栈,可以查看所有对象内存分配的周期。

主要功能:

  • 在一段时间内以对象类型为纬度,跟踪在此时间内的内存分配和释放情况。
  • 寻找代码中内存使用不合理的地方。

使用

  • 在Memory Monitor视图中单击启动追踪按钮(Start Allocation Tracking)
  • 操作应用,怀疑有内存泄漏或者内存变化较大的操作
  • 单击结束追踪按钮
  • 自动生成一个alloc结尾的文件,这个文件记录了这次追踪到的所有内存数据,并且在AndroidStudio中自动打开一个数据面板,显示当前生成alloc文件的内存数据。

查看面板数据

整个面板分为两个区域,上面是内存对象列表,下面是对象引用堆栈。

内存对象列表

列名 数据意义
Allocation Order 内存分配序列
Allocation Class 被分配的内存对象
Allocation Size 分配的内存大小
Thread ID 分配该内存的线程ID
Allocation Site 分配该对象的方法

对象引用堆栈
在内存对象列表中,选中某一个对象后,在下面的窗口显示调用堆栈,单击具体的堆栈可以进入具体的代码行。

Memory Analyzer Tool(MAT)

使用步骤

下载MAT
下载地址为MAT

获取HPROF文件
在Android Studio的Memory Monitor工具中,单击Dump Java Heap按钮,在左侧的Capture栏中的Heap Snapshot列表中看到Dump下来的HPROF文件,右击文件,在弹出的菜单中选择Export to standard.hprof选择项,即可转换成标准的HPROF文件,再使用MAT打开

MAT视图

在MAT窗口上,OverView是一个总体概览,显示总体的内存消耗情况和疑似问题。MAT提供了多种分析维度,其中Histogram、Dominator Tree、Top Consumers和Leak Suspects的分析维度不同。分析内存最常用的是Histogram、Dominator Tree。

  • Histogram(列出内存中的所有实例类型对象、对象的个数以及大小,并支持正则表达式查找)
  • Dominator Tree(列出最大的对象及其依赖存活的Object。分析流程和Histogram大同小异,但Dominator Tree能更方便地看出引用关系)
  • Top Consumers(通过图形列出最大的Object)
  • Leak Suspects(通过MAT自动分析泄漏的原因和泄漏的一份总体报告)

一般在Histogram、Dominator Tree视图中分析内存是否异常。

  • Class Name: 类名
  • Objects: 对象实例个数
  • Shallow Heap: 对象自身占用的内存大小,不包括它引用的对象。对分析内存泄漏意义不是很大。
  • Retained Heap: 是当前对象大小与当前对象可直接或间接引用到的对象的大小综合,包括被递归释放的。Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存大小。

常见内存泄漏场景

  1. 资源性对象未关闭: 资源型对象(如Cursor,File文件)往往都用了一些缓冲,在不使用时,应该及时关闭它们,以便它们的缓存数据能够及时回收。
  2. 注册对象未注销: 如果事件注册后未注销,会导致观察者列表中维持着对象的引用,阻止垃圾回收,一般发生在注册广播接收器、注册观察者等。
  3. 类的静态变量持有大数据对象
  4. 非静态内部类的静态实例
  5. Handler临时性内存泄漏
  6. 容器中的对象没清理造成的内存泄漏(集合、队列)
  7. WebView

优化内存空间

Android系统对每个应用进程也都分配了有限的堆内存,因此使用最小内存的对象或者资源可以减少内存开销,同时让GC能更高效地回收不再需要使用的对象,让应用堆内保持充足的可用内存,使应用更稳定高效地运行。

对象引用

强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,垃圾回收器就绝不会回收它。因此,如果强引用对象在应用的生命周期中不再需要使用,一定要记得释放或转成弱引用,以便让系统回收。

软引用

软引用在保持引用对象的同时,保证在虚拟机报告内存不足的情况之前,清除所有的软引用。垃圾收集器在运行时可能会(也可能不会)释放软引用对象。对象是否被释放取决于垃圾收集器的算法以及垃圾收集器运行时可用的内存数量。软引用可用来实现内存敏感的高速缓存。

弱引用

弱引用类的一个典型用途就是规范化映射。对于那些生存期相对较长,而且重新创建的开销也不高的对象来说,弱引用也比较有用。
弱引用和软引用的区别在于: 只具有弱引用的对象拥有更短暂的生命周期。

虚引用

虚引用类只用于跟踪即将被引用对象进行的收集。必须和ReferenceQueue类联合使用。

减少不必要的内存开销

AutoBoxing

避免AutoBoxing带来的效率问题,使用TraceView查看耗时,如果发现调用了大量的integer.value,就说明发生了AutoBoxing,需要立即优化代码。

内存复用

有效利用系统自带的资源

Android系统本身内置了大量的资源,比如一些通用的字符串,颜色自定义,常用Icon图片,还有些动画和页面的样式以及简单布局。直接使用系统资源不仅可以在一定程度上减少内存的开销,还可以减少应用程序的自身负重,减小APK的大小,并且复用性更好。

视图复用

出现大量重复子组件,而子组件是大量重复的,可以使用ViewHolder实现ConvertView复用。

对象池

可以在设计程序时显示地在程序中创建对象池,然后实现复用逻辑,对相同的类型数据使用同一块内存空间,也可以利用系统框架既有的具有复用特性的组件减少对象的重复创建,从而减少内存的分配与回收。

Bitmap对象的复用

利用Bitmap的inBitmap的高级特性,提高Android系统在Bitmap的分配与释放效率,不仅可以达到内存复用,还提高了读写速度。

使用最优的数据类型

Android应用开发,大部分使用Java语言编程,其中的很多数据结构和类型不一定是最省内存的。Android针对移动开发提出了一系列的数据类容器结构优化。

HashMap与ArrayMap

虽然HashMap非常有用,但它并不是最节约的容器,会占用大量内存。ArrayMap虽然性能相比更差,但是占用空间方面有很大优势。

  • 当对象的数目非常小(1000以内), 但是访问特别多,或者删除和插入频率不高时
  • 当有映射容器,有映射发生,并且所有映射的容器也是ArrayMap时。
枚举类型

使用注解的方式检查类型安全代替枚举类型。IntDef和StringDef,用来提供编译期的类型检查。

LruCache

LruCache,可以翻译为最近最少使用缓存,它用强引用保存需要缓存的对象,内部维护一个队列。

比较重要的几个方法

  • public final V get (K key)返回cache中key对应的值,调用这个方法后,被访问的值会移动到队列的尾部
  • public final V put (K key, V value)根据key存放value,存放的value会移动到队列的尾部
  • protected int sizeOf(K key, V value)返回每个缓存对象的大小,用来判断缓存是否快要满了,这个方法必须重写
  • protected void entryRemoved (boolean evicted, K key, V oldValue, V newValue)当一个缓存对象被丢弃时调用该方法,这是个空方法,可以重写。第一个参数为true, 缓存对象是为了腾出空间而被清理。第一个参数为false,缓存对象的entry被remove移除或者被put覆盖时。

使用LruCache时注意Lruache的容量,既不能太大,太大会造成其他可用内存变小,容易导致OOM,但又不能太小,否则起不到缓存的作用。

分配LruCache大小时考虑应用剩余内存有多大

  • 一次屏幕显示多少张图片,有多少张图片是缓存起来准备显示地。
  • 考虑设备的分辨率和尺寸,缓存相同的图片数,收集的dpi越大,需要的内存也越大。
  • 图片分辨率和像素质量决定了占用内存的大小。
  • 图片访问的频繁程度是多少,如果存在多个不同要求的图片类型,可以考虑用多个LruCache来做缓存,案遭访问的频率分配到不同的LruCache中。

图片内存优化

Android中显示图片和图片文件的大小无关,在Android设备上显示前需要把图片解码成位图格式,而位图格式只和位图的属性相关,压缩格式仅仅是减小了文件大小。

一张图片占用的内存 = 图片长度 * 图片宽度 * 单位像素占用的字节数

设置位图规格
Format Bits Per Pixel
ARGB_8888 32
RGB_565 16
ARGB_4444 16
ALPHA_8 8

Alpha_8格式主要用于Alpha通道模板,相当于做一个染色。图像要渲染两次,虽然减少内存,但增加了绘制的开销。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeStream(is,null,options);
inSampleSize

如果内存中的图片大于屏幕显示出的图片大小,或者大于指定屏幕区域的大小,这些高分辨率图片会导致严重的性能问题。使用inSampleSize可以重置这些图片大小,让它们符合实际显示的大小,既能减小内存的开销,也能提高显示的效率。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
BitmapFactory.decodeStream(is,null,options);

inSampleSize为2时获得只有1/2大小的图片,设置为4就是1/4大小的图片。图片总会比原始图片小一倍以上。

inScaled,inDensity和inTargetDensity

虽然inSampleSize可以实现图片的缩放,都是指数幂的缩放。如果想要更细地缩放图片,就需要使用位图的inScaled,inDensity和inTargetDensity功能。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = true;
options.inDensity = srcWidth;
options.inTargetDensity = dstWidth;
BitmapFactory.decodeStream(is,null,options);

当inScaled设置为true时,系统会按照现有的密度来划分目标密度。当这些属性的设置会因为使用过多的算法,导致更多的时间开销。所以最好结合使用,达到最佳的性能结合。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
options.inScaled = true;
options.inDensity = options.outWidth;
options.inSampleSize = 4;
options.inTargetDensity = dstWidth * options.inSampleSize;
options.inJustDecodeBounds = false;
BitmapFactory.decodeStream(is,null,options);
inBitmap

如果设置了该属性,那么当使用了该带有Options参数的decode方法加载内容时,decode方法会尝试重用一个已存在的位图。

Crash监控

Crash是由于代码异常而导致App非正常退出,导致应用程序无法继续使用,所有工作都停止的现象。
要降低Crash发生的概率,需要Crash监控和发生Crash时的堆栈信息,开发者拿到这些信息后分析导致Crash的原因并修复。

Java层Crash监控

在Android中,Java虚拟机为每个进程都设置了一个默认UncaughtExceptionHandler,用于处理本进程中未被try catch的Exception。因此只要实现了UncaughtExceptionHandler接口,并在进程启动时调用Thread.setDefaultUncaughtExceptionHandler()传入自定义UncaughtExceptionHandler。当出现没被catch的异常时,就会回调uncaughtException(Thread thread, Throwable ex)方法,可以在这里记录crash日志,并上报给服务器,也可以执行一些个性化的异常处理操作。

Crash上报机制

不管是Java层,还是Native导致了Crash,因为应用已经异常了,所以放生Crash后,尽量先保存日志到本地,在下一次网络正常时再上传日志。

ANR剖析

ANR也是Android应用程序无法继续运行的一种异常,一般是应用处理长时间没有结果响应导致主进程不能处理下一件事情。

  1. KeyDispathTimeout: 最常见的ANR类型是对输入事件5s内无响应。
  2. BroadcastTimeout: BroadcastReceiver在指定时间(原生系统默认是10s)内无法处理完成,并且没有结束执行onReceive。
  3. ServiceTimeout: 这种类型在Android应用中出现的概率很小,是指Service在特定的时间(原生系统是20s)内无法处理完成。

ANR分析

在Android系统上,如果发生ANR,Logcat会产生对应的日志和一个trace文件。

Logcat信息

  • ANR IN: 发生ANR的具体类
  • PID: 发生ANR的进程,系统在此时会生成trace文件,当前的时间点也是发生ANR的具体时间,以及生成trace文件的时间
  • Reason: 当前ANR的类型以及导致ANR的原因
  • CPU usage: CPU的使用情况,在日志中CPU usage有两个时间点,第一个是发生ANR前的CPU使用情况,第二个是发生ANR后的CPU使用情况。

trace文件

在Android Studio上使用Analyze Stacktrace对trace文件进行分析。

使用方法

  1. 在Android Studio的工具栏中,选择Analyze -> Analyze Stacktrace, 打开Analyze Stacktrace工具窗口
  2. 将traces.txt中的内容复制到窗口,单击Normalize按钮,生成Thread Dump列表
  3. 如果某个线程被标红,说明此线程被堵塞了,然后在右边的详细信息中查看堵塞的具体原因。

ANR监控

卡顿监控可以通过一个子线程向主线程发消息,通过时间差来判断是否发生卡顿。

提高后台进程存活率

在Android系统中,应用进程停止运行有以下几个原因:

  • 用户主动退出
  • Crash异常退出
  • 系统通过杀掉进程回收内存

虽然系统通过杀掉进程回收内存能更好的为其他应用提供内存,但对体验会有一点影响。

应用进程优先级

  • NATIVE_ADJ = -17: 系统创建的Native进程
  • SYSTEM_ADJ = -16: 系统进程,在运行的过程中永远不会杀掉
  • PERSISTENT_PROC_ADJ = -12: 核心进程,系统不会杀掉这类进程,但即使杀掉,影响面也没有SYSTEM_ADJ进程那么严重
  • PERSISTENT_SERVICE_ADJ = -11: 正在运行的服务进程,一般不会杀掉
  • FOREGROUND_APP_ADJ = 0: 前台进程,是指正在前台运行的应用,被杀概率不大
  • VISIBLE_APP_ADJ = 1: 可见进程,用户正在使用,或者有界面在显示,除非出现异常,否则系统不会杀掉这类进程
  • PERCEPTIBLE_APP_ADJ = 2: 可感知的进程,虽然不在前台,但应用进程还在状态,系统除非到内存非常紧张才会杀掉这类进程,比如播放音乐的应用
  • BACKUP_APP_ADJ = 3: 正在备份的进程
  • HEAVY_WEIGHT_APP_ADJ = 4: 高权重进程
  • SERVICE_ADJ = 5: 有Service进程
  • HOME_APP_ADJ = 6: 与Home有交互进程。比如widget小挂件之类,一般尽量避免杀掉此类进程
  • PREVIOUS_APP_ADJ = 7: 切换进程,可以理解为从可见进程切换过来的进程的状态
  • CACHED_APP_ADJ = 8: 缓存进程,也就是空进程
  • SERVICE_B_ADJ = 9: 不活跃的进程
  • HIDDEN_APP_MIN_ADJ = 15: 缓存进程,空进程,在内存不足的情况下会被优先杀掉
  • UNKNOWN_ADJ = 16: 最低级别进程,只有缓存的进程,才有可能设置这个级别

当内存不足时,进程优先级低的、占内存大的App进程将会被优先杀掉,系统杀进程的规则如下:

  • 进程优先级设置为PERSISTENT_PROC_ADJ被杀概率较低
  • 进程优先级HEAVY_WEIGHT_APP_ADJ,这种是Activity仅次于主进程,系统认为是高权重进程
  • 前台进程FOREGROUND_APP_ADJ不会被杀掉
  • 当Activity、Service的生命周期发生变化时都会调整进程的优先级
  • 进程中没有任何Activity存在会优先被杀
  • 空进程最容易被杀

提高进程优先级

  • 网络连接: 通过长连接和进程保持通信,使进程保持活动状态,但是如果系统内存非常紧张,也有可能被杀。
  • 利用系统现有机制: 一般可以注册系统消息,通过系统消息响应挂起进程
  • SyncAdapter: 利用Android系统提供的账号同步机制实现进程优先级提高

耗电优化

耗电检测工具

Android5.0引入了一个专门获取设备电量消耗信息的API: Battery Historian

Battery Historian是一款由Google提供的Android系统电量分析工具,和Systrace一样,是一款图形化数据分析工具,直观地展示出手机的电量消耗过程,通过输入电量分析文件,显示消耗情况,最后提供一些可供参考电量优化的方法。

Battery Historian使用步骤

  1. 初始化Battery Historian使用以下两个adb命令adb shell dumpsys batterystats --enable full-wake-history, shell dumpsys batterystats --reset
  2. 初始化完成后,操作需要测试电量的一些场景
  3. 保存数据 运行以下命令将bugreport信息保存为bugreport.txt文件 adb bugreport > bugreport.txt 成功后打开bugreport.txt,可以看到应用的耗电数据。(虽然数据很详细,但可读性差,因此需要生成可读性更高的html格式)
  4. 生成HTML报告 python historian.py -a bugreport.txt > battery.html historian.py脚本可以从Github上下载,下载地址
  5. 使用Chrome打开生成的HTML文件,即可查看详细的报告

报告参数解析

横坐标表示时间周期,以60s为一个时间周期,刻度上的时间单位是秒。其中起始时间就是前面初始化的时间,结束时间是获取bugreport的时间,而纵坐标就是各项具体的指标。

Android5.0 Battery Historian属性

属性 意义
battery-level 电量,显示出电量的变化。
plugged 充电状态,是否进行了充电,以及充电的时间范围
screen 屏幕状态,屏幕是否点亮
top 处于最上层的应用,可以通过此栏信息来判断是哪个应用程序对手机电量的影响,同时也得到该应用的耗电量信息。同时也记录了应用启动和运行的时间
wake_lock 记录wake_lock模块的工作时间
running 界面的状态,可以判断应用在无操作状态下电量的消耗
wake_lock_in 记录模块开始工作以及工作的时间
data_conn 数据连接方式的改变,比如2G/3G和Wi-Fi之间的切换
status 电池状态,如充电、放电和已充满等
phone_signal_strength 手机信号状态
plug 充电方式

Android 6.0 新增属性

属性 意义
Gps 是否开启GPS
Sync 是否和后台同步
Mobile_radio 是否开启Radio
Wake_reason 被唤醒的原因
Phone_in_call 是否进行通话

三大模块省电优化

显示

一般屏幕材质分为LCD和OLED,LCD和OLED这两种屏幕从发光方式上有着本质的区别,一种是靠外部光源照亮,一种是自发光。LCD当亮度一定,不论什么颜色耗电相同;OLED显示深色比浅色更省电。

网络

移动设备的网络连接方式分为移动运营网络和Wi-Fi,一般使用Wi-Fi比移动网络功耗低。

优化网络方案

  • 使用Wi-Fi传输数据时,应尽可能增大每个包的大小,并降低发包的频率。
  • 在蜂窝移动网络下,最好做到批量执行网络请求,尽量避免频繁的间隔网络请求。
  • 尽量在Wi-Fi环境下使用数据传输
  • 使用效率高的数据格式和解析方法,尽可能使用Protobuf。
  • 压缩数据格式,比如采用GZIP压缩,虽然解压缩需要消耗更多的CPU,但可以明显提高下载速度,使网络传输更快结束,可以节省更多的电量,并提高数据的获取速度。

CPU

减少CPU的开销

应用常用优化方案

计算优化

避开浮点运算的优化方法

  • 除法变乘法
  • 充分利用移位
  • 查表发,直接使用映射关系,但这会增加内存开销
  • 利用arm neon指令集做并行运算

避免WakeLock使用不当

合理使用PowerManager的WakeLock,避免导致严重的耗电问题

  • PARTIAL_WAKE_LOCK: 保持CPU正常运转,屏幕和键盘灯有可能会关闭
  • SCREEN_DIM_WAKE_LOCK: 保持CPU运转,允许保持屏幕显示但有可能变暗,允许关闭键盘灯。
  • SCREEN_BRIGHT_WAKE_LOCK: 保持CPU运转,允许保持屏幕高亮显示,允许关闭键盘灯
  • FULL_WAKE_LOCK: 保持CPU运转,保持屏幕高亮显示,键盘灯也保持亮度
  • ACQUIRE_CAUSES_WAKEUP: 强制使屏幕亮起,这种锁主要用于一些必要通知用户的操作
  • ON_AFTER_RELEASE: 当锁被释放时,保持屏幕亮起一段时间

使用Job Scheduler

Job Scheduler只有一系列的预置条件满足时,才执行对应的操作,这样既能省电,又保证了功能的完整性。

使用场景

  • 重要不紧急的任务,可以延迟执行,如定期数据库数据更新和数据上报
  • 耗电量较大的任务,比如充电时才希望执行的备份数据操作
  • 不紧急可以不执行的网络任务,如在Wi-Fi环境预加载数据
  • 可以批量执行的任务

Doze模式

Doze模式是通过限制应用访问网络以及其他一些操作的频率,来减少CPU的开销,达到省电的目的。Android6.0系统只要满足(未连接电源和屏幕关闭)这两个条件就会进入Doze模式。

同时为了保证应用正常工作,系统会周期性地退出休眠模式,在退出休眠模式这个时间里去执行那些在休眠模式下挂起的任务,这个时间窗口被称作维护窗口。同时随着系统处于Doze模式时长的增长,进入维护窗口的频率会逐步降低。

Doze模式状态机

  • ACTIVE: 手机设备处于激活活动状态,设备在使用或者在充电中
  • INACTIVE: 刚脱离ACTIVE状态,进入非活动状态,关闭屏幕并没有充电
  • IDLE_PENDING: IDLE预备态,准备进入IDLE态,在每隔30分钟让应用进入等待空闲预备状态
  • IDLE: 设备进入空闲状态
  • IDLE_MAINTENANCE: 处理挂起任务,在此状态可以执行在INACTIVE态挂起的任务

在Doze模式中的状态进入IDLE时,有以下行为会受到限制

  • 断开网络连接
  • 系统忽略Wake Lock
  • 标准闹钟AlarmManager定时任务延迟到下一个maintenance window进行处理。
  • 系统不会扫描热点Wi-Fi
  • 禁止同步工作
  • 停止JobScheduler任务调度

安装包大小优化

应用装包的构成

assets

assets目录可以根据应用需求存放任何文件夹架构,如配置文件、资源文件,这些文件的内容在程序运行过程中可以通过AssetManager类获得。

lib

该目录存放应用程序依赖的C/C++编写的native库文件。一般包含ARM、ARM-v7a、MIPS、X86文件夹。
由于X86和MIPS架构的易懂智能终端比较少,并且X86的设备基本都兼容了ARM指令集。所以一般只需包含ARM和ARM-V7a的so,如果不需要用到neon指令集,就只需要包含ARM架构下编译的so即可。

res

这个目录存放资源文件,在这个文件夹下的所有文件都会生成对应的ID映射到Android工程的.R文件中。

META-INF

保存应用的签名信息,签名信息可以验证APK文件的完整性。有利于确保Android应用的完整性和系统的安全性。META-INF目录下包含的文件有CERT.RSA、CERT.DSA、CERT.SF和MANIFEST.MF。其中CERT.RSA是开发者利用私钥对APK进行签名的签名文件,CERT.SF和MANIFEST.MF记录了文件中文件的SHA-1哈希值

AndroidManifest.xml

用来描述Android应用的配置文件。

classes.dex

Java可执行程序,需要先把Java文件编译成class文件,字节码都保存在class文件中,Java虚拟机可以通过解释并执行这些class文件。AndroidSDK中的dx工具可以对多个class文件进行合并重组、优化,达到减小体积,缩短运行时间的目的。

projuard.cfg

代码混淆配置文件

resources.arsc

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

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

代码混淆

ProGuard是一个开源的Java代码混淆器,并且默认集成到Android SDK中。

  • 压缩: 移除无效的类、属性、方法
  • 优化: 移除无用的结构
  • 混淆: 把类名、属性名、方法名替换为一到两个字母
ProGuard常用参数
ProGuard配置

-include {filename}: 从给定的文件中读取配置参数
-basedirectory {directoryname}: 指定基础目录为以后对应的档案名称
-injars {class_path}: 指定要处理的应用程序jar、war、ear和目录
-outjars {class_path}: 指定处理完后要输出的jar、war、ear和目录名称
-libaryjars {classpath}: 指定要处理的应用程序jar、war、ear和目录所需的程序库文件
-dontskipnonpubliclibraryclasses: 指定不忽略非公共的库类
-dontskipnonpubliclibraryclassmembers: 指定不忽略包可见的库类的成员

保留选项

-keep {Modifier} {class_specification}: 保护指定的类文件和类的成员
-keepclassmembers {modifier} {class_specification}: 保护指定类的成员
-keepclasseswithmembers {class_specification}: 保护指定的类和类的成员,但条件是所有指定的类和类成员要存在
-keepnames {class_specification}: 保护指定的类和类的成员名称
-keepclassmemebernames {class_specification}: 保护指定的类的成员名称
-keepclasswithmembernames {class_specification}: 保护指定的类和类的成员名称
-printseeds {filename}: 列出类和类的成员 -keep选项的清单,标准输出到给定的文件

压缩

-dontsshrink: 不压缩输入的类文件
-printusage {filename}: 输入无用文件

优化

-dontoptimize: 不优化输入的类文件
-assumenosideeffects {class_specification}: 优化时假设指定的方法,没有任何副作用
-allowaccessmodification: 优化时允许访问并修改有修饰符的类和类的成员

混淆

-dontobfuscate: 不混淆输入的类文件
-printmapping {filename}: 输出映射表
-applymapping {filename}: 重用映射增加混淆
-obfuscationdictionary {filename}: 使用给定文件中的关键字作为要混淆方法的名称
-overloadaggressively: 混淆时应用侵入式重载
-useuniqueclassmembernames: 确定统一的混淆类的成员名称来增加混淆
-renamesourcefileattribute {string}: 设置源文件中给定的字符串常量

资源优化

使用Android Lint删除冗余资源
  1. 单击菜单栏中的Analyze -> Run Inspection by Name,弹出工具选择框
  2. 在弹出的输入框中Unused resources,回车进入扫描前目录设置对话框
  3. 选择需要扫描的目录或整个工程,单击OK按钮开始扫描
  4. 扫描完成后生成报告,可在Android Studio中直接查看并删除确认无用的资源文件
资源文件最少化
  • 尽量使用一套图片资源,遇到一些图片在不同分辨率手机上变化差异过大的情况时,再考虑在相应文件夹下放入这个特定的图片
  • 使用一套图、一套布局、多套dimens.xml文件,在使用最小资源的情况下,解决多分辨率适配。
  • 使用轻量级的第三方库
  • 减少项目中的预置图片,预置图片改成由服务器下发,尽可能地将程序与资源分离。
图片优化

JPG支持最高级别的压缩,但这种压缩有损耗,并且没有透明通道。
资源打包工具AAPT在应用打包过程中会自动对PNG图片做压缩处理,但减小的大小比较有限.除了靠工具自动打包,一般还可以采用降低图片色彩位数和PNG图片压缩工具来减少图片大小。
对于非透明的大图,JPG文件会比PNG文件小很多,通常会减小到一半以上。一半用于应用中一些闪屏介绍页、全图背景等。

其他优化

  • 避免重复功能的库(建议选择性价比更高、更适合当前应用业务需求的一个库,在这个基础上增加相应的功能,扩展当前的库,而非引入新的开源库)
  • 使用WebP格式图片(WebP是一种新的图片格式,它支持透明度,压缩比比JPG更好,但效果不比JPG差,但只有Android4.2.2才很好地支持WebP格式)
  • 插件化(建议在用户使用率不高的功能模块上使用,或者使用预加载)