還在用findViewById,不來了解下其它方式?

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第1天,點選檢視活動詳情

眾所周知,都2220年了,findViewById已經是一種非常繁瑣的操作,如果要去獲取的id數量多,則對開發更加不友好。如果一個頁面id過多,經常會有如下場景:

``` TextView title = findViewById(R.id.tv_title); TextView title2 = findViewById(R.id.tv_title2); TextView title3 = findViewById(R.id.tv_title3); TextView title4 = findViewById(R.id.tv_title4); TextView title5 = findViewById(R.id.tv_title5); TextView title6 = findViewById(R.id.tv_title6); TextView title7 = findViewById(R.id.tv_title7); TextView title8 = findViewById(R.id.tv_title8); TextView title9 = findViewById(R.id.tv_title28); TextView title10 = findViewById(R.id.tv_title9); TextView title11 = findViewById(R.id.tv_title10); TextView title12 = findViewById(R.id.tv_title11); TextView title13 = findViewById(R.id.tv_title12); TextView title14 = findViewById(R.id.tv_title13); TextView title15 = findViewById(R.id.tv_title14); TextView title16 = findViewById(R.id.tv_title15); TextView title17 = findViewById(R.id.tv_title16); TextView title18 = findViewById(R.id.tv_title17); TextView title19 = findViewById(R.id.tv_title18);

... ``` 數量一多,你會發現,這已經極其不友好。其實不光是不友好有問題,瞭解findViewById的原理後,你也會發現其內部實現在一定情況下對整體效能有輕微影響。

findViewById() 的原理

findViewById()的流程原理其實非常簡單,以activity中的findViewById流程為例,activity要麼繼承自android.app.Activity,要麼繼承自androidx.appcompat.app.AppCompatActivity(你要是沒適配AndroidX的話那就是support包)。這其中:

1⃣️、android.app.Activity繼承類會通過getWindow得到Window物件來呼叫findViewById();

@Nullable public <T extends View> T findViewById(@IdRes int id) {     return getWindow().findViewById(id); } 2⃣️、androidx.appcompat.app.AppCompatActivity繼承類會通過getDelegate()得到AppCompatDelegate委派類的例項物件後呼叫其findViewByid(),這個物件實際是AppCompatDelegateImpl物件,建立其時傳入了activity.getWindow得到的window物件。

@SuppressWarnings("TypeParameterUnusedInFormals") @Override public <T extends View> T findViewById(@IdRes int id) {     return getDelegate().findViewById(id); } 1⃣️和2⃣️最後都會呼叫Window(getWindow)裡的findViewById()。 @Nullable public <T extends View> T findViewById(@IdRes int id) {     return getDecorView().findViewById(id); } Window類中通過getDecorView()來得到View物件(實際上是一個ViewGroup物件), @Nullable public final <T extends View> T findViewById(@IdRes int id) { if (id == NO_ID) { return null; } return findViewTraversal(id); } 通過呼叫findViewById()來呼叫ViewGroup中重寫的findViewTraversal()。下面原始碼圖片是通過線上瀏覽網站獲取到的ViewGroup類中findViewTraversal()的相關實現:

image.gif 可以看到這就是個遍歷方法,如果對應介面內子元素是個View,只要id配對上可以直接返回,如果是一個ViewGroup則會呼叫子ViewGroup或子View的這個方法,依次遍歷,直到找到目標id。很明顯這是個深度優先搜尋,時間複雜度為O(n)。

相關替代方案

1、 ButterKnife

大名鼎鼎的黃油刀,使用方式極為簡便,專案地址: http://github.com/JakeWharton/butterknife 在gradle中依賴: implementation 'com.jakewharton:butterknife:xxx' annotationProcessor 'com.jakewharton:butterknife-compiler:xxx' 對應在activity中操作為(具體一鍵生成方式這裡不表): ``` public class MainActivity extends AppCompatActivity { @BindView(R.id.tv_title1) TextView tvTitle1; @BindView(R.id.tv_title2) TextView tvTitle1; @BindView(R.id.tv_title3) TextView tvTitle3;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //繫結處理
        ButterKnife.bind(this);
    }

...... } ``` 其實ButterKnife只是通過註解對findViewById的進行的一個取代,增加程式碼的可讀性,而findViewById的各種缺點依然存在。當然,就這個開源框架而言,功能絕不僅僅是替代findViewById()。自從kt語言出來後,黃油刀的功效捉襟見肘,非Java版的老工程,不推薦。

2、kotlin-android-extensions

如果你專案可以使用kotlin,則可以使用kotlin-android-extensions。

在module的gradle中加入: plugins { id 'kotlin-android-extensions' } 可直接在對應類中通過其id的形式控制其控制元件,相當於已經獲取了一遍控制元件id。 ``` import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import com.test.demo.main.activity_main.*

class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) tv_hello.setOnClickListener {

    }
}

} ``` 這種方法其本質上依舊還是通過findViewById去實現,有興趣的小夥伴可以反編譯看看。雖然現在此法已不被官方推薦,但其便利性還是首屈一指。

3、ViewBinding

一曲新人笑,幾度舊人哭。此法一出,kotlin-android-extensions已不被官方推薦。

image.png (注意,此法只能在AndroidStudio3.6及更高版本上可用)

使用方式:在build.gradle中依賴:

android { ... buildFeatures { viewBinding true } } reload後,系統會為每個layout目錄下 XML 佈局檔案生成一個繫結類。每個繫結類均包含對根檢視以及具有 ID 的所有檢視的引用。系統會通過以下方式生成繫結類的名稱:將 XML 檔案的名稱轉換為駝峰式大小寫,並在末尾新增“Binding”一詞。 例如:某個佈局命名為activity_login,其所生成的繫結類的名稱就為LoginActivityBinding,這個繫結類就會完成findViewById的工作。

不同佈局會生成不同繫結類,他們所生成的路徑在:在build/generated/data_binding_base_class_source_out/debug/out/com/xxx/yyy/databinding/目錄下。

當然,如果不想xml檔案生成 Binding 類,可以在 xml 佈局檔案中根 view 寫入此屬性: tools:viewBindingIgnore="true" 其在程式碼中使用方式如下:(ViewBinding在Java和kotlin類中都可使用,這裡僅是拿kotlin類舉例)

· Activity: @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); binding.tvTest.setText("This is ViewBinding"); }

可見,setContentView()中的引數改為了XXXbing.getroot()。呼叫佈局中的某控制元件,只需要XXXbing.viewID(駝峰原則)可直接拿到例項物件(上述程式碼中的binding.tvTest控制元件在xml中的id為tv_test)。例如:xml中TextView控制元件id為tv_demo,則在activity中對應例項為XXXbing.tvDemo。

· Fragment中: ``` public class MyFragment extends Fragment {

private FragmentMyBinding binding;
private Context context;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    binding = FragmentMyBinding.inflate(getLayoutInflater(), container, false);
    return binding.getRoot();
}

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    binding.tvTitle.setText("Hello ViewBinding");
}

@Override
public void onDestroyView() {
    super.onDestroyView();
    binding = null;
}

} ``` 可以看出,跟Activity的引用方式區別不大,這裡需要稍微注意Fragment 的存在時間比其檢視長。在 Fragment對應onDestroyView()時要清除對繫結類例項的所有引用。

·RecyclerView adapter中: ``` public class MyAdapter extends RecyclerView.Adapter { 、、、、、、 @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { ItemLayoutBinding itemBinding = ItemLayoutBinding.inflate(inflater, parent, false); return new ViewHolder(itemBinding); }

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
    holder.textView.setText(mData.get(position));
}

static class ViewHolder extends RecyclerView.ViewHolder {
    TextView textView;

    public ViewHolder(@NonNull ItemLayoutBinding itemBinding) {
        super(itemBinding.getRoot());
        textView = itemBinding.textView;
    }
}

} ``` 可見,應用方式無大致區別。

總結

1、findViewById相容性好,適用所有場景,且靈活;

2、findViewById效能略差,底層就是個深度優先搜尋,且id過多情況下容易造成可讀性極差的情況,從上述的原理流程中不難看出,在Activity中呼叫findViewById,實際上是呼叫Window中的findViewById,但是Fragment中並沒有單獨的Window,Fragment中呼叫findViewById的效果和Activity中呼叫的效果一模一樣。所以如果一個Activity中有多個Fragment,Fragment中的控制元件名稱又有重複的,直接使用findViewById會爆錯。

3、ButterKnife可一鍵生成,方便至極,但缺點跟findViewById一樣。如果不是老工程,此法已不推薦使用。

4、Google官方表示,與使用 findViewById 相比,ViewBinding具有一些很顯著的優點:

· 空指標安全:由於檢視繫結(ViewBinding)會建立對檢視的直接引用,因此不存在因檢視 ID 無效而引發 Null 指標異常的風險。此外,如果檢視僅出現在佈局的某些配置中,則繫結類中包含其引用的欄位會使用 @Nullable 標記。(說白了就是讓你丫程式碼少爆空指標)

· 型別安全:每個繫結類中的欄位均具有與它們在 XML 檔案中引用的檢視相匹配的型別。這意味著不存在發生類轉換異常的風險。

這些差異意味著佈局和程式碼之間的不相容將會導致構建在編譯時(而非執行時)失敗。

下篇預告:第四種方式:DataBinding。