如何监控项目里所有UI线程的操作(实战,BlockCanary)?
Posted ihrthk
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何监控项目里所有UI线程的操作(实战,BlockCanary)?相关的知识,希望对你有一定的参考价值。
如何监控项目里所有UI线程的操作(实战,BlockCanary)?
背景
我们都知道,android系统是线程安全的,也就是只有主线才能更新UI。而保持界面的流畅性,就要求界面1秒钟刷新60次以上。也就是16ms需要刷新一次界面。但是在实际开发过程中,有各种各样的原因,无法达到这一指标。尤其是当项目已经成形,需求的不断变更,开发人员的变更、替换,等等,就变成的更为困难。寻找这些影响主线程执行效率的代码,就变成一个优化性能的重要方面。在学习前人的优秀的设计思想和代码实现,下面带着大家一步一步地实战。
原理
先说一下原理,我们都知道,Android是使用Message/Looper/Handler机制来更新UI的。如果在主线程handler的handleMessage
方法进行了耗时的操作,界面也就是不能及时更新,即UI卡顿。关键代码如下:
Handler.dispatchMessage()方法
/**
* Handle system messages here.
*/
public void dispatchMessage(Message msg)
if (msg.callback != null)
handleCallback(msg);
else
if (mCallback != null)
if (mCallback.handleMessage(msg))
return;
handleMessage(msg);
在来看一下Handler.dispatchMessage
是在哪里被调用的,通过查看源码原来是在Looper.loop
里调用,这里处主线程理消息队列的Looper。一个线程只有一个Looper,每一个消息都可以持有不同的Handler。也就是,所有的消息都会在这里进行处理和分发。所以只有监控此处就可以了。
Looper.loop()方法
/**
* Run the message queue in this thread. Be sure to call
* @link #quit() to end the 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();
找到dispatchMessage
的关键代码,即两个logging之间就是又可以出现耗时操作的地方。
// 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);
你可能要问了,mLogging是什么,我可以打印出来吗?答案:是可以的,不信可以看下源码
public final class Looper
private Printer mLogging;
/**
* Control logging of messages as they are processed by this Looper. If
* enabled, a log message will be written to <var>printer</var>
* at the beginning and ending of each message dispatch, identifying the
* target Handler and message contents.
*
* @param printer A Printer object that will receive log messages, or
* null to disable message logging.
*/
public void setMessageLogging(@Nullable Printer printer)
mLogging = printer;
/**
* Simple interface for printing text, allowing redirection to various
* targets. Standard implementations are @link android.util.LogPrinter,
* @link android.util.StringBuilderPrinter, and
* @link android.util.PrintWriterPrinter.
*/
public interface Printer
/**
* Write a line of text to the output. There is no need to terminate
* the given string with a newline.
*/
void println(String x);
Looper的mLogging居然是个私有的,而Printer还是接口(默认是没有打印的),不过没有关系Looper提供了
setMessageLogging(@Nullable Printer printer)
的公开方法,在自己实现一个Printer,传进出就好了。
1.编写能监控更新UI的代码
好吧,你已经知道,怎么做了。直接粘代码了。
/**
* Created by zhangls on 2016/7/6.
*/
public class AppContext extends Application
@Override
public void onCreate()
super.onCreate();
Looper.getMainLooper().setMessageLogging(new Printer()
@Override
public void println(String x)
Log.e("AppContext", x);
);
打印的log如下:
E/AppContext: <<<<< Finished to Handler (android.app.ActivityThread$H) 414a36b0 null
E/AppContext: >>>>> Dispatching to Handler (android.view.ViewRootImpl) 41d5dbd8 null: 1000
E/AppContext: <<<<< Finished to Handler (android.view.ViewRootImpl) 41d5dbd8 null
E/AppContext: >>>>> Dispatching to Handler (android.view.ViewRootImpl) 41d5dbd8
2.找出过滤出耗时操作的Log
这个也不复杂,不多说了,看了代码,你就懂了。
/**
* Created by zhangls on 2016/7/6.
*/
public class AppContext extends Application
@Override
public void onCreate()
super.onCreate();
Looper.getMainLooper().setMessageLogging(new MyPrinter());
private static class MyPrinter implements Printer
public static final String TAG = "Printer";
private long mFinishTimeMillis;
private long mStartTimeMillis;
private static final int START = 0;
private static final int FINISH = 1;
private static final int UNKONW = 2;
@Override
public void println(String x)
switch (isStart(x))
case START:
mStartTimeMillis = System.currentTimeMillis();
break;
case FINISH:
mFinishTimeMillis = System.currentTimeMillis();
long duration = mFinishTimeMillis - mStartTimeMillis;
if (isBlock(duration))
Log.e(TAG, "block time:" + duration);
break;
public int isStart(String x)
if (!TextUtils.isEmpty(x))
if (x.startsWith(">>>>> Dispatching to Handler"))
return START;
else if (x.startsWith("<<<<< Finished to Handler"))
return FINISH;
return UNKONW;
private boolean isBlock(long duration)
return duration > 16;
3.保存主线程所有的堆栈信息
先说下如何获取主线程的堆栈信息
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
由于这个代码是反射也是一个耗时操作,所以不得不专门写一个Thread来不断获取主线程的堆栈信息。
public class StackInfoCatcher extends Thread
private static final String TAG = "StackInfoCatcher";
private static final int SIZE = 1024;
private boolean stop = false;
private long mLastTime = 0;
private List<StackTraceInfo> mList = new ArrayList<>(SIZE);
@Override
public void run()
super.run();
while (!stop)
long currentTime = System.currentTimeMillis();
if ((currentTime - mLastTime) > 500)
mLastTime = System.currentTimeMillis();
StackTraceInfo info = new StackTraceInfo();
info.mTime = mLastTime;
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
info.mLog = stackTraceToString(stackTrace);
mList.add(info);
if (mList.size() > SIZE)
mList.remove(0);
public String stackTraceToString(StackTraceElement[] elements)
StringBuilder result = new StringBuilder();
if (null != elements && elements.length > 0)
for (int i = 0; i < elements.length; i++)
result.append("\\tat ");
result.append(elements[i].toString());
result.append("\\n");
return result.toString();
public void stopTrace()
stop = true;
private class StackTraceInfo
public long mTime;
public String mLog;
4.获取耗时操作对应的堆栈信息
当主线程遇到耗时操作,则发送广播交由工作线程处理。并把开始时间和结束时间也发送过去。寻找这个时间段主线程产生的LOG和堆栈信息。
class MyPrinter implements Printer
private MyPrinter(Context context)
this.mContext = context;
stackInfoCatcher = new StackInfoCatcher(context);
stackInfoCatcher.start();
@Override
public void println(String x)
switch (isStart(x))
case START:
mStartTimeMillis = System.currentTimeMillis();
break;
case FINISH:
mFinishTimeMillis = System.currentTimeMillis();
long duration = mFinishTimeMillis - mStartTimeMillis;
if (isBlock(duration))
LocalBroadcastManager manager= LocalBroadcastManager.getInstance(mContext);
Intent intent = new Intent("ACTION_BLOCK");
intent.putExtra("start", mStartTimeMillis);
intent.putExtra("finish", mFinishTimeMillis);
manager.sendBroadcast(intent);
break;
public class StackInfoCatcher extends Thread
public StackInfoCatcher(Context context)
this.mContext = context;
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context);
mBroadcastReceiver = new BroadcastReceiver()
@Override
public void onReceive(Context context, Intent intent)
long endTime = intent.getLongExtra("finish", 0);
long startTime = intent.getLongExtra("start", 0);
StackTraceInfo info = getInfoByTime(endTime, startTime);
if (null != info)
Log.e(TAG, "find block line" + (endTime - startTime));
Log.e(TAG, info.mLog);
else
Log.e(TAG, "no block line find");
;
manager.registerReceiver(mBroadcastReceiver, new IntentFilter("ACTION_BLOCK"));
public StackTraceInfo getInfoByTime(long endTime, long startTime)
for (StackTraceInfo info : mList)
if (info.mTime >= startTime && info.mTime <= endTime)
return info;
return null;
public void stopTrace()
stop = true;
LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mBroadcastReceiver);
5.测试并验证
编写一个Button,点击这个按钮主线程睡眠500ms。
public class MainActivity extends AppCompatActivity
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
public void click(View view)
try
Thread.sleep(500);
catch (InterruptedException e)
e.printStackTrace();
对应日志如下
E: no block line find
E: find block line526
E: at java.lang.VMThread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:1031)
at java.lang.Thread.sleep(Thread.java:1013)
at com.example.zhangls.myapplication1.MainActivity.click(MainActivity.java:21)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288)
at android.view.View.performClick(View.java:3511)
at android.view.View.onKeyUp(View.java:6073)
at android.widget.TextView.onKeyUp(TextView.java:5541)
at android.view.KeyEvent.dispatch(KeyEvent.java:3279)
at android.view.View.dispatchKeyEvent(View.java:5500)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1246)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1246)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1246)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1246)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1246)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1246)
at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchKeyEvent(PhoneWindow.java:1904)
at com.android.internal.policy.impl.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1386)
at android.app.Activity.dispatchKeyEvent(Activity.java:2327)
at android.support.v7.app.AppCompatActivity.dispatchKeyEvent(AppCompatActivity.java:534)
at android.support.v7.view.WindowCallbackWrapper.dispatchKeyEvent(WindowCallbackWrapper.java:50)
at android.support.v7.app.AppCompatDelegateImplBase$AppCompatWindowCallbackBase.dispatchKeyEvent(AppCompatDelegateImplBase.java:241)
at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1831)
at android.view.ViewRootImpl.deliverKeyEventPostIme(ViewRootImpl.java:3327)
at android.view.ViewRootImpl.handleFinishedEvent(ViewRootImpl.java:3300)
at android.view.ViewRootImpl.handleMessage(ViewRootImpl.java:2460)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4425)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:848)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:575)
at dalvik.system.NativeStart.main(Native Method)
总结
在主线程寸土寸金的地方,这个种办法本身也是占用主线程的执行时间的,所以在打包release版本的时候,要记得把这个去掉。
参考
- http://blog.zhaiyifan.cn/2016/01/16/BlockCanaryTransparentPerformanceMonitor/
- https://github.com/fengcunhan/BlockCanary
以上是关于如何监控项目里所有UI线程的操作(实战,BlockCanary)?的主要内容,如果未能解决你的问题,请参考以下文章