android 关于子线程更新 UI 的一些事

1,914 阅读7分钟
原文链接: www.jianshu.com

github: github.com/crazyandcod…
博客: crazyandcoder.github.io

第一次写关于源代码方面的内容,如果错误欢迎指正!

正文

我们在看一些书或者博客时总是会看到一句话“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日志


其中有一句经典的异常信息:

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

        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

没错了,是在这里面进行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