Android消息机制

作者: yabgchen | 来源:发表于2019-08-28 10:42 被阅读0次

    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. 总结

    • 在使用handler的时候,在handler所创建的线程需要维护一个唯一的Looper对象, 每个线程对应一个Looper,每个线程的Looper通过ThreadLocal来保证

    • Looper对象的内部又维护有唯一的一个MessageQueue,所以一个线程可以有多个handler,
      但是只能有一个Looper和一个MessageQueue。

    • Message在MessageQueue不是通过一个列表来存储的,而是将传入的Message存入到了上一个
      Message的next中,在取出的时候通过顶部的Message就能按放入的顺序依次取出Message。

    • Looper对象通过loop()方法开启了一个死循环,不断地从looper内的MessageQueue中取出Message,
      然后通过handler将消息分发传回handler所在的线程。

    • handler收到消息以后,通过handleMessage进行消息处理


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

    1. Handler的内存泄漏问题

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

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

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

    造成内存泄漏。

    解决的办法:

    • 使用静态内部类+弱引用的方式:

      静态内部类不会持有外部类的的引用,当需要引用外部类相关操作时,可以通过弱引用还获取到外部类相关操作,弱引用不会造成对象该回收回收不掉的问题。

      private Handler sHandler = new TestHandler(this);
      
      static class TestHandler extends Handler {
          private WeakReference<Activity> mActivity;
          TestHandler(Activity activity) {
              mActivity = new WeakReference<>(activity);
          }
        
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                Activity activity = mActivity.get();
                if (activity != null) {
                    //TODO:
                }   
            }
      }
      
    • 在外部类对象被销毁时,将MessageQueue中的消息清空。例如,在Activity的onDestroy时将消息清空。

      @Override
      protected void onDestroy() {
          handler.removeCallbacksAndMessages(null);
          super.onDestroy();
      }
      

    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. 参考文章

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

    相关文章

      网友评论

        本文标题:Android消息机制

        本文链接:https://www.haomeiwen.com/subject/rwbuectx.html