Android应用性能优化
性能优化分类
- 卡顿优化
- 内存优化
- 电量优化
- 网络优化
- 启动优化、安装包体积优化
官网性能优化指导(https://developer.android.com/topic/performance/index.html)
卡顿优化
卡顿:从用户角度说,App操作起来缓慢,响应不及时,列表滑动一顿一顿的,动画刷新不流畅等等一些直观感受。从系统角度来说,屏幕刷新的帧率不稳定,无法保证每秒绘制60帧,也就是说有掉帧的情况发生。
![](https://i-blog.csdnimg.cn/blog_migrate/dacd0ab0c8659c7f6ffa5ce1f0dbeb0c.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/6336f11cbea18cafa48999b7b4d8efd1.png)
VSync :垂直同步
Main Thread :主线程
Render Thread : 渲染线程
对于开发过程,出现卡顿的主要原因是主线程做了一些不该做的事,或者主线程做不了事情了。
- 布局渲染(解析、测量、布局、渲染)
- 动画执行
- Binder通信
- 界面响应
主线程主要是做以上四个方面的工作,如果在主线程做一些耗时操作(网络请求、IO读写等),或者被其他线程挂起(GC),那么页面刷新无法在16ms内完成,就会出现掉帧的情况。
掉帧检测方案
Looper
Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断取出message,调用其绑定的Handler在UI线程执行。如果在handler的dispatchMesaage方法里有耗时操作,就会发生卡顿。
下面看Looper.loop()方法源码:
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;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
//处理消息前,打印开始日志
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
//处理完消息后,打印结束日志
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
}
我们可以根据消息处理前后的日志输出作为检测点,计算出消息处理的耗时,如果超出16ms,说明发生了卡顿,此时就可以把UI线程的堆栈日志打印出来。
Looper.getMainLooper().setMessageLogging(new Printer() {
private static final String START = ">>>>> Dispatching";
private static final String END = "<<<<< Finished";
@Override
public void println(String x) {
if (x.startsWith(START)) {
UiBlockLogMonitor.getInstance().startMonitor();
}
if (x.startsWith(END)) {
UiBlockLogMonitor.getInstance().stopMonitor();
}
}
});
不过,由于系统定制的原因,打印出来的日志标识不一定标准,所以可以改为判断第一次日志输出和第二次日志输出。
Choreographer.FrameCallback
Choreographer官方说明(https://developer.android.com/reference/android/view/Choreographer.html)
Choreographer 编舞者,协调动画、输入和绘图的时间(api >= 16)。
Choreographer从显示子系统接收定时脉冲(如垂直同步),然后安排下一帧的渲染工作。
在开发中,我们并不直接使用Choreographer,当我们想要检测是否有丢帧发生时,可以利用Choreographer.FrameCallback回调的方式,获取每一帧开始绘制的时间,通过计算两帧之间的时间差,如果大于16ms,说明发生了丢帧。
//为Choreographer设置一个回调,当一帧开始渲染时触发。
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
long lastFrameTimeNanos = 0;
long currentFrameTimeNanos = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
}
currentFrameTimeNanos = frameTimeNanos;
long diffMs = TimeUnit.MILLISECONDS.convert(currentFrameTimeNanos - lastFrameTimeNanos, TimeUnit.NANOSECONDS);
lastFrameTimeNanos = currentFrameTimeNanos;
if (diffMs == 0) {
diffMs = (long) 16.7;
}
if (isShowFPS) {
long current = System.currentTimeMillis();
if (current - mLastFPSRefreshTs > refreshInterval) {
int fps = (int) (1000 / diffMs);
refreshFPS(fps);
mLastFPSRefreshTs = current;
}
}
if (diffMs > 16.7f) {
long droppedCount = (long) (diffMs / 16.7f);
if (droppedCount > 1) {
System.out.println("掉帧数 : " + droppedCount);
}
}
if (UiBlockLogMonitor.getInstance().isMonitor()) {
UiBlockLogMonitor.getInstance().stopMonitor();
}
if (isDetectContinue) {
UiBlockLogMonitor.getInstance().startMonitor();
Choreographer.getInstance().postFrameCallback(this);
}
}
});
问题检测工具
当发生掉帧时,需要判断是什么原因导致了UI线程耗时过程或阻塞。这时需要借助一些开发工具来帮助定位。
systrace
systrace 官方说明:https://developer.android.com/studio/command-line/systrace.html
systrace.py 是一个命令行工具,位于 …/sdk/platform-tools/systrace目录下。在应用运行时,它可以帮助我们收集和分析所有进程的计时信息,包含了CPU调度、应用线程、磁盘活动等Android内核数据,然后生成一份HTML报告。
systace对检测应用UI表现非常有效,因为它可以分析你的代码和帧率来识别出问题区域,然后提出可能的解决方案。示例
![](https://i-blog.csdnimg.cn/blog_migrate/6afb15a6fe69f91205be11510a8df5dd.jpeg)
如果其中的Expensive measure/layout 或 Long View#draw() 警告特别多,可能是因为页面层级比较深,导致测量、布局和渲染时间过长,从而引起掉帧。
检测布局层级是否太深最有效的工具就是开发者选项中的GPU过度绘制模式了,这个稍后会讲到。
systrace对每一种警告类型都做出了解释:
Scheduling delay
渲染一帧的工作被推迟了几个毫秒,从而导致了不合格。确保UI线程上的代码不会被其他线程上完成的工作阻塞,并且后台线程(例如,网络或位图加载)在android.os.Process#THREAD_PRIORITY_BACKGROUND中运行或更低,因此它们不太可能中断UI线程。
Expensive measure/layout pass
测量/布局花费了很长时间,导致掉帧,要避免在动画过程中触发重新布局。
Long View#draw()
记录无效的绘图命令花费了很长时间,在View或Drawable自定义视图时,要避免做耗时操作,尤其是Bitmap的分配和绘制。
Expensive Bitmap uploads
修改或新创建Bitmap视图要传送给GPU,如果像素总数很大,这个操作会很耗时。因此在每一帧中要尽量减少Bitmap变更的次数。
Inefficient View alpha usage
将alpha设置为半透明值(0<alpha<1)会很耗性能,尤其是对大视图。所以最好短暂地使用alpha属性。
traceview
TraceView 是 Android SDK 中内置的一个工具,它可以加载 trace 文件,用图形的形式展示代码的执行时间、次数及调用栈,便于我们分析。我们可以在Android Profiler或DDMS中启动它。
![](https://i-blog.csdnimg.cn/blog_migrate/12fb86e09a78eb8cf3acb37b136fd4ff.jpeg)
使用这个工具最关键的地方就是要理解各个统计维度的含义:
方法执行时间
Incl Cpu Time: 执行方法X及子方法占用Cpu的时间
Excl Cpu Time: 执行方法X占用Cpu时间,不包含子方法
Incl Real Time: 执行方法X及子方法总时间
Excl Real Time: 执行方法x总时间
Cpu Time/Call: 每次执行方法X占用Cpu时间
Real Time/Call: 每次执行方法X总时间
占用CPU比例
Incl Cpu Time%
Excl Cpu Time%
Incl Real Time%
Excl Real Time%
以上各个时间占Cpu执行耗时的百分比
调用次数
Calls + Recur Calls/Total: 方法X调用次数和递归调用次数
使用时只需要关注 Incl Real Time、Real Time/Call、Calls + Recur Calls/Total这三个指标即可,找出应用包名下的耗时方法调用后加以优化。
GPU过度绘制调试模式
开发者选项 -> 调试GPU过度绘制
![](https://i-blog.csdnimg.cn/blog_migrate/42e4e0b5b9d0c1740a26b202f465886c.png)
- 原色:没有过度绘制
- 蓝色:过度绘制1次
- 绿色:过度绘制2次
- 粉色:过度绘制3次
- 红色:过度绘制4次或更多
请注意,这些颜色是半透明的,因此,您在屏幕上看到的确切颜色取决于您的界面内容。
可以通过此功能查看哪些页面的布局层级过深。
常见卡顿原因及解决方案
过度绘制
去除不必要的背景色
- 设置窗口背景色为通用背景色,去除根布局背景色。
- 若页面背景色与通用背景色不一致,在页面渲染完成后移除窗口背景色
- 去除和列表背景色相同的Item背景色
布局视图树扁平化
- 移除嵌套布局
- 使用merge、include标签
- 使用性能消耗更小布局(TableLayout、ConstraintLayout)
减少透明色,即alpha属性的使用
- 通过使用半透明颜色值(#77000000)代替
其他
- 使用ViewStub标签,延迟加载不必要的视图
- 使用AsyncLayoutInflater异步解析视图
主线程耗时操作
- Json数据解析耗时(Cache类)
- 文件操作(获取所属渠道名称)
- Binder通信(获取系统属性(mac地址))
- 正则匹配(Hybird 通信)
- 相机操作:初始化、预览、停止预览、释放(反扫)
- 组件初始化(推送)
- 循环删除、创建View(更多页面)
- WebView首次初始化
处理方案评估:
异步 > 缓存 > 替代方案 > 保持原状
异步:
- 登录、退出登录后的数据处理
- 相机操作
- 组件初始化
示例:
//异步启动消息推送服务
private void startPushAsync(Context context) {
Subscription startPushSub = Observable.unsafeCreate(subscriber -> {
MyPushManager.getInstance().startPush(context);
MyPushManager.getInstance().connectHwPushAgent(mInteraction.getActivity());
}).compose(executorTransformer.transformer())
.subscribe(new DefaultSubscriber<Object>(context) {
@Override
protected void onFinally() {
super.onFinally();
}
});
addSubscription(startPushSub);
}
缓存:
- Cache类
- 系统属性
示例:
//获取应用渠道标识
public static String getChannel(Context context) {
if (!TextUtils.isEmpty(APP_CHANNEL)) {
return APP_CHANNEL;
}
...
zipfile = new ZipFile(sourceDir);
Enumeration<?> entries = zipfile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = ((ZipEntry) entries.nextElement());
String entryName = entry.getName();
if (entryName.contains(start_flag)) {
channel = entryName.replace(start_flag, "");
break;
}
}
}
替代方案:
- Hybird通信中的一处正则匹配
- 更多页面采用RecycleView嵌套
示例:
private void dispatchMessage(WVJBMessage message) {
String messageJSON = message2JSONObject(message).toString();
//使用JSONObject的quote方法,代替正则替换,效率更高
messageJSON = JSONObject.quote(messageJSON);
messageJSON = messageJSON.substring(1, messageJSON.length() - 1);
log("SEND", messageJSON);
executeJavascript("WebViewJavascriptBridge._handleMessageFromObjC('"
+ messageJSON + "');");
}
旧版本
String messageJSON = message2JSONObject(message).toString()
.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"")
.replaceAll("\'", "\\\\\'").replaceAll("\n", "\\\\\n")
.replaceAll("\r", "\\\\\r").replaceAll("\f", "\\\\\f");
保持原状:
- WebView首次初始化耗时
提前加载一个WebView窗口 (没必要)
异步初始化WebView (不支持)
主线程挂起
- 异步线程与主线程竞争CPU资源
设置异步线程优先级为Process.THREAD_PRIORITY_BACKGROUND,减少与主线程的竞争。
有两种设置优先级的方式:Thread.currentThread().setPriority() 和 Process.setThreadPriority(),两种设置方式相互独立,应该使用后者。
Process.setThreadPriority(Process.myTid(), Process.THREAD_PRIORITY_BACKGROUND);
同时可以提高主线程的优先级
Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
- 频繁GC使主线程挂起
后续内存优化
冷启动白屏
设置欢迎页窗口背景为应用Logo
优化效果
测试设备:手机低配版(512M Rom)未安装第三方App
测试标准:最大、平均掉帧数
测试App:我厂App release版
商米低配版 | 优化前 | 优化后 |
---|---|---|
最大掉帧数 | 65帧 | 48帧 |
平均掉帧数 | 15帧 | 7帧 |