在子线程中直接更新一个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必然就是主线程变量。
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实例。
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();
不过,这样做,貌似没有什么实际意义。