带着问题看这篇文章,你会有更多的收获。
- 为什么需要使用handler?什么场景下使用? 不用行不行?
- 主线程真的不能更新UI吗?
- Handler是如何使用的?
- Headler使用过程中会出现那些问题?
- IdleHandler是什么?什么场景下使用?
- 你听过同步消息屏障吗?有什么用?
- HandlerThread是什么?他的实现原理是什么?有什么用?
- IntentService是什么?有什么用?
一、Handler是什么,有什么用?
答:Android中的消息通信机制,用于 子线程与主线程间的通讯,实现了一种 非堵塞的消息传递机制,把子线程中的 UI更新信息 发送给主线程(UI线程),以此完成UI更新操作。Android中主线程不能进行耗时操作,所有的耗时操作都需要在子线程中进行,耗时操作完成后如果需要更新ui,就需要把信息传给主线程更新ui。
二、子线程真的不能更新UI吗?
我们举个例子看下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setMessage("演示在子线程更新ui");
AlertDialog alertDialog = builder.create();
alertDialog.show();
Looper.loop();
}
}).start();
}
复制代码
运行后:
可以看到,对话框正常弹出,而我们平时在 子线程中更新UI 的报错信息是这样的:
报错原因提示:只有创建这个View的线程才能更新这个view,因为我们的view基本上都是在mainThread中即默认线程中初始化的,所以此处会报错。所以子线程更新UI也行,但是只能更新子线程创建的View。
那么Android机制这么设定不让子线程具备更新主线程的能力呢?这是因为多个线程同时对同一个UI控件进行更新,容易发生不可控的错误,即线程安全问题。那么怎么解决这种线程安全问题?最简单的处理方式——加锁,不是加一个,是每层都要加锁(用户代码→GUI顶层→GUI底层…)但这样也意味着更多的 耗时,从而导致UI更新效率降低,界面卡顿等。
我们再看另一个在子线程更新UI的场景
public class MainActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.tv);
new Thread(new Runnable() {
@Override
public void run() {
textView.setText(Thread.currentThread().getName());
}
}).start();
}
}
复制代码
上面这段代码 直接在子线程中更新了UI,却没有报错:
上一步我们得到的结论是只有创建view的线程才具备修改这个view的权利,而此处的现象与我们得到的结论似乎不相符,那我们看下系统源码看下什么原因导致的。
ViewRootImp
在onCreate()
调用时还没创建,在onResume()
即ActivityThread.handleResumeActivity()
*执行后才创建;- 调用
View.requestLayout()
,最终调到ViewRootImpl.requestLayout()
,走到**checkThread()
**报错;
所以我们猜想可能是由于方法执行顺序导致子线程可以修改UI。我们来验证下:
public class MainActivity extends AppCompatActivity {
private TextView textView;
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.tv);
Log.d(TAG, "onCreate: ");
new Thread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "run: 在子线程修改UI");
textView.setText(Thread.currentThread().getName());
}
}).start();
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume: ");
}
}
复制代码
运行结果如下:
2022-03-13 11:31:03.370 5964-5964/com.firstcode.myapplication D/MainActivity: onCreate:
2022-03-13 11:31:03.373 5964-5998/com.firstcode.myapplication D/MainActivity: run: 在子线程修改UI
2022-03-13 11:31:03.435 5964-5964/com.firstcode.myapplication D/MainActivity: onResume:
复制代码
在子线程代码块中加上休眠模拟耗时操作:
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(300);
Log.d(TAG, "run: 在子线程修改UI");
} catch (InterruptedException e) {
e.printStackTrace();
}
textView.setText(Thread.currentThread().getName());
}
}).start();
复制代码
程序崩溃,报错日志如下:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7753)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1225)
at android.view.View.requestLayout(View.java:23093)
at android.view.View.requestLayout(View.java:23093)
at android.view.View.requestLayout(View.java:23093)
at android.view.View.requestLayout(View.java:23093)
at android.view.View.requestLayout(View.java:23093)
at android.view.View.requestLayout(View.java:23093)
at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3146)
复制代码
执行日志如下:
2022-03-13 11:39:09.674 6306-6306/com.firstcode.myapplication D/MainActivity: onCreate:
2022-03-13 11:39:09.684 6306-6306/com.firstcode.myapplication D/MainActivity: onResume:
2022-03-13 11:39:09.979 6306-6338/com.firstcode.myapplication D/MainActivity: run: 在子线程修改UI
复制代码
所以这里的子线程可以更新UI是因为方法执行顺序导致,先更新了UI再执行检查逻辑,取了个巧,而并不是真的可以在子线程中更新UI。
三、Handler是如何使用的?
3.1在默认线程(主线程中使用)
android.os.Handler handler = new Handler(){
@Override
public void handleMessage(final Message msg) {
//这里接受并处理消息
}
};
//发送消息
Message message = Message.obtain();
handler.sendMessage(message);
//或者
handler.post(new Runnable() {
@Override
public void run() {
}
});
复制代码
使用方式比较简单:
- 创建Handler对象;
- 发送消息就OK了
3.2 俩条线程间相互发送消息(子线程向主线程发送消息)
public class MainActivity extends AppCompatActivity {
private TextView textView;
private static final String TAG = "MainActivity";
android.os.Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
//这里接受并处理消息
Log.d(TAG, "handleMessage: " + msg.what);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread() {
@Override
public void run() {
super.run();
try {
//子线程向主线程发送
Message message1 = Message.obtain();
message1.what = 1;
handler.sendMessage(message1);
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
}
复制代码
2022-03-13 12:07:29.511 6843-6843/com.firstcode.myapplication D/MainActivity: handleMessage: 1
复制代码
3.3 俩条线程间相互发送消息(主线程向子线程发送消息)
其实俩条线程间相互发送消息,方式都是一样的,只要记住一点就好了:拿到对方的handler发送消息,比如A要向B发送消息,在A中拿到B的handler,调用handler.sendMessage()就好了。
public class MainActivity extends AppCompatActivity {
private TextView textView;
private static final String TAG = "MainActivity";
android.os.Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread() {
@Override
public void run() {
super.run();
Looper.prepare();
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
//这里接受并处理消息
Log.d(TAG, "handleMessage: " + msg.what);
}
};
Looper.loop();
}
}.start();
try {
Thread.sleep(200);//为了让handler先初始化完,否则会报空指针异常
} catch (InterruptedException e) {
e.printStackTrace();
}
Message message1 = Message.obtain();
message1.what = 2;
handler.sendMessage(message1);
}
}
复制代码
2022-03-13 12:13:48.756 7305-7337/com.firstcode.myapplication D/MainActivity: handleMessage: 2
复制代码
向子线程中发送信息稍微有点不同的是,需要先调用Looper的相关方法,至于为什么,看完原理就知道了。
建议调用 Message.obtain() 函数来获取一个Message实例,为啥?点进源码:
从源码,可以看到obtain()的逻辑:加锁 → 判断Message池是否为空
- ① 不为空,取一枚Message对象,正在使用标记置为0,池容量-1,返回此对象;
- ② 为空,新建一个Message对象返回;
此处复用Message,可避免**避免重复创建实例对象
,达到节约内存的目的,而且不难看出Message实际上是无头结点的单链表
**。
上述获取消息池的逻辑:
定位到下述代码,还可以知道:池的容量为50
然后问题来了,Message信息什么时候加到池中?
答:当Message 被Looper分发完后,会调用 recycleUnchecked()函数,回收没有在使用的Message对象。
标志设置为**FLAG_IN_USE
**,表示正在使用,相关属性重置,加锁,判断消息池是否满,未满,单链表头插法 将消息插入到表头。
四、Handler底层原理解析
终于来到稍微有点技术含量的环节,在观摩源码了解原理前,先说下几个涉及到的类。
4.1 涉及到的几个类
- handler :1.负责发送消息到MessageQueue;2.对消息进行处理
- Message :消息的载体
- Looper :内部维护MessageQueue,以轮训的方式不断从MQ中获取消息,发送给handler处理
- MessageQueue :消息队列,在Looper中,负责存储消息
- ThreadLocal : 一个线程有一个ThreadLocal,存储Looper,绑定线程和Looper,使得每一个线程的消息队列都是线程安全的
4.2 整体流程
4.3 Looper.prepare() :
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));
}
复制代码
Looper会借助 ThreadLocal 来实现与当前线程的绑定功能
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
复制代码
在这个流程里,做了几件事
- 创建「Looper」;
- 创建完Looper后,在Looper的构造方法中创建了用于管理消息的「MessageQueue」对象
- 通过ThreadLocal 来实现与当前线程的绑定
另外值得注意的是,在Looper中 prepareMainLooper方法中,执行的是prepare(false)方法,而我们自己创建的线程中执行prepare()方法时,默认执行prepare(true),这有什么区别呢?
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
复制代码
跟踪MessageQueue的源码:
void quit(boolean safe) {
if (!mQuitAllowed) { //用户无法主动停止主线程消息队列
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
// We can assume mPtr != 0 because mQuitting was previously false.
nativeWake(mPtr);
}
}
复制代码
可以看到,如果是子线程我们可以主动停止消息队列,但是主线程我们是没有这个权限的。
4.4 Looper.loop()
经过上一步,创建了Looper与MessageQueue对象,接着调用Looper.loop()开启轮询。
public static void loop() {
final Looper me = myLooper();//获取当前线程Looper实例
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;//获取消息队列
...
for (;;) {//开启一个死循环,不断从队列中获取消息
Message msg = queue.next(); // might block
msg.target.dispatchMessage(msg);//将消息分发给handler处理
}
复制代码
主要做了几件事:
- 获取当前线程Looper实例;
- 获取消息队列
- 开启一个死循环,不断从队列中获取消息
- 将消息分发给handler处理
myLooper() 函数:
@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
复制代码
ThreadLocal → 线程局部变量 → JDK提供的用于解决线程安全的工具类。 作用:为每个线程提供一个独立的变量副本 → 以解决并发访问的冲突问题
ThreadLocal.class:
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//从当前线程获取ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
复制代码
可以看到:每个Thread内部都维护了一个ThreadLocalMap,这个map的key是ThreadLocal
,value是set的那个值。get的时候,线程都是从自己的变量中取值,所以不存在线程安全问题。
总结下:
- Looper.prepare() 时会将当前的looper保存到ThreadLocal中。
- 每一个线程都有一个ThreadLocalMap,key是当前线程,value是上面保存的Looper。所以通过looper.mylooper会获取当当前线程的looper对象,并且是唯一的
主线程和子线程的Looper对象实例相互隔离的!!!
另外,线程为key也保证了每个线程只有一个Looper,而创建Looper对象时又会创建MessageQueue对象,所以间接保证每个线程最多只能有一个MessageQueue。
知道这个以后,有个问题就解惑了:
为什么子线程中不能直接 new Handler(),而主线程可以?
因为主线程与子线程不共享同一个Looper实例,主线程的Looper在启动时就通过 prepareMainLooper() 完成了初始化,而子线程还需要调用 Looper.prepare() 和 Looper.loop()开启轮询,否则会报错,不信,可以试试:
直接就奔溃了~
加上试试?
没有报错,程序正常运行。
对了,既然说Handler用于子线程和主线程通信,试试在主线程中给子线程的Handler发送信息,修改一波代码:
运行,直接报错:
原因:多线程并发的问题,当主线程执行到sendEnptyMessage时,子线程的Handler还没有创建。 一个简单的解决方法是:主线程延时给子线程发消息,修改后的代码示例如下:
运行结果如下:
可以,不过其实Android已经给我们封装好了一个轻量级的异步类 HandlerThread
4.5 当我们用Handler发送一个消息发生了什么?
Handler可以通过sendMessage()和 post() 发送消息,这两个最后调用的其实都是 sendMessageDelayed()完成的:
Handler.java:
public final boolean sendMessage(@NonNull Message msg) {
return sendMessageDelayed(msg, 0);
}
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}
复制代码
第二个参数:当前系统时间+延时时间,这个会影响「调度顺序」
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);
}
复制代码
获取当前线程Looper中的MessageQueue队列,判空,空打印异常,否则返回 enqueueMessage()
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
复制代码
这里的 mAsynchronous 是 异步消息的标志,如果Handler构造方法不传入这个参数,默认false: 这里涉及到了一个「同步屏障」的东西,等等再讲,跟:MessageQueue -> enqueueMessage
MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
synchronized (this) {
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 {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
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;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
复制代码
如果你了解数据结构中的单链表的话,这些都很简单。
4.6 Looper是怎么处理队列的消息的?
MessageQueue里有Message了,接着就该由Looper来处理了,定位到:Looper → loop函数
// Looper.loop()
final Looper me = myLooper(); // 获得当前线程的Looper实例
final MessageQueue queue = me.mQueue; // 获取消息队列
for (;;) { // 死循环
Message msg = queue.next(); // 取出队列中的消息
msg.target.dispatchMessage(msg); // 将消息分发给Handler
}
复制代码
queue.next() 从队列拿出消息,定位到:MessageQueue -> next函数:
这里的关键其实就是:nextPollTimeoutMillis,决定了堵塞与否,以及堵塞的时间,三种情况:
等于0时,不堵塞,立即返回,Looper第一次处理消息,有一个消息处理完 ;
大于0时,最长堵塞等待时间,期间有新消息进来,可能会了立即返回(立即执行);
等于-1时,无消息时,会一直堵塞;
此处是通过Linux的**epoll机制
**来堵塞,原因是需要处理 native侧
的事件。
没有消息时堵塞并进入休眠释放CPU资源,有消息时再唤醒线程。
4.7 分发给Handler的消息是怎么处理的?
通过MessageQueue的queue.next()拣出消息后,调用msg.target.dispatchMessage(msg) 把消息分发给对应的Handler,跟到:Handler -> dispatchMessage
五、IdleHandler是什么?
评论区有小伙子说:把idleHandler加上就完整了,那就安排下吧~
在 MessageQueue 类中有一个 static 的接口 IdleHanlder
翻译下注释:当线程将要进入堵塞,以等待更多消息时,会回调这个接口; 简单点说:当MessageQueue中无可处理的Message时回调; 作用:UI线程处理完所有View事务后,回调一些额外的操作,且不会堵塞主进程;
接口中只有一个 queueIdle() 函数,线程进入堵塞时执行的额外操作可以写这里, 返回值是true的话,执行完此方法后还会保留这个IdleHandler,否则删除。
使用方法也很简单,代码示例如下:
输出结果如下:
看下源码,了解下具体的原理:MessageQueue,定义了一个IdleHandler的列表和数组
定义了添加和删除IdleHandler的函数:
在 next() 函数中用到了 mIdleHandlers 列表:
六、一些其他问题
1.Looper在主线程中死循环,为啥不会ANR?
答:上面说了,Looper通过**queue.next()**获取消息队列消息,当队列为空,会堵塞,
此时主线程也堵塞在这里,好处是:main函数无法退出,APP不会一启动就结束!
你可能会问:主线程都堵住了,怎么响应用户操作和回调Activity生命周期相关的方法?
答:application启动时,可不止一个main线程,还有其他两个Binder线程:ApplicationThread 和 ActivityManagerProxy,用来和系统进程进行通信操作,接收系统进程发送的通知。
- 当系统受到因用户操作产生的通知时,会通过 Binder 方式跨进程通知 ApplicationThread;
- 它通过Handler机制,往 ActivityThread 的 MessageQueue 中插入消息,唤醒了主线程;
- queue.next() 能拿到消息了,然后 dispatchMessage 完成事件分发;
Tips:ActivityThread 中的内部类H中有具体实现
死循环不会ANR,但是 dispatchMessage 中又可能会ANR哦!如果你在此执行一些耗时操作,导致这个消息一直没处理完,后面又接收到了很多消息,堆积太多,就会引起ANR异常!!!
2.Handler泄露的原因及正确写法
上面说了,如果直接在Activity中初始化一个Handler对象,会报如下错误:
原因是:
在Java中,非静态内部类会持有一个外部类的隐式引用,可能会造成外部类无法被GC; 比如这里的Handler,就是非静态内部类,它会持有Activity的引用从而导致Activity无法正常释放。
而单单使用静态内部类,Handler就不能调用Activity里的非静态方法了,所以加上「弱引用」持有外部Activity。
代码示例如下:
private static class MyHandler extends Handler {
//创建一个弱引用持有外部类的对象
private final WeakReference<MainActivity> content;
private MyHandler(MainActivity content) {
this.content = new WeakReference<MainActivity>(content);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity= content.get();
if (activity != null) {
switch (msg.what) {
case 0: {
activity.notifyUI();
}
}
}
}
}
复制代码
另外,还有一种情况可能会引起内存泄漏:延时消息,Activity关闭消息还没处理完,可以在Activity的onDestroy()中调用:handler.removeCallbacksAndMessages(null)
移除Message/Runnable。
3.同步屏障机制
通过上面的学习,我们知道用Handler发送的Message后,MessageQueue的enqueueMessage()按照 时间戳升序 将消息插入到队列中,而Looper则按照顺序,每次取出一枚Message进行分发,一个处理完到下一个。
这时候,问题来了:有一个紧急的Message需要优先处理怎么破?你可能或说**直接sendMessage()**不就可以了,不用等待立马执行,看上去说得过去,不过可能有这样一个情况:
一个Message分发给Handler后,执行了耗时操作,后面一堆本该到点执行的Message在那里等着,这个时候你sendMessage(),还是得排在这堆Message后,等他们执行完,再到你!
对吧?Handler中加入了「同步屏障」这种机制,来实现「异步消息优先执行」的功能。
添加一个异步消息的方法很简单:
- 1、Handler构造方法中传入async参数,设置为true,使用此Handler添加的Message都是异步的;
- 2、创建Message对象时,直接调用setAsynchronous(true)
一般情况下:同步消息和异步消息没太大差别,但仅限于开启同步屏障之前。可以通过 MessageQueue 的 postSyncBarrier 函数来开启同步屏障:
行吧,这一步简单的说就是:往消息队列合适的位置插入了同步屏障类型的Message (target属性为null) 接着,在 MessageQueue 执行到 next() 函数时:
遇到target为null的Message,说明是同步屏障,循环遍历找出一条异步消息,然后处理。在同步屏障没移除前,只会处理异步消息,处理完所有的异步消息后,就会处于堵塞。如果想恢复处理同步消息,需要调用 removeSyncBarrier() 移除同步屏障:
在API 28的版本中,postSyncBarrier()已被标注hide,但依旧可在系统源码中找到相关应用,比如: 为了更快地响应UI刷新事件,在ViewRootImpl的scheduleTraversals函数中就用到了同步屏障: