Android中子線程真的不能更新UI嗎?

来源:http://www.cnblogs.com/xuyinhuan/archive/2016/10/04/5930287.html
-Advertisement-
Play Games

Android的UI訪問是沒有加鎖的,這樣在多個線程訪問UI是不安全的。所以Android中規定只能在UI線程中訪問UI。 但是有沒有極端的情況?使得我們在子線程中訪問UI也可以使程式跑起來呢?接下來我們用一個例子去證實一下。 新建一個工程,activity_main.xml佈局如下所示: 很簡單, ...


Android的UI訪問是沒有加鎖的,這樣在多個線程訪問UI是不安全的。所以Android中規定只能在UI線程中訪問UI。

但是有沒有極端的情況?使得我們在子線程中訪問UI也可以使程式跑起來呢?接下來我們用一個例子去證實一下。

新建一個工程,activity_main.xml佈局如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <TextView
        android:id="@+id/main_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:layout_centerInParent="true"
        />

</RelativeLayout>

很簡單,只是添加了一個居中的TextView

MainActivity代碼如下所示:

public class MainActivity extends AppCompatActivity {

    private TextView main_tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        main_tv = (TextView) findViewById(R.id.main_tv);

        new Thread(new Runnable() {

            @Override
            public void run() {
                main_tv.setText("子線程中訪問");
            }
        }).start();

    }

}

也是很簡單的幾行,在onCreate方法中創建了一個子線程,併進行UI訪問操作。

點擊運行。你會發現即使在子線程中訪問UI,程式一樣能跑起來。結果如下所示: 

咦,那為嘛以前在子線程中更新UI會報錯呢?難道真的可以在子線程中訪問UI?

先不急,這是一個極端的情況,修改MainActivity如下:

public class MainActivity extends AppCompatActivity {

    private TextView main_tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        main_tv = (TextView) findViewById(R.id.main_tv);

        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                main_tv.setText("子線程中訪問");
            }
        }).start();

    }

}

讓子線程睡眠200毫秒,醒來後再進行UI訪問。

結果你會發現,程式崩了。這才是正常的現象嘛。拋出瞭如下很熟悉的異常:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. 
at android.view.ViewRootImpl.checkThread(ViewRootImpl.Java:6581) 
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

……

作為一名開發者,我們應該認真閱讀一下這些異常信息,是可以根據這些異常信息來找到為什麼一開始的那種情況可以訪問UI的。那我們分析一下異常信息:

首先,從以下異常信息可以知道

at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6581)

這個異常是從android.view.ViewRootImpl的checkThread方法拋出的。

這裡順便鋪墊一個知識點:ViewRootImpl是ViewRoot的實現類。

那現在跟進ViewRootImpl的checkThread方法瞧瞧,源碼如下:

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

只有那麼幾行代碼而已的,而mThread是主線程,在應用程式啟動的時候,就已經被初始化了。

由此我們可以得出結論: 
在訪問UI的時候,ViewRoot會去檢查當前是哪個線程訪問的UI,如果不是主線程,那就會拋出如下異常:

Only the original thread that created a view hierarchy can touch its views

這好像並不能解釋什麼?繼續看到異常信息

at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

 

那現在就看看requestLayout方法,

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

這裡也是調用了checkThread()方法來檢查當前線程,咦?除了檢查線程好像沒有什麼信息。那再點進scheduleTraversals()方法看看

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

註意到postCallback方法的的第二個參數傳入了很像是一個後臺任務。那再點進去

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

找到了,那麼繼續跟進doTraversal()方法。

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

可以看到裡面調用了一個performTraversals()方法,View的繪製過程就是從這個performTraversals方法開始的。PerformTraversals方法的代碼有點長就不貼出來了,如果繼續跟進去就是學習View的繪製了。而我們現在知道了,每一次訪問了UI,Android都會重新繪製View。這個是很好理解的。

分析到了這裡,其實異常信息對我們幫助也不大了,它只告訴了我們子線程中訪問UI在哪裡拋出異常。 
而我們會思考:當訪問UI時,ViewRoot會調用checkThread方法去檢查當前訪問UI的線程是哪個,如果不是UI線程則會拋出異常,這是沒問題的。但是為什麼一開始在MainActivity的onCreate方法中創建一個子線程訪問UI,程式還是正常能跑起來呢?? 
唯一的解釋就是執行onCreate方法的那個時候ViewRootImpl還沒創建,無法去檢查當前線程。

那麼就可以這樣深入進去。尋找ViewRootImpl是在哪裡,是什麼時候創建的。好,繼續前進

在ActivityThread中,我們找到handleResumeActivity方法,如下:

final void handleResumeActivity(IBinder token,
        boolean clearHide, boolean isForward, boolean reallyResume) {
    // If we are getting ready to gc after going to the background, well
    // we are back active so skip it.
    unscheduleGcIdler();
    mSomeActivitiesChanged = true;

    // TODO Push resumeArgs into the activity for consideration
    ActivityClientRecord r = performResumeActivity(token, clearHide);

    if (r != null) {
        final Activity a = r.activity;

        //代碼省略

            r.activity.mVisibleFromServer = true;
            mNumVisibleActivities++;
            if (r.activity.mVisibleFromClient) {
                r.activity.makeVisible();
            }
        }

      //代碼省略    
}

可以看到內部調用了performResumeActivity方法,這個方法看名字肯定是回調onResume方法的入口的,那麼我們還是跟進去瞧瞧。

public final ActivityClientRecord performResumeActivity(IBinder token,
        boolean clearHide) {
    ActivityClientRecord r = mActivities.get(token);
    if (localLOGV) Slog.v(TAG, "Performing resume of " + r
            + " finished=" + r.activity.mFinished);
    if (r != null && !r.activity.mFinished) {
    //代碼省略
            r.activity.performResume();

    //代碼省略

    return r;
}

可以看到r.activity.performResume()這行代碼,跟進 performResume方法,如下:

final void performResume() {
    performRestart();

    mFragments.execPendingActions();

    mLastNonConfigurationInstances = null;

    mCalled = false;
    // mResumed is set by the instrumentation
    mInstrumentation.callActivityOnResume(this);

    //代碼省略

}

Instrumentation調用了callActivityOnResume方法,callActivityOnResume源碼如下:

public void callActivityOnResume(Activity activity) {
    activity.mResumed = true;
    activity.onResume();

    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                am.match(activity, activity, activity.getIntent());
            }
        }
    }
}

找到了,activity.onResume()。這也證實了,performResumeActivity方法確實是回調onResume方法的入口。

那麼現在我們看回來handleResumeActivity方法,執行完performResumeActivity方法回調了onResume方法後, 
會來到這一塊代碼:

r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
    r.activity.makeVisible();
}

activity調用了makeVisible方法,這應該是讓什麼顯示的吧,跟進去探探。

void makeVisible() {
    if (!mWindowAdded) {
        ViewManager wm = getWindowManager();
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    }
    mDecor.setVisibility(View.VISIBLE);
}

往WindowManager中添加DecorView,那現在應該關註的就是WindowManager的addView方法了。而WindowManager是一個介面來的,我們應該找到WindowManager的實現類才行,而WindowManager的實現類是WindowManagerImpl。這個和ViewRoot是一樣,就是名字多了個impl。

找到了WindowManagerImpl的addView方法,如下:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mDisplay, mParentWindow);
}

裡面調用了WindowManagerGlobal的addView方法,那現在就鎖定 
WindowManagerGlobal的addView方法:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {

    //代碼省略  


    ViewRootImpl root;
    View panelParentView = null;

    //代碼省略

        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }

    // do this last because it fires off messages to start doing things
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

終於擊破,ViewRootImpl是在WindowManagerGlobal的addView方法中創建的。

回顧前面的分析,總結一下: 
ViewRootImpl的創建在onResume方法回調之後,而我們一開篇是在onCreate方法中創建了子線程並訪問UI,在那個時刻,ViewRootImpl是沒有創建的,無法檢測當前線程是否是UI線程,所以程式沒有崩潰一樣能跑起來,而之後修改了程式,讓線程休眠了200毫秒後,程式就崩了。很明顯200毫秒後ViewRootImpl已經創建了,可以執行checkThread方法檢查當前線程。

這篇博客的分析如題目一樣,Android中子線程真的不能更新UI嗎?在onCreate方法中創建的子線程訪問UI是一種極端的情況,這個不仔細分析源碼是不知道的。我是最近看了一個面試題,才發現這個。

從中我也學習到了從異常信息中跟進源碼尋找答案,你呢?

 

本篇博客首發於我的CSDN博客:http://blog.csdn.net/xyh269



您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本章節主要為之前項目 JXHomepwner 添加照片功能(項目地址)。具體任務就是顯示一個 UIImagePickerController 對象,使用戶能夠為 JXItem 對象拍照並保存。拍攝的照片會和相應的 JXItem 對象建立關聯,當用戶進入某個 JXItem 對象的詳細視圖的時候,可以看 ...
  • 說明 JSBridge實現示例 目錄 前言 參考來源 楔子 JS實現部分 說明 實現 Android實現部分 說明 JSBridge類 實現 Callback類 實現 Webview容器關鍵代碼 實現 API 類實現 iOS實現部分 說明 WebViewJavascriptBridgeBase 實現 ...
  • 一直想弄清楚onTouchEvent,onInterceptTouchEvent,dispatchTouchEvent的執行順序,以及內部使用switch (event.getAction())中的執行順序。趁這次機會趕緊弄清楚。 重寫上面幾個方法後。我們在LogCat中看看列印的結果。 一.isO ...
  • 說明 JSBridge實現原理 目錄 前言 參考來源 前置技術要求 楔子 原理概述 簡介 url scheme介紹 實現流程 實現思路 第一步:設計出一個Native與JS交互的全局橋對象 第二步:JS如何調用Native 第三步:Native如何得知api被調用 第四步:分析url-參數和回調的格 ...
  • 基於OpenSLL的RSA加密應用(非演算法) === iOS開發中的小伙伴應該是經常用der和p12進行加密解密,而且在通常加密不止一種加密演算法,還可以加點兒鹽吧~本文章主要闡述的是在iOS中基於openSLL的RSA加密。一共有兩種方式,一種是基於p12加密解密的,還有一種是博客園官方提供的公鑰字 ...
  • 說明 Hybrid模式原生和H5交互原理 目錄 前言 參考來源 前置技術要求 楔子 Android、iOS原生和H5的基本通信機制 Android端 iOS端 原生和H5的另一種通訊方式:JSBridge 什麼是JSBridge 為什麼要用JSBridge JSBridge原理以及實現 前言 參考來 ...
  • 大家或許有遇到這個神坑,在Fragment中使用startActivityForResult能夠成功,可是在Fragment中的onActivityResult卻無法被調用。一不註意就讓人一夜愁白了頭。苦經探索(當然包括親愛的百度和谷歌),終於總結出了一些規律。 在Fragment中使用startA ...
  • 此文,將嘗試動態從某個不確定的文件夾中載入資源文件.文章,會繼續完善自定義的 imageNamed 函數,併為下一篇文章鋪墊. ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...