从源码角度谈谈 Handler 的场景应用原理
1、前言
今天,咱们从源码的角度来总结一下应用Handler
的一些场景的原理
2、Handler 的应用
2.1 线程间的通讯
在平时的开发当中,Handler
最多见的用法就是用于线程之间的通讯,特别是当咱们在子线程中去处理耗时的任务,当任务完成以后,咱们但愿将结果发送到主线程中进行处理,那么就会使用到Handler
当咱们在子线程中经过 Handler
放入在主线程中的循环队列时,那么主线程就会收到消息,以后,咱们就能够在主线程中进行后续的处理,例以下面这样:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.d("Handler", "HandleMessageThreadId=" + Thread.currentThread().getId());
}
};
private void startThread() {
new Thread() {
@Override
public void run() {
super.run();
Log.d("Handler", "ThreadId=" + Thread.currentThread().getId());
mHandler.sendEmptyMessage(0);
}
}.start();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("Handler", "MainThreadId=" + Thread.currentThread().getId());
startThread();
}
}
打印出上面的线程Id
,能够看到最后handleMessage
收到消息的时候是在主线程当中
2.2 实现延时操做
除了线程间的通讯以外,咱们还能够经过Handler
提供的sendXXXDelay
方法,实现延时操做,延时操做有两种目的
- 一种是但愿让不那么紧急的任务延后执行,例如在应用启动过程当中,咱们在
onCreate
方法中的任务不是那个紧急,那么能够经过Handler
发送一个延时消息出去,让它不占用主线程去渲染布局的资源,从而提升应用的启动速度。 - 另外一种就是防抖动操做,咱们收到一个命令,并非立刻执行它,而是经过
Handler
的sendxxxDelay
方法延时,若是在这段事件内又有一个相同的命令来到了,那么就把以前的消息移除,再放入一个新的延时消息。 例以下面的例子,当咱们点击一个按钮以后,咱们不马上执行任务,而是一段时间以后仍然没有收到第二次点击事件采起执行任务:
private Handler mDelayHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.d("Handler", "handleMessage");
}
};
private void performClickDelay() {
Log.d("Handler", "performClickDelay");
mDelayHandler.removeMessages(1);
mDelayHandler.sendEmptyMessageDelayed(1, 500);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTv = (TextView) findViewById(R.id.tv);
mTv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
performClickDelay();
}
});
}
咱们连续屡次点击按钮以后,只收到了一次消息
2.3 使用HandlerThread
在异步线程执行耗时操做
因为Looper
实际上是线程的一个私有变量(ThreadLocal
),主线程能够有Looper
,子线程一样也能够有Looper
,只不过从子线程中的Looper
中的MessageQueue
中取出消息以后,是在子线程当中处理的,那么咱们就能够经过它来执行异步操作
HandlerThread
就是基于这一思想来实现的,关于 HandlerThread
的内部实现,它的本质就是当异步线程启动以后,会初始化这个线程私有的 Looper
,所以,当咱们经过 HandlerThread
中的 getLooper()
方法得到这个 Looper
以后,在经过这个 Looper
来建立一个 Handler
,那么这个 Handler
的 handleMessage
回调时所在的线程就是这个异步线程。
下面是一个简单的事例:
private void useHandlerThread() {
final HandlerThread handlerThread = new HandlerThread("handlerThread");
System.out.println("MainThreadId=" + Thread.currentThread().getId() + ",handlerThreadId=" + handlerThread.getId());
handlerThread.start();
MyHandler handler = new MyHandler(handlerThread.getLooper());
handler.sendEmptyMessage(0);
}
private class MyHandler extends Handler {
MyHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
System.out.println("handleMessageThreadId=" + Thread.currentThread().getId());
}
}
咱们打印出handleMessage
的执行时的线程ID
,能够看到它就是HandlerThread
的线程ID
,所以咱们就能够经过发送消息的方式,在异步线程执行一些耗时的操作
2.4 使用AsyncQueryHandler
对ContentProvider
进行操做
若是你的本地数据使用ContentProvider
封装的,那么对这些数据的增删改查操做,为了避免影响主线程,应该在子线程中进行操做,而AsyncQueryHandler
就是系统为咱们提供的很方便的工具类,它经过Handler + HandlerThread
的方式,实现了异步的增删改查。函数
它是在HandlerThread
的基础之上扩展出来的,其基本思想以下图所示,这一框架就保证了发起命令和接收回调是在同一个线程,而任务的执行则是在另外一个线程
固然AsyncQueryHandler
限制了只能操做经过ContentProvider
封装的数据,咱们能够参考它的思想,进行扩展,实现对于数据库的增删改查
2.5 使用Handler
机制检测应用中的卡顿问题
对于每一个应用程序来讲,它的入口函数为ActivityThread
中的main()
方法:
public static void main(String[] args) {
//...
Looper.prepareMainLooper();
//...
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
而在main()
函数的最后,则构建一个在主线程中的循环队列,以后应用程序收到的事件以后,还主线程就会被唤醒,进行事件的处理,假如这一处理的事件过长,那么就会发生ANR
,所以,咱们就能够经过计算主线程中对于单次消息的处理时间,从而间隔地判断是否存在卡顿问题。 那么要怎么知道主线程中单次消息的处理时间呢,咱们查看Looper
的源码:
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.isTagEnabled(traceTag)) {
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(); } }
对于消息的处理对应这句:
msg.target.dispatchMessage(msg);
而在这句话的先后,则调用了Printer
类打印,那么咱们就能够经过这两次打印的之间的时长来得到单次消息的处理时间
public class MainLoopMonitor {
private static final String MSG_START = ">>>>> Dispatching";
private static final String MSG_END = "<<<<< Finished";
private static final int TIME = 1000;
private Handler mExecuteHandler;
private Runnable mExecuteRunnable;
private static class Holder {
private static final MainLoopMonitor INSTANCE = new MainLoopMonitor();
}
public static MainLoopMonitor getInstance() {
return Holder.INSTANCE;
}
private MainLoopMonitor() {
HandlerThread monitorThread = new HandlerThread("LooperMonitor");
monitorThread.start();
mExecuteHandler = new Handler(monitorThread.getLooper());
mExecuteRunnable = new ExecutorRunnable();
}
public void startMonitor() {
Looper.getMainLooper().setMessageLogging(new Printer() {
@Override
public void println(String x) {
if (x.startsWith(MSG_START)) {
mExecuteHandler.postDelayed(mExecuteRunnable, TIME);
} else if (x.startsWith(MSG_END)) {
mExecuteHandler.removeCallbacks(mExecuteRunnable);
}
}
});
}
private class ExecutorRunnable implements Runnable {
@Override
public void run() {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s).append("\n");
}
System.out.println("MainLooperMonitor:" + sb.toString());
}
}
}
假如咱们像下面这样,在主线程中进行了耗时的操作:
public void anrButton(View view) {
for (int i = 0; i < (1 << 30); i++) {
int b = 6;
}
}
3、Handler 使用注意事项
在介绍完Handler
的这些用法,咱们再来总结一下使用Handler
是比较容易犯的一些错误。
3.1 内存泄漏
一个比较常见的错误就是将定义Handler
的子类时将它做为Activity
的内部类,而因为内部类会默认持有外部类的引用,所以,若是这个内部类的实例在Activity
试图被回收的时候,没有被销毁掉,那么就会致使Activity
没法被回收,从而引发内存泄漏。
而Handler
实例没法被销毁掉最多见的状况就是,咱们经过它发送了一个延时消息出去,此时这个消息会被放入到该线程所对应的Looper
中的MessageQueue
当中,而该消息为了能在获得执行以后,回调到对应的Handler
,所以它会持有这个Handler
的实例。
3.2 在子线程中实例化 new Handler()
在子线程中new Handler()
有下面两个须要注意的点:
(1) 在 new Handler() 以前调用 Looper.prepare() 方法
另一个错误就是咱们在子线程当中,直接调用new Handler()
,那么这时候会抛出异常,例以下面这样:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
new Thread() {
@Override
public void run() {
mHandler = new MyHandler();
}
}.start();
}
Handler
的构造函数中,会去检查当前线程的 Looper
是否已经被初始化:
public Handler(Callback callback, boolean async) {
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
若是没有,那么就会抛出上面代码当中的异常,正确的作法,是须要保证咱们在new Handler
以前,要保证当前线程的Looper
对象已经被初始化,也就是Looper
当中的下面这个方法被调用:
/** Initialize the current thread as a looper.
* This gives you a chance to create handlers that then reference
* this looper, before actually starting the loop. Be sure to call
* {@link #loop()} after calling this method, and end it by calling
* {@link #quit()}.
*/
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
(2) 若是但愿经过该 Looper 接收消息那么要调用 Looper.loop() 方法,而且须要放在最后一句调用
假如咱们只调用了prepare()
方法,仅仅只是初始化了一个线程的私有的变量,此时是没法经过这个Looper
构建的Handler
来接收消息的,例以下面这样:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
new Thread() {
@Override
public void run() {
Looper.prepare();
mHandler = new MyHandler();
}
}.start();
}
public void anrButton(View view) {
mHandler.sendEmptyMessage(0);
}
private static class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
Log.e("TAG", "handleMessageId=" + Thread.currentThread().getId());
}
}
此时咱们应当调用loop
方法让这个循环队列开始工做,而且该调用必定要位于最后一句,由于一旦调用了loop
方法,那么它就会进入等待 -> 收到消息 -> 唤醒 -> 处理消息 -> 等待的循环过程,而这一过程只有等到loop
方法返回的时候才会结束,所以,咱们初始化Handler
的语句要放在loop
方法以前,相似于下面这样:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
new Thread() {
@Override
public void run() {
Looper.prepare();
mHandler = new MyHandler();
//最后一句再调用
Looper.loop();
}
}.start();
}
4、小结
Handler
的机制彷佛已经成为面试必问的题目,若是你们能在回答完内部的实现原理,再根据这些实现原理引伸出上面的几个应用和注意事项,能够加分很多哦~