分析為什么有時在非UI線程更新UI會崩潰

很多初學(xué)者肯定有這樣一個經(jīng)驗肩刃,在Activity的一個子線程中更新UI精绎,發(fā)現(xiàn)會報錯速缨。很多人知道這個錯誤,但卻不知道是什么原因引起的代乃。今天我們來分析一下是什么原因引起的旬牲。我今天以textview更新ui為例。
首先看代碼

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.generalwei.mytest.ThreadActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="hello"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:id="@+id/btn_change"
        android:text="更換UI"/>

</RelativeLayout>
public class ThreadActivity extends AppCompatActivity {

    private android.widget.TextView tv;
    private android.widget.Button btnchange;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread);
        this.btnchange = (Button) findViewById(R.id.btn_change);
        this.tv = (TextView) findViewById(R.id.tv);
        btnchange.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        tv.setText(new Date().getTime()+"");
                    }
                }).start();

            }
        });
    }
}

運行app搁吓,點擊btnchange按鈕原茅,發(fā)現(xiàn)app崩潰。查看日志,發(fā)現(xiàn)這樣一個錯誤:

我們來查看一下tv.setText()方法的源碼堕仔。

public final void setText(CharSequence text) {
        setText(text, mBufferType);
}

 public void setText(CharSequence text, BufferType type) {
        setText(text, type, true, 0);
        if (mCharWrapper != null) {
            mCharWrapper.mChars = null;
        }
    }


 private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
        ...
        if (mLayout != null) {
            checkForRelayout();
        }
        ...
    }

這里我們還是沒有看見什么原因引起的擂橘,繼續(xù)追查checkForRelayout()方法。

 private void checkForRelayout() {
       ... 
       invalidate();
       ...
}

查看invalidate()方法,我們會看見這樣一個注釋:

  /**
     * Invalidate the whole view. If the view is visible,
     * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
     * the future.
     * <p>
     * This must be called from a UI thread. To call from a non-UI thread, call
     * {@link #postInvalidate()}.
     */
  public void invalidate() {
        invalidate(true);
    }

意思是說如果view是可見的摩骨,這個方法會刷新view通贞。但是必須發(fā)生在ui線程上∧瘴澹看到這邊我們發(fā)現(xiàn)我們的追蹤是正確昌罩。那么接著看,為什么一定要在ui線程上更新ui唤冈。

  void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }


void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
            ...     
            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);
            }  
        ...
    }

ViewParent 是一個接口,ViewRootImpl是它的實現(xiàn)類,那么我們繼續(xù)追查,代碼如下:

@Override
    public void invalidateChild(View child, Rect dirty) {
        invalidateChildInParent(null, dirty);
    }
    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
      ...
    }

你會發(fā)現(xiàn)一個檢查線程的方法银伟,那么查看方法checkThread()你虹。

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

查看代碼后發(fā)現(xiàn)有一個mThread線程,它是會是UI線程嗎彤避,我們就查看Thread的來源傅物。

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

發(fā)現(xiàn)mThread是在ViewRootImpl創(chuàng)建的時候賦值,這個的線程一定是UI線程琉预。所以當(dāng)前線程不是UI線程的時候會拋異常董饰。

為什么在onResume之前非UI線程也能更新UI

發(fā)現(xiàn)mThread是在ViewRootImpl創(chuàng)建的時候賦值,這個的線程一定是UI線程圆米。所以當(dāng)前線程不是UI線程的時候會拋異常卒暂。
但是有時候在也能在非UI線程中更新,后來我們發(fā)現(xiàn)在onResume之前用非UI線程更新能UI娄帖,而onResume之后就不行了也祠。這是因為onResume之前還沒有創(chuàng)建ViewRootImpl這個類,ActivityThread類中有一個handleResumeActivity方法近速,這個方法是用來回調(diào)Activity的onResume方法诈嘿,具體的看如下代碼:

 final void handleResumeActivity(IBinder token,boolean clearHide, boolean   isForward, boolean reallyResume, int seq, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    if (!checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) {
        return;
    }

    // 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
    r = performResumeActivity(token, clearHide, reason);
    if (r != null) {
            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 (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
                if (a.mVisibleFromClient && !a.mWindowAdded) {
                    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;
            }
            ...
            // Tell the activity manager we have resumed.
           if (reallyResume) {
                try {
                    ActivityManagerNative.getDefault().activityResumed(token);
                } catch (RemoteException ex) {
                    throw ex.rethrowFromSystemServer();
                }
            }

        } else {
            // If an exception was thrown when trying to resume, then
            // just end this activity.
            try {
                ActivityManagerNative.getDefault()
                    .finishActivity(token, Activity.RESULT_CANCELED, null,
                            Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
        }
    }

我們可以看見這樣一個方法performResumeActivity()堪旧,它的源碼如下:

public final ActivityClientRecord performResumeActivity(IBinder token,boolean clearHide, String reason) {
    ...
    if (r != null && !r.activity.mFinished) {
        ...
        r.activity.performResume();
        ...
    }
}

這里可以看見它調(diào)用了activity.performResume(),那么再繼續(xù)查看下面的源碼:

final void performResume() {
    performRestart();
    ...
    mInstrumentation.callActivityOnResume(this);
    ...
    onPostResume();
    ...   
}

performRestart()方法主要是為了執(zhí)行回調(diào)onRestart方法,具體內(nèi)容就不做分析了奖亚。mInstrumentation.callActivityOnResume()方法則是為了回調(diào)Activity的OnResume()方法淳梦。onPostResume()方法這是為了激活Window。
在handleResumeActivity()方法中我們可以看見一個WindowManager類昔字,這個類是用來控制窗口顯示的爆袍,而它的addView是用來添加視圖。WindowManagerImpl是WindowManager的實現(xiàn)類李滴,WindowManagerImpl的addView方法代碼如下:

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

mGlobal是WindowManagerGlobal的對象螃宙,繼續(xù)看mGlobal.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.
            if (mSystemPropertyUpdater == null) {
                mSystemPropertyUpdater = new Runnable() {
                    @Override public void run() {
                        synchronized (mLock) {
                            for (int i = mRoots.size() - 1; i >= 0; --i) {
                                mRoots.get(i).loadSystemProperties();
                            }
                        }
                    }
                };
                SystemProperties.addChangeCallback(mSystemPropertyUpdater);
            }

            int index = findViewLocked(view, false);
            if (index >= 0) {
                if (mDyingViews.contains(view)) {
                    // Don't wait for MSG_DIE to make it's way through root's queue.
                    mRoots.get(index).doDie();
                } else {
                    throw new IllegalStateException("View " + view
                            + " has already been added to the window manager.");
                }
                // The previous removeView() had not completed executing. Now it has.
            }

            // If this is a panel window, then find the window it is being
            // attached to for future reference.
            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                final int count = mViews.size();
                for (int i = 0; i < count; i++) {
                    if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                        panelParentView = mViews.get(i);
                    }
                }
            }

            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }
       ...
    }

你可以看做ViewRootImpl的初始化時在這里進行的,這就是為什么在onResume之前可以更新UI了所坯。

為什么要這么設(shè)計呢谆扎?

因為所有的UI控件都是非線程安全的,如果在非UI線程更新UI會造成UI混亂芹助。所以一般我們會在Handler中更新UI堂湖。
如有寫的不當(dāng)之處,請多指教状土。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末无蜂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子蒙谓,更是在濱河造成了極大的恐慌斥季,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件累驮,死亡現(xiàn)場離奇詭異酣倾,居然都是意外死亡,警方通過查閱死者的電腦和手機谤专,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門躁锡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人置侍,你說我怎么就攤上這事映之。” “怎么了蜡坊?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵杠输,是天一觀的道長。 經(jīng)常有香客問我秕衙,道長抬伺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任灾梦,我火速辦了婚禮峡钓,結(jié)果婚禮上妓笙,老公的妹妹穿的比我還像新娘。我一直安慰自己能岩,他們只是感情好寞宫,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拉鹃,像睡著了一般辈赋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上膏燕,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天钥屈,我揣著相機與錄音,去河邊找鬼坝辫。 笑死篷就,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的近忙。 我是一名探鬼主播竭业,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼及舍!你這毒婦竟也來了未辆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤锯玛,失蹤者是張志新(化名)和其女友劉穎咐柜,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體攘残,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡拙友,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了肯腕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片献宫。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡钥平,死狀恐怖实撒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涉瘾,我是刑警寧澤知态,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站立叛,受9級特大地震影響负敏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜秘蛇,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一其做、第九天 我趴在偏房一處隱蔽的房頂上張望顶考。 院中可真熱鬧,春花似錦妖泄、人聲如沸驹沿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渊季。三九已至,卻和暖如春罚渐,著一層夾襖步出監(jiān)牢的瞬間却汉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工荷并, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留合砂,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓璧坟,卻偏偏與公主長得像既穆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子雀鹃,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容