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

我们在看一些书或者博客时总是会看到一句话“android更新UI操作都是在Main主线程中,子线程中不能进行UI更新操作”那么,在子线程中真的不能进行UI的更新操作吗?

1
2
3
4
5
6
7
8
9
//源码环境申明
compileSdkVersion 24
buildToolsVersion "24.0.2"
defaultConfig {
minSdkVersion 14
targetSdkVersion 24
}

首先我们来看一段代码:

在onCreate生命周期中添加子线程更新UI操作

1
2
3
4
5
6
7
8
9
10
11
12
@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操作

1
2
3
4
5
6
7
8
9
10
@Override
protected void onResume() {
super.onResume();
new Thread(new Runnable() {
@Override
public void run() {
text.setText("子线程刷新UI");
}
}).start();
}

然后运行一下APP,发现这次APP崩溃了,打印一下Log日志

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

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

相信这句话大家应该基本都碰到过吧,这又是怎么回事呢?

为什么在onCreate中更新没事,而在onResume中更新就崩溃了?

带着这个问题,我们来跟踪一下源码,然后给出答案!

首先我们来看一下崩溃日志,一般情况下我们都可以通过崩溃日志来追踪问题。我们通过异常栈来一步一步追踪根源。

1
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

第一句显示说明异常出现的原因:只有主线程才能进行更新UI操作。接下来一句:

1
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:5288)

我们进入到ViewRootImpl.checkThread方法中

Tip: 对于AndroidStudioIDE我们可以通过双击shift键来查找相关的文件

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

对于checkThread()方法,只有简单的几行代码,只是进行常规检查,如果当前线程不是主线程的话则会抛出异常。而mThread 这个变量是在构造函数中进行初始化的(记住,后面需要用到)。

1
2
3
4
5
6
7
8
public ViewRootImpl(Context context, Display display) {
//省略无关代码
//...
mThread = Thread.currentThread();
//...
//省略无关代码
}

上面只是简单的进行判断当前线程是否是主线程,没什么价值,我们继续看第三行异常log信息

1
at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:974)

进入到该方法中,查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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()中看看,

1
2
3
4
5
6
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}

也没什么,继续进入scheduleTraversals()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
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
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

里面有一个参数 mTraversalRunnable,runnable不是线程的意思嘛,我们开启子线程经常使用到的,我们跟踪一下,看看究竟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
好像也没什么多少有用信息,我们进入 **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");
}
1
2
3
我们在一开始的时候就注意到在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();
            }
    }

        //省略无关代码...
1
2
3
4
5
6
对于这一行代码
> 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();

}
1
2
3
4
5
6
我们大概浏览一下方法,然后删除不必要的代码,里面有个方法,
> r.activity.performResume();
继续进入,然后跟踪看看

final void performResume() {
performRestart();

    //省略无关代码...
    mCalled = false;

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


}
1
2
3
继续进入方法
> 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());
            }
        }
    }
}
1
没错了,是在这里面进行onResume的回调的,对于此我们可以继续回到**handleResumeActivity**方法中去了,接下继续分析后面的代码
    if (r != null) {
        final Activity a = r.activity;

        //省略无关代码...

        r.activity.mVisibleFromServer = true;
        mNumVisibleActivities++;
        if (r.activity.mVisibleFromClient) {
            r.activity.makeVisible();
        }
}
1
2
我们进入到makeVisible方法中去看看

void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}

1
2
往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);
}

1
2
进入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