還在用findViewById,不來了解下其它方式?
持續創作,加速成長!這是我參與「掘金日新計劃 · 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()的相關實現:
可以看到這就是個遍歷方法,如果對應介面內子元素是個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已不被官方推薦。
(注意,此法只能在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
@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。
- 一文講完Jetpack常用修飾符
- JetpackCompose中的Dialog、AlertDialog
- Activity互動問題,你確定都知道?
- Kotlin中的內建函式-apply、let
- Compose自定義動畫API指南
- Android EditText關於imeOptions的設定和響應
- Jetpack 之Glance Compose實現一個小元件
- Compose高級別API動畫指南
- Android錄音功能的實現及踩坑記錄
- LayoutInflater原始碼解析及常見相關報錯分析
- UI自動重新整理大法:DataBinding資料繫結
- DataBinding簡易入門
- 還在用findViewById,不來了解下其它方式?