button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
textView.setText("after changed");
}
});
开头我们就看到了如上的一段简单的伪码。因为这里我试图去还原一种场景,一种可能我们不少人最初接触Android时可能都会遇到的错误场景。
这种场景的逻辑很简单:在程序运行中,某个控件的值会被动态的改变。这个值通过某种途径获取,但该途径是耗时的(例如访问网络,文件读写等)。
上面的伪码中的逻辑是:点击按钮将会改变文本框的值,且这个值是通过某种耗时的操作获取到,于是我们通过将线程休眠5秒来模拟这个耗时操作。
好的,现在我们通过编译正式开始运行类似的代码。那么,我们首先会收到熟悉的一个错误,即“Application No Response(ANR)”。
接着,通过查阅相关的资料,我们明白了:原来我们像上面这样做时,耗时操作直接就是存在于主线程,即所谓的UI线程当中的。
那么这就代表着:这个时候的UI线程会因为执行我们的耗时操作而被堵塞,也自然就无法响应用户其它的UI操作。于是,就会引起ANR这个错误了。
现在我们了解了ANR出现的原因,所以我们自然就会通过一些手段来避开这种错误。我们决定将耗时的操作从UI线程拿走,放到一个新开的子线程中:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
textView.setText("after changed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
});
好的,现在我们模拟的耗时操作已经被我们放到了UI线程之外的线程。当我们信心十足的再次运行程序,确得到了如下的另一个异常信息:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
从异常信息中,我们看到系统似乎是在告诉我们一个信息,那就是:只有创建一个视图层次结构的原始线程才能触摸到它的视图。
那么,我们似乎就能够理解这种异常出现的原因了:我们将耗时操作放在了我们自己创建的分线程中,显然它并非原始线程,自然就不能够去访问View。
这样设计的初衷实际上是不难猜想的,如果任何线程都能去访问UI,请联想一下并发编程中各种不可预知且操蛋的问题,可能我们的界面最终就热闹了。
但是,现在我们针对于这一异常的解决方案似乎也不难给出了。既然只有主线程能够访问View,那么我们只需要将更新UI的操作放到主线程就OK了。
那么,这里就顺带一提了。不知道有没有人和我曾经一样,想当然的写出过类似下面一样的代码:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 这是在主线程里执行的
textView.setText("after changed");
}
});
是的,如上的代码十分的,非常的“想当然”。这当然是因为对Java多线程机制理解不够所造成的。更新UI的操作确实是放到了主线程,但是!!!:
这并不代表着,UI更新一定会在分线程的耗时操作全部完成后才会执行,这自然是因为线程执行权是随机切换的。也就是说,很可能出现的情况是:
分线程中的耗时操作现在并没有执行完成,即我们还没有得到一个正确的结果,便切换到了主线程执行UI的更新,这个时候自然就会出现错误。
这个时候,作为菜鸟的我们有点不知所措。于是,赶紧上网查查资料,看看有没有现成的解决方案吧。这时,通常“Handler”就会进入我们的视线了:
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 0x0001:
textView.setText("after changed");
}
}
};
//===============================================
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5 * 1000);
mHandler.sendEmptyMessage(0x0001);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
我们发现关于Handler的使用似乎十分容易不过,容易到当我们认为自己掌握了它的时候似乎都没有成就感:
- 首先,我们只需要建立一个Handler对象。
- 接着,我们会在需要的地方,通过该Handler对象发送指定的Message。
- 最后,该Handler对象通过handleMessage方法处理接收到的Message。
但我们沉下心来想一想:Handler为什么能够解决我们之前碰到的非原始线程不能更新UI的错误呢?它的实现原理如何?它能做的就只是更新UI吗?
掰扯了这么多,带着这些疑问,我们终于来到了我们这篇blog最终的目的,那就是搞清楚Android的消息机制(主要就是指Handler的运行机制)。
就像医生如果要弄清楚人体构造,方式当然是通过解剖来进行研究。而我们要研究一个对象的实现原理,最好的方式就是通过分析它的源码。
个人的习惯是,当我们没有一个十分明确的切入点的时候,选择构造函数切入通常是比较合适的,那我们现在就打开Handler的构造函数来看一下:
// 1.
public Handler() {
this(null, false);
}
// 2.
public Handler(Callback callback) {
this(callback, false);
}
// 3.
public Handler(Looper looper) {
this(looper, null, false);
}
// 4.
public Handler(Looper looper, Callback callback) {
this(looper, callback, false);
}
// 5.
public Handler(boolean async) {
this(null, async);
}
// 6.
public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
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;
}
// 7.
public Handler(Looper looper, Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
好的,分析一下我们目前所看的,我觉得我们至少可以很容易的分析并掌握两点:
- Handler自身提供了7种构造器,但实际上只有最后两种提供了具体实现。
- 我们发现各种构造器最终围绕了另外两个类,即Callback与Looper。我们推测它们肯定不是做摆设的。
现在我们来分别看一下唯一两个提供了具体实现的构造器,我们发现:
除了 ”if (FIND_POTENTIAL_LEAKS) “这一段看上去和反射有关的if代码块之外,这两个构造器剩下的实现其实基本上是完全一致的,即:
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
唯一的不同在于mLooper这一实例变量的赋值方式:
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
//==========================================
mLooper = looper;
“mLooper = Looper.myLooper();”这种方式究竟有何神奇,我们这里暂且不提。我们的注意力聚焦在以上看到的几个实例变量,打开源码看看:
final MessageQueue mQueue;
final Looper mLooper;
final Callback mCallback;
final boolean mAsynchronous;
mAsynchronous是一个布尔型的变量,并且我们看到默认情况它的构造值是false,从命名我们就不难推测到,它多半与异步有关。除此之外:
其余类型分别是”MessageQueue,Looper,Callback“。一定记住它们!!!正是它们配合Handler及Message完成了整个消息传递的架构。
OK,首先我们来看Callback这个东西,从命名来看绝逼与回调有关系,打开源码,果不其然正是定义在Handler内部的一个接口:
public interface Callback {
public boolean handleMessage(Message msg);
}
我们看到了其中唯一声明的一个方法接口,看上去似乎有点眼熟。是的,那么它与Handler自身的handleMessage有何联系?我们暂且提不提。
现在,我们再接着看Looper和MessageQueue两个类型。很遗憾的是,这里我们发现:这是另外单独定义的两个全新的类。也就是说:
目前我们似乎无法在逻辑上将其与Handler联系起来。我们现在只知道从命名上来说,它们似乎分别代表着“循环”与“消息队列”的意思。
那么,到了这一步似乎情况有点糟糕,因为似乎失去了下一步的切入点。没关系,这个时候我们回忆一下我们通常怎么样使用Handler:
- mHandler.post();
- mHandler.sendMessage();
没错,我们基本上就是通过以上两种方式去使用Handler。所以现在我们打开这两个方法相关的源码来看看:
public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
public final boolean sendMessage(Message msg)
{
return sendMessageDelayed(msg, 0);
}
由此我们发现的是:post函数最终调用的仍是send系列的函数;而sendMessage底部也依然是通过sendMessageDelayed调用的。
并且!查看一系列的send方法源码发现:它们最终都将通过sendMessageAtTime来完成整个调用。所以显然这将是我们下一个关注点。
先分析一下post方法的实现,我们看到其实Handler内部是通过getPostMessage对我们传入的Runnable对象进行了一次封装。
当我们看到getPostMessage方法的实现,我们会发现没什么大不了的,只是将传入Runnable对象赋值给了一个Message对象而已。
但我们也可能会观察到另外一点。就是我们可能会在使用Message时会通过构造器得到消息对象,而这里是通过静态方法obtain。
这二者有什么不同呢?我们先打开Message的构造器的方法来看一下:
/** Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}).
*/
public Message() {
}
好的,我们发现构造器实际上没有任何实现内容。而注释告诉我们:更推荐使用obtain系列的方法来获取一个Message对象。
那么我们就好奇了?为什么更推荐使用obtain呢?我们以无参的obtain方法为例,打开源码瞧一瞧:
/**
* Return a new Message instance from the global pool. Allows us to
* avoid allocating new objects in many cases.
*/
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
从以上代码我们可以看到的是:obtain最终的本质仍是产生Message对象。关键在于一个叫sPool的东西,这兄弟到底是个什么鬼呢?
实际上是Handler内部会通过这个叫做sPool的静态全局变量构建一个类似“池”的东西,而通过next属性我们不难推断”池”应该是以单链表来实现的。
再查看方法的注释:从全局池中返回一个新的消息实例。使我们能够避免在许多情况下分配新的对象。由此我们好像已经知道为何推荐obtain了。
包括网上很多打着类似“new message与obtainMessage之间区别”的资料里,一大段的文字之后,我们会发现最终实际有用的就类似一句话:
obtainMessage可以从池中获取Message对象,从而避免新的对象创建,达到节约内存的效果。但这样当然还是熄灭不了一颗好奇的心:
究竟为什么这个“池”能够避免新的对象创建呢?要解开这个疑问,我们还需要关注Handler类中的另一个方法“recycleUnchecked”的如下代码:
void recycleUnchecked() {
// 第一部分
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = -1;
when = 0;
target = null;
callback = null;
data = null;
// 第二部分
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
该方法顾名思义,主要的工作通常主要就是回收一个完成使命的Message对象。而这个回收动作发生的时机是什么呢?
通常来说,我们可以通过人为调用msg.recycle()来完成回收;另一种更常见的回收时机发生在MessageQuene当中,我们稍后会看到。
接下来我们该方法中的回收工作都做了什么,代码中注释为第一部分的代码做的工作很易懂,就是将回收的message对象的各个属性清空。
第二部分其实就是将回收的对象向“池”内添加的过程,而之前说到的obtain方法,其一旦判断sPoll不为null,就直接从池内获取闲置对象,不再创建。
到此实际上我们就已经分析了,为什么obtain能够节约内存开销的原理了。但如果你的数据结构和我一样渣,可能还会有点晕。没关系,看如下代码:
Message msg1 = Message.obtain();
msg1.recycle();
Message msg2 = Message.obtain();
我们对应于这三行简单的代码,来有代入感的分析一下它们运行的过程,相信就会有个比较清晰的理解了。
- 首先获取msg1的时候,这个时候sPool肯定是为null的。所以它的工作实际与直接通过构造器创建对象没有区别。
- 通过msg1对象调用recycle方法,最终进入之前所说的回收工作的第二部分执行。此时的结果为:msg1.next = sPoll(即null,没有next节点);sPoll = msg1;
- 这时我们再通过obtain去获取对象msg2,进入方法后,判断sPoll不为null。于是, Message m = msg1;注意:
这代表我们已经从池中取出了msg1,于是执行sPool = m.next时,我们说到msg1.next是null,所以sPool再次等于null,逻辑完全正确。
与此同时,我们也可以想得到,假设m.next不等于null时:sPool = m.next的逻辑实际上就转换成了,将sPool指向next节点,即代表我们已经取走一个对象了,池将指向下一个节点,即为我们下次要获取的消息对象。
好了,现在相信我们都清楚以上的概念了。我们的关注点将回到我们之前提到的关键位置,即sendMessageAtTime方法,打开其源码:
public boolean sendMessageAtTime(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);
}
我们发现该方法的实现很简单,但最终会调用另一个方法“enqueueMessage”,赶紧打开这个方法看一看:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
我们发现该方法显然使用了委托设计模式,将最终的方法实现委托了给了quene对象,即MessageQuene来实现。
对于MessageQuene中的enqueueMessage方法,该方法的源码个人觉得没必要全部关注。我们先看下面这小段代码:
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.recycle方法,记得我们之前说过的回收工作吗?这里正是另一种发生时机,这个时机的标准如上所示,正是:
“mQuitting”为true,而在什么时候mQuitting会被设置为true,我们稍后将会看到,这里先暂且一提。接着看另一端更为关键的代码:
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;
有了之前的基础,我们发现该方法所做的工作实际上很简单,它仍然是以单链表的形式,通过不断追加next节点达到向队列中添加Message的效果。
由此,我们发现:当我们通过handler对象post或send了一条消息,其实最终的工作很简单,就是向MessageQuene即消息队列中追加一条消息而已。
那么,接下来呢?自然的,消息追加到了队列当中。我们则需要从队列中依次取出消息对象,才能对其作出处理。苦苦寻觅一番之后:
我们发现了next()方法,该方法的实现归根结底是通过循环来不断的从队列中拉取消息,考虑到篇幅,我们不再贴出源码。唯一注意:
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
}
当没有新的消息来临之前,如上的代码将能够确保队列“阻塞”而一直等待新的消息对象来临。好了,我们总结一下:
MessageQuene将通过enqueueMessage方法向队列中插入消息,而通过next方法取出消息。但现在的关键点在:
关于enqueueMessage方法我们已经知道它在Handler当中被调用,而next方法目前我们只看到声明,还没看到调用的产生。
以next()方法的调用为关键字按图索骥,我们最终发现它在我们之前提到的另一个关键的东西”Lopper”中产生了调用,具体是Lopper中的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;
...
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
...
msg.target.dispatchMessage(msg);
...
...
msg.recycleUnchecked();
}
}
我们只保留了关于loop方法最为关键的部分,我们依次来分析一下,首先我们注意到的,肯定是一个名为“myLooper()”的方法调用:
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
我们从代码中看到逻辑十分简单清晰,就是通过myLooper()来获取looper对象,而最终的方式则是通过sThreadLocal来获取。
这里,就不得不提到一个功能强大的东西ThreadLocal。我们来看一下Looper的源码当中关于sThreadLocal的定义:
static final ThreadLocal sThreadLocal = new ThreadLocal();
这个东西的作用究竟如何?简单来说,我们知道普通的定义一个实例变量,它将创建在“堆”上。
而“堆”并非线程私有的,所以实例变量也将被线程共享。而ThreadLocal则是将变量的作用域限制为线程私有。举例来说:
static final ThreadLocal sThreadLocal = new ThreadLocal();
sThreadLocal.set("1");
new Thread(new Runnable() {
@Override
public void run() {
sThreadLocal.set("2");
}
}).start();
上面的代码通过sThreadLocal.get()来获取string,在主线程中和我们new的线程当中获取的值是独立的,分别是“1”和“2”。
接下来,我们看到的就是将会在一个无限循环中一直通过调用MessageQuene的next()方法来获取消息对象。
假设此次获取到了msg对象,则会通过msg.target调用dispatchMessage方法来分发消息。问题在于target是个什么东西?
在Message类中查看源码,我们可以知道target自身是一个Handler类型的对象。但通常我们都没有人为的去为这个变量赋值。
那么这个变量通常默认是什么呢?回到之前Handler类的enqueneMessage方法当中,看到如下代码:
msg.target = this;
也就是说,如果我们没有明确的去为Message对象的target域赋值,它将被默认赋值为发送这条Message的Handler对象自身。
那么,我们先要做的就简单了,回到Handler类当中,查看dispatchMessage方法的源码如下:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
这个方法的实现逻辑也很清晰,它的具体分发过程如下:
- 如果msg.callback不为null,那么将通过handleCallback方法来处理消息(实际上就是msg.callback.run());
- 否则进一步判断mCallback是否为null,不为null则通过mCallback.handleMessage来处理消息。
- 最后如果mCallback也为null,则会调用Handler自身的handleMessage方法来处理消息。
逻辑很简单,我们唯一注意的就是msg.callback和mCallback这两兄弟是指什么东西?很简单:
msg.callback是我们通过Message.obtain(Handler h, Runnable callback)或者通过Handler.post(Runnable r)传入的Runnable对象。
而mCallback就更熟悉了,回想我们之前查看Handler的构造器时看到的东西。
mCallback的本质就是Handler内部定义的接口Callback,所以通过它实际就是通过回调接口处理消息。
而这里,我觉得值得一说的是msg.callback这个东西。我们知道当它不为null,最终实际将通过message.callback.run()来处理消息。
也就是说最终实际上是调用了Runnable对象的run方法,但有Java的基础就会知道这样的调用实际与线程无关,只相当于普通的调用一个实例方法而已。
对于这点,我们一定要有清楚的认识,否则可能会因为使用不当造成一些想不到的错误。具体的例子我们暂且不说,放在最后的总结部分来看。
实际上到了这里,我们就已经把构建Android消息机制的四个关键,即Handler,Message,MessageQuene及Looper给联系起来了。简单总结一下:
- 通过调用Handler的post或者send系列的方法来发送一条Message。
- 这一条Message最终会加入到链表结构的MessageQuene当中存放。
- Looper会通过内部的loop方法不断调用MessageQuene的next()方法获取下一条Message
- 当Looper获取到Message方法后,又会通过Handler的dispatchMessage来分发并处理消息。
我相信到了这里,我们或多或少都会有些收获。但对于刚接触Andoid消息机制的朋友来说,还可能存在一个疑问,那就是:
通过之前我们的分析与理解,我们知道了对于Handler处理消息的机制来说,Lopper的参与是至关重要的。
但与此同时,我们发现之前我们似乎并没有创建Looper。我们不免会考虑,是系统帮助我们创建了吗?答案是肯定的。
回忆一下之前的代码,我们是通过无参的构造器来创建Handler对象的。我们也可以看到,该构造器最终会调用我们之前说到的第6个构造器。
然后我们发现在第6种构造器当中,是通过“mLooper = Looper.myLooper();”的方式来获取looper对象的。
这时我们就想起了之前的ThreadLocal,但即使是使用ThreadLocal,也起码得有一个ThreadLocal.set(Looper)的过程吧。
这个过程是在什么时候完成的呢?正常来说,我们推断这个过程很可能发生在Looper的构造器中。但一番查找我们发现Looper压根没提供公有的构造器。
经过苦苦的寻觅之后,最终我们会发现在Looper的静态方法“prepare”中,终于找到了我们想要看见的代码:
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,是一个属于当前Activity的实例域。这代表它的创建是在主线程中完成的。
而关于Looper的创建的确也是在Android的主线程,即ActivityThread中完成创建的。具体的调用位于主线程的入口main方法中。
并且,主线程里的Looper,是通过调用Looper类专门为主线程创建Looper对象封装的方法“prepareMainLooper()”来创建的。
现在我们再看关于”Can’t create handler inside thread that has not called Looper.prepare()”的这个异常,我们就很容易找到原因了。
因为通过源码我们知道这个异常就是在第6个构造器中,当通过Looper.myLooper()获取结果为null时报告的。
同时,我们知道我们在主线程创建Handler的时候,没有问题是因为主线程默认就创建了Looper对象。那么:
当我们要在主线程以外的线程中创建Handler对象的时候,只要我们也为它创建对应的Looper对象就行了。示例如下:
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Handler handler = new Handler();
Looper.loop();
}
}).start();
是的,在创建完对象后,别忘了调用loop()方法,因为这才是开启循环从消息队列中获取消息的关键。
好了,现在我们正式接着看loop()方法中接下来的代码msg.recycleUnchecked();。是的,我们很熟悉的”回收”,由此我们可以知道:
回收Message的另一个时机实际就是,当Message对象从队列中取出处理完成之后,就会进行回收,放入池内。
如果具体阅读MessageQuene的源码,我们会发现还有多种不同的回收时机,我们简单总结几种常见的时机:
- 人为调用message.recycle()来回收对象。
- message从队列中取出被处理完成后会自动回收。
- 调用Lopper.quit()/quitSafely(),该方法最终会调用MessageQuene的quit()方法。
- MessageQuene调用quit在合适的时机将自身队列中的Message对象进行回收。
- MessageQuene的quit方法还会将我们之前谈到的mQuitting设置为true,这代表着当调用了quit之后,再通过handler来send任何message,都将被直接回收。
到这里,对于Android的消息机制我们也就研究的差不多了。虽然我总觉得在写的过程中,我本来还有个别想总结的点,但最后似乎忘了,也想不起了。
我们最后总结一个问题,那就是Handler为什么能够执行更新UI的操作!现在我们就来分析一下这个过程,回想一下:
通常我们在另开的线程中执行耗时操作,当耗时操作执行完毕后,则调用sendMessage()最终让handler更新UI。
现在我们知道了,这时的Handler我们一定会是定义在主线程,即UI线程当中的。当我们在分线程中sendMessage的时候:
经过我们之前说到的一系列调用,最终会通过Handler来进行最终处理。而Handler本身是在主线程中运行的,自然也就能够操作UI。
所以说,如果说Handler能够让我们更新UI,不如说其本质是将操作切换到该Handler所在的线程来执行。
我们可以试着发送消息给在分线程中创建的Handler对象,然后在handleMessage仍然试图去访问UI。会发现结果当然是行不通的。
这里就说到我们之前说到的handler.post()这样的使用方式了,因为参数类型是Runnable,所以我们很容易认为是通过handler执行一个线程任务。
但实际情况是,假设Handler对象是在主线程中创建的。那么,通过post()方法,我们仍然可以去更新UI。这是为什么?
这就回到了我们之前说的,当handler去dispatchMessage后,如果判断msg.callback不等于null,就会通过msg.callback.run()来处理消息。
这个时候实际本质上就是在切换到主线程去执行了Runnable对象的实例方法run()而已。所以当然能够更新UI。
而我们可能会有这样一种需求,那就是想要在指定的时间之后去执行一个耗时的线程任务,这个时候可能会想到Handler的postDelayed方法。
我想说的使用误区就是,这个时候的Handler一定不要是创建在主线程当中的,因为这样耗时操作最终还是在主线程执行,自然也就会引发ANR。
如果我们一定想要通过Handler实现我们这样的需求,其实很简单,当然就是要把Handler创建在分线程当中,就像下面这样:
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();;
final Handler handler = new Handler();
Looper.loop();
handler.post(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
handler.sendEmptyMessage(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}).start();