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相關的任務。
是否也可以使用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啓動時組裝成整個列表信息。這樣就算單一模塊的編程也不會影響其他模塊的正常配置;
- 動態配置:模塊層級的順序是固定的,動態地實現模塊亂序加載也能正常貼合到各自層級中。佈局多變時,模塊可以加載不同佈局並且正常運行。可以使用外部信息,如服務器信息來調整分發模塊的機制。