Android中子執行緒真的不能更新UI嗎?
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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
很簡單,只是新增了一個居中的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();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
也是很簡單的幾行,在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();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
讓子執行緒睡眠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)
- 1
- 1
這個異常是從android.view.ViewRootImpl的checkThread方法丟擲的。
那現在跟進ViewRootImpl的checkThread方法瞧瞧,原始碼如下:
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
只有那麼幾行程式碼而已的,而mThread是主執行緒,在應用程式啟動的時候,就已經被初始化了。
由此我們可以得出結論:
在訪問UI的時候,ViewRootImpl會去檢查當前是哪個執行緒訪問的UI,如果不是主執行緒,那就會丟擲如下異常:
Only the original thread that created a view hierarchy can touch its views
- 1
- 1
這好像並不能解釋什麼?繼續看到異常資訊
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)
- 1
- 1
那現在就看看requestLayout方法,
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
這裡也是呼叫了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();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
注意到postCallback方法的的第二個引數傳入了很像是一個後臺任務。那再點進去
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
找到了,那麼繼續跟進doTraversal()方法。
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
可以看到裡面呼叫了一個performTraversals()方法,View的繪製過程就是從這個performTraversals方法開始的。PerformTraversals方法的程式碼有點長就不貼出來了,如果繼續跟進去就是學習View的繪製了。而我們現在知道了,每一次訪問了UI,Android都會重新繪製View。這個是很好理解的。
分析到了這裡,其實異常資訊對我們幫助也不大了,它只告訴了我們子執行緒中訪問UI在哪裡丟擲異常。
而我們會思考:當訪問UI時,ViewRootImpl會呼叫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();
}
}
//程式碼省略
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
可以看到內部呼叫了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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
可以看到r.activity.performResume()這行程式碼,跟進 performResume方法,如下:
final void performResume() {
performRestart();
mFragments.execPendingActions();
mLastNonConfigurationInstances = null;
mCalled = false;
// mResumed is set by the instrumentation
mInstrumentation.callActivityOnResume(this);
//程式碼省略
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
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());
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
找到了,activity.onResume()。這也證實了,performResumeActivity方法確實是回撥onResume方法的入口。
那麼現在我們看回來handleResumeActivity方法,執行完performResumeActivity方法回撥了onResume方法後,
會來到這一塊程式碼:
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
activity呼叫了makeVisible方法,這應該是讓什麼顯示的吧,跟進去探探。
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
往WindowManager中新增DecorView,那現在應該關注的就是WindowManager的addView方法了。而WindowManager是一個介面來的,我們應該找到WindowManager的實現類才行,而WindowManager的實現類是WindowManagerImpl。
找到了WindowManagerImpl的addView方法,如下:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
裡面呼叫了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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
終於擊破,ViewRootImpl是在WindowManagerGlobal的addView方法中建立的。
回顧前面的分析,總結一下:
ViewRootImpl的建立在onResume方法回撥之後,而我們一開篇是在onCreate方法中建立了子執行緒並訪問UI,在那個時刻,ViewRootImpl是沒有建立的,無法檢測當前執行緒是否是UI執行緒,所以程式沒有崩潰一樣能跑起來,而之後修改了程式,讓執行緒休眠了200毫秒後,程式就崩了。很明顯200毫秒後ViewRootImpl已經建立了,可以執行checkThread方法檢查當前執行緒。
這篇部落格的分析如題目一樣,Android中子執行緒真的不能更新UI嗎?在onCreate方法中建立的子執行緒訪問UI是一種極端的情況,這個不仔細分析原始碼是不知道的。我是最近看了一個面試題,才發現這個。
從中我也學習到了從異常資訊中跟進原始碼尋找答案,你呢?
相關文章
- Android 中子執行緒真的不能更新UI嗎?Android執行緒UI
- Android子執行緒真的不能更新UI麼Android執行緒UI
- Android中子執行緒更新主執行緒UI和ProgressBar的應用Android執行緒UI
- Android 不能在子執行緒中更新 UI 的討論和分析Android執行緒UI
- Android 子執行緒 UI 操作真的不可以?Android執行緒UI
- Android複習之旅--子執行緒更新UIAndroid執行緒UI
- Android新執行緒中更新主執行緒UI中的View方法彙總Android執行緒UIView
- Android的Activity啟動與子執行緒更新UIAndroid執行緒UI
- 如何在子執行緒中更新UI執行緒UI
- java中如何給多執行緒中子執行緒傳遞引數?Java執行緒
- 為何要在主執行緒上更新UI執行緒UI
- Java中執行緒池,你真的會用嗎?Java執行緒
- java多執行緒程式設計:你真的瞭解執行緒中斷嗎?Java執行緒程式設計
- 老問題:Android子執行緒中更新UI的3種方法Android執行緒UI
- iOS 在主執行緒操作UI不能保證安全iOS執行緒UI
- android 關於關於子執行緒更新UI的一些事Android執行緒UI
- MFC UI執行緒UI執行緒
- Java執行緒安全面試題,你真的瞭解嗎?Java執行緒面試題
- 你的單例模式真的是執行緒安全的嗎?單例模式執行緒
- [原] Android performClick無效,UI執行緒理解AndroidORMUI執行緒
- 關於“UI執行緒”UI執行緒
- 分析為什麼有時在非UI執行緒更新UI會崩潰UI執行緒
- 你真正瞭解UI執行緒更新的幾種方式嗎—面試必備之深度揭祕UI執行緒面試
- Android中UI執行緒與後臺執行緒互動設計的5種方法AndroidUI執行緒
- 子執行緒與UI執行緒的通訊(委託)執行緒UI
- Android多執行緒之執行緒池Android執行緒
- Android的執行緒和執行緒池Android執行緒
- Android程式框架:執行緒與執行緒池Android框架執行緒
- Android執行緒篇(二)Java執行緒池Android執行緒Java
- Android執行緒管理之ExecutorService執行緒池Android執行緒
- 程式設計之路第17天:不能在ui執行緒執行阻塞操作,請使用setTimeout代替????程式設計UI執行緒
- ConcurrentHashMap執行緒安全嗎?HashMap執行緒
- android程式與執行緒詳解二:執行緒Android執行緒
- Android執行緒池Android執行緒
- 多執行緒中不用sleep(),行嗎?執行緒
- Control的Invoke和BeginInvoke 是相對於支線執行緒(因為一般在支線執行緒中呼叫,用來更新主執行緒ui)Invoke立即插入主執行緒中執行,而BeginInvoke 要等主執行緒結束才執行執行緒UI
- 多執行緒C++更新MYSQL執行緒C++MySql
- 子執行緒 UI 問題捉蟲執行緒UI