自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

安卓to鴻蒙系列:ButterKnife(二)

系統(tǒng)
想讀懂ButterKnife難度還是很大的,一方面自己菜,另一方面代碼量大,代碼結(jié)構(gòu)復(fù)雜。而且很多概念不好理解(比如javapoet引入的各種類,綁定信息相關(guān)的幾個類BingdingSet、BingdingSet.Builder、xxxBingding等)。

想了解更多內(nèi)容,請訪問:

51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)

https://harmonyos.51cto.com

本文基于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)的。

  1. BindBitmap vs BindPixelMap//安卓的Bitmap對應(yīng)鴻蒙的PixelMap 
  2.  
  3. BindView vs BindComponent//安卓的View對應(yīng)鴻蒙的Component 
  4.  
  5. BindViews vs BindComponents//同上 
  6.  
  7. 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)秀資源參考:

靜態(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為例,如下注解代碼:

  1. //MainAbilitySlice 
  2. @BindComponent(ResourceTable.Id_viewRoot) 
  3. DirectionalLayout mDlViewRoot; 

 生成一個FieldViewBinding實例,其值為:

  1. final class FieldViewBinding implements MemberViewBinding { 
  2.     private final String name;//"mDlViewRoot" 
  3.     private final TypeName type;//DirectionalLayout 
  4.     private final boolean required;//true 
  • ViewBinding//表示某個控件的綁定信息,其中包括field和method(對于各種事件綁定)。如下代碼所示:
  1. final class ViewBinding { 
  2.     private final Id id; 
  3.     private final Map<ListenerClass, Map<ListenerMethod, Set<MethodViewBinding>>> methodBindings; 
  4.     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 之類的組件或資源

  1. //添加注釋、刪掉多余代碼的parseBindComponent() 
  2. private void parseBindComponent(Element element, Map<TypeElement, BindingSet.Builder> builderMap, 
  3.                                 Set<TypeElement> erasedTargetNames) { 
  4.     //獲取當前元素element的類級別的元素,即XXXAbility 
  5.     TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); 
  6.     
  7.     //獲取@BindComponent的值,即ResourceTable.Id_xxx 
  8.     int id = element.getAnnotation(BindComponent.class).value(); 
  9.      
  10.     //根據(jù)當前元素的類級別的元素先從Map中獲取BindingSet的內(nèi)部類Builder 
  11.     BindingSet.Builder builder = builderMap.get(enclosingElement); 
  12.     Id resourceId = elementToId(element, BindComponent.class, id); 
  13.      
  14.     //builder為空,說明當前類還沒有對應(yīng)的value,需要new一個出來,并放到builderMap中 
  15.     //builder會被BindComponent和OnClick等共用一個(享元模式?),并且它們以XXXAbility分組放在builderMap中。 
  16.     if (builder != null) { 
  17.         String existingBindingName = builder.findExistingBindingName(resourceId); 
  18.         if (existingBindingName != null) { 
  19.             return
  20.         } 
  21.     } else { 
  22.         builder = getOrCreateBindingBuilder(builderMap, enclosingElement); 
  23.     } 
  24.  
  25.     //@BindComponent修飾的字段的簡單名稱,即變量名比如mTextView 
  26.     String name = simpleName.toString(); 
  27.     //@BindComponent修飾的字段的類型,比如Text 
  28.     TypeName type = TypeName.get(elementType); 
  29.     //是否被@Nullable修飾 
  30.     boolean required = isFieldRequired(element); 
  31.  
  32.     //調(diào)用BindingSet的內(nèi)部類Builder中的addField方法封裝解析的信息 
  33.     builder.addField(resourceId, new FieldViewBinding(name, type, required)); 
  34.      
  35.     // Add the type-erased version to the valid binding targets set
  36.     //給所有含有自定義注解的類組成的Set集合中添加元素 
  37.     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的分析吧:

  1. private Map<TypeElement, BindingSet> findAndParseTargets1(RoundEnvironment env) { 
  2.     //這個不是最后返回的對象,這個只是BindingSet對應(yīng)的Builder類,保存了BindComponent、BindString、OnClick等等相關(guān)的綁定信息 
  3.     Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>(); 
  4.     Set<TypeElement> erasedTargetNames = new LinkedHashSet<>(); 
  5.  
  6.     //隱藏代碼 @BindXxx-------------------- 
  7.  
  8.     //綁定界面上的View 
  9.     for (Element element : env.getElementsAnnotatedWith(BindComponent.class)) { 
  10.         parseBindComponent(element, builderMap, erasedTargetNames); 
  11.     } 
  12.  
  13.     //隱藏代碼 bindListener.--------------- 
  14.  
  15.     Map<TypeElement, ClasspathBindingSet> classpathBindings = 
  16.             findAllSupertypeBindings(builderMap, erasedTargetNames); 
  17.     //組合所有類的關(guān)系  組成 樹 
  18.     //這里注釋也寫了,用隊列的方式,將超類與子類綁定,從根開始 
  19.     // Associate superclass binders with their subclass binders. This is a queue-based tree walk 
  20.     // which starts at the roots (superclasses) and walks to the leafs (subclasses). 
  21.     //這個獲取的所有的“類”  放入了隊列 
  22.     Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries = 
  23.             new ArrayDeque<>(builderMap.entrySet()); 
  24.     //即將返回的對象 
  25.     Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>(); 
  26.     while (!entries.isEmpty()) { 
  27.         Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst(); 
  28.  
  29.         TypeElement type = entry.getKey(); 
  30.         BindingSet.Builder builder = entry.getValue(); 
  31.         //拿隊列出來的第一個 在erasedTargetNames中查詢,父類是否在這個這個集合里 
  32.         //為什么要這樣? 因為可能 你在父類中 綁定了一個String 
  33.         //在子類中使用了這個String,所以必須先初始化父類的String 
  34.         //翻看生成好的代碼來看,初始化都是在構(gòu)造函數(shù)中進行綁定的 
  35.         //所以考慮繼承情況,必須把所有的類進行一個關(guān)聯(lián)。 
  36.         TypeElement parentType = findParentType(type, erasedTargetNames, classpathBindings.keySet()); 
  37.         if (parentType == null) { 
  38.             //如果沒有父類,則直接 放入 
  39.             bindingMap.put(type, builder.build()); 
  40.         } else { 
  41.             //如果父類有綁定 
  42.             //再從bindingMap(即將返回的對象)中取看看 是否已經(jīng)放進去了 
  43.             BindingInformationProvider parentBinding = bindingMap.get(parentType); 
  44.             if (parentBinding == null) { 
  45.                 //如果沒綁定進去 再從classpathBindings中取 一個父類 
  46.                 parentBinding = classpathBindings.get(parentType); 
  47.             } 
  48.             if (parentBinding != null) { 
  49.                 //如果這個父類不是空的 則和當前循環(huán)里的builder  子、父類綁定 
  50.                 builder.setParent(parentBinding); 
  51.                 //放入即將返回的map里 
  52.                 bindingMap.put(type, builder.build()); 
  53.             } else { 
  54.                 //翻譯是:有個超類綁定,但還沒有構(gòu)建它,放到后面繼續(xù)排隊 
  55.                 // Has a superclass binding but we haven't built it yet. Re-enqueue for later. 
  56.                 //比如:子類繼承父類都有綁定,這時候子類需要關(guān)聯(lián)父類,父類還沒初始化, 
  57.                 //把子類放到隊尾,等父類初始化完成在進行關(guān)聯(lián) 
  58.                 entries.addLast(entry); 
  59.             } 
  60.         } 
  61.     } 
  62.     //返回,這時候這個map的key 就全是“類信息” 
  63.     //value就是獲取好的 當前類里面需要綁定的內(nèi)容 
  64.     //并且 “類” 也已經(jīng)做好了繼承關(guān)系 
  65.     return bindingMap; 

總結(jié)一下這個方法,就像它的方法名一 樣 “找到并解析各個targets” 。這里有個疑問,targets是什么呢? targets就是待注入的字段、待綁定事件的方法, 比如下面代碼中的mTextView就是target,即“待注入的字段”

  1. @BindComponent(ResourceTable.Id_tv_hello) 
  2. 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é)一下:

  1. parseBindComponent()//調(diào)用Builder的`addField`,注入View。 
  2. parseResourceXXX()//調(diào)用Builder的`addResource`,注入各種資源,如String、intfloat、Dimen、Color、Array、PixelMap等。 
  3. findAndParseListener()//調(diào)用Builder的`addMethod`,綁定各種事件。 

5.debug Map<TypeElement, BindingSet> findAndParseTargets()

先說上面提到 targets是什么呢?,通過跟代碼,可知targets是被注解的Element, 比如MainAblilitySlice中的mDlViewRoot

  1. @BindComponent(ResourceTable.Id_viewRoot) 
  2. DirectionalLayout mDlViewRoot; 

方法的返回值是類和它的模板類的一一對應(yīng)。

  1. "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的幾個方法:

  1. private void error(Element element, String message, Object... args) { 
  2.     printMessage(Kind.ERROR, element, message, args); 
  3.  
  4. private void note(Element element, String message, Object... args) { 
  5.     printMessage(Kind.NOTE, element, message, args); 
  6.  
  7. private void printMessage(Kind kind, Element element, String message, Object[] args) { 
  8.     if (args.length > 0) { 
  9.         message = String.format(message, args); 
  10.     } 
  11.  
  12.     processingEnv.getMessager().printMessage(kind, message, element); 

注意: 一定要執(zhí)行g(shù)radlew --no-daemon :entry:clean :entry:compileDebugJavaWithJavac,compileDebugJavaWithJavac這個task。而且加上clean。不然看不到日志。

向apt程序傳參:

在ButterKnifeProcessor中覆寫了getSupportedOptions(),這樣我們可以向apt傳參了。

  1. @Override 
  2. public Set<String> getSupportedOptions() { 
  3.     ImmutableSet.Builder<String> builder = ImmutableSet.builder(); 
  4.     builder.add(OPTION_SDK_INT, OPTION_DEBUGGABLE); 
  5.     if (trees != null) { 
  6.         builder.add(IncrementalAnnotationProcessorType.ISOLATING.getProcessorOption()); 
  7.     } 
  8.     return builder.build(); 

 傳參方法:在entry中

  1. ohos { 
  2.     compileSdkVersion 5 
  3.     defaultConfig { 
  4.         compatibleSdkVersion 5 
  5.  
  6.         javaCompileOptions { 
  7.             annotationProcessorOptions { 
  8.                 arguments = ['butterknife.debuggable'"true"
  9.             } 
  10.         } 
  11.     } 

在init()方法中,我們可以取出傳入的值:

  1. @Override 
  2. public synchronized void init(ProcessingEnvironment env) { 
  3.     super.init(env); 
  4.     debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE)); 
  5.     env.getMessager().printMessage(Kind.NOTE, "--------------- debuggable = "+debuggable); 

移植butterknife_compiler

通過上面的代碼分析,我們知道在安卓和鴻蒙上butterknife_compiler的基本流程沒有變,所以移植工作要做的就是因為平臺class及api不同,做一些相應(yīng)的調(diào)整。

對比代碼后,差異不大。簡單列舉一下:

  1. findViewById的修改
  2. Resource相關(guān)api的修改
  3. Font的修改(NORMAL變成REGULAR)
  4. BindAnim在鴻蒙版上暫不支持
  5. 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等)。

想了解更多內(nèi)容,請訪問:

51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)

https://harmonyos.51cto.com

 

責任編輯:jianghua 來源: 鴻蒙社區(qū)
相關(guān)推薦

2021-05-11 14:43:16

鴻蒙HarmonyOS應(yīng)用

2021-04-27 09:22:28

鴻蒙HarmonyOS應(yīng)用

2021-04-26 09:46:10

鴻蒙HarmonyOS應(yīng)用

2013-12-12 16:51:43

安卓進化AndroidGoogle

2019-06-20 16:07:12

鴻蒙安卓操作系統(tǒng)

2020-09-10 09:30:03

鴻蒙安卓操作系統(tǒng)

2014-08-04 14:21:22

安卓架構(gòu)

2016-12-14 14:43:11

ButterknifeAndroid

2012-02-06 10:10:40

安卓iOS美國市場

2018-02-09 08:59:47

安卓FuchsiaiOS

2013-11-04 14:49:34

安卓

2021-05-18 15:44:13

IOS安卓鴻蒙

2019-07-12 16:00:25

華為禁令開發(fā)

2021-06-04 05:13:22

鴻蒙

2013-04-24 11:33:50

安卓

2013-10-17 10:17:41

安卓

2014-12-09 11:15:06

郵箱安卓移動端

2020-09-29 13:03:45

安卓應(yīng)用開發(fā)工具開發(fā)

2021-02-25 10:40:00

數(shù)據(jù)

2011-10-18 13:33:02

思亞諾CMMBDTV
點贊
收藏

51CTO技術(shù)棧公眾號