TWT Studio - Android 组培训 & 技术分享

Android消息机制

2019-08-28  本文已影响0人  yabgchen

Android的消息机制主要是指的Handler的运行机制以及Handler所附带的MessageQueueLooper的工作过程,这三者实际上是一个整体。

从开发的角度来看,Handler是消息机制的上层接口,通过它我们可以轻松的将一个任务切换到它所在的线程中去执行。以消息传递为例,在一个线程创建Handler,另外一个线程通过持有该Handler的引用调用sendMessage实现消息在线程之间的传递。MessageQueue的中文翻译是消息队列,内部采用单链表的数据结构来存储消息列表。Looper的中文翻译是循环,这里可以理解为消息循环。由于MessageQueue只是一个消息的存储单元,它不能去处理消息,而Looper就填补了这个功能,Looper会以无限循环的形式去查找是否有新消息,如果有的话就处理消息,否则就一直等待着。Looper中还有一个特殊的概念:ThreadLocal,当我们调用Looper.prepare()方法创建Looper时使用到它。在Handler内部就是通过ThreadLocal来获取每个线程的Looper的。

Handler的内部实现主要涉及到如下几个类: Thread、MessageQueue和Looper。这几类之间的关系可以用如下的图来简单说明:


Thread是最基础的,Looper和MessageQueue都构建在Thread之上,Handler又构建在Looper和MessageQueue之上,我们通过Handler间接地与下面这几个相对底层一点的类打交道。

下面我们来先分别介绍ThreadLocalMessageQueueLooper, 和Handler的工作机制,再将它们的工作流程进行一个大致的串讲。

1. ThreadLocal

1. 概述

ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。

上面的话可以理解为,ThreadLocal是一个全局变量,用来存储对应Thread的本地变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 举一个简单的例子:

public class MainActivity extends AppCompatActivity {
   private static final String TAG = "MainActivity";
   //定义ThreadLocal对象
   private ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<>();
​
​
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
​
       mBooleanThreadLocal.set(true);
       Log.d(TAG, "[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
​
       new Thread("Thread#1") {
           @Override
           public void run() {
               mBooleanThreadLocal.set(false);
               Log.d(TAG, "[Thread#1]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
          }
      }.start();
​
       new Thread("Thread#2") {
           @Override
           public void run() {
               Log.d(TAG, "[Thread#2]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
          }
      }.start();
  }
}

在上面的代码中,在主线程中设置mBooleanThreadLocal的值为true,在子线程1中设置mBooleanThreadLocal值为false,子线程2中不设置mBooleanThreadLocal的值。然后在3个线程中分别通过get方法获取值并打印出来。根据前面的描述,期望的打印结果应该是:主线程为true,子线程1为false,子线程2位null,因为子线程2中没有设置值。实际打印结果如下:


可以看到,尽管在三个线程中访问的为同一个对象,但是ThreadLocal为他们各自维护了一个该对象的副本,所以访问到的结果不同。

2. 实现原理

ThreadLocal是一个泛型类public class ThreadLocal<T>,要理解它的工作原理,可以从getset方法入手,先来看set方法:

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

可见,首先通过getMap方法来获取当前线程中的ThreadLocal数据,如果返回的ThreadLocalMap对象不为空,则调用set方法添加数据,否则就创建一个新对象并把值添加进去。ThreadLocalMap中定义了一个private Entry[] table;来存储数据,我们可以通过set方法将数据添加到table数组中。

再来看看get方法:

/**
     * 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);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

可以发现,ThreadLocalget方法同样是先取出当前线程的ThreadLocalMap 对象,如果这个对象为null就返回初始值,这个初始值由ThreadLocalinitialValue()方法来确定,默认情况下为null,默认实现如下所示:

protected T initialValue() {
    return null;
}

如果ThreadLocalMap 对象不为null,那么就取出table数组并找到相应的值返回回去。

ThreadLocalsetget方法可以看出,它们所操作的对象都是当前线程的ThreadLocalMap对象的table数组,因此在不同线程中访问同一个ThreadLocalsetget方法,它们对ThreadLocal所做的读/写操作仅限于各线程的内部,所以ThreadLocal可以在多个线程中互不干扰地存储和修改数据。

3. 与Android消息机制的联系

当我们在一个线程中创建一个Handler,调用Looper.prepare()时通过ThreadLocal保存当前线程下的Looper对象,而所有线程的Looper都由一个ThreadLocal来维护,也就是在所有线程中创建的Looper都存放在了同一个ThreadLocal中。而Handler又与Looper协同工作,大致关系如下图:


2. MessageQueue

最基础最底层的是Thread,每个线程内部都维护了一个消息队列——MessageQueue。消息队列MessageQueue,顾名思义,就是存放消息的队列(好像是废话…)。那队列中存储的消息是什么呢?假设我们在UI界面上单击了某个按钮,而此时程序又恰好收到了某个广播事件,那我们如何处理这两件事呢? 因为一个线程在某一时刻只能处理一件事情,不能同时处理多件事情,所以我们不能同时处理按钮的单击事件和广播事件,我们只能挨个对其进行处理,只要挨个处理就要有处理的先后顺序。 为此Android把UI界面上单击按钮的事件封装成了一个Message,将其放入到MessageQueue里面去,即将单击按钮事件的Message入栈到消息队列中,然后再将广播事件的封装成以Message,也将其入栈到消息队列中。也就是说一个Message对象表示的是线程需要处理的一件事情,消息队列就是一堆需要处理的Message的池。线程Thread会依次取出消息队列中的消息,依次对其进行处理。MessageQueue中有两个比较重要的方法,一个是enqueueMessage方法,一个是next方法。enqueueMessage方法用于将一个Message放入到消息队列MessageQueue中,next方法是从消息队列MessageQueue中阻塞式地取出一个Message。

enqueueMessage主要操作其实就是单链表的插入操作,这里就不再过多解释了。而next方法是一个无限循环的方法,如果消息队列中没有消息,那么next方法会一直阻塞在这里。当有新消息到来时,next方法会返回这条消息并将其从单链表中移除。

3. Looper

1. Looper的创建(使用ThreadLocal存储)

LooperAndroid的消息机制中扮演着消息循环的角色,具体来说就是它会不停地从MessageQueue中查看是否有新消息,如果有新消息就会立刻处理,否则就一直阻塞在哪里。首先来看一下它的构造方法,在构造方法中它会创建一个MessageQueue即消息队列,然后将当前线程的对象保存起来,如下所示:

@UnsupportedAppUsage
final MessageQueue mQueue;
final Thread mThread;

private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
}

创建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));
}

sThreadLocal即为存储各线程Looper的ThreadLocal对象,调用其get方法判断该线程是否已经持有了Looper,如果已经持有则抛出异常,这是为了保证对于每个线程Looper的唯一性。如果没有Looper,则为该线程创建一个Looper。

2. Looper的循环

Looper最重要的一个方法是loop方法,只有调用了loop后,消息循环系统才会真正地起作用,它的实现如下:

/**
     * 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();

        // Allow overriding a threshold with a system prop. e.g.
        // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow", 0);

        boolean slowDeliveryDetected = false;

        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
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            // Make sure the observer won't change while processing a transaction.
            final Observer observer = sObserver;

            final long traceTag = me.mTraceTag;
            long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
            if (thresholdOverride > 0) {
                slowDispatchThresholdMs = thresholdOverride;
                slowDeliveryThresholdMs = thresholdOverride;
            }
            final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
            final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

            final boolean needStartTime = logSlowDelivery || logSlowDispatch;
            final boolean needEndTime = logSlowDispatch;

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            final long dispatchEnd;
            Object token = null;
            if (observer != null) {
                token = observer.messageDispatchStarting();
            }
            long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
            try {
                msg.target.dispatchMessage(msg);
                if (observer != null) {
                    observer.messageDispatched(token, msg);
                }
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } catch (Exception exception) {
                if (observer != null) {
                    observer.dispatchingThrewException(token, msg, exception);
                }
                throw exception;
            } finally {
                ThreadLocalWorkSource.restore(origWorkSource);
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logSlowDelivery) {
                if (slowDeliveryDetected) {
                    if ((dispatchStart - msg.when) <= 10) {
                        Slog.w(TAG, "Drained");
                        slowDeliveryDetected = false;
                    }
                } else {
                    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                            msg)) {
                        // Once we write a slow delivery log, suppress until the queue drains.
                        slowDeliveryDetected = true;
                    }
                }
            }
            if (logSlowDispatch) {
                showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", 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.recycleUnchecked();
        }
    }

选取其中比较关键的部分做一下讲解:

  1. final MessageQueue queue = me.mQueue;

变量me是通过静态方法myLooper()获得的当前线程所绑定的Looper,me.mQueue是当前线程所关联的消息

队列。

  1. for (;;)

我们发现for循环没有设置循环终止的条件,所以这个for循环是个死循环。

  1. Message msg = queue.next(); // might block

我们通过消息队列MessageQueue的next方法从消息队列中取出一条消息,如果此时消息队列中有Message,

那么next方法会立即返回该Message,如果此时消息队列中没有Message,那么next方法就会阻塞式地等待获

取Message。

  1. msg.target.dispatchMessage(msg);

msg的target属性是Handler,该代码的意思是让Message所关联的Handler通过dispatchMessage方法让

Handler处理该Message,关于Handler的dispatchMessage方法将会在下面详细介绍。

loop方法是一个死循环,唯一跳出循环的方式是MessageQueuenext方法返回了null

Looper的quit方法被调用时,Looper就会调用MessageQueue的quit方法或quitSafely方法来通知消息队列退出,当消息队列被标记为退出状态时,它的next方法就会返回null。如果MessageQueuenext方法返回了新消息,Looper就会处理这条消息: msg.target.dispatchMessage(msg),这里的msg.target是发送这条消息的Handler对象,这样Handler发送的消息最终又交给它的dispatchMessage方法来处理了。但是这里不同的是,HandlerdispatchMessage方法是在创建Handler时所使用的Looper中执行的,这样就成功的将代码逻辑切换到指定的线程中去执行了。

4. Handler

1. 消息发送

Handler的工作主要包含消息的发送和接收过程。消息的发送最终是通过send的一系列方法来实现的。发送一条消息的典型过程如下所示:

public final boolean sendMessage(Message msg)
{
    return sendMessageDelayed(msg, 0);
}

public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

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);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

在enqueueMessage中有两件事需要注意:

  1. msg.target = this
    该代码将Message的target绑定为当前的Handler
  2. queue.enqueueMessage
    变量queue表示的是Handler所绑定的消息队列MessageQueue,通过调用queue.enqueueMessage(msg, uptimeMillis)我们将Message放入到消息队列中。

其他的一些调用最终也都归结到上面这个流程中:


2. 消息处理

通过上文可以发现,Handler发送消息的过程仅仅是向消息队列插入了一条消息,MessageQueuenext方法就会返回这条消息给LooperLooper收到消息后就开始处理了,最终消息由Looper交由Handler处理,即HandlerdispatchMessage方法会被调用,这时Handler就进入了处理消息的阶段。dispatchMessage的实现如下所示:

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

可以看到,如果我们设置了callback(Runnable对象)的话,则会直接调用handleCallback方法

private static void handleCallback(Message message) {
        message.callback.run();
}

即,如果我们在初始化Handler的时候设置了callback(Runnable)对象,则直接调用run方法。比如我们经常写的runOnUiThread方法:

runOnUiThread(new Runnable() {
            @Override
            public void run() {
                
            }
        });

public final void runOnUiThread(Runnable action) {
      if (Thread.currentThread() != mUiThread) {
          mHandler.post(action);
      } else {
          action.run();
      }
}

而如果msg.callback为空的话,会直接调用我们的mCallback.handleMessage(msg),即handler的handlerMessage方法。handlerMessage方法的执行也会在创建handler的线程中。

综上,我们可以看到Handler提供了三种途径处理Message,而且处理有前后优先级之分:首先尝试让postXXX中传递的Runnable执行,其次尝试让Handler构造函数中传入的Callback的handleMessage方法处理,最后才是让Handler自身的handleMessage方法处理Message。


3. post与send的比较

上述例子都是使用send方法,这里加一个post的例子帮助理解handler对于不同类型信息的处理

首先,调用post方法时依然调用了sendMessageDelayed,但是值得注意的是这里的参数有所不同,使用了getPostMessage(r)作为参数。

public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);//getPostMessage方法是两种发送消息的不同之处
    }

那么我们来看一看👀这个getPostMessage方法:

private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

可以看到,依然是把Runnable对象封装成了一个Message进行发送,不过这里设置了m.callback = r ,这也呼应了上文中提到的,如果我们在初始化Handler的时候设置了callback(Runnable)对象,则直接调用run方法。

4. Callback

/**
 * 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);
}

Handler.Callback是用来处理Message的一种手段,如果没有传递该参数,那么就应该重写Handler的handleMessage方法,也就是说为了使得Handler能够处理Message,我们有两种办法:

  1. 向Hanlder的构造函数传入一个Handler.Callback对象,并实现Handler.Callback的handleMessage方法
  2. 无需向Hanlder的构造函数传入Handler.Callback对象,但是需要重写Handler本身的handleMessage方法

也就是说无论哪种方式,我们都得通过某种方式实现handleMessage方法,这点与Java中对Thread的设计有异曲同工之处。

在Java中,如果我们想使用多线程,有两种办法:

  1. 向Thread的构造函数传入一个Runnable对象,并实现Runnable的run方法

  2. 无需向Thread的构造函数传入Runnable对象,但是要重写Thread本身的run方法

5. 总结

6. 补充:资源管理与内存泄漏

1. Handler的内存泄漏问题

Handler使用是用来进行线程间通信的,所以新开启的线程会持有Handler引用,如果在Activity等中创建Handler,并且是非静态内部类的形式,就有可能造成内存泄漏。

首先,非静态内部类是会隐式持有外部类的引用,所以当其他线程持有了该Handler,线程没有被销毁,则意味着Activity会一直被Handler持有引用而无法导致回收。

同时,MessageQueue中如果存在未处理完的Message,Message的target也是对Activity等的持有引用,也会

造成内存泄漏。

解决的办法:

2. 创建Message时的资源管理

使用Handler.obtainMessage()来获取Message对象的,和直接new一个Message有什么差别呢?

Message message = handler.obtainMessage();
Message message = new Message();

看一看obtain的代码:

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();
    }

在Message中有一个static Message变量sPool,这个变量是用于缓存Message对象的,在obtain中可以看到当需要一个Message对象时,如果sPool不为空则会返回当前sPool(Message),而将sPool指向了之前sPool的next对象,(之前讲MessageQueue时讲过Message的存储是以链式的形式存储的,通过Message的next指向下一个Message,这里就是返回了sPool当前这个Message,然后sPool重新指向了其下一个Message),然后将返回的Message的next指向置为空(断开链表),sPoolSize记录了当前缓存的Message的数量,如果sPool为空,则没有缓存的Message,则需要创建一个新的Message(new Message)。

那么,缓存中的sPool是哪里来的呢

public void recycle() {
        if (isInUse()) {
            if (gCheckRecycle) {
                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }
            return;
        }
        recycleUnchecked();
    }

void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        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++;
            }
        }
    }

recycle()是回收Message的方法,在Message处理完或者清空Message等时会调用。recycleUnchecked()方法中可以看到,将what、arg1、arg2、object等都重置了值,如果当前sPool(Message缓存池)的大小小于允许缓存的Message最大数量时,将要回收的Message的next指向sPool,将sPool指向了回收的Message对象(即将Message放到了sPool缓存池的头部)

7. 参考文章

这些是我在准备和学习过程中参考的一些博客,大家有兴趣可以自己再康康👀

上一篇下一篇

猜你喜欢

热点阅读