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

一、前言

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

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

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

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

二、AppCompatActivity

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

less 复制代码
 @Override
 protected void onCreate(@Nullable Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(mContentLayoutId);
 }

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

kotlin 复制代码
 class TestActivity : AppCompatActivity(R.layout.activity_test)

我们看看AppCompatActivitysetContentView藏着什么:

less 复制代码
 @Override
 public void setContentView(@LayoutRes int layoutResID) {
     //**
     getDelegate().setContentView(layoutResID);
 }

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

kotlin 复制代码
 @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

scss 复制代码
 @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 ,子装饰视图,想必在这里。我们继续往下看:

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

看样子藏在createSubDecor中, gogogo:

scss 复制代码
 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类:

java 复制代码
 /**
  * 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

csharp 复制代码
 @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()的内心。

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

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

ini 复制代码
 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~。

ini 复制代码
 <FrameLayout
     android:id="@android:id/content"
     android:layout_width="match_parent"
     android:layout_height="match_parent" />

四、倒着回去

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

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

捋一下:

  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

我们往上看一下:

less 复制代码
 public void setContentView(@LayoutRes int layoutResID) {
     getWindow().setContentView(layoutResID);
     initWindowDecorActionBar();
 }

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

六、LayoutInflater

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

用Google话来说:

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

请见下回分解。

七、总结

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

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

相关推荐
后端码匠4 小时前
MySQL 8.0安装(压缩包方式)
android·mysql·adb
梓仁沐白5 小时前
Android清单文件
android
董可伦7 小时前
Dinky 安装部署并配置提交 Flink Yarn 任务
android·adb·flink
每次的天空8 小时前
Android学习总结之Glide自定义三级缓存(面试篇)
android·学习·glide
恋猫de小郭8 小时前
如何查看项目是否支持最新 Android 16K Page Size 一文汇总
android·开发语言·javascript·kotlin
flying robot10 小时前
小结:Android系统架构
android·系统架构
xiaogai_gai10 小时前
有效的聚水潭数据集成到MySQL案例
android·数据库·mysql
鹅鹅鹅呢11 小时前
mysql 登录报错:ERROR 1045(28000):Access denied for user ‘root‘@‘localhost‘ (using password Yes)
android·数据库·mysql
在人间负债^11 小时前
假装自己是个小白 ---- 重新认识MySQL
android·数据库·mysql
Unity官方开发者社区11 小时前
Android App View——团结引擎车机版实现安卓应用原生嵌入 3D 开发场景
android·3d·团结引擎1.5·团结引擎车机版