從findViewById失效的問題到深入setContentView

山有木xi發表於2024-01-31

最近有幾個老專案需要更新 在接手時發現專案中出現佈局的點按操作無效以及很多的崩潰問題 簡單排查以後發現接手時專案中混用了 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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章