問題描述
做過android開發(fā)基本都遇見過 ViewRootImpl$CalledFromWrongThreadException民效,上網(wǎng)一查控乾,得到結(jié)果基本都是只能在主線程中更改 ui,子線程要修改 ui 只能 post 到主線程或者使用 handler 之類。但是仔細(xì)看看exception的描述并不是這樣的,“Only the original thread that created a view hierarchy can touch its views”勘畔,只有創(chuàng)建該 view 布局層次的原始線程才能夠修改其所屬 view 的布局屬性,所以“只能在主線程中更改 ui ”這句話本身是有點(diǎn)不嚴(yán)謹(jǐn)?shù)睦龌蹋酉聛矸治鲆幌隆?/p>
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)
相關(guān)博客介紹:
android 不能在子線程中更新ui的討論和分析:Activity 打開的過程分析;
java/android 設(shè)計(jì)模式學(xué)習(xí)筆記(9)---代理模式:AMS 的相關(guān)類圖和介紹爬立;
android WindowManager解析與騙取QQ密碼案例分析:界面 window 的創(chuàng)建過程钾唬;
java/android 設(shè)計(jì)模式學(xué)習(xí)筆記(8)---橋接模式:WMS 的相關(guān)類圖和介紹;
android IPC通信(下)-AIDL:AIDL 以及 Binder 的相關(guān)介紹;
Android 動(dòng)態(tài)代理以及利用動(dòng)態(tài)代理實(shí)現(xiàn) ServiceHook:ServiceHook 的相關(guān)介紹抡秆;
Android TransactionTooLargeException 解析奕巍,思考與監(jiān)控方案:TransactionTooLargeException 的解析以及監(jiān)控方案。
問題分析
我們根據(jù) exception 的 stackTrace 信息儒士,了解一下源碼的止,以 setText 為例,如果 textview 已經(jīng)被繪制出來了着撩,調(diào)用 setText 函數(shù)诅福,會(huì)調(diào)用到 View 的 invalidate 函數(shù),其中又會(huì)調(diào)用到 invalidateInternal 函數(shù)拖叙,接著調(diào)用到 parent.invalidateChildInParent 函數(shù)氓润,其中 parent 對(duì)象就是父控件 ViewGroup,最后會(huì)調(diào)用到 ViewRootImpl 的 invalidateChildInParent 函數(shù)薯鳍,為什么最后會(huì)調(diào)用到 ViewRootImpl 類中呢咖气,這里就需要說到布局的創(chuàng)建過程了:
Activity的啟動(dòng)和布局創(chuàng)建過程
先分析一下 Activity 啟動(dòng)過程,startActivity 和 startActivityForResult 函數(shù)用來啟動(dòng)一個(gè) activity挖滤,最后他們最終都會(huì)調(diào)用到一個(gè)函數(shù)
public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options)
中崩溪,接著函數(shù)中會(huì)調(diào)用 Instrumentation 的 execStartActivity 方法,該函數(shù)中會(huì)調(diào)用 ActivityManagerNative.getDefault().startActivity 方法斩松,ActivityManagerNative 類的定義
public abstract class ActivityManagerNative extends Binder implements IActivityManager
該類繼承自 Binder 并實(shí)現(xiàn)了 IActivityManager 這個(gè)接口伶唯,IActivityManager 繼承自 IInterface 接口,用過 AIDL 的應(yīng)該知道砸民,基本和這個(gè)結(jié)構(gòu)相似抵怎,所以肯定是用來跨進(jìn)程通信的,ActivityManagerService 類也是繼承自 ActivityManagerNative 接口岭参,因此 ActivityManagerService 也是一個(gè) Binder 實(shí)現(xiàn)子類反惕,他是 IActivityManager 接口的具體實(shí)現(xiàn)類,getDefault 函數(shù)是通過一個(gè) Singleton 對(duì)象對(duì)外提供演侯,他最后返回的是 ActivityManagerService 的 IBinder 對(duì)象姿染,所以 startActivity 方法最終實(shí)現(xiàn)是在 ActivityManagerService 類中(這里講的比較簡單,如果大家對(duì)相關(guān)類層次結(jié)構(gòu)和調(diào)用方式感興趣的秒际,可以看看我的博客: java/android 設(shè)計(jì)模式學(xué)習(xí)筆記(9)---代理模式悬赏,里面有詳細(xì)介紹到):
接著進(jìn)行完一系列的操作之后會(huì)回調(diào)到 IApplicationThread 中,這個(gè)接口也是繼承自 IInterface 接口娄徊,它是作為服務(wù)端接收 AMS 的指令并且執(zhí)行闽颇,是 ActivityThread 與 AMS 鏈接的橋梁,這個(gè)類是在哪作為橋梁的呢寄锐,在應(yīng)用剛啟動(dòng)的時(shí)候會(huì)調(diào)用 ActivityThread.main 函數(shù)(具體的可以看看博客:Android TransactionTooLargeException 解析兵多,思考與監(jiān)控方案)尖啡,在 main 函數(shù)中會(huì)調(diào)用 :
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 調(diào)用,將 ApplicationThread 對(duì)象設(shè)置進(jìn)了 AMS 中來作為 AMS 和 應(yīng)用進(jìn)程的橋梁剩膘,為什么需要這個(gè) ApplicationThread 橋梁呢衅斩,因?yàn)?AMS 的職責(zé)是管理 Activity 的生命周期和棧,所以很多時(shí)候都是 AMS 主動(dòng)調(diào)用到應(yīng)用進(jìn)程怠褐,不是簡單的一個(gè)應(yīng)用進(jìn)程調(diào)用系統(tǒng)進(jìn)程 Service 并且返回值的過程畏梆,所以必須要讓 AMS 持有一個(gè)應(yīng)用進(jìn)程的相關(guān)對(duì)象來進(jìn)行調(diào)用,這個(gè)對(duì)象就是 ApplicationThread 對(duì)象奈懒。ApplicationThreadNative 虛類則實(shí)現(xiàn)了 IApplicationThread 接口奠涌,在該虛類中的 onTransact 函數(shù)中,根據(jù) code 不同會(huì)進(jìn)行不同的操作筐赔,最后 ActivityThread 類的內(nèi)部類 ApplicationThread 繼承自 ApplicationThreadNative 類铣猩,最終的實(shí)現(xiàn)者就是 ApplicationThread 類,在 ApplicationThreadNative 中根據(jù) code 進(jìn)行不同操作的實(shí)現(xiàn)代碼都在 ApplicationThread 類中茴丰,這個(gè)過程執(zhí)行到最后會(huì)回調(diào)到 ApplicationThread 類中的 scheduleLaunchActivity 方法:
@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 這個(gè) Handler 類發(fā)送了一個(gè) message(關(guān)于 H 類可以去看看博客 Android TransactionTooLargeException 解析达皿,思考與監(jiān)控方案),其中調(diào)用了的 handleLaunchActivity 方法:
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();
}
}
}
這個(gè)方法通過 performLaunchActivity 方法獲取到一個(gè) Activity 對(duì)象贿肩,在 performLaunchActivity 函數(shù)中會(huì)調(diào)用該 activity 的 attach 方法峦椰,這個(gè)方法把一個(gè) ContextImpl 對(duì)象 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)
創(chuàng)建完 Window 之后汰规,activity 會(huì)為該 Window 設(shè)置回調(diào)汤功,Window 接收到外界狀態(tài)改變時(shí)就會(huì)回調(diào)到 activity 中。在 activity 中會(huì)調(diào)用 setContentView() 函數(shù)溜哮,它是調(diào)用 window.setContentView() 完成的滔金,最終的具體操作是在 PhoneWindow 中,PhoneWindow 的 setContentView 方法第一步會(huì)檢測(cè) DecorView 是否存在茂嗓,如果不存在餐茵,就會(huì)調(diào)用 generateDecor 函數(shù)直接創(chuàng)建一個(gè) DecorView;第二步就是將 activity 的視圖添加到 DecorView 的 mContentParent 中述吸;第三步是回調(diào) activity 中的 onContentChanged 方法通知 activity 視圖已經(jīng)發(fā)生改變忿族。
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 中蝌矛,接著會(huì)調(diào)用到 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 的關(guān)系代碼中已經(jīng)很清楚了,接下來分析一下 addView 方法入撒,其中最關(guān)鍵的代碼是:
ViewManager wm = a.getWindowManager();
....
wm.addView(decor, l);
而 a.getWindowManager 調(diào)用到的是 Activity.getWindowManager:
/** Retrieve the window manager for showing custom windows. */
public WindowManager getWindowManager() {
return mWindowManager;
}
這個(gè)值是在上面的 attach 方法里面設(shè)置的:
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 函數(shù)是在哪里調(diào)用到呢隆豹,還是 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) 這個(gè)返回的是什么呢?我們先看看 context 對(duì)象是什么茅逮,是 attach 函數(shù)的第一個(gè)參數(shù)噪伊,好簿煌,我們回到 ActivityThread 類調(diào)用 activity.attach 函數(shù)的地方:
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 函數(shù):
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;
}
可見,這里返回的是一個(gè) ContextImpl 對(duì)象鉴吹,而且這個(gè)對(duì)象會(huì)被 Activity 調(diào)用 attachBaseContext(context);
方法給設(shè)置到 mBase 對(duì)象里面,典型的裝飾者模式惩琉,所以最終肯定是調(diào)用到了 ContextImpl 類的 getSystemService 函數(shù):
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
然后調(diào)用到 SystemServiceRegistry.getSystemService 函數(shù)豆励,我們來看看 SystemServiceRegistry 類的相關(guān)幾個(gè)函數(shù):
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 類中有一個(gè)靜態(tài)塊代碼瞒渠,用來注冊(cè)所以基本的 Service 良蒸,例如 alarm,notification 等等等伍玖,其中的 WindowManager 就是通過這個(gè)注冊(cè)進(jìn)去的嫩痰,注意到這里返回的是一個(gè) WindowManagerImpl 對(duì)象,所以 PhoneWindow 的 setWindowManager 函數(shù) 的 wm 對(duì)象就是 WindowManagerImpl 對(duì)象窍箍,這就是一個(gè)典型的橋接模式串纺,WindowManager 接口繼承自 ViewManager 接口,最終實(shí)現(xiàn)類是 WindowManagerImpl 類(感興趣的可以去看看我的博客: java/android 設(shè)計(jì)模式學(xué)習(xí)筆記(8)---橋接模式椰棘,其實(shí)這里是有用到橋接模式的):
而 PhoneWindow 的 setWindowManager 則是在上面的 Activity.attach 方法中調(diào)用到的:
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
所以這里的
該類并沒有直接實(shí)現(xiàn) Window 的三大操作纺棺,而是全部交給了 WindowManagerGlobal 來處理,WindowManagerGlobal 以單例模式 的形式向外提供自己的實(shí)例邪狞,在 WindowManagerImpl 中有如下一段代碼:
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getinstance();
所以 WindowManagerImpl 將 addView 操作交給 WindowManagerGlobal 來實(shí)現(xiàn)祷蝌,WindowManagerGlobal 的 addView 函數(shù)中創(chuàng)建了一個(gè) ViewRootImpl 對(duì)象 root,然后調(diào)用 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 方法完成了三件事情帆卓,將外部參數(shù) DecorView 賦值給 mView 成員變量巨朦、標(biāo)記 DecorView 已添加到 ViewRootImpl、調(diào)用 requestLayout 方法請(qǐng)求布局剑令,那么繼續(xù)跟蹤代碼到 requestLayout() 方法:
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
scheduleTraversals 函數(shù)實(shí)際是 View 繪制的入口糊啡,該方法會(huì)通過 WindowSession 使用 IPC 方式調(diào)用 WindowManagerService 中的相關(guān)方法去添加窗口(這里我就不做詳細(xì)介紹了,感興趣的去看看我上面提到的博客: java/android 設(shè)計(jì)模式學(xué)習(xí)筆記(8)---橋接模式 和博客 Android TransactionTooLargeException 解析尚洽,思考與監(jiān)控方案)悔橄,scheduleTraversals 函數(shù)最后會(huì)調(diào)用到 doTraversal 方法,doTraversal 方法又調(diào)用 performTraversals 函數(shù)腺毫,performTraversals 函數(shù)就非常熟悉了癣疟,他會(huì)去調(diào)用 performMeasure,performLayout 和 performDraw 函數(shù)去進(jìn)行 view 的計(jì)算和繪制潮酒,我們只是在一個(gè)比較高的層次上概括性地梳理了它的整個(gè)脈絡(luò)睛挚,它的簡化結(jié)構(gòu):
接下來的繪制過程我在這就不說了,感興趣的我這推薦一篇非常好的博客:http://blog.csdn.net/jacklam200/article/details/50039189急黎,講的真的很詳細(xì)扎狱,或者可以看看這個(gè)英文資料Android Graphics Architecture侧到。
回到“ 為什么最后會(huì)調(diào)用到 ViewRootImpl 類中” 這個(gè)問題,從上面可以理解到,每個(gè) Window 都對(duì)應(yīng)著一個(gè) View 和一個(gè) ViewRootImpl,Window 和 View 是通過 ViewRootImpl 來建立關(guān)聯(lián)的半醉,所以 invalidateChildInParent 會(huì)一直 while 循環(huán)直到調(diào)用到 ViewRootImpl 的 invalidateChildInParent 函數(shù)中:
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);
這個(gè)問題就差不多清楚了工育,其他的可以再看看老羅的博客:http://blog.csdn.net/luoshengyang/article/details/8223770。
主線程與子線程ui討論
上面分析了 Activity 的啟動(dòng)和布局創(chuàng)建過程,其中知道 Activity 的創(chuàng)建需要新建一個(gè) ViewRootImpl 對(duì)象,看看 ViewRootImpl 的構(gòu)造函數(shù):
public ViewRootImpl(Context context, Display display) {
.....
mThread = Thread.currentThread();
.....
}
在初始化一個(gè) ViewRootImpl 函數(shù)的時(shí)候,會(huì)調(diào)用 native 方法矢腻,獲取到該線程對(duì)象 mThread,接著 setText 函數(shù)會(huì)調(diào)用到 requestLayout 方法(TextView 繪制出來之后射赛,調(diào)用 setText 才會(huì)去調(diào)用 requestLayout 方法多柑,沒有繪制出來之前,在子線程中調(diào)用 setText 是不會(huì)拋出 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.");
}
}
所以現(xiàn)在 “不能在子線程中更新 ui” 的問題已經(jīng)很清楚了楣责,不管 startActivity 函數(shù)調(diào)用在什么線程竣灌,ActivityThread 的內(nèi)部函數(shù)執(zhí)行是在主線程中的:
/**
* 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 對(duì)象的創(chuàng)建也是在主線程中,這就是說一個(gè) activity 的對(duì)應(yīng) ViewRootImpl 對(duì)象中的 mThread 一定是代表主線程腐魂,這就是“為什么不能在子線程中操作 UI 的”答案的解釋帐偎,問題解決!;滓佟削樊!
但是不是說這個(gè)答案不嚴(yán)謹(jǐn)么?是的兔毒,可不可以在子線程中添加 Window漫贞,并且創(chuàng)建 ViewRootImpl 呢?當(dāng)然可以育叁,在子線程中創(chuàng)建一個(gè) Window 就可以迅脐,思路是在子線程中調(diào)用 WindowManager 添加一個(gè) 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 不是系統(tǒng)層級(jí)的 Window豪嗽,我們可以使用 WindowManager 來添加自定義的系統(tǒng) Window谴蔑,那么問題又來了,系統(tǒng)級(jí)別 Window 是怎么添加的呢龟梦,老羅的另一篇博客 http://blog.csdn.net/luoshengyang/article/details/8498908 中介紹到: “對(duì)于非輸入法窗口隐锭、非壁紙窗口以及非 Activity 窗口來說,它們所對(duì)應(yīng)的 WindowToken 對(duì)象是在它們?cè)黾拥?WindowManagerService 服務(wù)的時(shí)候創(chuàng)建的......如果參數(shù) attrs 所描述的一個(gè) WindowManager.LayoutParams 對(duì)象的成員變量 token 所指向的一個(gè) IBinder 接口在 WindowManagerService 類的成員變量 mTokenMap 所描述的一個(gè) HashMap 中沒有一個(gè)對(duì)應(yīng)的 WindowToken 對(duì)象计贰,并且該 WindowManager.LayoutParams 對(duì)象的成員變量 type 的值不等于 TYPE_INPUT_METHOD钦睡、TYPE_WALLPAPER,以及不在FIRST_APPLICATION_WINDOW 和LAST_APPLICATION_WINDOW躁倒,那么就意味著這時(shí)候要增加的窗口就既不是輸入法窗口荞怒,也不是壁紙窗口和 Activity 窗口洒琢,因此,就需要以參數(shù) attrs 所描述的一個(gè) WindowManager.LayoutParams 對(duì)象的成員變量 token 所指向的一個(gè) IBinder 接口為參數(shù)來創(chuàng)建一個(gè) WindowToken 對(duì)象褐桌,并且將該 WindowToken對(duì)象保存在 WindowManagerService 類的成員變量 mTokenMap 和 mTokenList 中衰抑。”荧嵌。
了解上面之后停士,換一種思路,就可以在子線程中創(chuàng)建 view 并且添加到 windowManager 中完丽。
實(shí)現(xiàn)
有了思路之后,既可以來實(shí)現(xiàn)相關(guān)代碼了:
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);
}
運(yùn)行一下拇舀,報(bào)錯(cuò):
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)
這是因?yàn)?ViewRootImpl 類內(nèi)部會(huì)新建一個(gè) ViewRootHandler 類型的 mHandler 用來處理相關(guān)消息逻族,所以如果線程沒有 Looper 是會(huì)報(bào)錯(cuò)的,添加 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();
創(chuàng)建 Looper 之后聘鳞,需要在必要時(shí)候調(diào)用 quit 函數(shù)將其退出。這樣就成功顯示了
而且創(chuàng)建之后的 view 只能在子線程中修改要拂,不能在主線程中修改抠璃,要不然會(huì)拋出最開始的 ViewRootImpl$CalledFromWrongThreadException。
擴(kuò)展
為什么 android 會(huì)設(shè)計(jì)成只有創(chuàng)建 ViewRootImpl 的原始線程才能更改 ui 呢脱惰?這就要說到 Android 的單線程模型了搏嗡,因?yàn)槿绻С侄嗑€程修改 View 的話,由此產(chǎn)生的線程同步和線程安全問題將是非常繁瑣的拉一,所以 Android 直接就定死了采盒,View 的操作必須在創(chuàng)建它的 UI 線程,從而簡化了系統(tǒng)設(shè)計(jì)蔚润。
有沒有可以在其他非原始線程更新 ui 的情況呢磅氨?有,SurfaceView 就可以在其他線程更新嫡纠,具體的大家可以去網(wǎng)上了解一下相關(guān)資料烦租。