今天講一個老生常談的問題,"Android中子線程真的不能更新UI嗎?」java
Android中UI訪問是沒有加鎖的,這樣在多個線程訪問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的實例化也在這裏。
因此若是咱們在子線程中調用WindowManager的addView方法,是否是就能夠成功更新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 方法,
ViewRoot的performTraversals()方法會在measure結束後繼續執行,並調用View的layout()方法來執行此過程,以下所示:
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
在layout()方法中,首先會判斷視圖的寬高是否發生過變化,以肯定有沒有必要對當前的視圖進行重繪,這塊顯然沒有變化,天然也就不會繼續往下執行了。
下次若是有人問你 Android 中子線程真的不能更新 UI 嗎? 你能夠這麼回答:
任何線程均可以更新本身建立的 UI。只要保證知足下面幾個條件就行了
有同窗可能會問,保證上述條件 1 成立,不就能夠避免 checkThread 時候拋出異常了嗎?爲何還須要開啓消息循壞?
條件 1 能夠避免檢查異常,可是沒法保證 UI 能夠被繪製出來。
條件 2 可讓更新的 UI 效果呈現出來
注:ViewRootImpl 有一個 Choreographer 成員變量,ViewRootImpl 的構造函數中會調用 Choreographer#getInstance(); 方法,獲取一個當前線程的 Choreographer 局部實例。
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);(順便說一下,Handler的handleMessage()方法就是在這一步執行的)
在第二步裏面,會發生阻塞,若是消息隊列裏面沒有消息了,會無限制的阻塞下去,主線程休眠,釋放CPU資源,直到有消息進入消息隊列,喚醒線程。從這裏就能夠看出來,loop死循環自己大部分時間都處於休眠狀態,並不會佔用太多的資源,真正會形成線程阻塞的反而是在第三步裏的 msg.target.dispatchMessage(msg)方法,所以若是在生命週期或者handler的Handler的handleMessage執行耗時操做的話,纔會真正的阻塞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線程的崩潰。可是也並不可以攔截全部的奔潰,若是在Activity的onCreate出現崩潰,致使Activity建立失敗,那麼就會殺死該app並重啓。