作過android開發基本都碰見過 ViewRootImpl$CalledFromWrongThreadException,上網一查,獲得結果基本都是隻能在主線程中更改 ui,子線程要修改 ui 只能 post 到主線程或者使用 handler 之類。可是仔細看看exception的描述並非這樣的,「Only the original thread that created a view hierarchy can touch its views」,只有建立該 view 佈局層次的原始線程纔可以修改其所屬 view 的佈局屬性,因此「只能在主線程中更改 ui 」這句話自己是有點不嚴謹的,接下來分析一下。javascript
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6498)
at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:954)
at android.view.ViewGroup.invalidateChild(ViewGroup.java:4643)
at android.view.View.invalidateInternal(View.java:11775)
at android.view.View.invalidate(View.java:11739)
at android.view.View.invalidate(View.java:11723)
at android.widget.TextView.checkForRelayout(TextView.java:7002)
at android.widget.TextView.setText(TextView.java:4073)
at android.widget.TextView.setText(TextView.java:3931)
at android.widget.TextView.setText(TextView.java:3906)
at com.android.sample.HomeTestActivity$1.run(HomeTestActivity.java:114)
at java.lang.Thread.run(Thread.java:818)複製代碼
相關博客介紹:
android 不能在子線程中更新ui的討論和分析:Activity 打開的過程分析;
java/android 設計模式學習筆記(9)---代理模式:AMS 的相關類圖和介紹;
android WindowManager解析與騙取QQ密碼案例分析:界面 window 的建立過程;
java/android 設計模式學習筆記(8)---橋接模式:WMS 的相關類圖和介紹;
android IPC通訊(下)-AIDL:AIDL 以及 Binder 的相關介紹;
Android 動態代理以及利用動態代理實現 ServiceHook:ServiceHook 的相關介紹;
Android TransactionTooLargeException 解析,思考與監控方案:TransactionTooLargeException 的解析以及監控方案。java
咱們根據 exception 的 stackTrace 信息,瞭解一下源碼,以 setText 爲例,若是 textview 已經被繪製出來了,調用 setText 函數,會調用到 View 的 invalidate 函數,其中又會調用到 invalidateInternal 函數,接着調用到 parent.invalidateChildInParent 函數,其中 parent 對象就是父控件 ViewGroup,最後會調用到 ViewRootImpl 的 invalidateChildInParent 函數,爲何最後會調用到 ViewRootImpl 類中呢,這裏就須要說到佈局的建立過程了:android
先分析一下 Activity 啓動過程,startActivity 和 startActivityForResult 函數用來啓動一個 activity,最後他們最終都會調用到一個函數windows
public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options)複製代碼
中,接着函數中會調用 Instrumentation 的 execStartActivity 方法,該函數中會調用 ActivityManagerNative.getDefault().startActivity 方法,ActivityManagerNative 類的定義設計模式
public abstract class ActivityManagerNative extends Binder implements IActivityManager複製代碼
該類繼承自 Binder 並實現了 IActivityManager 這個接口,IActivityManager 繼承自 IInterface 接口,用過 AIDL 的應該知道,基本和這個結構類似,因此確定是用來跨進程通訊的,ActivityManagerService 類也是繼承自 ActivityManagerNative 接口,所以 ActivityManagerService 也是一個 Binder 實現子類,他是 IActivityManager 接口的具體實現類,getDefault 函數是經過一個 Singleton 對象對外提供,他最後返回的是 ActivityManagerService 的 IBinder 對象,因此 startActivity 方法最終實現是在 ActivityManagerService 類中(這裏講的比較簡單,若是你們對相關類層次結構和調用方式感興趣的,能夠看看個人博客: java/android 設計模式學習筆記(9)---代理模式,裏面有詳細介紹到):
安全
ActivityThread thread = new ActivityThread();
thread.attach(false);複製代碼
而後 attach 方法:多線程
final ApplicationThread mAppThread = new ApplicationThread();
.....
RuntimeInit.setApplicationObject(mAppThread.asBinder());
final IActivityManager mgr = ActivityManagerNative.getDefault();
try {
mgr.attachApplication(mAppThread);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}複製代碼
能夠看到這裏經過 AIDL 調用,將 ApplicationThread 對象設置進了 AMS 中來做爲 AMS 和 應用進程的橋樑,爲何須要這個 ApplicationThread 橋樑呢,由於 AMS 的職責是管理 Activity 的生命週期和棧,因此不少時候都是 AMS 主動調用到應用進程,不是簡單的一個應用進程調用系統進程 Service 而且返回值的過程,因此必需要讓 AMS 持有一個應用進程的相關對象來進行調用,這個對象就是 ApplicationThread 對象。ApplicationThreadNative 虛類則實現了 IApplicationThread 接口,在該虛類中的 onTransact 函數中,根據 code 不一樣會進行不一樣的操做,最後 ActivityThread 類的內部類 ApplicationThread 繼承自 ApplicationThreadNative 類,最終的實現者就是 ApplicationThread 類,在 ApplicationThreadNative 中根據 code 進行不一樣操做的實現代碼都在 ApplicationThread 類中,這個過程執行到最後會回調到 ApplicationThread 類中的 scheduleLaunchActivity 方法:app
@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
int procState, Bundle state, PersistableBundle persistentState,
List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
updateProcessState(procState, false);
ActivityClientRecord r = new ActivityClientRecord();
....
sendMessage(H.LAUNCH_ACTIVITY, r);
}複製代碼
最終給 H 這個 Handler 類發送了一個 message(關於 H 類能夠去看看博客 Android TransactionTooLargeException 解析,思考與監控方案),其中調用了的 handleLaunchActivity 方法:ide
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
mSomeActivitiesChanged = true;
if (r.profilerInfo != null) {
mProfiler.setProfiler(r.profilerInfo);
mProfiler.startProfiling();
}
// Make sure we are running with the most recent config.
handleConfigurationChanged(null, null);
if (localLOGV) Slog.v(
TAG, "Handling launch of " + r);
// Initialize before creating the activity
WindowManagerGlobal.initialize();
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
reportSizeConfigurations(r);
Bundle oldState = r.state;
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
if (!r.activity.mFinished && r.startsNotResumed) {
// The activity manager actually wants this one to start out paused, because it
// needs to be visible but isn't in the foreground. We accomplish this by going
// through the normal startup (because activities expect to go through onResume()
// the first time they run, before their window is displayed), and then pausing it.
// However, in this case we do -not- need to do the full pause cycle (of freezing
// and such) because the activity manager assumes it can just retain the current
// state it has.
performPauseActivityIfNeeded(r, reason);
// We need to keep around the original state, in case we need to be created again.
// But we only do this for pre-Honeycomb apps, which always save their state when
// pausing, so we can not have them save their state when restarting from a paused
// state. For HC and later, we want to (and can) let the state be saved as the
// normal part of stopping the activity.
if (r.isPreHoneycomb()) {
r.state = oldState;
}
}
} else {
// If there was an error, for any reason, tell the activity manager to stop us.
try {
ActivityManagerNative.getDefault()
.finishActivity(r.token, Activity.RESULT_CANCELED, null,
Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
}複製代碼
這個方法經過 performLaunchActivity 方法獲取到一個 Activity 對象,在 performLaunchActivity 函數中會調用該 activity 的 attach 方法,這個方法把一個 ContextImpl 對象 attach 到了 Activity 中,很是典型的裝飾者模式:函數
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
mUiThread = Thread.currentThread();
....
mLastNonConfigurationInstances = lastNonConfigurationInstances;
if (voiceInteractor != null) {
if (lastNonConfigurationInstances != null) {
mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
} else {
mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
Looper.myLooper());
}
}
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
}複製代碼
window 是經過下面方法獲取的
mWindow = new PhoneWindow(this)複製代碼
建立完 Window 以後,activity 會爲該 Window 設置回調,Window 接收到外界狀態改變時就會回調到 activity 中。在 activity 中會調用 setContentView() 函數,它是調用 window.setContentView() 完成的,最終的具體操做是在 PhoneWindow 中,PhoneWindow 的 setContentView 方法第一步會檢測 DecorView 是否存在,若是不存在,就會調用 generateDecor 函數直接建立一個 DecorView;第二步就是將 activity 的視圖添加到 DecorView 的 mContentParent 中;第三步是回調 activity 中的 onContentChanged 方法通知 activity 視圖已經發生改變。
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Window.Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}複製代碼
這些步驟完成以後,DecorView 尚未被 WindowManager 正式添加到 Window 中,接着會調用到 ActivityThread 類的 handleResumeActivity 方法將頂層視圖 DecorView 添加到 PhoneWindow 窗口,activity 的視圖才能被用戶看到:
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
.....
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);
}
.....
}複製代碼
DecorView 和 Window 的關係代碼中已經很清楚了,接下來分析一下 addView 方法,其中最關鍵的代碼是:
ViewManager wm = a.getWindowManager();
....
wm.addView(decor, l);複製代碼
而 a.getWindowManager 調用到的是 Activity.getWindowManager:
/** Retrieve the window manager for showing custom windows. */
public WindowManager getWindowManager() {
return mWindowManager;
}複製代碼
這個值是在上面的 attach 方法裏面設置的:
mWindow = new PhoneWindow(this);
.....
mWindowManager = mWindow.getWindowManager();複製代碼
因此咱們跟蹤 PhoneWindow 裏面的 getWindowManager 方法:
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow);
}
.....
/** * Set the window manager for use by this Window to, for example, * display panels. This is <em>not</em> used for displaying the * Window itself -- that must be done by the client. * * @param wm The window manager for adding new windows. */
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated
|| SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
if (wm == null) {
wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}
mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}
.....
/** * Return the window manager allowing this Window to display its own * windows. * * @return WindowManager The ViewManager. */
public WindowManager getWindowManager() {
return mWindowManager;
}複製代碼
setWindowManager 函數是在哪裏調用到呢,仍是 Activity.attach 方法:
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0)複製代碼
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE) 這個返回的是什麼呢?咱們先看看 context 對象是什麼,是 attach 函數的第一個參數,好,咱們回到 ActivityThread 類調用 activity.attach 函數的地方:
Context appContext = createBaseContextForActivity(r, activity);
......
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window);複製代碼
看看 createBaseContextForActivity 函數:
private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
int displayId = Display.DEFAULT_DISPLAY;
try {
displayId = ActivityManagerNative.getDefault().getActivityDisplayId(r.token);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
ContextImpl appContext = ContextImpl.createActivityContext(
this, r.packageInfo, r.token, displayId, r.overrideConfig);
appContext.setOuterContext(activity);
Context baseContext = appContext;
final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
// For debugging purposes, if the activity's package name contains the value of
// the "debug.use-second-display" system property as a substring, then show
// its content on a secondary display if there is one.
String pkgName = SystemProperties.get("debug.second-display.pkg");
if (pkgName != null && !pkgName.isEmpty()
&& r.packageInfo.mPackageName.contains(pkgName)) {
for (int id : dm.getDisplayIds()) {
if (id != Display.DEFAULT_DISPLAY) {
Display display =
dm.getCompatibleDisplay(id, appContext.getDisplayAdjustments(id));
baseContext = appContext.createDisplayContext(display);
break;
}
}
}
return baseContext;
}複製代碼
可見,這裏返回的是一個 ContextImpl 對象,並且這個對象會被 Activity 調用 attachBaseContext(context);
方法給設置到 mBase 對象裏面,典型的裝飾者模式,因此最終確定是調用到了 ContextImpl 類的 getSystemService 函數:
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}複製代碼
而後調用到 SystemServiceRegistry.getSystemService 函數,咱們來看看 SystemServiceRegistry 類的相關幾個函數:
private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
new HashMap<String, ServiceFetcher<?>>();
......
static {
registerService(Context.WINDOW_SERVICE, WindowManager.class,
new CachedServiceFetcher<WindowManager>() {
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx);
}});
}
.....
/** * Gets a system service from a given context. */
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
.......
/** * Statically registers a system service with the context. * This method must be called during static initialization only. */
private static <T> void registerService(String serviceName, Class<T> serviceClass,
ServiceFetcher<T> serviceFetcher) {
SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}複製代碼
咱們這裏能夠清楚的看到,SystemServiceRegistry 類中有一個靜態塊代碼,用來註冊因此基本的 Service ,例如 alarm,notification 等等等,其中的 WindowManager 就是經過這個註冊進去的,注意到這裏返回的是一個 WindowManagerImpl 對象,因此 PhoneWindow 的 setWindowManager 函數 的 wm 對象就是 WindowManagerImpl 對象,這就是一個典型的橋接模式,WindowManager 接口繼承自 ViewManager 接口,最終實現類是 WindowManagerImpl 類(感興趣的能夠去看看個人博客: java/android 設計模式學習筆記(8)---橋接模式,其實這裏是有用到橋接模式的):
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);複製代碼
因此這裏的
該類並無直接實現 Window 的三大操做,而是所有交給了 WindowManagerGlobal 來處理,WindowManagerGlobal 以單例模式 的形式向外提供本身的實例,在 WindowManagerImpl 中有以下一段代碼:
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getinstance();複製代碼
因此 WindowManagerImpl 將 addView 操做交給 WindowManagerGlobal 來實現,WindowManagerGlobal 的 addView 函數中建立了一個 ViewRootImpl 對象 root,而後調用 ViewRootImpl 類中的 setView 成員方法:
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
.....
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) {
....
}複製代碼
setView 方法完成了三件事情,將外部參數 DecorView 賦值給 mView 成員變量、標記 DecorView 已添加到 ViewRootImpl、調用 requestLayout 方法請求佈局,那麼繼續跟蹤代碼到 requestLayout() 方法:
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}複製代碼
scheduleTraversals 函數實際是 View 繪製的入口,該方法會經過 WindowSession 使用 IPC 方式調用 WindowManagerService 中的相關方法去添加窗口(這裏我就不作詳細介紹了,感興趣的去看看我上面提到的博客: java/android 設計模式學習筆記(8)---橋接模式 和博客 Android TransactionTooLargeException 解析,思考與監控方案),scheduleTraversals 函數最後會調用到 doTraversal 方法,doTraversal 方法又調用 performTraversals 函數,performTraversals 函數就很是熟悉了,他會去調用 performMeasure,performLayout 和 performDraw 函數去進行 view 的計算和繪製,咱們只是在一個比較高的層次上歸納性地梳理了它的整個脈絡,它的簡化結構:
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}
....
parent = parent.invalidateChildInParent(location, dirty);
....
} while (parent != null);複製代碼
這個問題就差很少清楚了,其餘的能夠再看看老羅的博客:blog.csdn.net/luoshengyan…
上面分析了 Activity 的啓動和佈局建立過程,其中知道 Activity 的建立須要新建一個 ViewRootImpl 對象,看看 ViewRootImpl 的構造函數:
public ViewRootImpl(Context context, Display display) {
.....
mThread = Thread.currentThread();
.....
}複製代碼
在初始化一個 ViewRootImpl 函數的時候,會調用 native 方法,獲取到該線程對象 mThread,接着 setText 函數會調用到 requestLayout 方法(TextView 繪製出來以後,調用 setText 纔會去調用 requestLayout 方法,沒有繪製出來以前,在子線程中調用 setText 是不會拋出 Exception):
public void requestLayout() {
.....
checkThread();
.....
}
....
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}複製代碼
因此如今 「不能在子線程中更新 ui」 的問題已經很清楚了,無論 startActivity 函數調用在什麼線程,ActivityThread 的內部函數執行是在主線程中的:
/** * This manages the execution of the main thread in an * application process, scheduling and executing activities, * broadcasts, and other operations on it as the activity * manager requests. */
public final class ActivityThread {
....
}複製代碼
因此 ViewRootImpl 對象的建立也是在主線程中,這就是說一個 activity 的對應 ViewRootImpl 對象中的 mThread 必定是表明主線程,這就是「爲何不能在子線程中操做 UI 的」答案的解釋,問題解決!!!
可是不是說這個答案不嚴謹麼?是的,可不能夠在子線程中添加 Window,而且建立 ViewRootImpl 呢?固然能夠,在子線程中建立一個 Window 就能夠,思路是在子線程中調用 WindowManager 添加一個 view,相似於
windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.height = WindowManager.LayoutParams.MATCH_PARENT;
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
params.format = PixelFormat.TRANSPARENT;
params.gravity = Gravity.CENTER;
params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
....
windowManager.addView(v, params);複製代碼
android WindowManager解析與騙取QQ密碼案例分析博客中介紹到 activity 和 dialog 不是系統層級的 Window,咱們可使用 WindowManager 來添加自定義的系統 Window,那麼問題又來了,系統級別 Window 是怎麼添加的呢,老羅的另外一篇博客 blog.csdn.net/luoshengyan… 中介紹到: 「對於非輸入法窗口、非壁紙窗口以及非 Activity 窗口來講,它們所對應的 WindowToken 對象是在它們增長到 WindowManagerService 服務的時候建立的......若是參數 attrs 所描述的一個 WindowManager.LayoutParams 對象的成員變量 token 所指向的一個 IBinder 接口在 WindowManagerService 類的成員變量 mTokenMap 所描述的一個 HashMap 中沒有一個對應的 WindowToken 對象,而且該 WindowManager.LayoutParams 對象的成員變量 type 的值不等於 TYPE_INPUT_METHOD、TYPE_WALLPAPER,以及不在FIRST_APPLICATION_WINDOW 和LAST_APPLICATION_WINDOW,那麼就意味着這時候要增長的窗口就既不是輸入法窗口,也不是壁紙窗口和 Activity 窗口,所以,就須要以參數 attrs 所描述的一個 WindowManager.LayoutParams 對象的成員變量 token 所指向的一個 IBinder 接口爲參數來建立一個 WindowToken 對象,而且將該 WindowToken對象保存在 WindowManagerService 類的成員變量 mTokenMap 和 mTokenList 中。」。
瞭解上面以後,換一種思路,就能夠在子線程中建立 view 而且添加到 windowManager 中。
有了思路以後,既能夠來實現相關代碼了:
new Thread(new Runnable() {
@Override
public void run() {
showWindow();
}
}).start();
......
private void showWindow(){
windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.height = WindowManager.LayoutParams.MATCH_PARENT;
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
params.format = PixelFormat.TRANSPARENT;
params.gravity = Gravity.CENTER;
params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
LayoutInflater inflater = LayoutInflater.from(this);
v = (RelativeLayoutWithKeyDetect) inflater.inflate(R.layout.window, null);
.....
windowManager.addView(v, params);
}複製代碼
運行一下,報錯:
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:200)
at android.os.Handler.<init>(Handler.java:114)
at android.view.ViewRootImpl$ViewRootHandler.<init>(ViewRootImpl.java:3185)
at android.view.ViewRootImpl.<init>(ViewRootImpl.java:3483)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:261)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
at com.android.grabqqpwd.BackgroundDetectService.showWindow(BackgroundDetectService.java:208)
at com.android.grabqqpwd.BackgroundDetectService.access$100(BackgroundDetectService.java:39)
at com.android.grabqqpwd.BackgroundDetectService$1.run(BackgroundDetectService.java:67)
at java.lang.Thread.run(Thread.java:818)複製代碼
這是由於 ViewRootImpl 類內部會新建一個 ViewRootHandler 類型的 mHandler 用來處理相關消息,因此若是線程沒有 Looper 是會報錯的,添加 Looper,修改代碼:
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
showWindow();
handler = new Handler(){
@Override
public void dispatchMessage(Message msg) {
Looper.myLooper().quit();
L.e("quit");
}
};
Looper.loop();
}
}).start();複製代碼
建立 Looper 以後,須要在必要時候調用 quit 函數將其退出。這樣就成功顯示了
爲何 android 會設計成只有建立 ViewRootImpl 的原始線程才能更改 ui 呢?這就要說到 Android 的單線程模型了,由於若是支持多線程修改 View 的話,由此產生的線程同步和線程安全問題將是很是繁瑣的,因此 Android 直接就定死了,View 的操做必須在建立它的 UI 線程,從而簡化了系統設計。 有沒有能夠在其餘非原始線程更新 ui 的狀況呢?有,SurfaceView 就能夠在其餘線程更新,具體的你們能夠去網上了解一下相關資料。