Handler消息机制
App中一般多会有多个线程,多线程之间难免需要进行通信。开发中线程通信用的最多的就是Handler,另外还有,例如子线程进行数据处理,在主线程中进行UI更新。
当然了除了Handler这种通信方式外,线程间的通信还有其他几种方式:管道Pip、共享内存、通过文件及数据库等。
文章目录
- 一,基础介绍
-
- 1.使用Handler
- 2.开发中的问题
- 二,消息机制
-
- 1.Looper
- 2.MessageQueue
- 3.Handler
- 三,面试题
-
- 1.android中实现多线程通信的方法?
- 2.系统为什么不允许在子线程中访问UI?
- 3.在子线程发送消息,却能够在主线程接收消息,主线程和子线程是怎么样切换的?
- 4.一个线程可以有几个Handler?几个Looper?
- 5.Handler如何实现延时任务的?
- 6.子线程可以更新UI吗?
- 7.Handler如何保证MessageQueue并发访问安全?
- 8.Handler的阻塞唤醒机制是怎么回事?
- 9.能不能让一个Message加急被处理?/ 什么是Handler同步屏障?
- 总结
一,基础介绍
1.使用Handler
如何使用Handler呢,很简单看代码:
public class TextActivity extends AppCompatActivity {private Handler handler;private Handler handler1;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);handler = new Handler();new Thread(new Runnable() {@Overridepublic void run() {Log.e("TextActivity", "run: " );handler1=new Handler();}}).start();}
}
上边的代码,运行结果
handler成功创建了,handler1出现了异常,说是没有Looper。只需要在添加一行代码就可以了。Looper.prepare()方法就是创建了一个Looper。
@Overridepublic void run() {Log.e("TextActivity", "run: " );//创建LooperLooper.prepare();handler1=new Handler();}
Looper.prepare()方法到底做了什么呢,我们先来看看抛异常的代码如下:
public Handler(@Nullable Callback callback, boolean async) {mLooper = Looper.myLooper();if (mLooper == null) {throw new RuntimeException("Can't create handler inside thread " + Thread.currentThread()+ " that has not called Looper.prepare()");}mQueue = mLooper.mQueue;mCallback = callback;mAsynchronous = async;}
上边代码可以看出当我们创建Handler的时候,如果没有取到Looper就会抛异常。
那么Looper.prepare()又执行了啥呢,其实就是创建了一个Looper,保存在了sThreadLocal中。ThreadLocal是线程隔离的,所以一个线程对应一个Looper。
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();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));}
为什么在主线程中创建Handler没有问题呢,那就先从ActivityThread中去找,在main函数中,我们看到了Looper.prepareMainLooper(),这行代码就是创建了一个主线程的Looper。
public static void main(String[] args) {// Call per-process mainline module initialization.initializeMainlineModules();Process.setArgV0("<pre-initialized>");//创建主线程looperLooper.prepareMainLooper();//开启消息循环Looper.loop()...省略
}
2.开发中的问题
当我们创建一个Handler时,会存在内存泄漏的问题。因为内部类会持有外部类的引用,我们在Activity中创建Handler,Activity被销毁时由于Handler可能存在延时任务依然持有引用,就导致GC无法回收Activity。
通常的处理办法是静态内部类+弱引用。另外在页面退出的时候,调用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。
protected static class BaseHandler extends Handler {private final WeakReference<BaseMvpActivity> mObjects;public BaseHandler(BaseMvpActivity mPresenter) {mObjects = new WeakReference<>(mPresenter);}@Overridepublic void handleMessage(Message msg) {BaseMvpActivity mPresenter = mObjects.get();if (mPresenter != null)mPresenter.handleMessage(msg);}}
二,消息机制
在使用Handler发送消息的时候,会涉及到这几个类,Looper,MessageQueue,Handler,Message。
1.Looper
看下Looper的成员变量
public final class Looper {//用于存放创建的Looper,ThreadLocal是线程隔离的一个类。一个线程对应一个Looper。@UnsupportedAppUsagestatic final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();@UnsupportedAppUsageprivate static Looper sMainLooper; // guarded by Looper.classprivate static Observer sObserver;@UnsupportedAppUsagefinal MessageQueue mQueue;final Thread mThread;...省略
}
创建Looper的方法
private static void prepare(boolean quitAllowed) {// 规定了一个线程只有一个Looper,也就是一个线程只能调用一次Looper.prepare()if (sThreadLocal.get() != null) {throw new RuntimeException("Only one Looper may be created per thread");}// 如果当前线程没有Looper,那么就创建一个,存到sThreadLocal中 sThreadLocal.set(new Looper(quitAllowed));}
开始运行消息循环:这是的消息循环是个死循环,为什么不会导致应用卡死。
这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
poop()方法是开启消息循环的,主线的loop()方法运行在ActivityThead#mian方法中。
public static void loop() {final Looper me = myLooper();me.mSlowDeliveryDetected = false;for (;;) {if (!loopOnce(me, ident, thresholdOverride)) {return;}}}
接下来看Looper的构造方法:
private Looper(boolean quitAllowed) {// 创建了MessageQueue,并供Looper持有 mQueue = new MessageQueue(quitAllowed);// 让Looper持有当前线程对象 mThread = Thread.currentThread();}
创建了消息队列MessageQueue,并让它供Looper持有,因为一个线程最大只有一个Looper对象,所以一个线程最多也只有一个消息队列。
2.MessageQueue
消息队列,存放了Handler发送的消息,供Looper循环取消息。
public final class MessageQueue {private static final String TAG = "MessageQueue";private static final boolean DEBUG = false;// True if the message queue can be quit.@UnsupportedAppUsageprivate final boolean mQuitAllowed;//消息实体Message mMessages;//存放IdleHandlerprivate final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();private SparseArray<FileDescriptorRecord> mFileDescriptorRecords;private IdleHandler[] mPendingIdleHandlers;private boolean mQuitting;
IdleHandler是一个只有一个方法的接口:
public static interface IdleHandler {boolean queueIdle();}
queueIdle方法会在MessageQueue中当前没有消息需要执行(Message空了或者下一个Message还要等一会儿才到执行时间)之后回调,方法的返回值是一个boolean值:当返回值为false时,MessageQueue会删除掉这个IdleHandler,之后不会再调用到这个对象。反之则继续持有这个对象,在下次空闲时再调用一次。
你在开发中这样用过吗?
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {@Overridepublic boolean queueIdle() {return false;}});
最后在next方法中,没有message可执行的情况下调用:
Message next() {// Return here if the message loop has already quit and been disposed.// This can happen if the application tries to restart a looper after quit// which is not supported.final long ptr = mPtr;if (ptr == 0) {return null;}int pendingIdleHandlerCount = -1; // -1 only during first iterationint nextPollTimeoutMillis = 0;for (;;) {...省略//最后才执行for (int i = 0; i < pendingIdleHandlerCount; i++) {final IdleHandler idler = mPendingIdleHandlers[i];mPendingIdleHandlers[i] = null; // release the reference to the handlerboolean keep = false;try {keep = idler.queueIdle();} catch (Throwable t) {Log.wtf(TAG, "IdleHandler threw exception", t);}if (!keep) {synchronized (this) {mIdleHandlers.remove(idler);}}}}}
源码中有一处地方用到了:在ActivityThread中通过IdleHandler来在空闲时调用GC,来减少卡顿的可能性:
void scheduleGcIdler() {if (!mGcIdlerScheduled) {mGcIdlerScheduled = true;Looper.myQueue().addIdleHandler(mGcIdler);}mH.removeMessages(H.GC_WHEN_IDLE);}final class GcIdler implements MessageQueue.IdleHandler {@Overridepublic final boolean queueIdle() {doGcIfNeeded();purgePendingResources();return false;}}
同步屏障
这里的同步并不是多线程中线程安全的那个同步,而是指,一个MessageQueue中的所有message的执行顺序的同步,即默认情况下所有message都是同步的,所有message按when排序,依次执行。而如果某些message优先级非常高,我们想让它在特定时间后尽早执行,应该怎么办呢?这时候就是同步屏障的用武之地了。
同步屏障功能的实现分为两个部分:
1:插入一个同步屏障,使同步屏障之后的同步message暂停执行;
2:插入异步message。
首先看插入同步屏障的方式,我们插入同步屏障的方法是调用MessageQueue#postSyncBarrier方法:
public int postSyncBarrier() {return postSyncBarrier(SystemClock.uptimeMillis());
}private int postSyncBarrier(long when) {// Enqueue a new sync barrier token.// We don't need to wake the queue because the purpose of a barrier is to stall it.synchronized (this) {final int token = mNextBarrierToken++;final Message msg = Message.obtain();msg.markInUse();msg.when = when;msg.arg1 = token;Message prev = null;Message p = mMessages;if (when != 0) {while (p != null && p.when <= when) {prev = p;p = p.next;}}if (prev != null) { // invariant: p == prev.nextmsg.next = p;prev.next = msg;} else {msg.next = p;mMessages = msg;}return token;}
}
public void removeSyncBarrier(int token) {// Remove a sync barrier token from the queue.// If the queue is no longer stalled by a barrier then wake it.synchronized (this) {Message prev = null;Message p = mMessages;while (p != null && (p.target != null || p.arg1 != token)) {//遍历任务队列prev = p;p = p.next;}if (p == null) { //没有找到token对应的同步屏障,抛出异常throw new IllegalStateException("The specified message queue synchronization "+ " barrier token has not been posted or has already been removed.");}final boolean needWake;if (prev != null) {prev.next = p.next; //从队列中移除同步屏障needWake = false;} else {mMessages = p.next;needWake = mMessages == null || mMessages.target != null;}p.recycleUnchecked();// If the loop is quitting then it is already awake.// We can assume mPtr != 0 when mQuitting is false.if (needWake && !mQuitting) {nativeWake(mPtr);}}}
postSyncBarrier会按照when的值,在消息队列中插入一个target为null的message,因为when的默认值是SystemClock.uptimeMillis()当前时间,所以可以想象,这个同步屏障大多数时候都会插入到队列头部。同时需要注意,这个方法是hide的,所以我们正常情况下调用不了。postSyncBarrier会返回一个token,后续可以通过removeSyncBarrier方法并传入这个token来删除相应的同步屏障。
3.Handler
在我们用Handler发送一条消息后,最终会调用:
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {MessageQueue queue = mQueue;if (queue == null) {RuntimeException e = new RuntimeException(this + " sendMessageAtTime() called with no mQueue");Log.w("Looper", e.getMessage(), e);return false;}return enqueueMessage(queue, msg, uptimeMillis);}
最终调用了MessageQueue中的enqueueMessage方法:
boolean enqueueMessage(Message msg, long when) {if (msg.target == null) {throw new IllegalArgumentException("Message must have a target.");}synchronized (this) {// 一个Message,只能发送一次 if (msg.isInUse()) {throw new IllegalStateException(msg + " This message is already in use.");}if (mQuitting) {IllegalStateException e = new IllegalStateException(msg.target + " sending message to a Handler on a dead thread");Log.w(TAG, e.getMessage(), e);msg.recycle();return false;}msg.markInUse();msg.when = when;Message p = mMessages;boolean needWake;if (p == null || when == 0 || when < p.when) {// New head, wake up the event queue if blocked.msg.next = p;mMessages = msg;needWake = mBlocked;} else {// 根据需要把消息插入到消息队列的合适位置,通常是调用xxxDelay方法,延时发送消息 needWake = mBlocked && p.target == null && msg.isAsynchronous();Message prev;for (;;) {prev = p;p = p.next;if (p == null || when < p.when) {break;}if (needWake && p.isAsynchronous()) {needWake = false;}}// 把消息插入到合适位置 msg.next = p; // invariant: p == prev.nextprev.next = msg;}// 如果队列阻塞了,则唤醒 if (needWake) {nativeWake(mPtr);}}return true;}
首先,判断了Message是否已经使用过了,如果使用过,则直接抛出异常,这是可以理解的,如果MessageQueue中已经存在一个Message,但是还没有得到处理,这时候如果再发送一次该Message,可能会导致处理前一个Message时,出现问题。
然后,会判断when,它是表示延迟的时间,我们这里没有延时,所以为0,满足if条件。把消息插入到消息队列的头部。如果when不为0,则需要把消息加入到消息队列的合适位置。
最后会去判断当前线程是否已经阻塞了,如果阻塞了,则需要调用本地方法去唤醒它。
总结:
三,面试题
1.android中实现多线程通信的方法?
view.post,activity.runOnUiThread,AsyncTask这些方式内部都是用了Handler。
2.系统为什么不允许在子线程中访问UI?
- View不是线程安全的,加上锁机制会让UI访问的逻辑变得复杂。
- 锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
3.在子线程发送消息,却能够在主线程接收消息,主线程和子线程是怎么样切换的?
这个问题很好回答,我们分两种情况,主要看Handler中的Looper是谁的looper。如果是主线程持的looper,那么就可以切换回主线程。如果是子线程looper,是不可以的。
①,第一种情况:持有主线程的Looper
mBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//第一种情况 由于handler在主线程创建的,被主线程持有,这里只是在子线程调用handler,所以不存在严格意义上的线程切换。因此发送消息的loop()方法在主线程执行的。new Thread(new Runnable() {@Overridepublic void run() {//Handler在主线程中创建的handler.sendEmptyMessage(0);}}).start();//第二种情况 这里是在子线程中创建Handler,理论上这个Handler被子线程持有,但这里传入了一个getMainLooper(),这是主线程的loop。发送消息也是在主线程执行的。new Thread(new Runnable() {@Overridepublic void run() {Handler handler1 = new Handler(Looper.getMainLooper());Looper.loop();Log.e(TAG, "run: handler1=" + handler1.getLooper());handler1.post(new Runnable() {@Overridepublic void run() {mBtn.setText("第二种情况");}});}}).start();}});
②,第二种情况:持有子线程的Looper
下边代码抛异常了,因为looper运行在主线程中。
mBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {new Thread(new Runnable() {@Overridepublic void run() {Looper.prepare();Handler handler1 = new Handler();Log.e(TAG, "run: handler1=" + handler1.getLooper());handler1.post(new Runnable() {@Overridepublic void run() {mBtn.setText("无法更新UI,抛异常");}});Looper.loop();}}).start();}});
4.一个线程可以有几个Handler?几个Looper?
一个线程可以有多个Handler,但是只有一个Looper。创建Handler之前,需要创建Looper,否则会报错。
5.Handler如何实现延时任务的?
通过nativePollOnce(long ptr, int timeoutMillis)设置了定时器延迟唤醒。
nativePollOnce获取的是链表表头信息,那MessageQueue如何保证链表内获取的消息顺序从而保证执行顺序?
事实上,当调用enqueueMessage(Message msg, long when)方法,MessageQueue会根据Message的执行时间msg.when进行排序,链表头的延迟时间小,尾部延迟时间最大。
如果在延时唤醒的过程中,又来了一个立即执行的message又该如何呢?
依照上面的思路,立即执行的消息同样也会先入链表,然后唤醒线程获取表头message,看是否到了执行时间。由于立即执行的消息其实是一个延时为0的message,在一个延迟的链表中,必然会放入表头,而且是无延迟的,所以会立即取出返回给loop去执行了,loop处理完消息,继续来拿表头的message。
当整个链表都是延迟执行的message时,如果此时插入的message也是延时执行的,是否一定要唤醒呢?
如果插入的message并非插入表头,说明拿的下一个message也不是自己,完全可以让线程继续休眠,没有必要唤醒,因为此时的定时器到期唤醒后拿到的正是待返回和执行的表头message。
6.子线程可以更新UI吗?
可以更新UI,分两种情况:
①,在onCreate中直接创建线程,特殊情况
为什么下面这种方式可以呢,这里只需要做个延时就不可以了。
public class TextActivity extends AppCompatActivity {private Button mBtn;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mBtn=findViewById(R.id.btn);new Thread(new Runnable() {@Overridepublic void run() {//Thread.sleep(2000); 加上这段代码就不行了mBtn.setText("你好");}}).start();}
}
因为检查View所在线程是在ViewRootImpl#requestLayout中进行的,而ViewRootImpl是在onResume中初始化的。
@Overridepublic void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {checkThread();//检查View所在的线程mLayoutRequested = true;scheduleTraversals();}}
2,使用post方法更新UI
new Thread(new Runnable() {@Overridepublic void run() {//方式1handler.post(new Runnable() {@Overridepublic void run() {mBtn.setText("你好");}});//方式2mBtn.post(new Runnable() {@Overridepublic void run() {mBtn.setText("你好");}});//方式3handler.sendEmptyMessage(0);//方式4runOnUiThread(new Runnable() {@Overridepublic void run() {mBtn.setText("你好");}});}}).start();
7.Handler如何保证MessageQueue并发访问安全?
循环加锁,配合阻塞唤醒机制。
我们可以看到他的等待是在锁外的,当队列中没有消息的时候,他会先释放锁,再进行等待,直到被唤醒。这样就不会造成死锁问题了。
Message next() {...for (;;) {...nativePollOnce(ptr, nextPollTimeoutMillis);synchronized (this) {...}}
}
8.Handler的阻塞唤醒机制是怎么回事?
Handler的阻塞唤醒机制是基于Linux的阻塞唤醒机制。
这个机制也是类似于handler机制的模式。在本地创建一个文件描述符,然后需要等待的一方则监听这个文件描述符,唤醒的一方只需要修改这个文件,那么等待的一方就会收到文件从而打破唤醒。和Looper监听MessageQueue,Handler添加message是比较类似的。
9.能不能让一个Message加急被处理?/ 什么是Handler同步屏障?
可以 / 一种使得异步消息可以被更快处理的机制
Handler 同步屏障是一个用于同步线程之间消息传递的机制,它可以用于保证发送给 Handler 的消息被及时处理,从而避免消息积压导致的卡顿或崩溃问题。