最近有几个老项目需要更新 , 在接手时发现项目中出现布局的点按操作无效以及很多的崩溃问题 , 简单排查以后发现接手时项目中混用了 ViewBinding 和 findViewById , 而出现问题的地方都是 findViewById 。
最开始想到的也是更新中目的之一 , 升级 Target 30 , 当时还在想难道是 Target 30 不给用 findViewById 了 ? 把步骤改为 ViewBinding 后正常执行 , 好像很像我的猜测 , 但是这不符合逻辑啊 , ViewBinding 的底层实际上还是 findViewById 。
继续看代码 , 发现项目的 Activity 全部继承了一个基类 Activity , 问题就出现在这里 , 先上代码
abstract class InitActivity : SimpleActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(getLayout()) initTitlebar() initData() initListener() } protected abstract fun getLayout(): Int protected abstract fun initTitlebar() protected abstract fun initListener() protected abstract fun initData() }
这个类很好理解 , 统一了 Activity 常用的方法 , 只要继承后实现即可 , 同时最重要的 , 在这个基类中实现了 setContentView , 通过 getLayout 获取布局 i d , 而我们要使用 ViewBinding , 需要在 setContentView 中注册 ViewBinding
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) }
也就是说 , 每个 Activity 都执行了两次 setContentView , 并且由于常用的初始化方法都是在基类中调用的 , 所有的 findViewById 都是基于第一次设置在 setContentView 的布局 , 当第二次设置 setContentView (binding.root) 后 , 第一次的布局就失效了 , 所以导致了遇到的问题 。
那么为什么 , 第二次调用 setContentView 会出现这样的问题 , setContentView 到底经历了什么 , 这就跟 View 的渲染流程有关了
先简单说明 View 的渲染 :
整个流程主要从
ActivityThread
类开始,途经
PhoneWindow
、
DecorView
、
LayoutInflater
、等类 。
首先是创建
activity
,创建
widow,
创建
decorview,
然后是调用
setContent
时候,创建
View
,然后解析生成
View
。
可以看到在 setContent 的时候 创建 View ,然后解析生成 View 。
来看看 setContent 经历了什么
首先 ,实现了三个重载的setContentView 方法, getDelegate() 方法负责创建 Activity 的代理类实例,然后调用 setContentView 方法添加显示的视图, Activity 通过代理模式添加要显示的视图。
在 getDelegate() 中负责创建 Activity 代理 AppCompatDelegate 类实例
再来到 AppCompatDelegateImpl 中的 setContentView 方法看看
其中 ensureSubDecor 的核心代码如下
而 createSubDecor 就很长了 , 一张图都放不下 , 在这个方法中主要干了三件事
1 、 this.mWindow.getDecorView(); 创建 Decorview, 并为它加载一个布局文件,找到这个布局文件中 R.id.content 的容器,赋值给 mContentParent 。这样我们就准备好了一个 DecorView 和其布局中 id 为 R.id.content 的容器。
AppCompatDelegateImpl(Context context, Window window, AppCompatCallback callback) { ...... this.mWindow = window; // mWindow 的初始化是在AppCompatDelegateImpl构造函数里 ..... } // 想要知道mWindow是啥就要找到AppCompatDelegateImpl(context,window,callback),那么这个构造函数初始化的时候传//入的window是啥,还记得最开始我们从setContentView说起 protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); } //往下传递 public class AppCompatActivity extends ... { public void setContentView(@LayoutRes int layoutResID) { this.getDelegate().setContentView(layoutResID); } //getDelegate().setContentView(layoutResID);先找getDelegate() //getDelegate()也在AppCompatActivity 中 @NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; } } // getDelegate() = AppCompatDelegate.create(this, this); public abstract class AppCompatDelegate { public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) { // 在这里初始化的,activity就是AppCompatActivity ,window就是activity.getWindow() return new AppCompatDelegateImpl(activity, activity.getWindow(), callback); } } //window就是AppCompatActivity.getWindow(),但是AppCompatActivity中没有getWindow()方法,getWindow()是在其父类Activity中实现 public class Activity extends ... ... { private Window mWindow; final void attach(Context context, ......) { attachBaseContext(context); mWindow = new PhoneWindow(this, window, activityConfigCallback); ...... } public @Nullable Window getWindow() { return mWindow; } }
2 、 给ViewGroup subDecor 根据主题、 style 选择合适布局文件并加载到 subDecor 中:
ContentFrameLayout contentView = (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content); ViewGroup windowContentView = (ViewGroup)this.mWindow.findViewById(R.id.content); // 这里就是上一步里面那个布局文件的R.id.content 容器 windowContentView.setId(View.NO_ID); // 把windowContentView的id设置为View.NO_ID 即 -1 contentView.setId(android.R.id.content); // 把contentView 的id设置为R.id.content
这样我们准备好了subDecor 和其布局中 id 为 action_bar_activity_content 的容器,并把这个容器的 id 改成 R.id.content
3、 this.mWindow.setContentView(subDecor); 将第 2 步的 subDecor 添加到 第 1 步准备好的 DecorView 的容器 mContentParent 中。
@Override public void setContentView(View view) { setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); } @Override 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,现在就是把subDecor加载到其中 mContentParent.addView(view, params); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true; }
也就是每次调用 set ContentView 都会修改整个 activity 的容器中的布局