若是你在網上搜索CalledFromWrongThreadException:Only the original thread that created a view hierarchy can touch its views. 那麼你確定能看到不少文章說android裏子線程不能刷新UI。這句話不能說錯,只是有些不太嚴謹。其實線程可否刷新UI的關鍵在於ViewRoot是否屬於該線程。 java
讓咱們一塊兒看看代碼吧! android
首先,CalledFromWrongThreadException這個異常是有下面的代碼拋出的: ide
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
該段代碼出自 framework/base/core/java/android/view/ViewRoot.java 函數
其次,看看RootView的構造函數: oop
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的調用順序: this
com.david.test.helloworld.MainActivity$TestThread2.run spa
-> android.widget.TextView.setText 線程
-> android.widget.TextView.checkForRelayout debug
-> android.view.View.invalidate code
-> 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的!!!