最近接触到一个比较有挑战性的项目,我发现里面使用大量的消息机制,现在这篇博客我想具体分析一下:android里面的消息到底是什么东西,消息机制到底有什么好处呢?
其实说到android消息机制,我们可能很快就会想到message/handle/looper这三个对象,没错在android里面就是通过此三个对象实现消息机制的。那么我想问的是,为什么我们需要消息机制呢?说到底就是为了更好、更流畅的用户体验,因为在app里面我们或多或少都有可能会设计到一些比较耗时的操作,这个时候如果我们只是一味的将当前所有的操作都停留在一个线程上面的话,那么该线程将特别的卡,导致用户界面会出现长时间的卡顿、没有响应的情况。那么,我们能不能聪明一点将一些耗时的操作放置在工作线程上面呢?当前是可以的,这个时候我们的主线程-UI线程,就可以只负责处理UI更新、展示的工作了,当工作线程完成之后,我们就可以通过某种机制通知UI进行更新了,说到这里你也许就能明白消息机制的产生初衷了。就是为了多个线程之间的互通互信,最常用的场景也许就是HTTP网络请求了。
好了,让我们先通过如下代码实例:
btnMsgTest.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { WorkThread workThread = new WorkThread(); workThread.start(); } });
上面这段代码我只是订阅了一个按钮点击事件,在该按钮点击事件里面我们开启了一个新的工作线程,让我们来看看工作线程的实现代码:
class WorkThread extends Thread { @Override public void run() { btnMsgTest.setText("你好"); } }
其实很简单,我们只是在工作线程里面修改一下按钮的text属性值,但是当前点击按钮的时候,却发现程序报错了:
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6556) at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:942) at android.view.ViewGroup.invalidateChild(ViewGroup.java:5081) at android.view.View.invalidateInternal(View.java:12713) at android.view.View.invalidate(View.java:12677) at android.view.View.invalidate(View.java:12661) at android.widget.TextView.checkForRelayout(TextView.java:7159) at android.widget.TextView.setText(TextView.java:4342) at android.widget.TextView.setText(TextView.java:4199) at android.widget.TextView.setText(TextView.java:4174) at example.xiaocai.com.testgumpcome.MainActivity$WorkThread.run(MainActivity.java:157)03-05 04:15:00.741 1600-1613/example.xiaocai.com.testgumpcome E/Surface﹕ getSlotFromBufferLocked: unknown buffer: 0xee9f20e0
这是为什么呢?让我们来分析一下上面的exception提示,意思是说:UI更新只能在它自己原始的线程上面。那么问题就来了,我们怎么将工作线程里面的更新通知到UI上面呢?这个时候消息机制就可以大展拳脚了。
我们先来看一下message对象,这个对象就像理解的字面意思一样就是真正的消息对象,我们可以通过如下代码定义一个消息:
Message message=handler.obtainMessage();message.what=1;message.obj="hello";handler.sendMessage(message);
注意我们这里是从利用obtainMessage方法,创建一个message对象,而不是利用new Message创建新的message对象。说点题外话,这两者有什么区别吗?通过源码分析我们会发现obtainMessage是从message池里面分配一个对象,这样就避免创建新的对象了。
细心的你,也许从上面的代码里面还能看到handler对象,下面我们就来说一说handler对象吧!我们创建好message之后,怎样发送出去呢?然后又是怎样调度处理的呢?我们可以直接在代码里面创建一个handler对象,如下代码:
Handler handler2=new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); } };
然后我们就可以通过该handler2对象调用sendMessage方法就可以将消息发送出去了。通过这个流程分析,我们大概就能猜出handler的作用了,它就消息的具体操作对象,通过handler我们可以发送各种类型的message,可以处理消息等。
说到这里,我们还有个疑问就是如果如果我想要发送多个message对象,那么不同的message对象又是怎样调度执行的呢?请看下面的代码:
Message message=handler.obtainMessage();message.what=1;message.obj="hello";handler.sendMessage(message);Message message1=handler.obtainMessage();message1.what=2;message1.obj="world";handler.sendMessage(message1);
这种情况下,是不是就意味着后台会有个MessageQuene呢?要不然,多个消息是放在哪里呢?然后又是通过哪种方式进行message对象调度呢?没错,多个message会通过looper对象进行调度执行。
说到这里,让我们利用消息机制重新来修改最上面的实例代码:
class WorkThread extends Thread { @Override public void run() {// btnMsgTest.setText("你好"); Message message=handler.obtainMessage(); message.what=1; message.obj="修改按钮文本"; handler.sendMessage(message); } }
这个时候我们在工作线程里面就不是直接更新UI了,而是发送一个message,然后在handler里面就行UI更新,让我们来看看这块的代码:
private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 1: btnMsgTest.setText("你好"); break; } } };
看到这里也许聪明的你就会感到奇怪了,我怎么没有看到looper的身影呢?这样new出来的Handler就能确定一定是在UI线程上面吗?亲,先别着急,让我们来看看与上面同等的一段代码,也许你就明白了。
private Handler handler2=new Handler(Looper.getMainLooper(),new Handler.Callback() { @Override public boolean handleMessage(Message msg) { btnMsgTest.setText("通过looper设置"); return false; } });
在这段代码里面我们就能看到Looper的身影了,同时我们调用了getMainLooper方法获取到了主线程也就是我们需要的UI线程。看到这里我们就能够通过下面这幅图来形象的说明三者之间的关系了。
通过上面的分析,我们能够了解android消息机制到底是怎么回事,同时还能够比较熟练的使用它们。下面让我们再深入一步,研究一下android消息机制的源码,更加直观的了解到message是怎样发送、调度、处理的。首先来看看message源码:
public final class Message implements Parcelable
我们可以看到message实现了一个Parcelable接口,该接口的主要作用就是包装、传输数据。没错,message里面我们当然需要传输数据了。然后让我们来看看message里面包括的常用属性信息代码:
/** * User-defined message code so that the recipient can identify * what this message is about. Each {@link Handler} has its own name-space * for message codes, so you do not need to worry about yours conflicting * with other handlers. */ public int what; /** * arg1 and arg2 are lower-cost alternatives to using * {@link #setData(Bundle) setData()} if you only need to store a * few integer values. */ public int arg1; /** * arg1 and arg2 are lower-cost alternatives to using * {@link #setData(Bundle) setData()} if you only need to store a * few integer values. */ public int arg2; /** * An arbitrary object to send to the recipient. When using * {@link Messenger} to send the message across processes this can only * be non-null if it contains a Parcelable of a framework class (not one * implemented by the application). For other data transfer use * {@link #setData}. * *Note that Parcelable objects here are not supported prior to * the {@link android.os.Build.VERSION_CODES#FROYO} release. */ public Object obj;
以上四个属性,在我们设置一个message信息的时候,可能会经常使用到,具体的时候场景大家可以在注释里面看出。最后让我们来看看主要的方法:
/** * 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; sPoolSize--; return m; } } return new Message(); }
通过上面的代码我们可以看出,如果消息池不为空的话,那么我们就同步的从消息池里面分配消息,否则的话就实例化一个message对象。这样的话,我们一般创建一个message对象的时候,就建议使用obtain方法,这样就可以很好的提高应用性能。message里面还有个回收方法,代码如下:
/** * Return a Message instance to the global pool. You MUST NOT touch * the Message after calling this function -- it has effectively been * freed. */ public void recycle() { clearForRecycle(); synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } }
/*package*/ void clearForRecycle() { flags = 0; what = 0; arg1 = 0; arg2 = 0; obj = null; replyTo = null; when = 0; target = null; callback = null; data = null; }
我们可以看到其实在recycle方法里面实现是很简单的,只是将一些变量置为默认值,同时将消息池变大。剩下其他的方法就是实现Parcelable接口,实现数据的包装。
接下来让我们看看Handler里面最主要的方法,首当其冲的就是消息分发方法,源码如下:
/** * Handle system messages here. */ public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } }
大意就是说:如果我们在message里面已经写了回调方法那么就直接调用message里面的方法进行处理。如果message里面没有实现回调处理,那么Handler里面的接口回调,代码如下:
/** * Callback interface you can use when instantiating a Handler to avoid * having to implement your own subclass of Handler. * * @param msg A {@link android.os.Message Message} object * @return True if no further handling is desired */ public interface Callback { public boolean handleMessage(Message msg); }
这也就是为什么我们能够直接在new一个Handler的时候,复写里面的handlerMessage方法进行消息处理的原因。其次就是刚刚我们看到的构造方法,源码如下所示:
/** * Use the provided {@link Looper} instead of the default one and take a callback * interface in which to handle messages. * * @param looper The looper, must not be null. * @param callback The callback interface in which to handle messages, or null. */ public Handler(Looper looper, Callback callback) { this(looper, callback, false); }
通过一个Looper对象获取线程环境,然后实现Callback回调。最后比较重要的方法就是一系列发送消息的方法,源码如下:
/** * Pushes a message onto the end of the message queue after all pending messages * before the current time. It will be received in {@link #handleMessage}, * in the thread attached to this handler. * * @return Returns true if the message was successfully placed in to the * message queue. Returns false on failure, usually because the * looper processing the message queue is exiting. */ public final boolean sendMessage(Message msg) { return sendMessageDelayed(msg, 0); } /** * Sends a Message containing only the what value. * * @return Returns true if the message was successfully placed in to the * message queue. Returns false on failure, usually because the * looper processing the message queue is exiting. */ public final boolean sendEmptyMessage(int what) { return sendEmptyMessageDelayed(what, 0); } /** * Sends a Message containing only the what value, to be delivered * after the specified amount of time elapses. * @see #sendMessageDelayed(android.os.Message, long) * * @return Returns true if the message was successfully placed in to the * message queue. Returns false on failure, usually because the * looper processing the message queue is exiting. */ public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { Message msg = Message.obtain(); msg.what = what; return sendMessageDelayed(msg, delayMillis); } /** * Sends a Message containing only the what value, to be delivered * at a specific time. * @see #sendMessageAtTime(android.os.Message, long) * * @return Returns true if the message was successfully placed in to the * message queue. Returns false on failure, usually because the * looper processing the message queue is exiting. */ public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) { Message msg = Message.obtain(); msg.what = what; return sendMessageAtTime(msg, uptimeMillis); } /** * Enqueue a message into the message queue after all pending messages * before (current time + delayMillis). You will receive it in * {@link #handleMessage}, in the thread attached to this handler. * * @return Returns true if the message was successfully placed in to the * message queue. Returns false on failure, usually because the * looper processing the message queue is exiting. Note that a * result of true does not mean the message will be processed -- if * the looper is quit before the delivery time of the message * occurs then the message will be dropped. */ public final boolean sendMessageDelayed(Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); } /** * Enqueue a message into the message queue after all pending messages * before the absolute time (in milliseconds) uptimeMillis. * The time-base is {@link android.os.SystemClock#uptimeMillis}. * You will receive it in {@link #handleMessage}, in the thread attached * to this handler. * * @param uptimeMillis The absolute time at which the message should be * delivered, using the * {@link android.os.SystemClock#uptimeMillis} time-base. * * @return Returns true if the message was successfully placed in to the * message queue. Returns false on failure, usually because the * looper processing the message queue is exiting. Note that a * result of true does not mean the message will be processed -- if * the looper is quit before the delivery time of the message * occurs then the message will be dropped. */ 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); } /** * Enqueue a message at the front of the message queue, to be processed on * the next iteration of the message loop. You will receive it in * {@link #handleMessage}, in the thread attached to this handler. * This method is only for use in very special circumstances -- it * can easily starve the message queue, cause ordering problems, or have * other unexpected side-effects. * * @return Returns true if the message was successfully placed in to the * message queue. Returns false on failure, usually because the * looper processing the message queue is exiting. */ public final boolean sendMessageAtFrontOfQueue(Message msg) { 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, 0); }
上面方法的不同之处就在于发送的时间、发送的一些属性内容不同罢了。
好了,让我们来看看Looper里面的源码,首先最主要的方法有prepare,从名字你也许就能够看出,该方法最主要就是准备好messagequeue,然后调用loop方法从该messagequeue里面调度message对象,源码如下:
/** * Run the message queue in this thread. Be sure to call * {@link #quit()} to end the 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; // 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 Printer logging = me.mLogging; if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } msg.target.dispatchMessage(msg); 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.recycle(); } }
大意如下:循环遍历message对象,同时分发message对象。然后还有两个比较重要的方法就是getMainLooper和getMyLooper,源码如下:
/** * Initialize the current thread as a looper, marking it as an * application's main looper. The main looper for your application * is created by the Android environment, so you should never need * to call this function yourself. See also: {@link #prepare()} */ public static void prepareMainLooper() { prepare(false); synchronized (Looper.class) { if (sMainLooper != null) { throw new IllegalStateException("The main Looper has already been prepared."); } sMainLooper = myLooper(); } } /** Returns the application's main looper, which lives in the main thread of the application. */ public static Looper getMainLooper() { synchronized (Looper.class) { return sMainLooper; } }/** * Return the Looper object associated with the current thread. Returns * null if the calling thread is not associated with a Looper. */ public static Looper myLooper() { return sThreadLocal.get(); }
好了,今天博客就先到这里吧,如果有什么不对的地方欢迎拍砖。