Android組件化架構學習筆記——組件化分發2

1.組件化列表配置:

1.1:Javapoet語法基礎:

Javapoet是Java編譯時註解開發的工具類庫,Javapoet提供編寫Java代碼的接口,在編譯器中自動生成源代碼。

Javappoet中有五種常用的類:

  • ParamterSpec:參數聲明;
  • MethodSpec:構造函數或方法聲明;
  • TypeSpec:類/接口或者枚舉聲明;
  • FieldSpec:成員變量;
  • JavaFile:包含擁有一個類對象的Java文件。
public static void main(String[] args) throws Exception {
  MethodSpec main = MethodSpec.methodBuilder("main")
      //Modifiers是修飾關鍵詞
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
       //添加String[]類型的名爲args
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

    //    HelloWorld是類名
  TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)    //在類中添加方法
    .build();

  JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();    //HelloWorld是類名

  javaFile.writeTo(System.out);    //輸出到調試面板中
}
  • XXXSpec類是使用典型的建造者設計模式;
  • xxxBuilder是聲明命名,如classBuilder類構造/constructorBuilder構造器/enumBuilder枚舉/interfaceBuilder接口構造。
  • addModifier是添加修飾的關鍵詞。

1.2JavaPeot中常用方法和屬性:

$L/$T/$S/$N都是佔位符。

  • $L:常量;
  • $T:String類型;
  • $S:變量制定類型,可以通過ClassName來指定外部類名;
  • $N:生成的方法名或變量名。

FieldSpec

  • Initializer:添加變量初始化值。

MethodSpec:

  • addParameter:添加方法中的參數;
  • addStatement:添加一行中顯示的內容;
  • addCode:可以通過拼接字符串來編寫代碼;

另一種方式,可以通過SpannableBuilder來完成代碼拼接。

在編寫for/while時需要用到一下關鍵字:

  • beginControlFlow:在行的末尾添加大括號開始符“{”和代碼縮進;
  • endControlFlow:在末尾添加結束大括號結束符“}”和縮進;
  • addJavadoc("XXX"):在方法上添加註解;
  • addAnnotation(Overrde.class):在方法上添加註解。

TypeSpec:

  • addStaticImport:引用需要的Java包;
  • addMehod:在類中添加方法;
  • addField:添加參數;
  • build:創建編寫代碼。

JavaFile:

  • build:創建Java文件;
  • writeTo:將代碼塊輸入到某個地址中。

編寫順序必須遵循:

FieldSpec -> ParameterSpec -> MethodSpec -> TypeSpec ->JavaFile

2.編譯時註解配置:

Android studio並沒有提供一個關於編譯時註解的開發模版給開發者用,使用IntellijIDEA可以完全支持編譯時註解的編寫。Android studio只能通過JavaLibarary模塊來進行改造。

ModuleBus爲例子。

配置編譯時註解運行入口有兩種方式:

  • 使用Google的AutoService,通過編譯時註解來添加javax.annotation.processing.Processor文件;
  • 在根目錄在新建resource文件夾,在裏面建立META-INF.service文件夾,然後定義javax.annotation.processing.Processor文件。在此文件中添加運行javapeot代碼的包名和地址。

如:ModuleBus的compile庫配置的processor是:com.cangwang.process.ModuleUnitProcessor

配置此入口索引,Javapeot框架才能在程序編譯時先運行processor。

  • 需要在Base module中添加註解飲用的annotation模塊:
compile project(':annotation')
  • 在業務module的build.gradle中配置annotation模塊:
dependencies{
    annotationProcessor project(":compile")
}

3.集成配置列表:

在編寫前需要在compiler中添加build.gradle來引用一些庫:

apply plugin: 'com.android.application'
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
   
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementaion 'com.squareup:javapeot:1.7.0'
    implementation project(':annotation')
    implementation 'org.apache.commons:commons-lang3:3.4'
    implementation 'org.apache.commons:commons-collections4:4.1'
}

build.gradle中需要引用Java基礎環境和auto-service.javapoet兩個庫,以及自定義annotation庫。對配置列表使用編譯時註解的步驟如下所述:

  • ModuleUnit用於記錄每個module配置相關的信息;
  • ModuleUnitProcessor用於讀取註解,生成Java代碼文件庫;
  • app啓動時,application彙總所有模塊的信息;
  • ModuleCenter將所有模塊的信息保存爲模塊列表;
  • 當分發的Activity/Fragment啓動後,通過ModuleBus抽取不同模版配置的module列表,完成啓動加載。

4.加載優化:

優化的核心是時間和空間之間的轉換,可以使用三種加載優化策略:

  • 預加載;
  • 線程加載;
  • 懶加載。

編譯時註解屬於預加載處理,其原理是通過編譯時註解來優化配置列表,通過application啓動時進行初始化來配置組件化列表。編譯時註解可以說是外部優化的非常有效的方式,很多先進的框架都在使用。其餘的兩種是Android和Java內部提供的有效的加載方式----使用線程加載和懶加載對分發架構進行優化。

4.1 線程加載:

app中的UI更新只能在UI線程(主線程)中進行。多模塊涉及UI界面渲染加載時,只能通過UI線程上進行串行的處理。可以只使用for循環直接在主線程中加載。

使用for循環直接在主線程中加載,單頁面加載少量模塊時,這樣的加載並不會出現問題。

但在開發迭代時,模塊越來越多,全部模塊加載出來的時間會越來越長,究其原因是模塊初始化需要在UI線程中串行加載。特別是低端的機型中,在CPU和GPU運行不理想的主線程加載緩慢非常容易造成ANR和初始化界面卡頓等問題。

解決方案:抽取可以在後臺線程運行的代碼,保留模塊在UI線程中進行初始化,這裏就涉及到Java線程的操作和Android中的線程切換。

UI線程在app初始化是一直存在的,Android提供handler機制可以將切換任務拋給UI線程上進行。在分發模塊設置起初,各個module中的init方法用於初始化界面工作。儘量不在init中做過多主線程的操作。

工作線程(非UI線程)可以通過新建線程或獲取線程池中緩存的方式實現。使用工作線程操作一些UI相關的任務。

Java線程池相關知識

是否也可以使用Android原生的AsynTask來進行切換呢?AsynTask也可以設置串行操作,如模塊過多,加載時間長。突然使用橫豎屏等重新構建activity操作,容易導致引用未釋放,從而造成內存泄露。如AsynTask使用被聲明爲activity的非靜態的內部類,那麼AsynTask會保留一個創建AsynTask的對activity的引用。如activity已經被銷燬,AsynTask的後臺線程還執行,它將繼續在內存中保存這個引用,導致activity無法被回收引起內存泄露。handler使用的時候也需要注意內存泄露的問題,在destroy頁面銷燬時一定要釋放handler和持有的context。

進一步可以使用RxJava的方式來進行邏輯優化,RxJava的線程切換核心思想也是線程池+handler組合。

4.2 模塊懶加載:

app界面設計不會讓所有的內容全部呈現,不然會內容過多/用戶關注重點被幹擾等問題。不需要立刻顯示的模塊可以使用懶加載的形式來進行加載。

view中顯示狀態:

  • Visible: 有界面佈局佔位且顯示在界面中;
  • Invisible:有界面佈局佔位,但未渲染在界面中顯示;
  • Gone:只創建了實例,並未在界面佈局佔位,也未渲染在界面中顯示。

view的繪製機制是:

創建view -> 界面佈局佔位 -> 界面渲染

因爲有層級的顯示順序是可以通過addView來完成佈局的動態加載的,那麼加載就必須佔位來保證顯示順序。

使用Invisible就可以保證佔用佈局,並且不顯示在界面上。但是使用Invisible並不能進行懶加載。

使用懶加載有兩種不同的方式:

  • 先添加一個空ViewGroup佔位,在收到事件或其他觸發任務時,使用真正的佈局替換空的ViewGroup,然後進行初始化;
  • 通過使用ViewStub的形式來完成佔位,通過收到事件獲取其他觸發任務時用真正的ViewStub,然後進行初始化。

第一種方式是添加,第二種方式是替換,不同之處在於第一種方式比第二種方式多了一個ViewGroup的層級。

ViewStub擁有一個layout屬性和inflated屬性。在佈局xml文件中填寫需要加載的layout佈局和佈局對應inflated,ViewStub在被setVisible時會自動加載layout屬性種的佈局,findViewById綁定inflated就能綁定layout控件。

ViewStub的運行原理:視圖加載時,在onMeasure方法中調用setMeasureDimension傳遞的參數MeasureWidth和measureHeight的值爲0,在draw方法被置空,ViewStub只要被inflated後,會實例化定製的佈局,並添加到ViewStub的父視圖中顯示。ViewStub的setVisibility()方法被調用時,指定的layout佈局同樣也可以被實例化。

5.層級限制:

  • 循環加載:佈局是在UI線程中加載的,其原理是通過使用handler消息隊列來實現線性加載。模塊界面佈局是有佔位層級的,通過addView的方式來添加到界面中顯示,就必須確定模塊加載的順序。這種方式好處在於非常直觀地看清了全部模塊的層級順序,容易調整層級。缺點在於模塊列表只能放置在application module中,需要完全暴露包名和路徑帶列表中,無法做到每個模塊配置的絕對接耦。
  • 編譯時註解列表:組件化列表配置中使用了編譯時註解,規定了模塊註解參數的排列規則,將每個模塊的加載順序和初始化配置都安排在每個模塊編譯時生成,Application啓動時,通過註解信息排列加載順序的方式來完成加載模塊操作。
  • 懶加載:懶加載使用了ViewStub的方式來佈局佔位,好處在於一開始就可以給全部模塊安排xml佈局,優化頁面加載速度和模塊的加載的靈活性。
  • 層級優化策略:1.全部模塊都使用ViewStub的方式來佈局佔位,好處在於一開始就可以給全部模塊安排層級順序,可以更合理地安排層級加載的順序。線程池只能通過單線程順序加載進行限制,可以有效利用多線程來優化加載速度,如選ViewGroup來佈局佔位,就可以通過addView的形式來添加多個view模塊。2. 分發模塊進化成多模版的機制。多模版的產生會讓頁面更加多樣化,但是多樣化就預示着模塊會更加多變,模塊間的靈活,需要充分考慮佈局層級順序和顯示效果,實現局部的動態加載。

6.多模版設計:

使用腳本配置,可以把app啓動時ModuleCenter彙總合成module信息列表,提前在編譯期就構建完成Module信息列表。Android在運行期間可以簡單讀取腳本數據格式時json和xml的數據,考慮到數據傳輸速度,首選使用json數據,json讀取數據的速度一般比xml快一倍以上。而且現在大多網絡數據都使用json格式。

  • 編譯時運行的compiler module是無法直接使用Java的json庫的需要添加庫的引用,如Gson;
  • 註解的元素不需要變更,需要編寫製作json註解器。需要形成每個module的json文件;通過便利讀取ModuleUnit的註解來編寫一個JsonObject的對象,然後添加到JsonArray中,ModuleGroup分解爲多個ModuleUnit執行以上的操作,然後將JsonArray對象信息寫入到註解module的src/assets/center.json文件中;
  • 彙總各個module的center.json信息,然後保存一份到app module中;
  • 彙總各個module的center.json到app的center.json中,先讀取每個module的center.json文件並轉化爲ModuleUnitBean數據,彙總到一個LinkedList中,然後通過列表排序,寫入app module的center.json中。因爲app module中並沒有相應的註解,其註解器只會運行init方法,並不會process方法。
  • ModuleCenter需要添加相應的解析JsonObject的方法。

每個模塊通過編譯時註解生成一個關於自身佈局json文件,模塊被彙總到application module中編譯時,在排序產生一個主json文件,記錄各個層級啓動/模塊佈局等信息,然後在app啓動時讀取json文件來加載模版信息。

動態加載:

腳本配置,再添加服務器配置就可以動態配置加載模版信息,這種設計可以通過服務器動態加載配置json文件來完成模塊加載。只設置協議請求返回json文件就可以配置新的啓動信息。

本地json文件再app加載後保存在assets目錄中,json文件下載後應該保存在應用的/data/data/{packageName}/file中,再次獲取時也應該在此處獲取。

其流程是:

  • 啓動app -> 讀取沙盒緩存json文件 -> 配置模塊;
  • 啓動app -> 請求動態json -> 下載json -> 保存json文件到沙盒用於下次啓動。

7.小結:

組件化分發在單界面中承擔着非常複雜的多業務開發任務,而且提供了很好的解決方案,是代碼模塊接耦和人力資源接綁的一種新型高效的方案。

分發的核心思想:接耦配置和動態配置。

  • 接耦配置:將每個模塊的配置信息在各自模塊中生成,並且在啓動初期彙總列表。模塊信息對其他模塊不可見,只在app啓動時組裝成整個列表信息。這樣就算單一模塊的編程也不會影響其他模塊的正常配置;
  • 動態配置:模塊層級的順序是固定的,動態地實現模塊亂序加載也能正常貼合到各自層級中。佈局多變時,模塊可以加載不同佈局並且正常運行。可以使用外部信息,如服務器信息來調整分發模塊的機制。

 

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