組件通信註解框架實踐

組件通信註解框架實踐

目錄介紹

  • 01.爲何需要組件間通信
  • 02.實現同級組件通信方式
  • 03.先看一個簡單的案例
  • 04.項目組件通信流程
  • 05.逆向簡化註冊流程
  • 06.這個註解是做什麼的
  • 07.註解是如何生成代碼
  • 08.如何定義註解處理器
  • 09.項目庫的設計和完善
  • 10.封裝該庫有哪些特點
  • 11.一些常見的報錯問題
  • 12.部分原理分析的說明

01.爲何需要組件間通信

  • 明確一個前提:各個業務組件之間不會是相互隔離而是必然存在一些交互的;
    • 業務複用:在Module A需要引用Module B提供的某個功能,比如需要版本更新業務邏輯,而我們一般都是使用強引用的Class顯式的調用;
    • 業務複用:在Module A需要調用Module B提供的某個方法,例如別的Module調用用戶模塊退出登錄的方法;
    • 業務獲取參數:登陸環境下,在Module A,C,D,E多個業務組件需要拿到Module B登陸註冊組件中用戶信息id,name,info等參數訪問接口數據;
  • 這幾種調用形式大家很容易明白,正常開發中大家也是毫不猶豫的調用。但是在組件化開發的時候卻有很大的問題:
    • 由於業務組件之間沒有相互依賴,組件Module B的Activity Class在自己的Module中,那Module A必然引用不到,這樣無法調用類的功能方法;由此:必然需要一種支持組件化需求的交互方式,提供平行級別的組件間調用函數通信交互的功能。
  • 項目庫開源地址

02.實現同級組件通信方式

  • 至於關於頁面跳轉
    • 那肯定是首選路由,比如阿里的ARouter。但是涉及到組件之間業務複用,業務邏輯的交互等等,就有點難搞了……那該怎麼處理比較方便呢?
  • 組件業務邏輯交互通信
    • 比如業務組件層劃分
      • 組件A,組件B,組件C,組件D,組件E等等,這些業務組件並不是相互依賴,它們之間是相同的層級!
    • 舉一個業務案例
      • 比如有個選擇用戶學員的彈窗,代碼寫到了組件A中,這個時候組件C和組件D需要複用組件A中的彈窗,該業務邏輯如何處理?
      • 比如組件E是我的用戶相關的業務邏輯,App登陸後,組件B和組件C需要用到用戶的id去請求接口,這個時候如何獲取組件E中用戶id呢?
    • 該層級下定義一個公共通信組件
      • 接口通信組件【被各個業務組件依賴】,該相同層級的其他業務組件都需要依賴這個通信組件。這個時候各個模塊都可以拿到通信組件的類……
  • 需要具備的那些特點
    • 使用簡單方便,避免同級組件相互依賴。代碼入侵性要低,支持業務交互,自動化等特性。

03.先看一個簡單的案例

  • 先說一下業務場景
    • 版本更新業務組件(處理更新彈窗,apk下載,apk的md5校驗,安裝等邏輯,還涉及到一些業務邏輯,比如更新模式普通或者強更,還有渠道,還有時間段等)
    • 主模塊首頁,我的組件,設置中心組件等多個module組件中都會用到版本更新功能,除了主模塊外,其他組件沒有依賴版本更新組件,那麼如何調用裏面的更新彈窗業務邏輯呢?
  • 創建一個接口通信組件
    • 如上所示,各個同級的業務組件,A,B,C,D等都依賴該接口通信組件。那麼這樣就會拿到通信組件的類,爲了實現通信交互。可以在該接口通信組件中定義接口並暴露抽象更新彈窗方法,那麼在版本更新組件中寫接口實現類。
    • 創建一個map集合,存儲實現類的全路徑,然後put到map集合中;這樣可以get拿到實現類的路徑,就可以利用反射創建實例對象。
  • 通信組件幾個主要類
    • BusinessTransfer,主要是map集合中get獲取和put添加接口類的對象,利用反射機制創建實例對象。該類放到通信組件中
    • IUpdateManager,該類是版本更新接口類,定義更新抽象方法。該類放到通信組件中
    • UpdateManagerImpl,該類是IUpdateManager接口實現類,主要是具體業務邏輯的實現。該類放到具體實現庫代碼中,比如我的組件
  • 主要實現的代碼如下所示
    //接口
    public interface IUpdateManager extends Serializable {
    
        void checkUpdate(UpdateManagerCallBack updateManagerCallBack);
    
        interface UpdateManagerCallBack {
            void updateCallBack(boolean isNeedUpdate);
        }
    }
    
    //接口實現類
    public class UpdateManagerImpl implements IUpdateManager {
        @Override
        public void checkUpdate(UpdateManagerCallBack updateManagerCallBack) {
            try {
                IConfigService configService = DsxxjServiceTransfer.$().getConfigureService();
                String data = configService.getConfig(KEY_APP_UPDATE);
                if (TextUtils.isEmpty(data)) {
                    if (updateManagerCallBack != null) {
                        updateManagerCallBack.updateCallBack(false);
                    }
                    return;
                }
                ForceUpdateEntity xPageUpdateEntity = JSON.parseObject(data, ForceUpdateEntity.class);
                ForceUpdateManager.getInstance().checkForUpdate(xPageUpdateEntity, updateManagerCallBack);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    //如何使用
    //在初始化時注入,建議放在application中設置,調用setImpl其實就是把路徑字符串put到map集合中
    BusinessTransfer businessTransfer = BusinessTransfer.$();
    businessTransfer.setImpl(BusinessTransfer.BUSINESS_IMPL_UPDATE_MANAGER,
            PACKAGE_NAME + ".base.businessimpl.UpdateManagerImpl");
    
  • 那麼如何調用呢?可以在各個組件中調用,代碼如下所示……
    //版本更新
    BusinessTransfer.$().getUpdate().checkUpdate(new IUpdateManager.UpdateManagerCallBack() {
        @Override
        public void updateCallBack(boolean isNeedUpdate) {
    
        }
    });
    
  • 反射創建接口的實現類對象
    String className = implsMap.get(key);
    try {
        return (T) Class.forName(className).newInstance();
    } catch (InstantiationException e) {
        e.printStackTrace();
    }
    
  • 這種方式存在幾個問題
    • 1.注入的時候要填寫正確的包名,否則在運行期會出錯,且不容易找到;
    • 2.針對接口實現類,不能混淆,否則會導致反射找不到具體的類,因爲是根據類的全路徑反射創建對象;所以每次寫一個接口+實現類,都要在混淆文件中添加一下,比較麻煩……
    • 3.每次添加新的接口通信,都需要手動去注入到map集合,稍微有點麻煩,能否改爲自動註冊呢?
    • 4.每次還要在Transfer的類中,添加獲取該接口對象的方法,能否自動一點?
    • 5.可能出現空指針,一旦忘記沒有注入或者反射創建對象失敗,則直接導致崩潰……

04.項目組件通信流程

  • 具體實現方案
    • 比方說,主app中的首頁有版本更新,業務組件用戶中心的設置頁面也有版本更新,而版本升級的邏輯是寫在版本更新業務組件中。這個時候操作如下所示
    • image

05.逆向簡化註冊流程

  • 在module通信組件中定義接口,注意需要繼承IRouteApi接口
    public interface IUpdateManager extends IRouteApi {
    
        void checkUpdate(UpdateManagerCallBack updateManagerCallBack);
    
        interface UpdateManagerCallBack {
            void updateCallBack(boolean isNeedUpdate);
        }
    
    }
    
  • 在需要實現服務的組件中寫接口實現類,注意需要添加註解
    @RouteImpl(IUpdateManager.class)
    public class UpdateImpl implements IUpdateManager {
        @Override
        public void checkUpdate(UpdateManagerCallBack updateManagerCallBack) {
            //省略
        }
    }
    
  • 如何獲取服務的實例對象
    //無返回值的案例
    //設置監聽
    IUpdateManager iUpdateManager = TransferManager.getInstance().getApi(IUpdateManager.class);
    iUpdateManager.checkUpdate(new IUpdateManager.UpdateManagerCallBack() {
       @Override
       public void updateCallBack(boolean isNeedUpdate) {
    
       }
    });
    
    //有返回值的案例
    userApi = TransferManager.getInstance().getApi(IUserManager.class);
    String userInfo = userApi.getUserInfo();
    
  • 關於get/put主要是存屬什麼呢
    /**
     * key表示的是自定義通信接口
     * value表示自定義通信接口的實現類
     */
    private Map<Class, Class> apiImplementMap = new HashMap<>();
    
  • 代碼混淆
    -keep class com.yc.api.**{*;}
    -keep public class * implements com.yc.api.** { *; }
    
  • 不需要在額外添加通信接口實現類的混淆代碼
    • 因爲用到了反射,而且是用Class.forName(name)創建反射對象。所以必須保證name路徑是正確的,否則找不到類。
    • 該庫,你定義的實現類已經繼承了我定義的接口,因爲針對繼承com.yc.api.**的子類,會忽略混淆。已經處理……所以不需要你額外處理混淆問題!

06.這個註解是做什麼的

  • 這個註解有什麼用呢
    • 框架會在項目的編譯器掃描所有添加@RouteImpl註解的XxxImpl接口實現類,然後傳入接口類的class對象。這樣就可以通過註解拿到接口和接口的實現類……
  • apt編譯後生成的代碼
    • build--->generated--->ap_generated_sources--->debug---->out---->com.yc.api.contract
    • 這段代碼什麼意思:編譯器生成代碼,並且該類是繼承自己自定義的接口,調用IRegister接口中的register方法,key是接口class,value是接口實現類class,直接在編譯器把接口和實現類存儲起來。用的時候直接取……
    public class IUpdateManager$$Contract implements IRouteContract {
      @Override
      public void register(IRegister register) {
        register.register(IUpdateManager.class, UpdateImpl.class);
      }
    }
    
    • image

07.註解是如何生成代碼

  • 如何拿到註解標註的類,看個案例
    @RouteImpl(IUserInfoManager.class)
    public class Test implements IUserInfoManager {
        @Override
        public String getUserId() {
            return null;
        }
    }
    
    private void test(){
        //這個地方先寫個假的業務代碼,實際apt中是通過roundEnvironment對象拿到註解標記的類
        Class c = Test.class;
        //Set<? extends Element> annotated = roundEnvironment.getElementsAnnotatedWith(typeElement);
        //找到修飾了註解RouteImpl的類
        RouteImpl annotation = (RouteImpl) c.getAnnotation(RouteImpl.class);
        if (annotation != null) {
            try {
                //獲取ContentView的屬性值
                Class value = annotation.value();
                String name = value.getName();
                System.out.println("註解標記的類名"+name);
            } catch (RuntimeException e) {
                e.printStackTrace();
                System.out.println("註解標記的類名"+e.getMessage());
            }
        }
    }
    
  • 手動編程還是自動生成
    • 在代碼的編寫過程中自己手動實現,也可以通過apt生成。作爲一個框架,當然是自動解析RouteImpl註解然後生成這些類文件更好了。要想自動生成代碼的映射關係,那麼便要了解apt和javapoet了。

08.如何定義註解處理器

  • apt工具瞭解一下
    • APT是Annotation Processing Tool的簡稱,即註解處理工具。它是在編譯期對代碼中指定的註解進行解析,然後做一些其他處理(如通過javapoet生成新的Java文件)。
  • 定義註解處理器
    • 用來在編譯期掃描加入@RouteImpl註解的類,然後做處理。這也是apt最核心的一步,新建RouteImplProcessor 繼承自 AbstractProcessor,然後實現process方法。在項目編譯期會執行RouterProcessor的process()方法,我們便可以在這個方法裏處理RouteImpl註解了。
  • 初始化自定義Processor
    @AutoService(Processor.class)
    public class RouteImplProcessor extends AbstractProcessor {
    
    }
    
  • 在init方法中初始化獲取文件生成器信息
    /**
     * 初始化方法
     * @param processingEnvironment                 獲取信息
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //文件生成器 類/資源
        filer = processingEnv.getFiler();
        //節點工具類 (類、函數、屬性都是節點)
        elements = processingEnv.getElementUtils();
    }
    
  • 在process方法中拿到註解標記的類信息
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (TypeElement typeElement : set) {
            Set<? extends Element> annotated = roundEnvironment.getElementsAnnotatedWith(typeElement);
            for (Element apiImplElement : annotated) {
                //被 RouteImpl 註解的節點集合
                RouteImpl annotation = apiImplElement.getAnnotation(RouteImpl.class);
                if (annotation == null || !(apiImplElement instanceof TypeElement)) {
                    continue;
                }
                ApiContract<ClassName> apiNameContract = ElementTool.getApiClassNameContract(elements,
                        annotationValueVisitor,(TypeElement) apiImplElement);
                if (RouteConstants.LOG){
                    System.out.println("RouteImplProcessor--------process-------apiNameContract---"+apiNameContract);
                }
            }
        }
        return true;
    }
    
  • 然後生成代碼,主要是指定生成代碼路徑,然後創建typeSpec註解生成代碼。
    • 這個javapoet工具,目前還緊緊是套用ARouter,創建類名,添加接口,添加註解,添加方法,添加修飾符,添加函數體等等。也就是說將一個類代碼拆分成n個部分,然後逆向拼接到一起。最後去write寫入代碼……
    //生成註解類相關代碼
    TypeSpec typeSpec = buildClass(apiNameContract);
    String s = typeSpec.toString();
    if (RouteConstants.LOG){
        System.out.println("RouteImplProcessor--------process-------typeSpec---"+s);
    }
    try {
        //指定路徑:com.yc.api.contract
        JavaFile.builder(RouteConstants.PACKAGE_NAME_CONTRACT, typeSpec)
                .build()
                .writeTo(filer);
    } catch (IOException e) {
        e.printStackTrace();
    }
    
  • 來看看怎麼創建註解類
    • 大概思路就是,將我們平時的類,拆分,然後拼接成實體。ParameterSpec是創建參數的實現,MethodSpec是函數的生成實現等等……
    private TypeSpec buildClass(ApiContract<ClassName> apiNameContract) {
        String simpleName = apiNameContract.getApi().simpleName();
        //獲取 com.yc.api.IRouteContract 信息,也就是IRouteContract接口的路徑
        TypeElement typeElement = elements.getTypeElement(RouteConstants.INTERFACE_NAME_CONTRACT);
        ClassName className = ClassName.get(typeElement);
        String name = simpleName + RouteConstants.SEPARATOR + RouteConstants.CONTRACT;
        //這裏面又有添加方法註解,添加修飾符,添加參數規格,添加函數題,添加返回值等等
        MethodSpec methodSpec = buildMethod(apiNameContract);
        //創建類名
        return TypeSpec.classBuilder(name)
                //添加super接口
                .addSuperinterface(className)
                //添加修飾符
                .addModifiers(Modifier.PUBLIC)
                //添加方法【然後這裏面又有添加方法註解,添加修飾符,添加參數規格,添加函數題,添加返回值等等】
                .addMethod(methodSpec)
                //創建
                .build();
    }
    

09.項目庫的設計和完善

  • ModuleBus主要由三部分組成,包括對外提供的api調用模塊、註解模塊以及編譯時通過註解生產相關的類模塊。
    • api-compiler 編譯期解析註解信息並生成相應類以便進行注入的模塊
    • api-manager 註解的聲明和信息存儲類的模塊,以及開發調用的api功能和具體實現
  • 編譯生成代碼發生在編譯器
    • 編譯期是在項目編譯的時候,這個時候還沒有開始打包,也就是沒有生成apk呢!框架在這個時期根據註解去掃描所有文件,然後生成路由映射文件。這些文件都會統一打包到apk裏!
  • 無需初始化操作
    • 先看ARouter,會有初始化,主要是收集路由映射關係文件,在程序啓動的時候掃描這些生成的類文件,然後獲取到映射關係信息,保存起來。這個封裝庫不需要初始化,簡化步驟,在獲取的時候如果沒有則在put操作map集合。具體看代碼!

10.封裝該庫有哪些特點

  • 註解生成代碼自動註冊
    • 使用apt註解在編譯階段生成服務接口與實現的映射註冊幫助類,其實這部分就相當於是替代了之前在application初始化注入的步驟,獲取服務時自動使用幫助類完成註冊,不必手動調用註冊方法。
  • 避免空指針崩潰
    • 無服務實現註冊時,使用空對象模式 + 動態代理的設計提前暴露調用錯誤,主要拋出異常,在測試時就發現問題,防止空指針異常。
  • 代碼入侵性低
    • 無需改動之前的代碼,只需要在之前的接口和接口實現類按照約定添加註解規範即可。其接口+接口實現類還是用之前的,完全無影響……
  • 按照你需要來加載
    • 首次獲取接口服務的時候,用反射生成映射註冊幫助類的實例,再返回實現的實例。
  • 豐富的代碼案例
    • 代碼案例豐富,提供豐富的案例,然後多個業務場景,儘可能完善好demo。
  • 該庫註解生成代碼在編譯器
    • 在編譯器生成代碼,並且該類是繼承自己自定義的接口,存儲的是map集合,key是接口class,value是接口實現類class,直接在編譯器把接口和實現類存儲起來。用的時候直接取……

11.一些常見的報錯問題

  • Didn't find class "com.yc.api.contract.IUserManager$$Contract" on path
    • 註解生成的代碼失敗導致出現這個問題。爲什麼會出現這種情況?修改gradle的構建版本……
    public class IUpdateManager$$Contract implements IApiContract {
      @Override
      public void register(IRegister register) {
        register.register(IUpdateManager.class, UpdateImpl.class);
      }
    }
    
  • 關於apt編譯器不能生成代碼的問題,可能會有這麼一些關鍵點
    • 第一查看module的依賴,如果沒有依賴請先添加依賴
    implementation project(path: ':api-manager')
    annotationProcessor project(path: ':api-compiler')
    
    • 第二查看寫完wirter的流沒有關閉,會造成生成文件,但文件內容爲空,或者不全;
    • 第三可能是Android Gradle及構建版本問題,我的是3.4.1 + 5.2.1,會出現不兼容的情況,大神建議3.3.2 + 4.10.1以下都可以。聽了建議降低版本果然構建編譯,新的文件生成了。

12.部分原理分析的說明

  • 註解是如何生成代碼的?也就是javapoet原理……
    • 這個javapoet工具,目前還緊緊是套用ARouter,創建類名,添加接口,添加註解,添加方法,添加修飾符,添加函數體等等。也就是說將一個類代碼拆分成n個部分,然後逆向拼接到一起。最後去write寫入代碼……
    • 但是,怎麼拼接和並且創建.java文件的原理,待完善。目前處於會用……
  • Class.forName(name)反射如何找到name路徑的這個類,從jvm層面分析?
    • 待完善
  • new和Class.forName("").newInstance()創建對象有何區別?
    A a = (A)Class.forName("com.yc.demo.impl.UpdateImpl").newInstance();
    A a = new A();
    
    • 它們的區別在於創建對象的方式不一樣,前者(newInstance)是使用類加載機制,後者(new)是創建一個新類。
    • 爲什麼會有兩種創建對象方式?
      • 主要考慮到軟件的可伸縮、可擴展和可重用等軟件設計思想。
    • 從JVM的角度上看:
      • 我們使用關鍵字new創建一個類的時候,這個類可以沒有被加載。但是使用newInstance()方法的時候,就必須保證:1、這個類已經加載;2、這個類已經連接了。
      • 而完成上面兩個步驟的正是Class的靜態方法forName()所完成的,這個靜態方法調用了啓動類加載器,即加載 java API的那個加載器。
      • 現在可以看出,newInstance()實際上是把new這個方式分解爲兩步,即首先調用Class加載方法加載某個類,然後實例化。 這樣分步的好處是顯而易見的。我們可以在調用class的靜態加載方法forName時獲得更好的靈活性,提供給了一種降耦的手段。
    • 區別
      • 首先,newInstance( )是一個方法,而new是一個關鍵字;其次,Class下的newInstance()的使用有侷限,因爲它生成對象只能調用無參的構造函數,而使用 new關鍵字生成對象沒有這個限制。

項目地址:https://github.com/yangchong211/YCLiveDataBus

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章