編輯:關於android開發
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,程序一樣能跑起來。結果如下所示:
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
Android幾種常見的多渠道(批量)打包方式介紹 多渠道打包,主要是為了統計不同的渠道上包的下載數量,渠道越多,我們需要打的包數量越多,這個時候,我們沒法去使用單純
MSM8909+Android5.1.1 SPI驅動開發(PSAM部分) MSM8909+Android5.1.1SPI驅動開發(PSAM部分) 1.
為RecyclerView打造通用Adapter 讓RecyclerView更加好用,recyclerviewadapter原文出處: 張鴻洋 (Granker
Android中Canvas繪圖之MaskFilter圖文詳解(附源碼下載) 如果對Canvas繪圖不熟悉,強烈建議您閱讀博文《Android中Canvas繪圖基礎詳