Android一杯冰美式的时间--去找setContentView

811 阅读6分钟

一、前言

在 Android 应用开发的世界里,setContentView 几乎是每个开发者都会接触到的方法。它的作用至关重要——负责将视图(View)或布局(Layout)展示在屏幕上。尽管这看起来是一个简单直接的操作,但其背后实际上隐藏着 Android 系统中复杂而精妙的窗口管理和视图渲染机制。最近,在与同行的交流中,我发现我几乎将这一块忘得一干二净。因此,我决定从 ActivitysetContentView 方法入手,重新梳理并深入探讨 Android 的窗口管理和视图展示原理,希望能够为大家带来新的理解和启发。阿弥陀佛。

吨吨吨,喝一杯冰美式。要睡着了。

PS:因为代码中有大量的样式、动画、配置相关的代码,我会选择性的省略,如果你想看完整的,我使用的是“appcompact1.3.1”、SDK31。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

二、AppCompatActivity

一般而言你都会如此使用setConentView

 @Override
 protected void onCreate(@Nullable Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(mContentLayoutId);
 }

当然你也可以在这么使用:

 class TestActivity : AppCompatActivity(R.layout.activity_test)

我们看看AppCompatActivitysetContentView藏着什么:

 @Override
 public void setContentView(@LayoutRes int layoutResID) {
     //**
     getDelegate().setContentView(layoutResID);
 }

可以看到在AppCompatActivity中,使用的getDelegate,这看起来像是委托啊!点下去看看:

 @NonNull
 public AppCompatDelegate getDelegate() {
     if (mDelegate == null) {
         mDelegate = AppCompatDelegate.create(this, this);
     }
     return mDelegate;
 }

我们找到了全新的AppCompatDelegate!官方如是说:

此类表示一个委托,您可以使用该委托将 AppCompat 的支持扩展到任何 Activity.只能 Activity 与一个 AppCompatDelegate 实例链接,因此应保留从 create(Activity, AppCompatCallback) 返回的实例,直到 Activity 被销毁。

但是AppCompatDelegate是一个抽象类,我们可以很轻松的找到它的实现类AppCompatDelegateImpl

我们可以在这里看到setContentView

 @Override
 public void setContentView(int resId) {
     ensureSubDecor();
     ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
     contentParent.removeAllViews();
     LayoutInflater.from(mContext).inflate(resId, contentParent);
     mAppCompatWindowCallback.getWrapped().onContentChanged();
 }

可以看到,我们提供的LayoutId,最后会添加到contentParent这个View上,那么mSubDecor是从哪儿来的?看到调用方法的名字,ensure sub decor ,子装饰视图,想必在这里。我们继续往下看:

 private void ensureSubDecor() {
     // 检查子装饰(sub decor)是否已经设置
     if (!mSubDecorInstalled) {
         // 创建子装饰视图
         mSubDecor = createSubDecor();
         //**
     }
 }

看样子藏在createSubDecor中, gogogo:

 private ViewGroup createSubDecor() {
     TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
     //**省略 获取当前上下文的主题属性,设置对应的样式
     a.recycle();
 ​
     // 确保窗口已安装其装饰
     ensureWindow();
     mWindow.getDecorView();
 ​
     // 获取布局填充器
     final LayoutInflater inflater = LayoutInflater.from(mContext);
     ViewGroup subDecor = null;
 ​
     // 根据是否有标题和是否为浮动窗口来决定使用哪个布局
     if (!mWindowNoTitle) {
         if (mIsFloating) {
             // 如果是浮动窗口,则使用对话框标题装饰
             subDecor = (ViewGroup) inflater.inflate(
                     R.layout.abc_dialog_title_material, null);
             //**
         } else if (mHasActionBar) {
             // 如果有动作栏,则使用特定主题创建布局
             // 使用主题化上下文填充视图并设置为内容视图
             subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                     .inflate(R.layout.abc_screen_toolbar, null);
             //**
         }
     } else {
         // 根据是否覆盖动作模式选择不同布局
         if (mOverlayActionMode) {
             subDecor = (ViewGroup) inflater.inflate(
                     R.layout.abc_screen_simple_overlay_action_mode, null);
             //**
         } else {
             subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
             //**
         }
     }
     //**
     // 将窗口的内容视图设置为subDecor
     mWindow.setContentView(subDecor);
     //**
     return subDecor;
 }
 ​

可以看到这个方法返回一个配置好的 subDecorsubDecor使用的是系统的布局,根据配置的不同,使用了不同的xml。返回给我们用于添加LayoutId,但是它是如何显示的还是不清楚,但是注意到mWindow.setContentView(subDecor);

我们点下去一看,回来到Window类:

 /**
  * Convenience for
  * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}
  * 将屏幕内容设置为显式视图。此视图直接放置在屏幕的视图层次结构中。它本身可以是一个复杂的视图层次结构。
  * @param view The desired content to display.
  * @see #setContentView(View, android.view.ViewGroup.LayoutParams)
  */
 public abstract void setContentView(View view);

显然这是整个视图显示过程中非常核心的一步。但是它是抽象的!不过根据我们小学二年级就学过的

The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window.

是的,Window只有一个实现类-PhoneWindow

三、PhoneWindow

我们找到PhoneWindow

 @Override
 public void setContentView(View view, ViewGroup.LayoutParams params) {
     if (mContentParent == null) {
         // 如果内容父视图还未创建,则进行安装
         installDecor();
     } 
     //**
     if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
         //**
     } else {
         // 将视图添加到内容父视图中
         mContentParent.addView(view, params);
     }
     //**
 }
 ​

我们看看如何初始化mContentParent,走进installDecor()的内心。

 private void installDecor() {
     mForceDecorInstall = false;
     if (mDecor == null) {
         // 创建窗口装饰视图
         mDecor = generateDecor(-1);
         //**
     } else {
         mDecor.setWindow(this);
     }
 ​
     if (mContentParent == null) {
         // 生成并设置内容布局
         mContentParent = generateLayout(mDecor);
            //**
         } else {
            //**
         }
         // 省略涉及到过渡管理器和动画的配置
     }
 }
 ​

在这里我们可以看到两个,generateDecor,但是在createSubDecor中我们已经创建过了:

 protected ViewGroup generateLayout(DecorView decor) {
     // ... 省略了一部分属性设置代码 ...
 ​
     // 根据窗口特性选择布局资源
     int layoutResource;
     int features = getLocalFeatures();
     // 根据不同的特性标志选择不同的布局资源
     if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
         if (mIsFloating) {
             layoutResource = res.resourceId;
         } else {
             layoutResource = R.layout.screen_title_icons;
         }
     } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 
             && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
         layoutResource = R.layout.screen_progress;
     } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
         if (mIsFloating) {
             layoutResource = res.resourceId;
         } else {
             layoutResource = R.layout.screen_custom_title;
         }
     } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
         if (mIsFloating) {
             layoutResource = res.resourceId;
         } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
             layoutResource = a.getResourceId(
                     R.styleable.Window_windowActionBarFullscreenDecorLayout,
                     R.layout.screen_action_bar);
         } else {
             layoutResource = R.layout.screen_title;
         }
     } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
         layoutResource = R.layout.screen_simple_overlay_action_mode;
     } else {
         layoutResource = R.layout.screen_simple;
     }
     // 装饰视图开始变化
     mDecor.startChanging();
     // 使用LayoutInflater加载布局资源
     mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
 ​
     // 获取内容父视图
     ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
     // ... 省略了其他设置代码 ...
 ​
     // 装饰视图完成变化
     mDecor.finishChanging();
 ​
     return contentParent;
 }
 ​

在这个方法中,会根据不同的特性标志选择不同的布局资源,但是这些布局都有一个显著的特点。他们都有一个id为content的FrameLayout。就是那个老生常谈的android.R.id.content~。

 <FrameLayout
     android:id="@android:id/content"
     android:layout_width="match_parent"
     android:layout_height="match_parent" />

四、倒着回去

至此,我们知道了PhoneWindowsetContentView中的contentParent来自哪里:

image-20231222153352137

那我们也是知道了AppCompatDelegate中的setContentViewcontentParent来自哪里:

image-20231222153544012

捋一下:

  1. 在活动的 onCreate 方法中调用 setContentView,传入布局资源ID或者直接传入一个视图(View)对象。
  2. AppCompatDelegate中调用Window.setContentView
  3. PhoneWindow 对象负责创建和管理顶层视图容器,DecorView。如果 DecorView 还未创建,Window 会通过调用 generateDecor 方法来创建它。DecorView中一定有一个ID为android.R.id.content的FrameLayout。
  4. AppCompatDelegateImpl将视图添加到android.R.id.content

五、Activity

为什么没有提及Activity呢?细心的大家应该发现了,AppCompatActivity的setContentView是一个重写方法,它完全重写了父类。

Overrides method in Activity

我们往上看一下:

 public void setContentView(@LayoutRes int layoutResID) {
     getWindow().setContentView(layoutResID);
     initWindowDecorActionBar();
 }

显然,最后Activity也是回到了PhoneWindow,至于为什么要这样呢?你猜~~~~嘻嘻。

六、LayoutInflater

不知道大家有没有注意到PhoneWindow和AppCompatDelegate中的LayoutInflater,那么DecorView和我们的Layout是如何渲染到屏幕的呢?请看LayoutInflater。

用Google话来说:

将布局 XML 文件实例化到其相应的 View 对象中

请见下回分解。

七、总结

啊,有点困。冰美式压不住我的睡意。刀了。下一篇我们来说说LayoutInflater。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏