在子线程中更新view的问题

在子线程中直接更新一个view,比如设置一个textView的文字

new Thread(new Runnable() {
            @Override
            public void run() {
                textView.setText("我是子线程中的view");
            }
        }).start();
一般情况下会抛出异常:

03-30 14:44:49.275: E/AndroidRuntime(14685): android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
03-30 14:44:49.275: E/AndroidRuntime(14685): 	at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7315)
03-30 14:44:49.275: E/AndroidRuntime(14685): 	at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1113)
03-30 14:44:49.275: E/AndroidRuntime(14685): 	at android.view.ViewGroup.invalidateChild(ViewGroup.java:5219)
03-30 14:44:49.275: E/AndroidRuntime(14685): 	at android.view.View.invalidateInternal(View.java:12893)
03-30 14:44:49.275: E/AndroidRuntime(14685): 	at android.view.View.invalidate(View.java:12853)
看上面的异常调用链,可以知道该异常是ViewRootImpl类的checkThread抛出的,看源码:

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }
检测当前线程:Thread.currentThread(也就是执行view.invalidate线程,在这里就是我们自己new出来的线程)和ViewRootImpl这个对象所在的线程(mThread,在这里就是我们的主线程)是否是一致的,不一致就抛出异常。(其实只是检测当前更新view的线程和创建ViewRootImpl的线程是不是一致,并非一定要是主线程,在子线程创建ViewRootImpl和在当前这个线程中执行view的更新操作也是不会出错的,这也就是为什么android的UI操作是单线程模型了)。


那么mThread这个线程是什么时候赋值的呢?在ViewRootImpl的构造函数里面赋的值

public ViewRootImpl(Context context, Display display) {
        //省略一些

        mThread = Thread.currentThread();
// 省略一些
}
也就是,一旦创建了ViewRootImpl实例,那么就有了mThread。而这个ViewRootImpl实例往往是在我们的主线程中创建的,所以,这个mThread必然就是主线程变量。
那么ViewRootImpl这个实例又是在什么时候创建的呢?相对于我们的主线程,找到ActivityThread的handleResumeActivity这个入口,这个入口是被系统调用的,执行在activity的onCreate方法的后面。进入这个函数体看下:

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;

            if (localLOGV) Slog.v(
                TAG, "Resume " + r + " started activity: " +
                a.mStartedActivity + ", hideForNow: " + r.hideForNow
                + ", finished: " + a.mFinished);

            final int forwardBit = isForward ?
                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

            // If the window hasn't yet been added to the window manager,
            // and this guy didn't finish itself or start another activity,
            // then go ahead and add the window.
            boolean willBeVisible = !a.mStartedActivity;
            if (!willBeVisible) {
                try {
                    willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
                            a.getActivityToken());
                } catch (RemoteException e) {
                }
            }
            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 (a.mVisibleFromClient) {
                    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;
            }

            // Get rid of anything left hanging around.
            cleanUpPendingRemoveWindows(r);

            // The window is now visible if it has been added, we are not
            // simply finishing, and we are not starting another activity.
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                if (r.newConfig != null) {
                    r.tmpConfig.setTo(r.newConfig);
                    if (r.overrideConfig != null) {
                        r.tmpConfig.updateFrom(r.overrideConfig);
                    }
                    if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "
                            + r.activityInfo.name + " with newConfig " + r.tmpConfig);
                    performConfigurationChanged(r.activity, r.tmpConfig);
                    freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig));
                    r.newConfig = null;
                }
                if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward="
                        + isForward);
                WindowManager.LayoutParams l = r.window.getAttributes();
                if ((l.softInputMode
                        & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
                        != forwardBit) {
                    l.softInputMode = (l.softInputMode
                            & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
                            | forwardBit;
                    if (r.activity.mVisibleFromClient) {
                        ViewManager wm = a.getWindowManager();
                        View decor = r.window.getDecorView();
                        wm.updateViewLayout(decor, l);
                    }
                }
                r.activity.mVisibleFromServer = true;
                mNumVisibleActivities++;
                if (r.activity.mVisibleFromClient) {
                    r.activity.makeVisible();
                }
            }

            if (!r.onlyLocalRequest) {
                r.nextIdle = mNewActivities;
                mNewActivities = r;
                if (localLOGV) Slog.v(
                    TAG, "Scheduling idle handler for " + r);
                Looper.myQueue().addIdleHandler(new Idler());
            }
            r.onlyLocalRequest = false;

            // Tell the activity manager we have resumed.
            if (reallyResume) {
                try {
                    ActivityManagerNative.getDefault().activityResumed(token);
                } catch (RemoteException ex) {
                }
            }

        } else {
            // If an exception was thrown when trying to resume, then
            // just end this activity.
            try {
                ActivityManagerNative.getDefault()
                    .finishActivity(token, Activity.RESULT_CANCELED, null, false);
            } catch (RemoteException ex) {
            }
        }
    }
在最开始的时候会去调用performResumeActivity方法,该方法最终会调用activity的onResume方法,调用的大致流程是这样的:

performResumeActivity--> 内部调用r.activity.performResume()---> 切换到activity的performResume方法,其内部调用mInstrumentation.callActivityOnResume(this)---> 切换到Instrumentation的callActivityOnResume内部,调用了activity.onResume()

接着,再看handleResumeActivity这个方法,在其中间部分有这么一个调用:

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);
    }
首先,获取窗口管理者,然后把我们的decorView(也就是整个界面的根布局)加入其中,再这个方法里面就会去创建ViewRootImpl实例了。

这里ViewManager是个接口,具体实现类是ViewManagerImpl,我们进入这个类的addView方法看看:

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
mGlobal是个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);
        }

      //省略
    }
看,这里面创建了ViewRootImpl实例。

所以说,在执行完activity的onResume后才有了ViewRootImpl实例,那么在这个之后在子线程中更新主线程里面的view的话,就会抛异常了。
从handleResumeActivity方法里面还可以看到,在执行完activity的onResume后才会去执行makeVisible方法,这个方法最终会调用decorView的setVisible方法使整个视图呈现给用户,也就是说当我们执行完onResume方法后,未必就能马上看见视图。

从上面我们得知,如果我们在onResume之前去子线程更新view,由于我们的ViewRootImpl还没创建,整个view树也还没关联到WindowManager中,这个时候也就不会去执行checkThread检查,那么也就不会抛出异常,比如,我们在onCreate这么做:

new Thread(new Runnable() {
            public void run() {
                /*try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }*/
                TextView textView = new TextView(MainActivity.this);
                textView.setText("我是子线程中的view");
                setContentView(textView);
            }
        }).start();
其实,这里仅仅是将textView这个对象加入到了decorView树中,并设置了textView的text属性,在这个线程执行完成后,decorView还没添加到WindowManager中,所以也就不存在绘制,仅仅是保存了这些对象的状态而已。

一旦执行完onResume后,会去把decorView树添加到WM中,这个时候就开始绘制了,就把在onCreate里面textView设置的text属性绘制出来了。这里,所谓的在子线程里面更新view其实是个假象。真正的绘制还是在主线程中完成的。

我们试着把注释去掉,那绝逼要抛异常了。

以上,异常的检测都是依赖于ViewRootImpl的创建线程和当前view的刷新线程是否一致。那么我们也可以完全自己新开一个线程来完成ViewRootImpl的创建,和在当前线程更新view。必要条件是我们要把viewRootImpl实例添加到windowManager中。

//这个示例说明,子线程也是可以创建更新view的,主要是子线程中创建了一个ViewRootImpl实例
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);//需要一个延时,否则会报:
                    /*android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
                at android.view.ViewRootImpl.setView(ViewRootImpl.java:685)
                at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:319)
                at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
                at com.txt.mydemo.ChildThreadUiActivity$1.run(ChildThreadUiActivity.java:47)
                at java.lang.Thread.run(Thread.java:818)*/
                    //添加的窗口需要依附于一个已经创建好的activity,延迟1s。创建时机是?
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Looper.prepare();//必须的,因为在windowManager的addView调用链里面会有创建ViewRootImpl对象,
                // 在ViewRootImpl里面有个继承自Handler的ViewRootHandler会被创建。如果不执行,会有如下错误:
                /*java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
                at android.os.Handler.(Handler.java:209)
                at android.os.Handler.(Handler.java:123)
                at android.view.ViewRootImpl$ViewRootHandler.(ViewRootImpl.java:3644)
                at android.view.ViewRootImpl.(ViewRootImpl.java:3953)
                at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:306)
                at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
                at com.txt.mydemo.ChildThreadUiActivity$1.run(ChildThreadUiActivity.java:35)
                at java.lang.Thread.run(Thread.java:818)*/
                TextView textView = new TextView(ChildThreadUiActivity.this);
                textView.setText("我是子线程中的view");
                WindowManager windowManager = ChildThreadUiActivity.this.getWindowManager();
                WindowManager.LayoutParams params = new WindowManager.LayoutParams(200, 200, 200, 200,
                        WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                        WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
                windowManager.addView(textView,params);
                Looper.loop();
            }
        }).start();
不过,这样做,貌似没有什么实际意义。

你可能感兴趣的:(基础知识,疑难杂症)