安卓to鴻蒙系列:ButterKnife(二)
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
本文基于https://gitee.com/openharmony-tpc/butterknife 分析JakeWharton/butterknife的源碼,及移植到鴻蒙需要做的工作。
如果對apt的概念及實踐不熟悉,請先移步:安卓to鴻蒙系列:ButterKnife(一),然后再來閱讀本文,會事半功倍!
butterknife項目結(jié)構(gòu):
和我們在安卓to鴻蒙系列:ButterKnife(一)寫的“乞丐版BufferKnife”一樣,主要由三個module組成
- butterknife_annotations//編譯時、運行時都用到
定義了注解
- butterknife_compiler//編譯時用到
apt的主要實現(xiàn)部分。注解的解析、處理,生成模板文件
- butterknife//運行時用到
對外的工具類,供用戶使用,完成注入操作。 butterknife_runtime 定義運行時用到的一些類及工具方法。
移植butterknife_annotations
直接對比openharmony-tpc和JakeWharton的這個module吧:
可以看到有增、刪、改。
其中增加和刪除的是一一對應(yīng)的。
- BindBitmap vs BindPixelMap//安卓的Bitmap對應(yīng)鴻蒙的PixelMap
- BindView vs BindComponent//安卓的View對應(yīng)鴻蒙的Component
- BindViews vs BindComponents//同上
- BindDrawable vs BindElement //安卓的Drawable對應(yīng)鴻蒙的ohos.agp.components.element.Element
其它都是修改,以BindingString為例,只是去掉了鴻蒙沒有的注解StringRes、修改注釋。
ps:對于鴻蒙沒有的注解,還有一個辦法就是把androidx或support包下相應(yīng)的文件copy進來,并且包名也保持一致,這樣我們就不需要修改BindingString這一類文件了。經(jīng)過對比發(fā)現(xiàn),可以減少不小的工作量。
還有一些修改稍復(fù)雜一些,以O(shè)nClick為例。只要寫過兩個平臺的代碼,還是很容易理解的,只是做相應(yīng)的等價替換。
分析butterknife_compiler的源碼
優(yōu)秀資源參考:
- ButterKnife編譯時生成代碼原理:butterknife-compiler源碼分析
- 拆 Jake Wharton 系列之 ButterKnife
- 學(xué)習(xí)筆記ButterKnife
- ButterKnife解析
靜態(tài)分析
1.主要的幾個類
- ButterKnifeProcessor//注解入口類,apt程序必須繼承AbstractProcessor,沒什么好說的。
- BindingSet//從名字可知,這個類是綁定信息的集合。舉例:MainAbilitySlice對應(yīng)一個BindingSet,也就對應(yīng)一個xxxx_ViewBinding。
BindingSet的實例存在于編譯期,執(zhí)行它的brewJava()方法生成xxxx_ViewBinding文件。
- 各種XxxBinding,如:FieldViewBinding、MethodViewBinding、ResourceBinding的各種子類,表示某字段的綁定信息。
以MainAbilitySlice和FieldViewBinding為例,如下注解代碼:
- //MainAbilitySlice
- @BindComponent(ResourceTable.Id_viewRoot)
- DirectionalLayout mDlViewRoot;
生成一個FieldViewBinding實例,其值為:
- final class FieldViewBinding implements MemberViewBinding {
- private final String name;//"mDlViewRoot"
- private final TypeName type;//DirectionalLayout
- private final boolean required;//true
- }
- ViewBinding//表示某個控件的綁定信息,其中包括field和method(對于各種事件綁定)。如下代碼所示:
- final class ViewBinding {
- private final Id id;
- private final Map<ListenerClass, Map<ListenerMethod, Set<MethodViewBinding>>> methodBindings;
- private final FieldViewBinding fieldBinding;
- }
2.ButterKnifeProcessor#process()方法相當注解執(zhí)行的main方法(會進入多次),主要干了兩件事findAndParseTargets()和brewJava(),如下圖所示:
用到的工具:SequenceDiagram - IntelliJ IDEA插件,直接在插件市場搜索、安裝就行,用來生成方法調(diào)用時序圖
3.其中ButterKnifeProcessor#findAndParseTargets()的主要功能是找到并解析各個targets,如下圖所示:
ps:這個圖省略了很多parseXXX()方法,只保留了一個parseBindComponent(),因為功能類似。不然圖太長了。
由上圖可知findAndParseTargets()方法實現(xiàn)了以下三件事:
解析注入對象相關(guān)的注解parseXXX()
處理@BindComponent,@BindString 之類的組件或資源
- //添加注釋、刪掉多余代碼的parseBindComponent()
- private void parseBindComponent(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
- Set<TypeElement> erasedTargetNames) {
- //獲取當前元素element的類級別的元素,即XXXAbility
- TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
- //獲取@BindComponent的值,即ResourceTable.Id_xxx
- int id = element.getAnnotation(BindComponent.class).value();
- //根據(jù)當前元素的類級別的元素先從Map中獲取BindingSet的內(nèi)部類Builder
- BindingSet.Builder builder = builderMap.get(enclosingElement);
- Id resourceId = elementToId(element, BindComponent.class, id);
- //builder為空,說明當前類還沒有對應(yīng)的value,需要new一個出來,并放到builderMap中
- //builder會被BindComponent和OnClick等共用一個(享元模式?),并且它們以XXXAbility分組放在builderMap中。
- if (builder != null) {
- String existingBindingName = builder.findExistingBindingName(resourceId);
- if (existingBindingName != null) {
- return;
- }
- } else {
- builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
- }
- //@BindComponent修飾的字段的簡單名稱,即變量名比如mTextView
- String name = simpleName.toString();
- //@BindComponent修飾的字段的類型,比如Text
- TypeName type = TypeName.get(elementType);
- //是否被@Nullable修飾
- boolean required = isFieldRequired(element);
- //調(diào)用BindingSet的內(nèi)部類Builder中的addField方法封裝解析的信息
- builder.addField(resourceId, new FieldViewBinding(name, type, required));
- // Add the type-erased version to the valid binding targets set.
- //給所有含有自定義注解的類組成的Set集合中添加元素
- erasedTargetNames.add(enclosingElement);
- }
- 解析事件綁定相關(guān)的注解findAndParseListener()
處理@OnClick,@OnItemClick,@OnTextChanged 之類的Listener
通過以上兩步,完成了注解信息的掃描收集,并將解析的信息保存到builderMap和erasedTargetNames兩個集合中;
- findAllSupertypeBindings(),findParentType(),及findAndParseTargets()
第三步,對上面提到的builderMap和erasedTargetNames兩個集合中的信息進行重新整理,最終返回一個以TypeElement為key,BindingSet為vaule的bindingMap集合。
下面直接帖學(xué)習(xí)筆記ButterKnife的分析吧:
- private Map<TypeElement, BindingSet> findAndParseTargets1(RoundEnvironment env) {
- //這個不是最后返回的對象,這個只是BindingSet對應(yīng)的Builder類,保存了BindComponent、BindString、OnClick等等相關(guān)的綁定信息
- Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
- Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
- //隱藏代碼 @BindXxx--------------------
- //綁定界面上的View
- for (Element element : env.getElementsAnnotatedWith(BindComponent.class)) {
- parseBindComponent(element, builderMap, erasedTargetNames);
- }
- //隱藏代碼 bindListener.---------------
- Map<TypeElement, ClasspathBindingSet> classpathBindings =
- findAllSupertypeBindings(builderMap, erasedTargetNames);
- //組合所有類的關(guān)系 組成 樹
- //這里注釋也寫了,用隊列的方式,將超類與子類綁定,從根開始
- // Associate superclass binders with their subclass binders. This is a queue-based tree walk
- // which starts at the roots (superclasses) and walks to the leafs (subclasses).
- //這個獲取的所有的“類” 放入了隊列
- Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
- new ArrayDeque<>(builderMap.entrySet());
- //即將返回的對象
- Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
- while (!entries.isEmpty()) {
- Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();
- TypeElement type = entry.getKey();
- BindingSet.Builder builder = entry.getValue();
- //拿隊列出來的第一個 在erasedTargetNames中查詢,父類是否在這個這個集合里
- //為什么要這樣? 因為可能 你在父類中 綁定了一個String
- //在子類中使用了這個String,所以必須先初始化父類的String
- //翻看生成好的代碼來看,初始化都是在構(gòu)造函數(shù)中進行綁定的
- //所以考慮繼承情況,必須把所有的類進行一個關(guān)聯(lián)。
- TypeElement parentType = findParentType(type, erasedTargetNames, classpathBindings.keySet());
- if (parentType == null) {
- //如果沒有父類,則直接 放入
- bindingMap.put(type, builder.build());
- } else {
- //如果父類有綁定
- //再從bindingMap(即將返回的對象)中取看看 是否已經(jīng)放進去了
- BindingInformationProvider parentBinding = bindingMap.get(parentType);
- if (parentBinding == null) {
- //如果沒綁定進去 再從classpathBindings中取 一個父類
- parentBinding = classpathBindings.get(parentType);
- }
- if (parentBinding != null) {
- //如果這個父類不是空的 則和當前循環(huán)里的builder 子、父類綁定
- builder.setParent(parentBinding);
- //放入即將返回的map里
- bindingMap.put(type, builder.build());
- } else {
- //翻譯是:有個超類綁定,但還沒有構(gòu)建它,放到后面繼續(xù)排隊
- // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
- //比如:子類繼承父類都有綁定,這時候子類需要關(guān)聯(lián)父類,父類還沒初始化,
- //把子類放到隊尾,等父類初始化完成在進行關(guān)聯(lián)
- entries.addLast(entry);
- }
- }
- }
- //返回,這時候這個map的key 就全是“類信息”
- //value就是獲取好的 當前類里面需要綁定的內(nèi)容
- //并且 “類” 也已經(jīng)做好了繼承關(guān)系
- return bindingMap;
- }
總結(jié)一下這個方法,就像它的方法名一 樣 “找到并解析各個targets” 。這里有個疑問,targets是什么呢? targets就是待注入的字段、待綁定事件的方法, 比如下面代碼中的mTextView就是target,即“待注入的字段”
- @BindComponent(ResourceTable.Id_tv_hello)
- Text mTextView;
動態(tài)分析
動態(tài)分析主要是驗證一下上面靜態(tài)分析的結(jié)論。對于比較復(fù)雜的代碼,需要debug跟一下代碼,查看運行時關(guān)鍵變量的值。
怎么調(diào)試butterknife_compiler?
參考:https://www.w3ma.com/how-to-debug-an-annotation-processor-in-android-studio/
1.新建一個remote debug,比如命名為aptDebug
因為apt過程在編譯期,所以需要remote debug。什么是remote debug,可以自己google一下。
2.在butterknife根目錄的命令行中運行g(shù)radlew --no-daemon -Dorg.gradle.debug=true :entry:clean :entry:compileDebugJavaWithJavac,編譯過程處于等待調(diào)試的狀態(tài),如下如:
:entry:clean加上它是表示重新構(gòu)建。
-Dorg.gradle.debug=true設(shè)置為true時,Gradle將在啟用遠程調(diào)試的情況下運行構(gòu)建,偵聽端口5005。這等效于將-agentlib:jdwp = transport = dt_socket,server = y,suspend = y,address = 5005添加到 JVM命令行,它將掛起虛擬機,直到連接了調(diào)試器。
3.設(shè)置斷點,然后點擊小蟲子debug按鈕。
4.以debug parseBindComponent()為例:
小技巧:條件斷點在這里會提高調(diào)試的效率。自己google一下。
通過debug跟代碼,可知void parseBindComponent(Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames)方法的輸入element為@BindComponent注解的java元素,輸出builderMap和erasedTargetNames。
其中BinderingSet$Builder為BinderingSet的構(gòu)建者,這里用到了Builder構(gòu)建者設(shè)計模式。
其中BinderingSet記錄了模板類XXX_ViewBinding的生成規(guī)則。
在parseBindComponent方法中,調(diào)用Builder的addField添加了一條生成java文件的規(guī)則,即注入View。其它的parseXXX()方法類似??偨Y(jié)一下:
- parseBindComponent()//調(diào)用Builder的`addField`,注入View。
- parseResourceXXX()//調(diào)用Builder的`addResource`,注入各種資源,如String、int、float、Dimen、Color、Array、PixelMap等。
- findAndParseListener()//調(diào)用Builder的`addMethod`,綁定各種事件。
5.debug Map<TypeElement, BindingSet> findAndParseTargets()
先說上面提到 targets是什么呢?,通過跟代碼,可知targets是被注解的Element, 比如MainAblilitySlice中的mDlViewRoot
- @BindComponent(ResourceTable.Id_viewRoot)
- DirectionalLayout mDlViewRoot;
方法的返回值是類和它的模板類的一一對應(yīng)。
- 如"com.example.butterknife.slice.MainAbilitySlice" -> "com.example.butterknife.slice.MainAbilitySlice_ViewBinding"
日志分析:
debug的效率其實很低。聽說10倍程序員都愛打日志。所以,我們也要知道apt的messager的用法及注意事項有哪些?
雖然System.out.println();也可以打日志。但是messager會根據(jù)日志類型,把Kind.WARNING和Kind.ERROR類型的日志做統(tǒng)計,方便我們定位問題。
在ButterKnifeProcessor中有封裝messager的幾個方法:
- private void error(Element element, String message, Object... args) {
- printMessage(Kind.ERROR, element, message, args);
- }
- private void note(Element element, String message, Object... args) {
- printMessage(Kind.NOTE, element, message, args);
- }
- private void printMessage(Kind kind, Element element, String message, Object[] args) {
- if (args.length > 0) {
- message = String.format(message, args);
- }
- processingEnv.getMessager().printMessage(kind, message, element);
- }
注意: 一定要執(zhí)行g(shù)radlew --no-daemon :entry:clean :entry:compileDebugJavaWithJavac,compileDebugJavaWithJavac這個task。而且加上clean。不然看不到日志。
向apt程序傳參:
在ButterKnifeProcessor中覆寫了getSupportedOptions(),這樣我們可以向apt傳參了。
- @Override
- public Set<String> getSupportedOptions() {
- ImmutableSet.Builder<String> builder = ImmutableSet.builder();
- builder.add(OPTION_SDK_INT, OPTION_DEBUGGABLE);
- if (trees != null) {
- builder.add(IncrementalAnnotationProcessorType.ISOLATING.getProcessorOption());
- }
- return builder.build();
- }
傳參方法:在entry中
- ohos {
- compileSdkVersion 5
- defaultConfig {
- compatibleSdkVersion 5
- javaCompileOptions {
- annotationProcessorOptions {
- arguments = ['butterknife.debuggable': "true"]
- }
- }
- }
- }
在init()方法中,我們可以取出傳入的值:
- @Override
- public synchronized void init(ProcessingEnvironment env) {
- super.init(env);
- debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
- env.getMessager().printMessage(Kind.NOTE, "--------------- debuggable = "+debuggable);
- }
移植butterknife_compiler
通過上面的代碼分析,我們知道在安卓和鴻蒙上butterknife_compiler的基本流程沒有變,所以移植工作要做的就是因為平臺class及api不同,做一些相應(yīng)的調(diào)整。
對比代碼后,差異不大。簡單列舉一下:
- findViewById的修改
- Resource相關(guān)api的修改
- Font的修改(NORMAL變成REGULAR)
- BindAnim在鴻蒙版上暫不支持
- COLOR_STATE_LIST在鴻蒙版上暫不支持
移植butterknife_runtime
butterknife_runtime同時被butterknife_compiler和butterknife兩個module依賴,其中大多是一些工具類,同樣,做相應(yīng)的api修改就可以。
對比代碼后,差異比較大。但是都集中的對資源Resource的加載差異上,以及androidx.annotation.UiThread之類的注解(直接刪掉就好)。
移植butterknife
butterknife這個module只有一個類ButterKnife,這個類的作用就是通過反射實例化XXX_ViewBinding,并提供一系列靜態(tài)方法如Unbinder bind(Ability target)來實現(xiàn)target中變量的注入和方法的綁定。同樣,做相應(yīng)的api修改就可以。
歡迎有興趣的朋友可以完善entry中的用例
目前該庫有一些bug,比如:Unbinder bind(Ability target)注入Ability會失敗。我相信,bug不止這一個。
發(fā)現(xiàn)bug,提issue。我們一起將它完善。
總結(jié)
距離寫完安卓to鴻蒙系列:ButterKnife(一)已經(jīng)有兩個多月,當時,寫一個乞丐版ButterKnife覺得還是很easy的。但是,想讀懂ButterKnife難度還是很大的,一方面自己菜,另一方面代碼量大,代碼結(jié)構(gòu)復(fù)雜。而且很多概念不好理解(比如javapoet引入的各種類,綁定信息相關(guān)的幾個類BingdingSet、BingdingSet.Builder、xxxBingding等)。
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)