从findViewById失效的问题到深入setContentView

308 阅读4分钟

前言

在 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的容器中的布局

原文地址:mp.weixin.qq.com/s/ir1vw1ud-…