前言
在 Android 开发中,findViewById 是一个非常常用的函数,它的主要作用是通过组件的 ID 来引用 XML 布局中的视图元素。尽管它在早期的 Android 开发中无处不在,但随着 Android Studio 3.6 推出了 ViewBinding,它逐渐成为了 findViewById 的替代方案,提供了更为简便且类型安全的视图访问方式。
findViewById 问题与 ViewBinding 的应用
最近在接手一些老项目时,我发现其中存在布局点击事件无效和频繁崩溃的问题。经过简单排查,发现问题的根源在于项目中混用了 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获取布局id,而我们要使用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.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
也就是每次调用setContentView都会修改整个activity的容器中的布局