最近有几个老项目需要更新 , 在接手时发现项目中出现布局的点按操作无效以及很多的崩溃问题 , 简单排查以后发现接手时项目中混用了 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 的容器中的布局
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69917874/viewspace-3005923/ ,如需转载,请注明出处,否则将追究法律责任。