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

來源:互聯網
上載者:User

標籤:

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的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的時候,ViewRootImpl會去檢查當前是哪個線程訪問的UI,如果不是主線程,那就會拋出如下異常:

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

這好像並不能解釋什嗎?繼續看到異常資訊

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

那現在就看看requestLayout方法,

@Overridepublic 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時,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();            }        }      //代碼省略    }

可以看到內部調用了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。

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

@Overridepublic 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是一種極端的情況,這個不仔細分析源碼是不知道的。我是最近看了一個面試題,才發現這個。

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

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.