官网性能优化指导(https://developer.android.com/topic/performance/index.html)
卡顿:从用户角度说,App操作起来缓慢,响应不及时,列表滑动一顿一顿的,动画刷新不流畅等等一些直观感受。从系统角度来说,屏幕刷新的帧率不稳定,无法保证每秒绘制60帧,也就是说有掉帧的情况发生。
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("<<<< + 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 = "<<<<;
@Override
public void println(String x) {
if (x.startsWith(START)) {
UiBlockLogMonitor.getInstance().startMonitor();
}
if (x.startsWith(END)) {
UiBlockLogMonitor.getInstance().stopMonitor();
}
}
});
不过,由于系统定制的原因,打印出来的日志标识不一定标准,所以可以改为判断第一次日志输出和第二次日志输出。
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 官方说明:https://developer.android.com/studio/command-line/systrace.html
systrace.py 是一个命令行工具,位于 ../sdk/platform-tools/systrace目录下。在应用运行时,它可以帮助我们收集和分析所有进程的计时信息,包含了CPU调度、应用线程、磁盘活动等Android内核数据,然后生成一份HTML报告。
systace对检测应用UI表现非常有效,因为它可以分析你的代码和帧率来识别出问题区域,然后提出可能的解决方案。示例
如果其中的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
TraceView 是 Android SDK 中内置的一个工具,它可以加载 trace 文件,用图形的形式展示代码的执行时间、次数及调用栈,便于我们分析。我们可以在Android Profiler或DDMS中启动它。
使用这个工具最关键的地方就是要理解各个统计维度的含义:
方法执行时间
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过度绘制
请注意,这些颜色是半透明的,因此,您在屏幕上看到的确切颜色取决于您的界面内容。
可以通过此功能查看哪些页面的布局层级过深。
去除不必要的背景色
1. 设置窗口背景色为通用背景色,去除根布局背景色。
2. 若页面背景色与通用背景色不一致,在页面渲染完成后移除窗口背景色
3. 去除和列表背景色相同的Item背景色
布局视图树扁平化
1. 移除嵌套布局
2. 使用merge、include标签
3. 使用性能消耗更小布局(TableLayout、ConstraintLayout)
减少透明色,即alpha属性的使用
1. 通过使用半透明颜色值(#77000000)代替
其他
1. 使用ViewStub标签,延迟加载不必要的视图
2. 使用AsyncLayoutInflater异步解析视图
处理方案评估:
异步 > 缓存 > 替代方案 > 保持原状
异步:
1. 登录、退出登录后的数据处理
2. 相机操作
3. 组件初始化
示例:
//异步启动消息推送服务
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(context) {
@Override
protected void onFinally() {
super.onFinally();
}
});
addSubscription(startPushSub);
}
缓存:
1. Cache类
2. 系统属性
示例:
//获取应用渠道标识
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;
}
}
}
替代方案:
1. Hybird通信中的一处正则匹配
2. 更多页面采用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");
保持原状:
1. WebView首次初始化耗时
提前加载一个WebView窗口 (没必要)
异步初始化WebView (不支持)
设置异步线程优先级为Process.THREAD_PRIORITY_BACKGROUND,减少与主线程的竞争。
有两种设置优先级的方式:Thread.currentThread().setPriority() 和 Process.setThreadPriority(),两种设置方式相互独立,应该使用后者。
Process.setThreadPriority(Process.myTid(), Process.THREAD_PRIORITY_BACKGROUND);
同时可以提高主线程的优先级
Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
后续内存优化
设置欢迎页窗口背景为应用Logo
测试设备:手机低配版(512M Rom)未安装第三方App
测试标准:最大、平均掉帧数
测试App:我厂App release版
商米低配版 | 优化前 | 优化后 |
---|---|---|
最大掉帧数 | 65帧 | 48帧 |
平均掉帧数 | 15帧 | 7帧 |