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工具来打开文件查看具体的耗电情况。
网友评论