一文读懂 View & Window 机制(一)
Android 系统中,Window 在代码层次上是一个抽象类,在概念上表示的是一个窗口。Android 中所有的视图都是通过 Window 来呈现的,例如 Activity、Dialog 和 Toast 等,它们实际上都是挂载在 Window 上的。大部分情况下应用层开发者很少需要来和 Window 打交道,Activity 已经隐藏了 Window 的具体实现逻辑了,但我觉得来了解 Window 机制的一个比较大的好处是可以加深我们对 View 绘制流程以及事件分发机制的了解,这两个操作就涉及到我们的日常开发了,实现自定义 View 和解决 View 的滑动冲突时都需要我们掌握这方面的知识点,而这两个操作和 Window 机制有很大的关联。视图树只有被挂载到 Window 后才会触发视图树的绘制流程,之后视图树才有机会接收到用户的触摸事件。所以说,视图树被挂载到了 Window 上是 Activity 和 Dialog 等视图能够展示到屏幕上且和用户做交互的前置条件
本文就以 Activity 为例子,展开讲解 Activity 是如何挂载到 Window 上的,基于 Android API 30 进行分析,希望对你有所帮助 😇😇
一、Window
Window 存在的意义是什么呢?
大部分情况下,用户都是在和应用的 Activity 做交互,应用在 Activity 上接收用户的输入并在 Activity 上向用户做出交互反馈。例如,在 Activity 中显示了一个 Button,当用户点击后就会触发 OnClickListener,这个过程中用户就是在和 Activity 中的视图树做交互,此时还没有什么问题。可是,当需要在 Activity 上弹出 Dialog 时,系统需要确保 Dialog 是会覆盖在 Activity 之上的,有触摸事件时也需要确保 Dialog 是先于 Activity 接收到的;当启动一个新的 Activity 时又需要覆盖住上一个 Activity 显示的 Dialog;在弹出 Toast 时,又需要确保 Toast 是覆盖在 Dialog 之上的
这种种要求就涉及到了一个层次管理问题,系统需要对当前屏幕上显示的多个视图树进行统一管理,这样才能来决定不同视图树的显示层次以及在接收触摸事件时的优先级。系统就通过 Window 这个概念来实现上述目的
想要在屏幕上显示一个 Window 并不算多复杂,代码大致如下所示
private val windowManager by lazy {
context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
}
private val floatBallView by lazy {
FloatBallView(context)
}
private val floatBallWindowParams: WindowManager.LayoutParams by lazy {
WindowManager.LayoutParams().apply {
width = FloatBallView.VIEW_WIDTH
height = FloatBallView.VIEW_HEIGHT
gravity = Gravity.START or Gravity.CENTER_VERTICAL
flags =
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
type = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
} else {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}
}
}
fun showFloatBall() {
windowManager.addView(floatBallView, floatBallWindowParams)
}
复制代码
显示一个 Window 最基本的操作流程有:
- 声明希望显示的 View,即本例子中的 floatBallView,其承载了我们希望用户看到的视图界面
- 声明 View 的位置参数和交互逻辑,即本例子中的 floatBallWindowParams,其规定了 floatBallView 在屏幕上的位置,以及和用户之间的交互逻辑
- 通过 WindowManager 来添加 floatBallView,从而将 floatBallView 挂载到 Window 上,WindowManager 是外界访问 Window 的入口
当中,WindowManager.LayoutParams 的 flags 属性就用于控制 Window 的显示特性和交互逻辑,常见的有以下几个:
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE。表示当前 Window 不需要获取焦点,也不需要接收各种按键输入事件,按键事件会直接传递给下层具有焦点的 Window
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL。表示当前 Window 区域的单击事件希望自己处理,其它区域的事件则传递给其它 Window
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED。表示当前 Window 希望显示在锁屏界面
此外,WindowManager.LayoutParams 的 type 属性就用于表示 Window 的类型。Window 有三种类型:应用 Window、子 Window、系统 Window。应用类Window 对应 Activity。子 Window 具有依赖关系,不能单独存在,需要附属在特定的父 Window 之中,比如 Dialog 就是一个子 Window。系统 Window 是需要声明权限才能创建的 Window,比如 Toast 和 statusBar 都是系统 Window
从这也可以看出,系统 Window 是处于最顶层的,所以说 type 属性也用于控制 Window 的显示层级,显示层级高的 Window 就会覆盖在显示层级低的 Window 之上。应用 Window 的层级范围是 1~99,子 Window 的层级范围是 1000~1999,系统 Window 的层级范围是 2000~2999。如果想要让我们创建的 Window 位于其它 Window 之上,那么就需要使用比较大的层级值了,但想要显示自定义的系统级 Window 的话就必须向系统动态申请权限
WindowManager.LayoutParams 内就声明了这些层级值,我们可以择需选取。例如,系统状态栏本身也是一个 Window,其 type 值就是 TYPE_STATUS_BAR
public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
public int type;
//应用 Window 的开始值
public static final int FIRST_APPLICATION_WINDOW = 1;
//应用 Window 的结束值
public static final int LAST_APPLICATION_WINDOW = 99;
//子 Window 的开始值
public static final int FIRST_SUB_WINDOW = 1000;
//子 Window 的结束值
public static final int LAST_SUB_WINDOW = 1999;
//系统 Window 的开始值
public static final int FIRST_SYSTEM_WINDOW = 2000;
//系统状态栏
public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;
//系统 Window 的结束值
public static final int LAST_SYSTEM_WINDOW = 2999;
}
复制代码
二、WindowManager
每个 Window 都会关联一个 View,想要显示 Window 也离不开 WindowManager,WindowManager 就提供了对 View 进行操作的能力。WindowManager 本身是一个接口,其又继承了另一个接口 ViewManager,WindowManager 最基本的三种操作行为就由 ViewManager 来定义,即添加 View、更新 View、移除 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 的实现类是 WindowManagerImpl,其三种基本的操作行为都交由了 WindowManagerGlobal 去实现,这里使用到了桥接模式
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
}
复制代码
这里主要看下 WindowManagerGlobal 是如何实现 addView
方法的即可
首先,WindowManagerGlobal 会对入参参数进行校验,并对 LayoutParams 做下参数调整。例如,如果当前要显示的是子 Window 的话,那么就需要使其 LayoutParams 遵循父 Window 的要求才行
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
···
}
复制代码
之后就会为当前的视图树(即 view)构建一个关联的 ViewRootImpl 对象,通过 ViewRootImpl 来绘制视图树并完成 Window 的添加过程。ViewRootImpl 的 setView
方法会触发启动整个视图树的绘制流程,即完成视图树的 Measure、Layout、Draw 流程,具体流程可以看我的另一篇文章:一文读懂 View 的 Measure、Layout、Draw 流程
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
···
ViewRootImpl root;
View panelParentView = null;
···
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 {
//启动和 view 关联的整个视图树的绘制流程
root.setView(view, wparams, panelParentView, userId);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
复制代码
ViewRootImpl 内部最终会通过 WindowSession 来完成 Window 的添加过程,mWindowSession
是一个Binder对象,真正的实现类是 Session,也就是说,Window 的添加过程涉及到了 IPC 调用。后面就比较复杂了,能力有限就不继续看下去了
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
adjustLayoutParamsForCompatibility(mWindowAttributes);
res = mWindowSession.addToDisplayAsUser(
mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls
);
setFrame(mTmpFrame);
复制代码
需要注意的是,这里所讲的视图树代表的是很多种不同的视图形式。我们知道,在启动一个 Activity 或者显示一个 Dialog 的时候,都需要为它们指定一个布局文件,布局文件会通过 LayoutInflater 加载映射为一个具体的 View 对象,即最终 Activity 和 Dialog 都会被映射为一个 View 类型的视图树,它们都会通过 WindowManager 的 addView
方法来显示到屏幕上,WindowManager 对于 Activity 和 Dialog 来说具有统一的操作行为入口
三、Activity & Window
这里就以 Activity 为例子来展开讲解 Window 相关的知识点,所以也需要先对 Activity 的组成结构做个大致的介绍。Activity 和 Window 之间的关系可以用以下图片来表示
每个 Activity 均包含一个 Window 对象,即 Activity 和 Window 是一对一的关系
Window 是一个抽象类,其唯一的实现类是 PhoneWindow
PhoneWindow 内部包含一个 DecorView,DecorView 是 FrameLayout 的子类,其内部包含一个 LinearLayout,LinearLayout 中又包含两个自上而下的 childView,即 ActionBar 和 ContentParent。我们平时在 Activity 中调用的
setContentView
方法实际上就是在向 ContentParent 执行addView
操作
Window 这个抽象类里定义了多个和 UI 操作相关的方法,我们平时在 Activity 中调用的setContentView
和findViewById
方法都会被转交由 Window 来实现,Window 是 Activity 和视图树系统交互的入口。例如,其 getDecorView()
方法就用于获取内嵌的 DecorView,findViewById()
方法就会将具体逻辑转交由 DecorView 来实现,因为 DecorView 才是真正包含 contentView
的容器类
public abstract class Window {
public Window(Context context) {
mContext = context;
mFeatures = mLocalFeatures = getDefaultFeatures(context);
}
public abstract void setContentView(@LayoutRes int layoutResID);
@Nullable
public <T extends View> T findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
public abstract void setTitle(CharSequence title);
public abstract @NonNull View getDecorView();
···
}
复制代码
四、Activity # setContentView
每个 Activity 内部都包含一个 Window 对象 mWindow
,在 attach
方法中完成初始化,这说明 Activity 和 Window 是一对一的关系。mWindow
对象对应的是 PhoneWindow 类,这也是 Window 的唯一实现类
public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback,
AutofillManager.AutofillClient, ContentCaptureManager.ContentCaptureClient {
@UnsupportedAppUsage
private Window mWindow;
@UnsupportedAppUsage
private WindowManager mWindowManager;
@UnsupportedAppUsage
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,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
//初始化 mWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
···
}
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
}
复制代码
Activity 的attach
方法又是在 ActivityThread 的 performLaunchActivity
方法中被调用的,在通过反射生成 Activity 实例后就会调用attach
方法,且可以看到该方法的调用时机是早于 Activity 的 onCreate
方法的。所以说,在生成 Activity 实例后不久其 Window 对象就已经被初始化了,而且早于各个生命周期回调函数
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
···
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
···
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);
···
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
return activity;
}
复制代码
此外,从 Activity 的setContentView
的方法签名来看,具体逻辑都交由了 Window 的同名方法来实现,传入的 layoutResID
就是我们希望在屏幕上呈现的布局,那么 PhoneWindow 自然就需要去加载该布局文件生成对应的 View。而为了能够有一个对 View 进行统一管理的入口,View 应该要包含在一个指定的 ViewGroup 中才行,该 ViewGroup 指的就是 DecorView
下面就再来看下 PhoneWindow 是如何处理这一个流程的
五、PhoneWindow # setContentView
PhoneWindow 的 setContentView
方法的逻辑可以总结为:
- PhoneWindow 内部包含一个 DecorView 对象
mDecor
。DecorView 是 FrameLayout 的子类,其内部包含两个我们经常会接触到的 childView:actionBar 和 contentParent,actionBar 即 Activity 的标题栏,contentParent 即 Activity 的视图内容容器 - 如果
mContentParent
为 null 的话则调用installDecor()
方法来初始化 DecorView,从而同时初始化mContentParent
;不为 null 的话则移除mContentParent
的所有childView
,为layoutResID
腾出位置(不考虑转场动画,实际上最终的操作都一样) - 通过
LayoutInflater.inflate
生成layoutResID
对应的 View,并将其添加到mContentParent
中,从而将我们的目标视图挂载到一个统一的容器中(不考虑转场动画,实际上最终的操作都一样) - 当 ContentView 添加完毕后会回调
Callback.onContentChanged
方法,我们可以通过重写 Activity 的该方法从而得到布局内容改变的通知
所以说,Activity 的 setContentView
方法实际上就是在向 DecorView 的 mContentParent
执行 addView
操作,所以该方法才叫setContentView
而非setView
public class PhoneWindow extends Window implements MenuBuilder.Callback {
private DecorView mDecor;
ViewGroup mContentParent;
@Override
public void setContentView(int layoutResID) {
// 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)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//将 layoutResID 对应的 View 添加到 mContentParent 中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
//回调通知 contentView 发生变化了
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeFrameworkOptionalFitsSystemWindows();
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
if (decorContentParent != null) {
mDecorContentParent = decorContentParent;
···
} else {
···
}
···
}
}
}
复制代码
mContentParent
通过 generateLayout
方法来完成初始化,该方法主要完成的操作有两个:
- 读取我们为 Activity 设置的 theme 属性,以此配置基础的 UI 风格。例如,如果我们设置了
<item name="windowNoTitle">true</item>
的话,那么就会执行requestFeature(FEATURE_NO_TITLE)
来隐藏标题栏 - 根据 features 来选择合适的布局文件,得到
layoutResource
。之所以会有多种布局文件,是因为不同的 Activity 会有不同的显示要求,有的要求显示 title,有的要求显示 leftIcon,而有的可能全都不需要,为了避免控件冗余就需要来选择合适的布局文件。而虽然每种布局文件结构上略有不同,但均会包含一个 ID 名为content
的 FrameLayout,mContentParent
就对应该 FrameLayout。DecorView 会拿到layoutResource
并生成对应的 View 对象(对应 DecorView 中的mContentRoot
),并将其添加为mContentParent
的 childView
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
···
//第一步
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
···
// Inflate the window decor.
//第二步
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
···
//交由 DecorView 去生成 layoutResource 对应的 View
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//正常来说每种 layoutResource 都会包含一个 ID 为 ID_ANDROID_CONTENT 的 ViewGroup
//如果找不到的话就直接抛出异常
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
···
return contentParent;
}
复制代码
作者:业志陈
链接:https://juejin.cn/post/6942303848996274213
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。