参考内容:AndroidUI体系
问题:
在Activity中Window和View如何分工协作?
何时在Activity中获取View宽高?
这篇文章主要是回答以上的问题,但是在回答以上的问题中也会出现一些子问题。弄清楚这些问题,有利于我们了解Window WindowManager Activity View的关系。
我们知道ActivityThread相当于是App的主函数类,Activity#onCreate的开始可以追溯到ActivityThread#handleLaunchActivity中:
public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
// .....
// 初始化全局的WindowManagerGlobal
WindowManagerGlobal.initialize();
// 在performLaunchActivity中会调用Activity#attach
final Activity a = performLaunchActivity(r, customIntent);
// ....
return a;
} handleLaunchActivity中首先初始化了WindowManagerGlobal,从名字可以看出,这个类一定和Window以及WindowManager是有关的,至于这个类如何和Window和WM产生关联,我们待会会总结。然后在handleLaunchActivity中,又调用了performLaunchActivity,在这个方法中会调用Activity#attach。
ActivityThread#performLaunchActivity
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// ...
Activity activity = null;
// 通过反射的方式获取Activity,Instrumentation是一个和Activity的启动密切相关的类
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
// .....
} catch (Exception e) {
// ......
}
// 获取当前的Application的实例
try {
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
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, r.configCallback,
r.assistToken);
}
r.setState(ON_CREATE);
// updatePendingActivityConfiguration() reads from mActivities to update
// ActivityClientRecord which runs in a different thread. Protect modifications to
// mActivities to avoid race.
synchronized (mResourcesManager) {
mActivities.put(r.token, r);
}
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
// .....
}
}
return activity;
} performActivity通过反射机制创建了Activity的实例,并且构建了Application的实例,当attach方法执行完成之后,使用记录当前的Activity状态是ON_CREATE。那么,不言而喻,attach方法中应该会调用我们实现的Activity#onCreate回调。接着我们来看看Activity#attach方法:
Activity#attach
// ... 省略参数
final void attach( ) {
// ....
// 创建PhoneWindow实例
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
// .....
// 将Window和WindowManager绑定
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
// .....
} 因为Activity的回调方法中通常我们使用setContentView加载当前resId指定的布局,但是这个布局是在DecorView的ContentView中,而DecorView又在PhoneWindow中。所以当前Activity#attach时需要创建PhoneWindow的实例,并且绑定PhoneWindow到WindowManager中。

继续看ActivityThread#performLaunchActivity
当Activity#attach执行完成之后,performLaunchActivity继续执行,会执行到这一行代码:
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState); 在Instrumentation#callActivityOnCreate中会调用Activity#performCreate,这里面就回调用我们写的onCreate回调。因此,一个不太标准的从ActivityThread#handlePerformLaunchActivity到Activity#onCreate的时序图,如下图所示:

虽然我们知道在Activity的生命周期方法调度完成时我们可以看到我们写的布局文件,但是我们目前并不能看到Window和View的关系,于是我们可以猜想,既然onCreate方法中没有,那么,Window加载View的代码可能会在onResume上。这是因为在官方文档中也说明,onResume是程序到前台的标志。
ActivityThread#handleResumeActivity
ActivityThread#handleResumeActivity方法首先调用performResumeActivity,在performResumeActivity完成之后在会调用WindowManager#addView向Window中添加布局。
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
// ...
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
// 当前的Activity已经在onCreate中创建好了
final Activity a = r.activity;
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();
// ....
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
// ....
}
}
//...
}
// ....
} 所以,实际上布局是在onResume完成之后才被加载在PhoneWindow中的,不过具体的内容我们还是需要看看performResumeActivity:
ActivityThread#performResumeActivity
public ActivityClientRecord performResumeActivity(IBinder token, boolean finalStateRequest,
String reason) {
final ActivityClientRecord r = mActivities.get(token);
// 对于Lifecycle的一些判断
if (r.getLifecycleState() == ON_RESUME) {
if (!finalStateRequest) {
// ... 如果已经是onResume 就回抛出异常
}
return null;
}
// ....
try {
// ...
// 调用Activity#performResume
r.activity.performResume(r.startsNotResumed, reason);
r.state = null;
r.persistentState = null;
r.setState(ON_RESUME);
reportTopResumedActivityChanged(r, r.isTopResumedActivity, "topWhenResuming");
} catch (Exception e) {
// ....
}
return r;
} 套路和前面的都差不多,在ActivityThread中执行Activity#performResume然后转到启动相关类Instrumentation#callActivityOnResume,在执行onResume回调。

因此我们可以做一个小小的总结,关于Activity中Window和View,他们之间的合作关系和Activity的生命周期是密切相关的:
onCreate阶段:

但是在onCreate中,并不会把View加载到PhoneWindow中,这个说来也非常好理解,毕竟我们在回调中才解析布局文件xml,怎么会在PhoneWindow创建之前addView呢?
onResume阶段:

刚才我们看到WindowManager可以将DecorView加载到PhoneWindow中,这个过程还可以仔细地分析一番。在分析之前我们先总结一下Window相关的类之间的关系:

ViewManager只是一个接口,定义了基本的对于View的添加和删除工作
public interface ViewManager {
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
WindowManager也是一个接口,但是我们通常操作的都是这个类,他的实现类是WindowManageImpl。WindowManagerImpl又通过将职责委托给WindowManagerGlobal实现,之所以使用这么复杂的【套娃】逻辑,好处有两点:
这是一种外观模式,我们通过WindowManager就可以操作WindowManagerGlobal和framework层通信。
WindowManagerGlobal也是一个全局单例。根据上面的代码分析,创建Activity就回创建对应的WindowManager和PhoneWindow,但是这些所有的WindowManager都基于WindowManagerGlobal,节省内存。
回到问题【ViewRootImpl如何成为Window和View的桥梁?】本身,我们需要查看一下WindowManager#addView的代码:
// WindowManagerImpl#addView
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
// WindowManagerGlobal#addView
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
// ...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// ...
// 创建了ViewRootImpl
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 {
// 调用ViewRootImpl#setView
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// ..
}
}
} 在WindowManagerGlobal中初始化了ViewRootImpl,然后调用了setView。跟踪ViewRootImpl#setView可以发现这个方法最后会调用WindowSession#addToDispaly,再调用WindowManagerService#addWindow,整体的调用链如图:

所以可以看到ViewRootImpl确实充当了一个桥梁,上面抓住了WindowManager,下面连接的是WindowSession(是IWindowSession.Stub的实现类,是Binder机制的一部分)。
这个问题可以转换成另外的一个子问题,View#measure是在什么时候执行的?
View#measure的执行时机
上面已经说过个, onResume会先于WindowManager#addView,所以回调函数本身会在测量之前执行,这样在onResume中尝试获取宽高一定会返回0,更不用说在onCreate中。
解决问题的思路:
handler.post( Runnable {}, delay):这里不可以不加delay。如果不加delay,这个消息在消息队列中还是会先执行,而我们的目的是想等到onResume执行完成,WindowManager#addView之后再拿到View宽高,这个大小设置为100ms。
View.post(Runnable {}): View.post的原理就是将当前的Runnable放入了HandlerActionQueue的数组中,然后在ViewRootImpl#performTraversals中执行。ViewRootImpl#performTraversals是执行测量 布局 绘制的开始,肯定也会在WindowManager#addView之后
public boolean post(Runnable action) {
// ....
// getRunQueue返回一个HandlerActionQueue
getRunQueue().post(action);
return true;
}
public class HandlerActionQueue {
private HandlerAction[] mActions;
private int mCount;
public void post(Runnable action) {
postDelayed(action, 0);
}
// 放入数组,具体的执行动作是在ViewRootImpl#performTraversals
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
} 3. onWindowFocusChanged回调:当失去焦点时会被调用
4. addOnGlobalLayoutListener:当ViewTree变化的时候会被调用。
这个例子中代码会报错吗?
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.activity_main)
// 这行代码会报错吗?
thread {
val textView = findViewById<TextView>(R.id.test).apply {
this.text = "new message"
}
}
} 其实并不会,因为在这里,textView被setContentView加载之后设置setText时只是将String变量存储到TextView中,此时TextView并没有开始performTraversals,不会检查UI线程,所以没有问题。