Android在4.0之後執行線程更新UI操作會報異常:CalledFromWrongThreadException:Only the original thread that created a view hierarchy can touch its views.那麼你肯定能看到很多文章說android裡子線程不能刷新UI。這句話不能說錯,只是有些不太嚴謹。其實線程能否刷新UI的關鍵在於ViewRoot是否屬於該線程。
首先,CalledFromWrongThreadException這個異常是有下面的代碼拋出的:
復制代碼
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
其次,看看RootView的構造函數:
復制代碼
public ViewRoot(Context context) {
super();
if (MEASURE_LATENCY && lt == null) {
lt = new LatencyTimer(100, 1000);
}
// For debug only
//++sInstanceCount;
// Initialize the statics when this class is first instantiated. This is
// done here instead of in the static block because Zygote does not
// allow the spawning of threads.
getWindowSession(context.getMainLooper());
mThread = Thread.currentThread();
mLocation = new WindowLeaked(null);
mLocation.fillInStackTrace();
mWidth = -1;
mHeight = -1;
mDirty = new Rect();
mTempRect = new Rect();
mVisRect = new Rect();
mWinFrame = new Rect();
mWindow = new W(this, context);
mInputMethodCallback = new InputMethodCallback(this);
mViewVisibility = View.GONE;
mTransparentRegion = new Region();
mPreviousTransparentRegion = new Region();
mFirst = true; // true for the first time the view is added
mAdded = false;
mAttachInfo = new View.AttachInfo(sWindowSession, mWindow, this, this);
mViewConfiguration = ViewConfiguration.get(context);
mDensity = context.getResources().getDisplayMetrics().densityDpi;
}
復制代碼
最後,我們看看ViewRoot.checkThread的調用順序:
復制代碼
com.david.test.helloworld.MainActivity$TestThread2.run
-> android.widget.TextView.setText
-> android.widget.TextView.checkForRelayout
-> android.view.View.invalidate
-> android.view.ViewGroup.invalidateChild
-> android.view.ViewRoot.invalidateChildInParent
-> android.view.ViewRoot.invalidateChild
-> android.view.ViewRoot.checkThread
復制代碼
到這裡相信網友已經明白CalledFromWrongThreadException為什麼出現了。那到底非主線程以外的線程能否刷新UI呢?答案當然是能,前提條件是它要擁有自己的ViewRoot。如果你要直接創建ViewRoot的實例的話,你會失望的發現不能找到這個類。那麼我們要如何做呢?讓我們用實例來說說吧,代碼如下:
復制代碼
class TestThread1 extends Thread{
@Override
public void run() {
Looper.prepare();
TextView tx = new TextView(MainActivity.this);
tx.setText("test11111111111111111");
WindowManager wm = MainActivity.this.getWindowManager();
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
250, 250, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.TYPE_TOAST,PixelFormat.OPAQUE);
wm.addView(tx, params);
Looper.loop();
}
}
復制代碼
MainActivity是建立android工程時生成的入口類,TestThread1是MainActivity的內部類。感興趣的話,試試吧!看看是不是在屏幕上看到了"test11111111111111111"?
最後,說說那裡創建了ViewRoot,這裡:wm.addView(tx, params)。還是看看具體流程吧:
WindowManagerImpl.addView(View view, ViewGroup.LayoutParams params)
-> WindowManagerImpl.addView(View view, ViewGroup.LayoutParams params, boolean nest)
奧妙就在這裡,具體看看代碼吧!
復制代碼
private void addView(View view, ViewGroup.LayoutParams params, boolean nest)
{
if (Config.LOGV) Log.v("WindowManager", "addView view=" + view);
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException(
"Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams
= (WindowManager.LayoutParams)params;
ViewRoot root;
View panelParentView = null;
synchronized (this) {
// Here's an odd/questionable case: if someone tries to add a
// view multiple times, then we simply bump up a nesting count
// and they need to remove the view the corresponding number of
// times to have it actually removed from the window manager.
// This is useful specifically for the notification manager,
// which can continually add/remove the same view as a
// notification gets updated.
int index = findViewLocked(view, false);
if (index >= 0) {
if (!nest) {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
root = mRoots[index];
root.mAddNesting++;
// Update layout parameters.
view.setLayoutParams(wparams);
root.setLayoutParams(wparams, true);
return;
}
// 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 != null ? mViews.length : 0;
for (int i=0; i<count; i++) {
if (mRoots[i].mWindow.asBinder() == wparams.token) {
panelParentView = mViews[i];
}
}
}
root = new ViewRoot(view.getContext());
root.mAddNesting = 1;
view.setLayoutParams(wparams);
if (mViews == null) {
index = 1;
mViews = new View[1];
mRoots = new ViewRoot[1];
mParams = new WindowManager.LayoutParams[1];
} else {
index = mViews.length + 1;
Object[] old = mViews;
mViews = new View[index];
System.arraycopy(old, 0, mViews, 0, index-1);
old = mRoots;
mRoots = new ViewRoot[index];
System.arraycopy(old, 0, mRoots, 0, index-1);
old = mParams;
mParams = new WindowManager.LayoutParams[index];
System.arraycopy(old, 0, mParams, 0, index-1);
}
index--;
mViews[index] = view;
mRoots[index] = root;
mParams[index] = wparams;
// do this last because it fires off messages to start doing things
root.setView(view, wparams, panelParentView);
}
復制代碼
出自:frameworks/base/core/java/android/view/WindowManagerImpl.java
Ok,相信到了這裡,大家都已經明白了:子線程是能夠刷新UI的!!!
復制代碼
public class TestActivity extends Activity {
Button btn = null;
/** Called when the activity is first created. */
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btn = (Button) findViewById(R.id.Button01);
TestThread2 t = new TestThread2(btn);
t.start();
}
class TestThread2 extends Thread {
Button btn = null;
public TestThread2(Button btn) {
this.btn = btn;
}
@Override
public void run() {
btn.setText("TestThread2.run");
}
}
}
復制代碼
建立一個工程,將上述代碼拷貝進去,運行看看吧! Btn的文本一定改變為"TestThread2.run"了。
那麼這到底是怎麼回事呢?當我發現這個問題時,也困惑了。經過一番調查後,真相大白。現在和大家分享一下。奧秘在於ViewRoot的建立時間,它是在ActivityThread.java的final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward)裡創建的。
看看代碼吧:
復制代碼
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
ActivityRecord 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;
}
// 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) {
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "
+ r.activityInfo.name + " with newConfig " + r.newConfig);
performConfigurationChanged(r.activity, r.newConfig);
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();
}
}
r.nextIdle = mNewActivities;
mNewActivities = r;
if (localLOGV) Slog.v(
TAG, "Scheduling idle handler for " + r);
Looper.myQueue().addIdleHandler(new Idler());
} else {
// If an exception was thrown when trying to resume, then
// just end this activity.
try {
ActivityManagerNative.getDefault()
.finishActivity(token, Activity.RESULT_CANCELED, null);
} catch (RemoteException ex) {
}
}
}
復制代碼
在Activity.onResume前,ViewRoot實例沒有建立,所以沒有ViewRoot.checkThread檢查。而btn.setText時設定的文本卻保留了下來,所以當ViewRoot真正去刷新界面時,就把"TestThread2.run"刷了出來!
注意onStart,因為onStart在oncreate->onStart->onResume過程中,子線程刷新UI沒問題的,但是在onPause->onRestart->onStart過程中,就有問題了。