1. 程式人生 > >[轉]Android限制只能在主執行緒中進行UI訪問的實現原理

[轉]Android限制只能在主執行緒中進行UI訪問的實現原理

目錄

Android限制只能在主執行緒中進行UI訪問

我們知道,Android中規定了訪問UI只能在主執行緒中進行,如果在子執行緒中訪問UI的話,程式就會丟擲異常Only the original thread that created a view hierarchy can touch its views.

檢視原始碼後可以發現,這個驗證工作是由ViewRootImplcheckThread()方法來完成的

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

我們來看下這個方法,其中mThread是一個final

型別,賦值是在ViewRootImpl的構造方法中,指向mThread = Thread.currentThread();

final Thread mThread;

public ViewRootImpl(Context context, Display display) {
        ...
        mThread = Thread.currentThread();
        ...
}

mThread和當前呼叫的Thread.currentThread()不是一個Thread的話,即可判定當前不是UI執行緒中執行。

這裡再多說一點,系統為什麼不允許在子執行緒中訪問UI呢?這是因為Android的UI空間不是執行緒安全的,如果在多執行緒中併發訪問可能會導致UI空間處於不可預期的狀態。

Thread的實現

Android Thread 的構造方法

涉及到的 Android 原始碼路徑:

libcore/luni/src/main/java/java/lang/Runnable.java
libcore/luni/src/main/java/java/lang/Thread.java
libcore/luni/src/main/java/java/lang/ThreadGroup.java
libcore/luni/src/main/java/java/lang/VMThread.java
dalvik/vm/native/java_lang_VMThread.cpp
dalvik/vm/Thread.cpp

首先來分析 Android Thread,它實現了 Runnable 介面

// libcore/luni/src/main/java/java/lang/Thread.java
public class Thread implements Runnable {
    ...
}

Runnable 只有一個無參無返回值的 run() 介面:

// libcore/luni/src/main/java/java/lang/Runnable.java
/**
 * Represents a command that can be executed. Often used to run code in a
 * different {@link Thread}.
 */
public interface Runnable {

    /**
     * Starts executing the active part of the class' code. This method is
     * called when a thread is started that has been created with a class which
     * implements {@code Runnable}.
     */
    public void run();
}

Android Thread存在六種狀態,這些狀態定義在列舉 State 中,原始碼註釋寫的很清晰

// libcore/luni/src/main/java/java/lang/Thread.java
/**
 * A representation of a thread's state. A given thread may only be in one
 * state at a time.
 */
public enum State {
    /**
     * The thread has been created, but has never been started.
     */
    NEW,
    /**
     * The thread may be run.
     */
    RUNNABLE,
    /**
     * The thread is blocked and waiting for a lock.
     */
    BLOCKED,
    /**
     * The thread is waiting.
     */
    WAITING,
    /**
     * The thread is waiting for a specified amount of time.
     */
    TIMED_WAITING,
    /**
     * The thread has been terminated.
     */
    TERMINATED
}

Android Thread 類中一些關鍵成員變數如下:

// libcore/luni/src/main/java/java/lang/Thread.java
volatile VMThread vmThread;
volatile ThreadGroup group;
volatile String name;
volatile int priority;
volatile long stackSize;
Runnable target;
private static int count = 0;
private long id;
ThreadLocal.Values localValues;
  • vmThread:可視為對 dalvik thread 的簡單封裝,Thread 類通過 VMThread 裡面的 JNI 方法來呼叫 dalvik 中操作執行緒的方法,通過它的成員變數 threadvmata,我們可以將 Android Thread 和 dalvik Thread 的關聯起來;
  • group:每一個執行緒都屬於一個group,當執行緒被建立時就會加入一個特定的group,當執行緒執行結束,會從這個 group 中移除;
  • priority:執行緒優先順序;
  • stackSize:執行緒棧大小;
  • target:一個 Runnable 物件,Thread 的 run() 方法中會轉調該 targetrun() 方法,這是執行緒真正處理事務的地方;
  • id:執行緒 id,通過遞增 count 得到該id,如果沒有顯式給執行緒設定名字,那麼就會使用 Thread+id 當作執行緒的名字。注意這不是真正意義上的執行緒 id,即在 logcat 中列印的 tid 並不是這個 id,那 tid 是指 dalvik 執行緒的 id;
  • localValues:執行緒本地儲存(TLS)資料,而TLS的作用是能將資料和執行的特定的執行緒聯絡起來。

接下來,我們來看Android Thread 的建構函式,大部分建構函式都是通過轉調靜態函式 create 實現的

// libcore/luni/src/main/java/java/lang/Thread.java
public Thread() {
    create(null, null, null, 0);
}

下面來詳細分析 create 這個關鍵函式:

// libcore/luni/src/main/java/java/lang/Thread.java
private void create(ThreadGroup group, Runnable runnable, String threadName, long stackSize) {
    Thread currentThread = Thread.currentThread();if (group == null) {
        group = currentThread.getThreadGroup();
    }

    ...

    this.group = group;

    synchronized (Thread.class) {
        id = ++Thread.count;
    }

    if (threadName == null) {
        this.name = "Thread-" + id;
    } else {
        this.name = threadName;
    }

    this.target = runnable;
    this.stackSize = stackSize;

    this.priority = currentThread.getPriority();

    this.contextClassLoader = currentThread.contextClassLoader;

    // Transfer over InheritableThreadLocals.
    if (currentThread.inheritableValues != null) {
        inheritableValues = new ThreadLocal.Values(currentThread.inheritableValues);
    }

    // add ourselves to our ThreadGroup of choice
    this.group.addThread(this);}

首先看下[create]❶部分的程式碼,通過靜態函式 currentThread 獲取建立執行緒所在的當前執行緒,然後將當前執行緒的一些屬性傳遞給即將建立的新執行緒。這是通過 VMThread 轉調 dalvik 中的程式碼實現的。

// android/libcore/luni/src/main/java/java/lang/Thread.java
public static Thread currentThread() {
    return VMThread.currentThread();
}

VMThread 的 currentThread 是一個 native 方法,其 JNI 實現為

// dalvik/vm/native/java_lang_VMThread.cpp

static void Dalvik_java_lang_VMThread_currentThread(const u4* args,
    JValue* pResult)
{
    ...
    RETURN_PTR(dvmThreadSelf()->threadObj);
}

來看下 dvmThreadSelf() 方法,每一個 dalvik 執行緒都會將自身存放在key 為 pthreadKeySelf 的執行緒本地儲存中,獲取當前執行緒時,只需要根據這個 key 查詢獲取即可

// dalvik/vm/Thread.cpp 中:
Thread* dvmThreadSelf()
{
    return (Thread*) pthread_getspecific(gDvm.pthreadKeySelf);
}

dalvik Thread 有一個名為 threadObj 的成員變數:

// dalvik/vm/Thread.h
/* the java/lang/Thread that we are associated with */
Object*     threadObj;

在之後的分析中我們可以看到,dalvik Thread 這個成員變數 threadObj 關聯的就是對應的 Android Thread 物件,所以通過 native 方法 VMThread.currentThread() 返回的是儲存在 TLS 中的當前 dalvik 執行緒對應的 Android Thread。

接著分析上面[create]❷部分的程式碼,如果沒有給新執行緒指定 group ,那麼就會指定 group 為當前執行緒所在的 group 中,然後給新執行緒設定 namepriority 等。最後通過呼叫 ThreadGroupaddThread 方法將新執行緒新增到 group 中:

// libcore/libart/src/main/java/java/lang/ThreadGroup.java
/**
 * Called by the Thread constructor.
 */
final void addThread(Thread thread) throws IllegalThreadStateException {
    synchronized (threadRefs) {
        ...
        threadRefs.add(new WeakReference<Thread>(thread));
    }
}

ThreadGroup 的程式碼相對簡單,它有一個名為 threadRefs 的列表,持有屬於同一組的 thread 引用,可以對一組 thread 進行一些執行緒操作。

上面分析的是 Android Thread 的構造過程,從上面的分析可以看出,Android Thread 的構造方法僅僅是設定了一些執行緒屬性,並沒有真正去建立一個新的 dalvik Thread,dalvik Thread 建立過程要等到客戶程式碼呼叫 Android Thread 的 start() 方法才會進行。

Android Thread 的start()方法

下面我們來分析 Java Thread 的 start() 方法:

// libcore/luni/src/main/java/java/lang/Thread.java
public synchronized void start() {
    checkNotStarted();

    hasBeenStarted = true;

    VMThread.create(this, stackSize);
}

Android Thread 的 start 方法很簡單,僅僅是轉調 VMThread 的 native 方法

// dalvik/vm/native/java_lang_VMThread.cpp
static void Dalvik_java_lang_VMThread_create(const u4* args, JValue* pResult)
{
    Object* threadObj = (Object*) args[0];
    s8 stackSize = GET_ARG_LONG(args, 1);
 
    /* copying collector will pin threadObj for us since it was an argument */
    dvmCreateInterpThread(threadObj, (int) stackSize);
    RETURN_VOID();
}

dvmCreateInterpThread 的實現

// dalvik/vm/Thread.cpp
bool dvmCreateInterpThread(Object* threadObj, int reqStackSize)
{
    Thread* self = dvmThreadSelf();
    ...
    Thread* newThread = allocThread(stackSize); 
    newThread->threadObj = threadObj;
    ...
    Object* vmThreadObj = dvmAllocObject(gDvm.classJavaLangVMThread, ALLOC_DEFAULT);
    dvmSetFieldInt(vmThreadObj, gDvm.offJavaLangVMThread_vmData, (u4)newThread);
    dvmSetFieldObject(threadObj, gDvm.offJavaLangThread_vmThread, vmThreadObj);
    ...
    pthread_t threadHandle;
    int cc = pthread_create(&threadHandle, &threadAttr, interpThreadStart, newThread);
 
    /*
     * Tell the new thread to start.
     *
     * We must hold the thread list lock before messing with another thread.
     * In the general case we would also need to verify that newThread was
     * still in the thread list, but in our case the thread has not started
     * executing user code and therefore has not had a chance to exit.
     *
     * We move it to VMWAIT, and it then shifts itself to RUNNING, which
     * comes with a suspend-pending check.
     */
    dvmLockThreadList(self);
 
    assert(newThread->status == THREAD_STARTING);
    newThread->status = THREAD_VMWAIT;
    pthread_cond_broadcast(&gDvm.threadStartCond);
 
    dvmUnlockThreadList();
    ...
}
 
/*
 * Alloc and initialize a Thread struct.
 *
 * Does not create any objects, just stuff on the system (malloc) heap.
 */
static Thread* allocThread(int interpStackSize)
{
    Thread* thread;
    thread = (Thread*) calloc(1, sizeof(Thread));
    ...
    thread->status = THREAD_INITIALIZING;
}

首先,通過呼叫 allocThread 建立一個名為 newThread 的 dalvik Thread 並設定一些屬性,將設定其成員變數 threadObj 為傳入的 Android Thread,這樣 dalvik Thread 就與Android Thread 關聯起來了;
然後建立一個名為 vmThreadObjVMThread 物件,設定其成員變數 vmDatanewThread,設定 Android Thread threadObj 的成員變數 vmThread 為這個 vmThreadObj,這樣 Android Thread 通過 VMThread 的成員變數 vmData 就和 dalvik Thread 關聯起來了。
最後,通過 pthread_create 建立 pthread 執行緒,並讓這個執行緒 start,這樣就會進入該執行緒的 thread entry 執行,下來我們來看新執行緒的 thread entry 方法 interpThreadStart,同樣只列出關鍵的地方:

// dalvik/vm/Thread.cpp
/*
 * pthread entry function for threads started from interpreted code.
 */
static void* interpThreadStart(void* arg)
{
    Thread* self = (Thread*) arg;
 
    ...
 
    /*
     * Finish initializing the Thread struct.
     */
    dvmLockThreadList(self);
    prepareThread(self);
 
    ...
 
    /*
     * Change our state so the GC will wait for us from now on.  If a GC is
     * in progress this call will suspend us.
     */
    dvmChangeStatus(self, THREAD_RUNNING);
 
    /*
     * Execute the "run" method.
     *
     * At this point our stack is empty, so somebody who comes looking for
     * stack traces right now won't have much to look at.  This is normal.
     */
    Method* run = self->threadObj->clazz->vtable[gDvm.voffJavaLangThread_run];
    JValue unused;
 
    ALOGV("threadid=%d: calling run()", self->threadId);
    assert(strcmp(run->name, "run") == 0);
    dvmCallMethod(self, run, self->threadObj, &unused);
    ALOGV("threadid=%d: exiting", self->threadId);
 
    /*
     * Remove the thread from various lists, report its death, and free
     * its resources.
     */
    dvmDetachCurrentThread();
 
    return NULL;
}

/*
 * Finish initialization of a Thread struct.
 *
 * This must be called while executing in the new thread, but before the
 * thread is added to the thread list.
 *
 * NOTE: The threadListLock must be held by the caller (needed for
 * assignThreadId()).
 */
static bool prepareThread(Thread* thread)
{
    assignThreadId(thread);
    thread->handle = pthread_self();
    thread->systemTid = dvmGetSysThreadId();
 
    setThreadSelf(thread);
    ...
 
    return true;
}
 
/*
 * Explore our sense of self.  Stuffs the thread pointer into TLS.
 */
static void setThreadSelf(Thread* thread)
{
    int cc;
 
    cc = pthread_setspecific(gDvm.pthreadKeySelf, thread);
    ...
}

在新執行緒的 thread entry 方法 interpThreadStart 中,首先設定執行緒的名字,然後通過呼叫 prepareThread 設定執行緒 id 以及其它一些屬性,並呼叫 setThreadSelf 將新 dalvik Thread 自身儲存在 TLS 中,這樣之後就能通過 dvmThreadSelf 方法從 TLS 中獲取它。然後修改狀態為 THREAD_RUNNING,並呼叫對應 Android Thread 的 run 方法,執行客戶程式碼:

// libcore/luni/src/main/java/java/lang/Thread.java
public void run() {
    if (target != null) {
        target.run();
    }
}

target 在前面已經做了介紹,它是執行緒真正處理邏輯事務的地方。一旦邏輯事務處理完畢從 run 中返回,執行緒就會回到 interpThreadStart 方法中,繼續執行 dvmDetachCurrentThread 方法:

// dalvik/vm/Thread.cpp
/*
 * Detach the thread from the various data structures, notify other threads
 * that are waiting to "join" it, and free up all heap-allocated storage.
 * /
void dvmDetachCurrentThread()
{
    Thread* self = dvmThreadSelf();
    Object* vmThread;
    Object* group;
    ...
    group = dvmGetFieldObject(self->threadObj, gDvm.offJavaLangThread_group);
    /*
     * Remove the thread from the thread group.
     */
    if (group != NULL) {
        Method* removeThread =
            group->clazz->vtable[gDvm.voffJavaLangThreadGroup_removeThread];
        JValue unused;
        dvmCallMethod(self, removeThread, group, &unused, self->threadObj);
    }
 
    /*
     * Clear the vmThread reference in the Thread object.  Interpreted code
     * will now see that this Thread is not running.  As this may be the
     * only reference to the VMThread object that the VM knows about, we
     * have to create an internal reference to it first.
     */
    vmThread = dvmGetFieldObject(self->threadObj,
                    gDvm.offJavaLangThread_vmThread);
    dvmAddTrackedAlloc(vmThread, self);
    dvmSetFieldObject(self->threadObj, gDvm.offJavaLangThread_vmThread, NULL);
 
    /* clear out our struct Thread pointer, since it's going away */
    dvmSetFieldObject(vmThread, gDvm.offJavaLangVMThread_vmData, NULL);
 
    ...
 
    /*
     * Thread.join() is implemented as an Object.wait() on the VMThread
     * object.  Signal anyone who is waiting.
     */
    dvmLockObject(self, vmThread);
    dvmObjectNotifyAll(self, vmThread);
    dvmUnlockObject(self, vmThread);
 
    dvmReleaseTrackedAlloc(vmThread, self);
    vmThread = NULL;
 
    ...
 
    dvmLockThreadList(self);
 
    /*
     * Lose the JNI context.
     */
    dvmDestroyJNIEnv(self->jniEnv);
    self->jniEnv = NULL;
 
    self->status = THREAD_ZOMBIE;
 
    /*
     * Remove ourselves from the internal thread list.
     */
    unlinkThread(self);
 
    ...
 
    releaseThreadId(self);
    dvmUnlockThreadList();
 
    setThreadSelf(NULL);
 
    freeThread(self);
}
 
/*
 * Free a Thread struct, and all the stuff allocated within.
 */
static void freeThread(Thread* thread)
{
    ...
    free(thread);
}

dvmDetachCurrentThread 函式裡,首先獲取當前執行緒 self,這裡獲得的就是當前執行 thread entry 的新執行緒,然後通過其對應的 Android Thread 物件 threadObj 獲取該物件所在 group,然後將 threadObj 這個 Android Thread 物件從 group 中移除;
接著清除 Android 與 dalvik 執行緒之間的關聯關係,並通知 join 該執行緒的其它執行緒;
最後,設定執行緒狀態為 THREAD_ZOMBIE,清除 TLS 中儲存的執行緒值,並通過呼叫 freeThread 釋放記憶體,至此執行緒就終結了。

如何在我們自己的程式碼中去檢測當前Thread是不是UI執行緒呢?

最後來說下在app中檢測當前Thread是不是UI執行緒的方法:

if(Looper.myLooper() == Looper.getMainLooper()) {
   // Current Thread is Main Thread.
}

或者

if(Looper.getMainLooper().getThread() == Thread.currentThread()) {
   // Current Thread is Main Thread.
}

參考文章:
https://blog.csdn.net/kesalin/article/details/37659547
https://stackoverflow.com/questions/11411022/how-to-check-if-current-thread-is-not-main-thread