从findViewById失效的问题到深入setContentView

最近有几个老项目需要更新 在接手时发现项目中出现布局的点按操作无效以及很多的崩溃问题 简单排查以后发现接手时项目中混用了 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 的容器中的布局



请使用浏览器的分享功能分享到微信等