深入理解Android消息机制

简介: 深入理解Android消息机制

前言

  Android的消息机制主要说的是Handler的运行机制,相信大家对Handler已经非常熟悉了,Handler可以轻松的将一个任务切换到Handler所在的线程中去执行。最熟悉的就是我们只能在UI线程中更新UI,所以我们经常来用Handler来更新UI,但Handler并不是专门用来更新UI的。本文源码基于Android8.0。

一、为什么只能在主线程中访问UI

    可能我们每个人都知道在Android中只能在主线程中访问UI,但是为什么是这样呢,难道谷歌当时设计的时候不会考虑这个问题?首先Android的UI控件不是线程安全的,如果我们在多个线程中可以同时操作UI,那么UI控件会处于不可预期的状态,如果在设计的时候对访问UI加上锁机制,一方面会让访问UI的逻辑变得复杂,得不偿失,另一方便锁机制会使得访问UI效率降低,所以综合考虑上述方面使用Handler是最为简单的方式了。

    UI操作的验证由ViewRootImpl来完成的,源码如下:  

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

image.gif

  所以我们经常可以看到如果在子线程中处理UI报错Only the original thread that created a view hierarchy can touch its views.的异常。

二、Android的消息机制

      Android的消息机制主要说的是Handler的运行机制,Handler的运行需要MessageQueue和Looper,MessageQueue就是消息队列,它是采用单链表的数据结构来存储消息列表。而Looper会不断的查看MessageQueue中是否有消息,当有消息的时候就取出来。Hanlder在创建的时候会采用当前线程的Looper来构造消息循环系统,但是默认的线程是没有Looper的,比如我们在子线程创建一个Hanlder(这里只是便于演示所以不采用线程池了,建议标准开发中使用线程池)

/**
 * 子线程使用Hanlder测试方法
 */
private void ThreadHandlerTest() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            Handler handler = new Handler();
        }
    }).start();
}

image.gif

运行结果报错如下:

 java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-2,5,main] that has not called Looper.prepare()

                                                                                 at android.os.Handler.<init>(Handler.java:206)

                                                                                 at android.os.Handler.<init>(Handler.java:119)

                                                                                 at lonbon.com.hanlderlooperdemo.MainActivity$1.run(MainActivity.java:24)

                                                                                 at java.lang.Thread.run(Thread.java:764)

这只因为默认的线程中没有Looper,所以我们要为当前线程创建Looper对象:

Looper.prepare();
Handler handler = new Handler();
Looper.loop();

image.gif

我们通过Looper.prepare()方法创建Looper对象,.loop()方法开启消息循环。关于looper详细的下面讲解。

那么我们可能有疑问了,我们在主线程中使用Handler的时候没有创建looper对象也可以正常使用,那是因为默认的UI线程创建的时候默认创建了looper对象。

创建完Handler之后,通过handler的send或者post方法发送消息,这个消息会被存储到消息队列,Looper发现消息队列中有新的消息便会处理这个消息,然后handlermessage方法或者Runable方法会被调用,大致过程如图所示。

image.gif

三、ThreadLocal

    ThreadLocal是Looper中的特殊概念,用来在当前线程中存储数据,我们获取当前线程的Looper也是通过ThreadLocal操作的,当然,日常开发中我们能使用的ThreadLocal的地方并不多。比如我们在两个不同线程中进行如下操作:

     首先我们声明一个String类型的ThreadLocal变量,创建两个线程分别使用set方法赋值,然后打印。

private ThreadLocal<String> threadLocal = new ThreadLocal<>();

image.gif

/**
 * 测试线程1
 */
private void ThreadTest1() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            threadLocal.set("BigYellow");
            Log.d(TAG,threadLocal.get());
        }
    }).start();
}

image.gif

/**
 * 测试线程2
 */
private void ThreadTest2() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            threadLocal.set("大黄");
            Log.d(TAG,threadLocal.get());
        }
    }).start();
}

image.gif

运行打印,日志如下:

02-12 10:21:37.961 11719-12135/? D/TAG: BigYellow

02-12 10:21:37.966 11719-12136/? D/TAG: 大黄

我们可以看到取出的分别是各自线程对应的值,如果我们在主线程中呢?显然是null因为我们没有在主线程中存值。

接下来我们从源码的角度来分析ThreadLocal的存取值过程,首先我们看set方法。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

image.gif

首先通过getMap方法获取当前线程的ThreadLocalMap,如果map不为空就通过map的set方法将值存储,如果为空则创建map,

我们来看下ThreadLocalMap,ThreadLocalMap是一个存储当前线程数据的Map集合,set方法源码如下所示:

private void set(ThreadLocal<?> key, Object value) {
    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) {
            e.value = value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

image.gif

首先定义了一个Entry类型的数组,我们主要来看for循环中的操作,for循环主要做的就是为插入值得位置找到合适的位置,通过不断到table数组中去寻找,直到存放的entry为null

if (k == key) {
    e.value = value;
    return;
}

image.gif

如果key的值相同说明该线程曾经设置过Threadlocal,直接赋值即可。

if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
}

image.gif

Entry继承的是WeakReference,这是弱引用带来的坑

image.gif

所以要判断是否为null,如果为null就进行置换操作,即

replaceStaleEntry(key, value, i);

image.gif

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;
    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

image.gif

当table数组中存储的ThreadLocal对应的值还在但是key不存在了,就认为Entry过期了,

int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
     (e = tab[i]) != null;
     i = prevIndex(i, len))
    if (e.get() == null)
        slotToExpunge = i;

image.gif

上述代码检查脏数据,清理整个table,否则会因为GC问题导致很严重的后果。

if (k == key) {
    e.value = value;
    tab[i] = tab[staleSlot];
    tab[staleSlot] = e;
    // Start expunge at preceding stale entry if it exists
    if (slotToExpunge == staleSlot)
        slotToExpunge = i;
    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    return;
}

image.gif

如果找到key了我们需要进行替换,将过期数据进行删除刷新。源代码中注释的很清楚了,这里就不一一解释了。

个人感觉和之前早期版本(6.0之前)的set方法变化很大。

我们接下来来看ThreadLocal的get方法,首先同样的获取当前线程的ThreadLocalMap,获取map的entry对象,如果不为空的话就从中取值即可。如果map为空就回到setInitialValue初始化方法.

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

image.gif

四、Looper

  Looper我们上面说了是用来构建消息循环系统,我们通过ThreadLocal来获取当前线程的Looper对象.我们上面也说到了如何在子线程中创建looper,通过Looper的prepare方法为当前线程创建一个looper,通过loop方法开启消息循环。在主线程中创建Looper是通过

Looper.prepareMainLooper();

image.gif

方法,因为UI线程的Looper比较特殊是默认创建好的,所以我们可以通过下列代码来获取主线程的looper

Looper.getMainLooper();

image.gif

我们可以开启looper肯定也可以关闭looper,关闭looper有这个方法,一个是

getMainLooper().quit();

image.gif

public void quit() {
    mQueue.quit(false);
}

image.gif

quit方法会直接退出looper,另一种方法是

getMainLooper().quitSafely();

image.gif

和quit方法不同的是quitSafely方法调用后在消息队列中的消息处理完成之后在退出,就像方法名一样是安全退出。所以如果我们在子线程中手动创建了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();
    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);
        }
        final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
        final long traceTag = me.mTraceTag;
        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        final long end;
        try {
            msg.target.dispatchMessage(msg);
            end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        if (slowDispatchThresholdMs > 0) {
            final long time = end - start;
            if (time > slowDispatchThresholdMs) {
                Slog.w(TAG, "Dispatch took " + time + "ms on "
                        + Thread.currentThread().getName() + ", h=" +
                        msg.target + " cb=" + msg.callback + " msg=" + msg.what);
            }
        }
        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();
    }
}

image.gif

从中我们可以看到looper会不断的调用

queue.next()

image.gif

方法从消息队列中取出消息,如果为空就一直等待,如果有消息就调用

msg.target.dispatchMessage(msg);

image.gif

方法处理msg.target就是发送这条消息的Handler对象,而handler的dispatchMessage方法又是在创建Handler所在的Looper执行的,所以这样就将消息交给指定的线程去处理了。

五、消息队列MessageQueue与Handler

 消息队列MessageQueue主要有插入和读取两个操作,读取成功后也就相当于删除。

 插入方法对应的enqueueMessage源码如下:

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

image.gif

我们上面也说了消息队列其实是一个单链表,所以就相当于单链表的插入操作。读取方法是next方法,读取后将消息移除,这里就不作过多解释了。

而我们日常开发中最常用的就是创建Hanlder的匿名内部类方式(这种方式记得处理内存泄漏),然后通过hanlder.send方法发送消息,而send方法最终又会调用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);
}

image.gif

我们看最终返回值就可以看到其实调用handler的send方法就是调用enqueueMessage方法往消息队列中插入了一条消息,然后不断循环的looper进去取出又交给handler处理,这样就构成了Android的消息机制。

上文源码基于Android8.0,如有纰漏欢迎指出探讨。

目录
相关文章
|
6月前
|
前端开发 编译器 Android开发
构建高效Android应用:探究Kotlin协程的异步处理机制
【4月更文挑战第2天】在现代移动应用开发中,提供流畅且响应迅速的用户体验是至关重要的。随着Android平台的发展,Kotlin语言凭借其简洁性和功能性编程的特点成为了主流选择之一。特别地,Kotlin协程作为一种新型的轻量级线程管理机制,为开发者提供了强大的异步处理能力,从而显著提升了应用程序的性能和响应速度。本文将深入探讨Kotlin协程在Android中的应用,分析其原理、实现以及如何通过协程优化应用性能。
|
6月前
|
存储 Java Android开发
Android系统升级的机制概要
Android系统升级的机制概要
119 0
|
4天前
|
存储 安全 Android开发
探索Android与iOS的隐私保护机制
在数字化时代,移动设备已成为我们生活的一部分,而隐私安全是用户最为关注的问题之一。本文将深入探讨Android和iOS两大主流操作系统在隐私保护方面的策略和实现方式,分析它们各自的优势和不足,以及如何更好地保护用户的隐私。
|
1月前
|
消息中间件 存储 Java
Android消息处理机制(Handler+Looper+Message+MessageQueue)
Android消息处理机制(Handler+Looper+Message+MessageQueue)
38 2
|
23天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
|
27天前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
47 1
|
28天前
|
存储 安全 数据安全/隐私保护
探索安卓与iOS的隐私保护机制####
【10月更文挑战第15天】 本文深入剖析了安卓和iOS两大操作系统在隐私保护方面的策略与技术实现,旨在揭示两者如何通过不同的技术手段来保障用户数据的安全与隐私。文章将逐一探讨各自的隐私控制功能、加密措施以及用户权限管理,为读者提供一个全面而深入的理解。 ####
50 1
|
30天前
|
消息中间件 存储 Java
Android消息处理机制(Handler+Looper+Message+MessageQueue)
Android消息处理机制(Handler+Looper+Message+MessageQueue)
44 2
|
2月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
67 8
|
3月前
|
消息中间件 存储 Java
Android面试高频知识点(2) 详解Android消息处理机制(Handler)
Android 消息处理机制估计都被写烂了,但是依然还是要写一下,因为Android应用程序是通过消息来驱动的,Android某种意义上也可以说成是一个以消息驱动的系统,UI、事件、生命周期都和消息处理机制息息相关,并且消息处理机制在整个Android知识体系中也是尤其重要,在太多的源码分析的文章讲得比较繁琐,很多人对整个消息处理机制依然是懵懵懂懂,这篇文章通过一些问答的模式结合Android主线程(UI线程)的工作原理来讲解,源码注释很全,还有结合流程图,如果你对Android 消息处理机制还不是很理解,我相信只要你静下心来耐心的看,肯定会有不少的收获的。
202 3
Android面试高频知识点(2) 详解Android消息处理机制(Handler)