android 關於關於子執行緒更新UI的一些事

ostracod發表於2017-03-28

我們在看一些書或者部落格時總是會看到一句話“android更新UI操作都是在Main主執行緒中,子執行緒中不能進行UI更新操作”那麼,在子執行緒中真的不能進行UI的更新操作嗎?

//原始碼環境申明

compileSdkVersion 24
buildToolsVersion "24.0.2"

defaultConfig {
        minSdkVersion 14
        targetSdkVersion 24
}複製程式碼

首先我們來看一段程式碼:

在onCreate生命週期中新增子執行緒更新UI操作

@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        new Thread(new Runnable() {
            @Override
            public void run() {
                text.setText("子執行緒重新整理UI");
            }
        }).start();
    }複製程式碼

我們執行一下程式,發現APP沒有崩潰,同時頁面上顯示了“子執行緒重新整理UI”咦?不是說子執行緒中不能進行UI的更新操作嘛?為什麼這個可以呢?

我們在onResume生命週期中新增子執行緒更新UI操作

  @Override
    protected void onResume() {
        super.onResume();
        new Thread(new Runnable() {
            @Override
            public void run() {
                text.setText("子執行緒重新整理UI");
            }
        }).start();
    }複製程式碼

然後執行一下APP,發現這次APP崩潰了,列印一下Log日誌

android 關於關於子執行緒更新UI的一些事

其中有一句經典的異常資訊:

Only the original thread that created a view hierarchy can touch its views.複製程式碼

相信這句話大家應該基本都碰到過吧,這又是怎麼回事呢?

為什麼在onCreate中更新沒事,而在onResume中更新就崩潰了?

帶著這個問題,我們來跟蹤一下原始碼,然後給出答案!

首先我們來看一下崩潰日誌,一般情況下我們都可以通過崩潰日誌來追蹤問題。我們通過異常棧來一步一步追蹤根源。

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.複製程式碼

第一句顯示說明異常出現的原因:只有主執行緒才能進行更新UI操作。接下來一句:

at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:5288)複製程式碼

我們進入到ViewRootImpl.checkThread方法中

Tip: 對於AndroidStudioIDE我們可以通過雙擊shift鍵來查詢相關的檔案

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

對於checkThread()方法,只有簡單的幾行程式碼,只是進行常規檢查,如果當前執行緒不是主執行緒的話則會丟擲異常。而mThread 這個變數是在建構函式中進行初始化的(記住,後面需要用到)。

   public ViewRootImpl(Context context, Display display) {
        //省略無關程式碼
        //...
        mThread = Thread.currentThread();
        //...
        //省略無關程式碼

    }複製程式碼

上面只是簡單的進行判斷當前執行緒是否是主執行緒,沒什麼價值,我們繼續看第三行異常log資訊

at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:974)複製程式碼

進入到該方法中,檢視

  @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;
        }

        //省略無關程式碼

        invalidateRectOnScreen(dirty);

        return null;
    }複製程式碼

好像並沒有什麼有用的提示,第一行也只是進行執行緒判斷,我們進入到 invalidate()中看看,

void invalidate() {
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            scheduleTraversals();
        }
    }複製程式碼

也沒什麼,繼續進入scheduleTraversals()方法

  void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }複製程式碼

好像也沒什麼嘛,但是我們注意觀察,可以發現

mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);複製程式碼

裡面有一個引數 mTraversalRunnable,runnable不是執行緒的意思嘛,我們開啟子執行緒經常使用到的,我們跟蹤一下,看看究竟。

final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

 final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }複製程式碼

咦,好像有點看頭哦!對於執行緒操作,我們一般有兩種方法

  1. 實現Runnable介面,實現run方法
  2. 繼承java的原有執行緒Thread類,實現run方法

這兩種方法的區別就是第二種繼承Thread,Thread類也是通過實現Runnable方法來實現的,本質上沒啥多大區別,只不過繼承Thread類可以使用裡面的一些方法而已。

而TraversalRunnable 類是直接實現Runnable介面,裡面只有一個方法,我們進入到裡面瞧瞧:

 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的繪製了。通過以上程式碼我們可以知道每一次訪問了UI,Android都會重新繪製View。

其實到了這裡我們可以總結一點了,當訪問UI時,ViewRootImpl會呼叫checkThread方法去檢查當前訪問UI的執行緒是哪個,如果不是UI執行緒則會丟擲異常

但是為什麼在OnCreate中可以操作子執行緒更新UI,而在OnResume中則不可以?而當前執行緒則是在ViewRootImpl的建構函式中進行初始化的,因此我們可以大膽的猜測一下:

ViewRootImpl類在onCreate中還沒建立完成,而在onResume中已經建立完成了。

為了驗證上面的猜想是不是正確的,我們需要解決的問題就是

ViewRootImpl在哪裡進行建立的?

對於以上問題,我們需要用到這個類ActivityThread,這個類是幹啥用的呢?看名字就知道是關於主執行緒的,我們瀏覽一下這個類,發現有個main方法,類似與java的main方法,這就是APP的全域性入口處,

public static void main(String[] args) {
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
        SamplingProfilerIntegration.start();

        // 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();

        // Set the reporter for event logging in libcore
        EventLogger.setReporter(new EventLoggingReporter());

        // 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();

        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        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");
    }複製程式碼

我們在一開始的時候就注意到在onCreate中是可以進行子執行緒UI更新操作的,而在OnResume中是不可以的,我們當時猜測是因為在onResume中ViewRootImpl已經建立初始化完成了,所以能夠進行checkThread檢查,對於此我們需要了解,onResume是在哪裡回撥的,於是我們進入ActivityThread類中,裡面有個方法:

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        //省略無關程式碼...

        // TODO Push resumeArgs into the activity for consideration
        r = performResumeActivity(token, clearHide, reason);

        if (r != null) {
            final Activity a = r.activity;

                //省略無關程式碼...

                r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
        }

            //省略無關程式碼...複製程式碼

對於這一行程式碼

r = performResumeActivity(token, clearHide, reason);

我們可以大概知道這是進行resume回到的,到底是不是呢?我們進去看看,


    public final ActivityClientRecord performResumeActivity(IBinder token,
            boolean clearHide, String reason) {

        if (r != null && !r.activity.mFinished) {

            //省略無關程式碼...
            r.activity.performResume();

    }複製程式碼

我們大概瀏覽一下方法,然後刪除不必要的程式碼,裡面有個方法,

r.activity.performResume();

繼續進入,然後跟蹤看看

final void performResume() {
        performRestart();

        //省略無關程式碼...
        mCalled = false;

        // mResumed is set by the instrumentation
        mInstrumentation.callActivityOnResume(this);


    }複製程式碼

繼續進入方法

mInstrumentation.callActivityOnResume(this);

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());
                }
            }
        }
    }複製程式碼

沒錯了,是在這裡面進行onResume的回撥的,對於此我們可以繼續回到handleResumeActivity方法中去了,接下繼續分析後面的程式碼

            if (r != null) {
                final Activity a = r.activity;

                //省略無關程式碼...

                r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
        }複製程式碼

我們進入到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。

 @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }複製程式碼

進入addview方法檢視

   public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (display == null) {
            throw new IllegalArgumentException("display must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        } else {
            // If there's no parent, then hardware acceleration for this view is
            // set from the application's hardware acceleration setting.
            final Context context = view.getContext();
            if (context != null
                    && (context.getApplicationInfo().flags
                            & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
                wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
            }
        }

        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);
        }

        // 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;
        }
    }複製程式碼

沒錯,就是這個

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

對ViewRootImpl進行初始化,然後進行UI執行緒檢測操作,到了此處,我們程式碼分析基本就結束了,來總結一下原始碼分析的過程。

首先,我們在onCreate中進行子執行緒操作UI,沒有崩潰,而在onResume中進行子執行緒操作UI崩潰了,對於異常log資訊,我們知道,檢測當前執行緒是否是主執行緒的操作是在ViewRootImpl中,此我們猜想,ViewRootImpl的初始化是在onResume中進行的,對於此我們一步步進行跟蹤原始碼進行檢視。從而得出結論是正確的。


參考文章:http://blog.csdn.net/xyh269/article/details/52728861

相關文章