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

今天講一個老生常談的問題,"Android中子線程真的不能更新UI嗎?java

AndroidUI訪問是沒有加鎖的,這樣在多個線程訪問UI是不安全的。android

因此Android中規定只能在UI線程中訪問UI。子線程更新是不被容許的。面試

那麼子線程訪問UI會報錯嗎?安全

 首先,咱們在佈局文件隨意定義一個textview:app

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

    <TextView android:id="@+id/tv_test" android:layout_width="100dp" android:layout_height="wrap_content" android:paddingTop="10dp" android:paddingBottom="10dp" android:textColor="@color/yellow" android:background="#000000" android:text="test" android:textSize="16sp" android:gravity="center"/>
</LinearLayout>

接着咱們在activity中開了個子線程修改UI異步

 

class TestActivity :AppCompatActivity(R.layout.activity_test) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Thread(object :Runnable{ override fun run() { tv_test.text = "這是修改後的text" } }).start() } }

 

展現,並無報錯:ide

 

緊接着,咱們試試延時試試:函數

 

class TestActivity :AppCompatActivity(R.layout.activity_test) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Thread(object :Runnable{ override fun run() { Thread.sleep(3000) tv_test.text = "這是修改後的text" } }).start() } }

 

運行試了下,3s後竟然奔潰了,這是爲啥呢?oop

查看錯誤日誌:佈局

 

經典問題出現了,查看源碼能夠看到,是在ViewRootImpl.checkThread中拋出的這個異常.

 

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

 

當訪問 UI 時,ViewRootImpl 會調用 checkThread方法去檢查當前訪問 UI 的線程是否爲建立 UI 的那個線程,若是不是。則會拋出異常。

那爲啥要必定須要checkThread?根據handler的相關知識:

 

由於UI控件不是線程安全的。那爲啥不加鎖呢?

一是加鎖會讓UI訪問變得複雜;

二是加鎖會下降UI訪問效率,會阻塞一些線程訪問UI

因此乾脆使用單線程模型處理UI操做,使用時用Handler切換便可。

 

 

疑問點:爲何一開始在 MainActivity onCreate 方法中建立一個子線程訪問 UI,程序仍是正常能跑起來呢???

 

咱們能夠看看tv_text.setText觸發的調用流程:

 

TextView#setText -->View#requestLayout() 知足條件: --> ViewParent # requestLayout --> ViewRootImpl # requestLayout -->View#invalidate -->  View#invalidate(boolean) -->  View#invalidateInternal //若是 if mAttachInfo 以及 mParent 都不爲空
        -->ViewParent#invalidateChild -->ViewRootImpl#invalidateChild -->ViewRootImpl#invalidateChildInParent --------------------- View#invalidateInternal // Propagate the damage rectangle to the parent view.
        final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; if (p != null && ai != null && l < r && t < b) { final Rect damage = ai.mTmpInvalRect; damage.set(l, t, r, b); p.invalidateChild(this, damage); } ViewRootImpl#invalidateChildInParent @Override public ViewParent invalidateChildInParent(int[] location, Rect dirty) { checkThread(); if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty); if (dirty == null) { invalidate(); return null; } else if (dirty.isEmpty() && !mIsAnimating) { return null; }

 

咱們仔細看一下mThread,他這個錯誤信息並非:

Only the UI Thread ... 而是 Only the original thread

這個mThread是什麼?

ViewRootImpl的成員變量,咱們重點應該關注它何時賦值的:

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

ViewRootImpl構造的時候賦值的,賦值的就是當前的Thread對象。

也就是說,ViewRootImpl在哪一個線程建立的,你後續的UI更新就須要在哪一個線程執行,跟是否是UI線程毫無關係。

那麼,VIewRootImpl是何時建立的呢?

咱們啓動Activity繪製UI的方法在onResume方法裏,因此咱們找到Activity的線程ActivityThread類。在ActivityThread中,咱們找到handleResumeActivity方法,內部調用了performResumeActivity方法,逐層跟進會發現調用了activity.onResume()方法,因此performResumeActivity確實是resume的入口。

 

ActivityThread.java#handleResumeActivity

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) { ... if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; wm.addView(decor, l); } // If the window has already been added, but during resume // we started another activity, then don't yet make the // window visible.
            } else if (!willBeVisible) { if (localLOGV) Slog.v( TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } ... }

wm.addView(decor, l);是他進行的View的加載,咱們去看看他的實現方法,在WindowManager的實現類WindowManagerImpl裏:

 

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

 

發現他是調用WindowManagerGlobal的方法實現的,最後咱們找到了最終實現addView的方法:

WindowManagerGlobal.java#addView

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ... ViewRootImpl root; View panelParentView = null; synchronized (mLock) { // Start watching for system property changes.
 ... 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; } }

果真在這裏,View的加載最後就是在這裏實現的,而ViewRootImpl的實例化也在這裏。

因此若是咱們在子線程中調用WindowManageraddView方法,是否是就能夠成功更新UI呢?

 

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Thread(object :Runnable{ override fun run() { Thread.sleep(3000) // tv_test.text = "這是修改後的text"
                val tx = TextView(this@TestActivity) tx.text = "這是修改後的text" tx.setBackgroundColor(Color.WHITE) val layoutParams = WindowManager.LayoutParams( 200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW, WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE ) windowManager.addView(tx, layoutParams) } }).start() }

 

 錯誤緣由是沒有啓動Looper。原來是由於在ViewRootImpl類裏新建了ViewRootHandler的實例mHandler在子線程中加上Looper.prepare()Looper.loop(),而後成功了~

 

Thread(object :Runnable{ override fun run() { // tv_test.text = "這是修改後的text"
                Thread.sleep(3000) Looper.prepare() val tx = TextView(this@TestActivity) tx.text = "這是修改後的text" tx.setBackgroundColor(Color.BLACK) tx.setTextColor(Color.YELLOW) val layoutParams = WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, 100, 0, 0, WindowManager.LayoutParams.TYPE_DRAWN_APPLICATION, WindowManager.LayoutParams.TYPE_STATUS_BAR, PixelFormat.OPAQUE ) windowManager.addView(tx, layoutParams) Looper.loop() } }).start()

 

運行成功!

 

擴展點:爲何子線程須要加Looper.loop(),主線程不用?

 

總結:

ViewRootImpl 的建立在 onResume 方法回調以後,而咱們一開篇是在 onCreate 方法中建立了子線程並訪問 UI,在那個時刻,ViewRootImpl 尚未建立,咱們在所以 ViewRootImpl#checkThread 沒有被調用到,也就是說,檢測當前線程是不是建立的 UI 那個線程 的邏輯沒有執行到,因此程序沒有崩潰同樣能跑起來。而以後修改了程序,讓線程休眠了 3000 毫秒後,程序就崩了。很明顯 3000 毫秒後 ViewRootImpl 已經建立了,能夠執行 checkThread 方法檢查當前線程。

等等,還沒完?

在測試的時候,偶然發現這麼寫不會報錯??懷疑人生!!?

 

Thread(object :Runnable{ override fun run() { Thread.sleep(3000) tv_test.text = tv_test.text.toString()+"abc" } }).start()

 

 

 

 仔細想一想,想明白了,這塊就涉及到View的繪製流程了,設置值時,執行到View.requestLayout->ViewRootImpl#requestLayout 方法,

ViewRootperformTraversals()方法會在measure結束後繼續執行,並調用Viewlayout()方法來執行此過程,以下所示:

 

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

 

在layout()方法中,首先會判斷視圖的寬高是否發生過變化,以肯定有沒有必要對當前的視圖進行重繪,這塊顯然沒有變化,天然也就不會繼續往下執行了。

下次若是有人問你 Android 中子線程真的不能更新 UI 嗎? 你能夠這麼回答:

任何線程均可以更新本身建立的 UI。只要保證知足下面幾個條件就行了

  1.  在 ViewRootImpl 還沒建立出來以前。
    1. UI 修改的操做沒有線程限制。由於 checkThread 方法不會被執行到。
  2. 在 ViewRootImpl 建立完成以後
    •  保證「建立 ViewRootImpl 的操做」和「執行修改 UI 的操做」在同一個線程便可。也就是說,要在同一個線程調用 ViewManager#addView 和 ViewManager#updateViewLayout 的方法。
      •  注:ViewManager 是一個接口,WindowManger 接口繼承了這個接口,咱們一般都是經過 WindowManger(具體實現爲 WindowMangerImpl) 進行 view 的 add remove update 操做的。
    • 對應的線程須要建立 Looper 而且調用 Looper#loop 方法,開啓消息循環。

有同窗可能會問,保證上述條件 1 成立,不就能夠避免 checkThread 時候拋出異常了嗎?爲何還須要開啓消息循壞?

 條件 1 能夠避免檢查異常,可是沒法保證 UI 能夠被繪製出來。

 條件 2 可讓更新的 UI 效果呈現出來

  •  WindowManger#addView 最終會調用 WindowManageGlobal#addView 方法,進而觸發ViewRootImpl#setView 方法,該方法內部會調用ViewRootImpl#requestLayout 方法。
  •  根據 UI 繪製原理,下一步就是 scheduleTraversals 了,該方法會往消息隊列中插入一條消息屏障,而後調用 Choreographer#postCallback 方法,往 looper 中插入一條異步的 MSG_DO_SCHEDULE_CALLBACK 消息。等待垂直同步信號回來以後執行。

 注:ViewRootImpl 有一個 Choreographer 成員變量,ViewRootImpl 的構造函數中會調用 Choreographer#getInstance(); 方法,獲取一個當前線程的 Choreographer 局部實例。

 

使用子線程更新 UI 有實際應用場景嗎?

Android 中的 SurfaceView 一般會經過一個子線程來進行頁面的刷新。若是咱們的自定義 View 須要頻繁刷新,或者刷新時數據處理量比較大,那麼能夠考慮使用 SurfaceView 來取代 View

 

擴展知識-使用Looper實現日誌捕獲:

講這個以前,先說下Looper的流程吧~

 

前面說到,須要在子線程中調用Looper.loop開啓循環。那麼咱們的主線程爲何沒有調用呢?

這裏又涉及到handler的事件分發機制了,查看源碼得知,在ActivityThread中,系統已經幫咱們調用了Looper.prepareMainLooper()

 

public static void main(String[] args) { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain"); // Install selective syscall interception
 AndroidOs.install(); // CloseGuard defaults to true and can be quite spammy. We // disable it here, but selectively enable it later (via // StrictMode) on debug builds, but using DropBox, not logs.
    CloseGuard.setEnabled(false); Environment.initForCurrentUser(); // Make sure TrustedCertificateStore looks in the right place for CA certificates
    final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId()); TrustedCertificateStore.setDefaultUserDirectory(configDir); Process.setArgV0("<pre-initialized>"); Looper.prepareMainLooper(); // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line. // It will be in the format "seq=114"
    long startSeq = 0; if (args != null) { for (int i = args.length - 1; i >= 0; --i) { if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) { startSeq = Long.parseLong( args[i].substring(PROC_START_SEQ_IDENT.length())); } } } ActivityThread thread = new ActivityThread(); thread.attach(false, startSeq); if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } if (false) { Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread")); } // End of event ActivityThreadMain.
 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited");
}

 

而後執行Looper.loop(),不斷從隊列中抽取message

 

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; //.................. //開啓死循環
        for (;;) { //二、拿到隊列中的消息
            Message msg = queue.next(); // might block
            if (msg == null) { // No message indicates that the message queue is quitting.
                return; } //省略部分不相關的代碼 //..................
            try { //三、執行隊列中的消息
 msg.target.dispatchMessage(msg); dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } finally { if (traceTag != 0) { Trace.traceEnd(traceTag); } } //..................
 msg.recycleUnchecked(); }

 

loop()方法中,代碼很是簡單,分三步走

 1、獲取到looper中的 MessageQueue

 2、開啓一個死循環,從MessageQueue 中不斷的取出消息

 3、執行取出來的消息  msg.target.dispatchMessage(msg);(順便說一下,HandlerhandleMessage()方法就是在這一步執行的)

在第二步裏面,會發生阻塞,若是消息隊列裏面沒有消息了,會無限制的阻塞下去,主線程休眠,釋放CPU資源,直到有消息進入消息隊列,喚醒線程。從這裏就能夠看出來,loop死循環自己大部分時間都處於休眠狀態,並不會佔用太多的資源,真正會形成線程阻塞的反而是在第三步裏的  msg.target.dispatchMessage(msg)方法,所以若是在生命週期或者handlerHandlerhandleMessage執行耗時操做的話,纔會真正的阻塞UI線程。

 由此咱們能夠自定義一個handler+Looper截全局崩潰(主線程),避免 APP 退出。

相關代碼以下:

 

class MyApp : Application() { override fun onCreate() { super.onCreate() var handler = Handler(Looper.getMainLooper()) handler.post { while (true){ try { Looper.loop() }catch (e:Throwable){ e.printStackTrace() if (e.message != null && e.message!!.startsWith("Unable to start activity")) { System.gc(); _restart(); android.os.Process.killProcess(android.os.Process.myPid()); break } } } } Thread.setDefaultUncaughtExceptionHandler { t, e -> e.printStackTrace() } } private fun _restart() { val intent = getPackageManager().getLaunchIntentForPackage(getPackageName()) intent?.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(intent) } 
}

 

testDemo:

 

var data:ArrayList<String> ?= null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) tv_test.setOnClickListener { data!!.add("1") } }

不添加上述代碼時,點擊文本:

頁面閃退,報錯:

 

添加後:

異常捕獲,頁面不受影響

 

 

 

經過上面的代碼就能夠就能夠實現攔截UI線程的崩潰。可是也並不可以攔截全部的奔潰,若是在ActivityonCreate出現崩潰,致使Activity建立失敗,那麼就會殺死該app並重啓。

 

參考連接:

相關文章
相關標籤/搜索