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;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handler = new Handler();
new Thread(new Runnable() {
@Override
public void run() {
Log.e("TextActivity", "run: " );
handler1=new Handler();
}
}).start();
}
}
上边的代码,运行结果
handler成功创建了,handler1出现了异常,说是没有Looper。只需要在添加一行代码就可以了。Looper.prepare()方法就是创建了一个Looper。
@Override
public void run() {
Log.e("TextActivity", "run: " );
//创建Looper
Looper.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>");
//创建主线程looper
Looper.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);
}
@Override
public 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。
@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
@UnsupportedAppUsage
private static Looper sMainLooper; // guarded by Looper.class
private static Observer sObserver;
@UnsupportedAppUsage
final 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.
@UnsupportedAppUsage
private final boolean mQuitAllowed;
//消息实体
Message mMessages;
//存放IdleHandler
private 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() {
@Override
public 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 iteration
int nextPollTimeoutMillis = 0;
for (;;) {
...省略
//最后才执行
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean 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 {
@Override
public 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.next
msg.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.next
prev.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() {
@Override
public void onClick(View v) {
//第一种情况 由于handler在主线程创建的,被主线程持有,这里只是在子线程调用handler,所以不存在严格意义上的线程切换。因此发送消息的loop()方法在主线程执行的。
new Thread(new Runnable() {
@Override
public void run() {
//Handler在主线程中创建的
handler.sendEmptyMessage(0);
}
}).start();
//第二种情况 这里是在子线程中创建Handler,理论上这个Handler被子线程持有,但这里传入了一个getMainLooper(),这是主线程的loop。发送消息也是在主线程执行的。
new Thread(new Runnable() {
@Override
public void run() {
Handler handler1 = new Handler(Looper.getMainLooper());
Looper.loop();
Log.e(TAG, "run: handler1=" + handler1.getLooper());
handler1.post(new Runnable() {
@Override
public void run() {
mBtn.setText("第二种情况");
}
});
}
}).start();
}
});
②,第二种情况:持有子线程的Looper
下边代码抛异常了,因为looper运行在主线程中。
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Handler handler1 = new Handler();
Log.e(TAG, "run: handler1=" + handler1.getLooper());
handler1.post(new Runnable() {
@Override
public 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;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtn=findViewById(R.id.btn);
new Thread(new Runnable() {
@Override
public void run() {
//Thread.sleep(2000); 加上这段代码就不行了
mBtn.setText("你好");
}
}).start();
}
}
因为检查View所在线程是在ViewRootImpl#requestLayout中进行的,而ViewRootImpl是在onResume中初始化的。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();//检查View所在的线程
mLayoutRequested = true;
scheduleTraversals();
}
}
2,使用post方法更新UI
new Thread(new Runnable() {
@Override
public void run() {
//方式1
handler.post(new Runnable() {
@Override
public void run() {
mBtn.setText("你好");
}
});
//方式2
mBtn.post(new Runnable() {
@Override
public void run() {
mBtn.setText("你好");
}
});
//方式3
handler.sendEmptyMessage(0);
//方式4
runOnUiThread(new Runnable() {
@Override
public 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 的消息被及时处理,从而避免消息积压导致的卡顿或崩溃问题。