在开发调试app时, 总是碰到各种卡段/跳帧的问题。 Android系统每隔16ms会刷新一帧图像,两帧图像的刷新周期超过16ms就会感觉卡顿。
深度解析图像绘制过程: GPU 三缓存和垂直同步
https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/
下面就聊一下我的经验。
卡顿的原因包括 1、 主线程执行了耗时操作(PS:UI渲染是在主线程执行的, 所以主线程的耗时操作会影响UI绘制)。 2、 app占用内存过高(大多数情况因为图片没回收、文件没关闭、数据库cursor没关闭等。) 3、手机CPU/内存使用率过高(打开很多app)。
一、 最笨的方法(当然也是最有效的方法), 逐段注释你怀疑的代码, 然后运行观察效果, 并逐渐缩小范围直到找出原因。
二、点击app各个按钮并执行adb shell cat /proc/meminfo或adb shell procrank命令并配合图形工具实时观测内存变化, 尤其关注是内存抖动的情况。 技巧:使用StrickMode判断Cursor泄漏。
三、 每个android进程至少有1个looper在循环处理Message, 我们可以看看Message的执行时间,判断UI线程是否有耗时操作。
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
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
final long traceTag = me.mTraceTag;
if (traceTag != 0) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
try {
msg.target.dispatchMessage(msg);
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
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();
}
}
每个Message都会打印“Dispatching to”和“Finished to” , 我们只要计算二者的时间差是否超过阈值就可以了。 因为mLoggin默认是空的, 所以我们要先设置一下。
Looper.getMainLooper().setMessageLogging(new Printer() {
private final String START = ">>>>> Dispatching";
private final String END = "<<<<< Finished";
private long startTime = 0;
private final int PERIOD = 1000; //单位:毫秒。判断耗时操作的阈值
@Override
public void println(String x) {
if (x.startsWith(START)) {
startTime = System.currentTimeMillis();
}
if (x.startsWith(END) && System.currentTimeMillis()-startTime>PERIOD ) {
//打印堆栈
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s.toString() + "\n");
}
Log.e("TAG", sb.toString());
}
}
});
四、使用merge、ViewStub、uiautomakeviewer和hierarchyviewer减少布局层级和渲染控件, 尽量不使用layout_weight(measure了2次),使用LinearLayout替换RelativeLayout。 如果确定了宽高,尽量不实用wrap_content和match_parent。
五、使用JakeWharton开发的hugo三方库(基于AOP思想的AspectJ实现的)分析函数耗时, 这位大神是square公司的,还开发了OkHttp,ButterKnife。
github地址:https://github.com/JakeWharton/hugo
Simply add @DebugLog
to your methods and you will automatically get all of the things listed above logged for free.
@DebugLog
public String getName(String first, String last) {
SystemClock.sleep(15); // Don't ever really do this!
return first + " " + last;
}
V/Example: ⇢ getName(first="Jake", last="Wharton")
V/Example: ⇠ getName [16ms] = "Jake Wharton"