Android 性能优化
谈 Android 性能优化, 总结起来分为四大问题: 流畅, 稳定, 省电, 省流量.
1, 流畅
我们试着分析下 APP 操作起来感觉不流畅的原因: 1, 因为网络请求而等待感觉卡顿; 2, 绘制页面时无法及时显示而感觉卡顿. 第一个原因受当时用户所在的环境影响较大, 可能网络较差或者是服务器处理时间过长等, 这个时候优化就得从网络数据缓存, 压缩传输数据, 使用 DNS 服务, 后台服务器优化等方面着手. 但是这方面 APP 端能做的有限, 我们的重点还是关注页面绘制优化.
1.1,Android 绘制原理
APP 通过对 View 树的递归完成测量, 布局和绘制之后会通过 IPC 将这一帧的数据发送给 SurfaceFlinger 服务进程, SurfaceFlinger 拿到数据之后开始进行渲染, 然后再刷新屏幕. 这里需要注意的是屏幕的刷新机制:
Android 每 16ms 发送 VSNCY 同步信号来发起一次屏幕的刷新. 在显示内容的数据内存上采用了双缓冲机制, 一个为前台缓冲区, 一个为后台缓冲区. 只有当另一个缓冲区准备好数据之后才会通知显示设备切换缓冲区中的数据, 这样能有效的解决屏幕闪烁的问题. 在收到 VSNCY 信号时, 假如当前开始渲染帧 A 的数据, 同时 CPU 开始处理下一帧 B 的数据, 当下一次 VSNCY 信号到来时, 显示设备应该切换至帧 B, 这时如果 CPU 还没处理完帧 B 的数据, 那么显示设备依然只能显示 A, 也就是发生了丢帧的情况 (从直观角度上来看就是卡顿). 当然如果渲染帧 A 用时时超过了 16ms, 同样的也会发生丢帧的情况.
那么通过简单的了解了绘制原理, 我们就可以知道了导致卡顿的原因是:
绘制一帧需要耗费很长时间
主线程在 16ms 内无法完成数据的准备
1.2, 优化绘制内容
1.2.1, 布局优化
减少层级
因为层级越少, ViewTree 在递归测量和绘制的时间就会越短. 所以我们可以通过合理的使用 RelativeLayout 和 LineaLayout, 合理使用 Merge 标签 (merge 标签只能作为复用布局的 root 元素来使用). 关于查看 layout 层级关系时, 我们可以通过 hierarchy View 工具来查看.
加快显示
使用 ViewStub 标签, 只有显示调用显示方法时才会加载. 但只能加载一次并且加载显示之后不能在通过 ViewStub 进行操作了.
布局复用
使用 include 标签来引入一个统一的布局, 方便维护修改.
1.2.2, 避免过度绘制
过度绘制是指在屏幕上的某个像素在同一帧的时间内被绘制了多次. 简单点就是看不见的部分就不用绘制了. 例如两个 View 有重叠的部分, 被覆盖的部分 UI 就不用绘制了. 一般产生过度绘制的主要原因有:
xml 布局时控件有重叠并且都要设置自己的背景
自定义控件时同一个区域被绘制了多次
在自定义 View 时, 如果发现有重叠部分, 可以使用 clipRect() 来控制绘制的区域.
检测是否过度绘制, 可以使用开发者模式开发过度绘制选项, 就可以看到过度绘制的区域.
1.3, 检测卡顿
我们如何才能检测到 UI 是否发生了卡顿现象呢? 如果你了解 handler 机制的话, 那自然也就知道 Looper 对象. 检测和监控卡顿我们可以利用 Looper 中的 Printer 对象来实现.
- public static void loop() {
- final Looper me = myLooper();
- if (me == null) {
- throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
- }
- final MessageQueue queue = me.mQueue;
- Binder.clearCallingIdentity();
- final long ident = Binder.clearCallingIdentity();
- for (;;) {
- Message msg = queue.next(); // might block
- if (msg == null) {
- return;
- }
- // This must be in a local variable, in case a UI event sets the logger
- final Printer logging = me.mLogging;
- if (logging != null) {
- logging.println(">>>>> Dispatching to" + msg.target + " " +
- msg.callback + ":" + msg.what);
- }
- final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
- final long traceTag = me.mTraceTag;
- if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
- Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
- }
- final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
- final long end;
- try {
- msg.target.dispatchMessage(msg);
- end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
- } finally {
- if (traceTag != 0) {
- Trace.traceEnd(traceTag);
- }
- }
- if (slowDispatchThresholdMs> 0) {
- final long time = end - start;
- if (time> slowDispatchThresholdMs) {
- Slog.w(TAG, "Dispatch took" + time + "ms on"
- + Thread.currentThread().getName() + ", h=" +
- msg.target + "cb=" + msg.callback + "msg=" + msg.what);
- }
- }
- if (logging != null) {
- logging.println("<<<<<Finished to" + msg.target + " " + msg.callback);
- }
- final long newIdent = Binder.clearCallingIdentity();
- msg.recycleUnchecked();
- }
- }
在每个 message 处理的前后都会调用 Printer.println() 方法, 如果主线程卡住了说明 dispatchMessage 内部出现了问题. 根据这个我们就能检测出 UI 是否发生了卡顿.
2, 内存优化
内存优化也是性能优化中的重要一项, 当内存发生泄漏时就有可能导致 OOM, 而且当内存发生抖动也就是 gc 频繁时也会导致卡顿. 因为 gc 时所有的线程都会停止工作, 因此我们需要减少 gc 的频率. 我们都知道 gc 的作用的回收无任何引用的对象占据的内存空间, 那怎么来判断对象是否被引用了呢? gc 会选择一些还存活的对象作为内存遍历的根节点 GC Roots, 通过对 GC Roots 的可达性来判断是否需要回收. 那么什么时候会引起 gc 呢?
Android 系统中为了整个系统的内存控制需要, 为每一个应用都设置了一个硬性的 Dalvik heap size 最大限制阀值.
当分配内存时发现内存不够的情况下引起的 gc;
当内存达到一定的阀值时出发 gc;
显示的调用 gc.
所以我们不应该频繁的显示的调用 gc.
2.1, 避免内存泄露
分配出去的内存, 当不需要时对这部分内存没有收回, 这时就发送了内存泄露. 常见的内存泄露场景:
资源性对象未关闭
资源性对象 (coursor,file 等), 在不使用的时候应该及时关闭他们.
注册对象未注销
如果事件注册后未注销, 会导致观察者列表中维持着对象的引用, 阻止垃圾回收, 一般发生在注册广播接收器, 注册观察者等.
类的静态变量持有大数据对象
静态变量长期维持对象的引用, 阻止垃圾回收, 会造成内存不足等问题.
非静态内部类的静态实例
非静态内部类会维持一个到外部类实例的引用, 如果非静态内部类的实例是静态的, 就会间接长期维持着外部类的引用, 阻止被系统回收.
Handler 临时性内存泄露
Message 发出之后会存放在 messageQueue 中, 如果这个 message 还没来的及处理, Activity 退出了, 但是 message 对象的 target 是指向 handle, 这样就会导致 Handler 无法回收. 如果这个 handler 是非静态的, 还会导致 Activity 不会别回收.
容器中的对象没清理造成的内存泄露
通常把一些对象的引用加入集合中, 在不需要该对象时, 如果没有把它的引用从集合中清理掉, 这个集合就会越来越大.
webView 内存泄露
我们都知道 WebView 都存在内存泄露的问题, 在应用中只要使用一次 webview, 那这部分的内存就不会释放掉. 通常解决方案就是为它单独开启一个进程.
2.2, 内存优化
当然是不是没有内存泄露就可以了呢? 不是的, 前面也提到了每个应用的内存使用都是有限制的, 所以在内存的使用上我们就得注意了.
减少不必要的内存开销;
尽量使用自动装箱对象, 这样能避免创建相对应对象; 在内存上根据相应场景对内存进行复用, 比如视图的复用 (listView),Bitmap 对象的复用.
使用最优的数据类型
ArrayMap 相对于 HashMap 而言避免了过多的内存开销, 因为 ArrayMap 内部使用两个小数组实现. 还有比如 SpareArray 等.
避免使用枚举
我们知道枚举在转换成 class 时都是静态常量, 相比于普通常量占用内存要高很多. 可以使用 Android 提供的注解 IntDef,StringDef 等来实现类型安全.
图片内存优化
图片在移动开发中占用的内存是非常突出的, 那么关于图片的内存优化我们能做些什么呢? 一般从压缩图片来减少内存的使用: 降低位图的规格 (RGB_8888/RGB_565 等规格); 根据需要缩放图片和压缩图片质量等.
2.3, 内存泄露检测
借助第三方库 LeakCanary 检测内存泄露
借助 android studio 提供的 Android profile 工具分析内存
3, 数据存储优化
数据的存储方式主要分为 ContentProvider, 文件, sharedPreference 及 sqlite 数据库四种数据存储方式. 这里主要看 SQLite 数据库的一些关于性能的注意点.
使用 SQLiteStatement 插入数据
与使用普通的执行 executeSql() 而言, 它的插入数据花费的时间更短, 而且能在一定的程度上防止 SQL 语句的注入.
使用事务
频繁的插入数据, 在没有显示的创建事务时, 插入时会频繁的创建事务会影响插入的效率. 显式创建事务时能更快的插入数据, 另外开启事务能够保证原子性提交.
4, 代码优化
尽管我们平时在写代码的时候需要时刻注意了代码编写规范, 但是还是会有些小问题存在. 那么我们就需要借助一些代码静态扫描工具来检测我们的代码, 比使用 Android studio 自带的 Android Lint 工具, findbugs 等. 当然我们可能也需要收集一些我们代码没有捕获到的异常, 我们可以利用 Java 虚拟机为每个进程都设置了一个 UNcaughtExceptionHandler, 实现这个接口就能收集到没有捕获的异常, 然后返回到我们的服务器, 根据这些异常再进行定位问题.
5, 耗电优化
我们可以借助 google 提供的 Android 系统电量分析工具 Battery Historian, 通过这个可以直观的展示出手机的电量消耗过程. 用法:
1, 初始化 Battery Historian;
adb shell dumpsys batterystats --enable full-wake-history
adb dumpsys batterystats --reset
2, 初始化完成之后, 操作需要测量电量的一些场景;
3, 保存数据
adb bugreport> bugreport.txt
保存完数据之后, 可以借助 Battery Historian https://github.com/google/battery-historian 工具来打开文件查看具体的耗电情况.
来源: http://www.jianshu.com/p/fbedee82d243