

Android UI性能优化 检测应用中的UI卡顿
source link: https://blog.csdn.net/lmj623565791/article/details/58626355
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Android UI性能优化 检测应用中的UI卡顿
本文已在我的公众号hongyangAndroid首发。
转载请标明出处:
http://blog.csdn.net/lmj623565791/article/details/58626355
本文出自张鸿洋的博客
在做app性能优化的时候,大家都希望能够写出丝滑的UI界面,以前写过一篇博客,主要是基于Google当时发布的性能优化典范,主要提供一些UI优化性能示例:
实际上,由于各种机型的配置不同、代码迭代历史悠久,代码中可能会存在很多在UI线程耗时的操作,所以我们希望有一套简单检测机制,帮助我们定位耗时发生的位置。
本篇博客主要描述如何检测应用在UI线程的卡顿,目前已经有两种比较典型方式来检测了:
- 利用UI线程Looper打印的日志
- 利用Choreographer
两种方式都有一些开源项目,例如:
其实编写本篇文章,主要是因为发现一个还比较有意思的方案,该方法的灵感来源于一篇给我微信投稿的文章:
该项目主要用于捕获UI线程的crash,当我看完该项目原理的时候,也可以用来作为检测卡段方案,可能还可以做一些别的事情。
所以,本文出现了3种检测UI卡顿的方案,3种方案原理都比较简单,接下来将逐个介绍。
二、利用loop()中打印的日志
(1)原理
大家都知道在Android UI线程中有个Looper,在其loop方法中会不断取出Message,调用其绑定的Handler在UI线程进行执行。
大致代码如下:
public static void loop() {
final Looper me = myLooper();
final MessageQueue queue = me.mQueue;
// ...
for (;;) {
Message msg = queue.next(); // might block
// 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);
}
// focus
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// ...
}
msg.recycleUnchecked();
}
}
所以很多时候,我们只要有办法检测:
msg.target.dispatchMessage(msg);
此行代码的执行时间,就能够检测到部分UI线程是否有耗时操作了。可以看到在执行此代码前后,如果设置了logging,会分别打印出>>>>> Dispatching to
和<<<<< Finished to
这样的log。
我们可以通过计算两次log之间的时间差值,大致代码如下:
public class BlockDetectByPrinter {
public static void start() {
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)) {
LogMonitor.getInstance().startMonitor();
}
if (x.startsWith(END)) {
LogMonitor.getInstance().removeMonitor();
}
}
});
}
}
假设我们的阈值是1000ms,当我在匹配到>>>>> Dispatching
时,我会在1000ms毫秒后执行一个任务(打印出UI线程的堆栈信息,会在非UI线程中进行);正常情况下,肯定是低于1000ms执行完成的,所以当我匹配到<<<<< Finished
,会移除该任务。
大概代码如下:
public class LogMonitor {
private static LogMonitor sInstance = new LogMonitor();
private HandlerThread mLogThread = new HandlerThread("log");
private Handler mIoHandler;
private static final long TIME_BLOCK = 1000L;
private LogMonitor() {
mLogThread.start();
mIoHandler = new Handler(mLogThread.getLooper());
}
private static Runnable mLogRunnable = new Runnable() {
@Override
public void run() {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s.toString() + "\n");
}
Log.e("TAG", sb.toString());
}
};
public static LogMonitor getInstance() {
return sInstance;
}
public boolean isMonitor() {
return mIoHandler.hasCallbacks(mLogRunnable);
}
public void startMonitor() {
mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK);
}
public void removeMonitor() {
mIoHandler.removeCallbacks(mLogRunnable);
}
}
我们利用了HandlerThread这个类,同样利用了Looper机制,只不过在非UI线程中,如果执行耗时达到我们设置的阈值,则会执行mLogRunnable
,打印出UI线程当前的堆栈信息;如果你阈值时间之内完成,则会remove掉该runnable。
(2)测试
用法很简单,在Application的onCreate中调用:
BlockDetectByPrinter.start();
然后我们在Activity里面,点击一个按钮,让睡眠2s,测试下:
findViewById(R.id.id_btn02)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
});
运行点击时,会打印出log:
02-21 00:26:26.408 2999-3014/com.zhy.testlp E/TAG:
java.lang.VMThread.sleep(Native Method)
java.lang.Thread.sleep(Thread.java:1013)
java.lang.Thread.sleep(Thread.java:995)
com.zhy.testlp.MainActivity$2.onClick(MainActivity.java:70)
android.view.View.performClick(View.java:4438)
android.view.View$PerformClick.run(View.java:18422)
android.os.Handler.handleCallback(Handler.java:733)
android.os.Handler.dispatchMessage(Handler.java:95)
会打印出耗时相关代码的信息,然后可以通过该log定位到耗时的地方。
三、 利用Choreographer
Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染。SDK中包含了一个相关类,以及相关回调。理论上来说两次回调的时间周期应该在16ms,如果超过了16ms我们则认为发生了卡顿,我们主要就是利用两次回调间的时间周期来判断:
大致代码如下:
public class BlockDetectByChoreographer {
public static void start() {
Choreographer.getInstance()
.postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long l) {
if (LogMonitor.getInstance().isMonitor()) {
LogMonitor.getInstance().removeMonitor();
}
LogMonitor.getInstance().startMonitor();
Choreographer.getInstance().postFrameCallback(this);
}
});
}
}
第一次的时候开始检测,如果大于阈值则输出相关堆栈信息,否则则移除。
使用方式和上述一致。
四、 利用Looper机制
先看一段代码:
new Handler(Looper.getMainLooper())
.post(new Runnable() {
@Override
public void run() {}
}
该代码在UI线程中的MessageQueue中插入一个Message,最终会在loop()方法中取出并执行。
假设,我在run方法中,拿到MessageQueue,自己执行原本的Looper.loop()
方法逻辑,那么后续的UI线程的Message就会将直接让我们处理,这样我们就可以做一些事情:
public class BlockDetectByLooper {
private static final String FIELD_mQueue = "mQueue";
private static final String METHOD_next = "next";
public static void start() {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
Looper mainLooper = Looper.getMainLooper();
final Looper me = mainLooper;
final MessageQueue queue;
Field fieldQueue = me.getClass().getDeclaredField(FIELD_mQueue);
fieldQueue.setAccessible(true);
queue = (MessageQueue) fieldQueue.get(me);
Method methodNext = queue.getClass().getDeclaredMethod(METHOD_next);
methodNext.setAccessible(true);
Binder.clearCallingIdentity();
for (; ; ) {
Message msg = (Message) methodNext.invoke(queue);
if (msg == null) {
return;
}
LogMonitor.getInstance().startMonitor();
msg.getTarget().dispatchMessage(msg);
msg.recycle();
LogMonitor.getInstance().removeMonitor();
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
其实很简单,将Looper.loop里面本身的代码直接copy来了这里。当这个消息被处理后,后续的消息都将会在这里进行处理。
中间有变量和方法需要反射来调用,不过不影响查看
msg.getTarget().dispatchMessage(msg);
执行时间,但是就不要在线上使用这种方式了。
不过该方式和以上两个方案对比,并无优势,不过这个思路挺有意思的。
使用方式和上述一致。
最后,可以考虑将卡顿日志输出到文件,慢慢分析;可以结合上述原理以及自己需求开发做一个合适的方案,也可以参考已有开源方案。
我的微信公众号:hongyangAndroid
(可以给我留言你想学习的文章,支持投稿)
Recommend
-
72
在之前的文章中提到过,现有市面上的 iPhone 老设备(特指 iPhone 6s 之前的设备)占有率高达 40%,iOS app 卡顿的发生率发生概率也很高。卡顿里有一类卡顿又尤其严重:主线程长期不响应而导致的系统 Watchdog 强杀。...
-
44
原文链接 不管是应用秒变幻灯片,还是启动过久被杀,基本都是开发者必经的体验。就像没人希望堵车一样,卡顿永远是不受用户欢迎的,所以如何发现卡顿是开发者需要直面的难题。虽然导致卡顿的原因有很多,但卡顿的表现总是大同小异。如果把卡顿当做病症看待,两者分...
-
5
这几天闲得无聊,就打开手机上的开发者模式里面的“GPU过度绘制”功能,看看别家的App做的咋样,然后很偶然的打开了“简书”,然后就被它的过度绘制惊呆了,于是写了这篇性能分析的文章,从一个只有APK文件的角度,说下如何寻找布局中可能存在的性能问...
-
8
卡顿 & ANR 在各 APP 中都是非常影响用户体验的问题,关于其的分析和治理一直也是个老生常谈的话题。过去调查卡顿 & ANR 问题主要依赖上报的堆栈和 traceInfo 文件,通过这些信息还原问题的现场情况。但是在实践过程中发现,现有监控机制下堆栈的抓取时...
-
9
万字干货:IM 会话列表卡顿优化实践(内附代码)本文干货充足,篇幅较长,建议收藏后阅读,避免迷路。融云公众号后台回复【列表卡顿】,可直接领取本文涉及所有资料。一、背景
-
7
借助友盟+U-APM实现终端卡顿优化的全记录目前手机SOC的性能越来越少,很多程序员在终端程序的开发过程中也不太注意性能方面的优化,尤其是不注意对齐和分支优化,但是这两种问题一旦出现所...
-
10
动手点关注 干货不迷路 👆 希望本文可以带给大家一个相对全局的视角看待卡顿问题,认识到卡顿是什么、卡顿的成因、卡顿的分类、卡顿的优化和一些经验积累,有的放矢地解决 App 流畅性问题。接下来会从以下五个方面进行...
-
6
VMware 虚拟机运行缓慢,用一段时间后虚拟系统变卡,本文主要从硬件、配置、系统上给出优化的方案 一、硬件(土豪适用) SSD固态硬盘 将虚拟机系统安装到SSD上,其他工作的文件通过挂载本地磁盘进行,操作见...
-
10
Android请求打开蓝牙导致应用很卡顿的问题 ...
-
3
前端性能优化--卡顿的监控和定位 By
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK