組件化開發如今基本上屬於基礎操做了,你們通常都會使用 ARouter 、LiveDataBus 做爲組件化通訊的解決方案,那爲何會選擇ARouter,ARouter又是怎麼實現的呢?這篇文章主要就 搭建組件化開發的準備工做 、組件化跳轉分析,若是理解了這篇文章,對於查看ARouter源碼應該是會有很大的幫助的。至於ARouter等分析,網上有不少的講解,這裏就不分析ARouter源碼了,文章末尾會給出ARouter源碼時序圖和總結,可忽略。html
ps: 爲何寫本文,由於筆者最近被問道,爲何要用ARouter,ARouter它究竟是解決什麼問題,你能就一個點來分析嗎?被問到該問題了?筆者是從它的跳轉回答的,畢竟跳轉簡單。恰好記錄並回憶一下。java
參考資料android
談談APT和JavaPoet的一些使用技巧和要點github
組件化的優點想必你們都知道,能夠總結爲四點數組
編譯速度 咱們能夠按需求測試單一業務模塊,而不須要總體打包運行,節約了實踐,有效的提高了咱們的開發速度markdown
解耦 極度的下降了模塊之間的耦合,便於後期維護與更新,當產品提出一個新業務時,徹底能夠新建一個業務組件,集成和摒棄都很方便架構
功能重用 某一塊的功能在另外的組件化項目中使用只須要單獨依賴這一模塊便可app
團隊開發效率 組件化架構是團隊開發必然會選擇的一種開發方式,它能有效的使團隊更好的協做ide
組件化開發,通常能夠分爲三層,分別爲 殼工程、業務組件、基礎依賴庫,業務組件間互不關聯,而且業務組件須要能夠單獨運行測試,總體都是圍繞解耦來開展的,下面開始進行組件化開發前所須要作的準備工做
須要制定規範,對包名和項目模塊的劃分規範化,不一樣模塊內不能有相同名字的類文件,避免打包失敗等衝突問題
接下來的寫法是最廣泛的,就是有點小瑕疵:不支持 AS 的自動補充功能,也沒法使用代碼自動跟蹤,所以能夠考慮使用 buildSrc。buildSrc 是 Android 項目中一個比較特殊的 project,在 buildSrc 中能夠編寫 Groovy 語言。
在咱們建立的模塊中,有一些,例如 compileSdkVersion 、buildToolsVersion 或者是集成的第三方依賴庫,它們都有對應的版本號,若是不進行統一管理,後續維護很麻煩,總不能對全部模塊一個個手動修改版本。因此咱們能夠在gradle.properties文件中,添加配置,例如
gradle.properties
CompileSdkVersion = 30// 這裏不能和compileSdkVersion 同樣,會報錯
模塊的build.gradle
android{
compileSdkVersion CompileSdkVersion.toInteger()
}
複製代碼
全部模塊版本號都按照上面的寫,每次改版本號都按照gradle.properties裏面定義的修改就好。可是,細心的你必定會發現,如今網上的例子,這些寫的不多,既然這樣寫也能作到統一管理,爲何不推薦呢?答案就在 CompileSdkVersion.toInteger()
這裏,這裏拿到CompileSdkVersion後還須要轉換,若是使用下面建立gradle文件的作法,徹底能夠省去。
在項目根目錄下新建一個conffig.gradle 文件,和全局build.gradle同一層級
config.gradle
ext{
android=[
compileSdkVersion:29,
buildToolsVersion:'29.0.2',
targetSdkVersion:29,
]
dependencies = [
appCompact : 'androidx.appcompat:appcompat:1.0.2'
]
}
根目錄的build.gradle中,頂部加入
apply from:"config.gradle"
使用的時候以下
compileSdkVersion rootProject.ext.android.compileSdkVersion
implementation rootProject.ext.dependencies.appCompact
複製代碼
注意,在implementation dependencies 時候是能夠這樣寫的
implementation 'androidx.test.ext:junit:1.1.0','androidx.test.espresso:espresso-core:3.1.1'
複製代碼
可是你在config.gradle中千萬不能也相似這樣寫
dependencies = [
appCompact : '\'androidx.appcompat:appcompat:1.0.2\',\'androidx.test.espresso:espresso-core:3.1.1\''
]
複製代碼
由於在build.gradle中你把全部依賴放到implementation後面,用逗號分隔,這個逗號和字符串的逗號不同,你在config.gradle中那樣寫的其實至關於在build.gradle implementation dependencies 時這樣寫
implementation 'androidx.test.ext:junit:1.1.0,androidx.test.espresso:espresso-core:3.1.1'
複製代碼
那你可能會問,這樣寫不行的話,那我怎麼在config.gradle中實現對全部模塊須要的公共依賴庫集中管理呢?能夠按照下面這樣寫
ext {
....
dependencies = [
publicImplementation: [
'androidx.test.ext:junit:1.1.0',
'androidx.test.espresso:espresso-core:3.1.1'
],
appCompact : 'androidx.appcompat:appcompat:1.0.2'
]
}
implementation rootProject.ext.dependencies.publicImplementation //每一個模塊都寫上這句話就行了
複製代碼
這樣就完了嗎?還有咱們本身寫的的公共庫也要集中管理,通常咱們都會在模塊的build.gradle中一個個這樣寫
implementation project(path: ':basic')
複製代碼
如今咱們經過gradle來管理,以下
ext {
....
dependencies = [
other:[
':basic',
]
]
}
rootProject.ext.dependencies.other.each{
implementation project(it)
}
複製代碼
Library不能在Gradle文件中有applicationId
AndroidManifest.xml文件區分
在開發過程當中,須要獨立測試,避免不了常常在Application和Library之間隨意切換。在模塊,包括殼工程app模塊運行時,Application類只能有一個。
首先咱們在config.gradle中配置,爲何不在gradle.properties中配置,以前也說了
ext {
android = [
compileSdkVersion: 29,
buildToolsVersion: '29.0.2',
targetSdkVersion : 29,
isApplication:false,
]
....
}
複製代碼
而後在各個模塊的build.gradle文件頂部加入如下判斷
if(rootProject.ext.android.isApplication) {
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
複製代碼
Library不能在Gradle文件中有applicationId
android { defaultConfig { if(rootProject.ext.android.isApplication){ applicationId "com.cov.moduletest" //做爲依賴庫,是不能有applicationId的 } .... }
在app模塊的gradle中也須要有區分
dependencies { ..... if(!rootProject.ext.android.isApplication){ implementation project(path: ':customer') //只有當業務模塊是依賴的時候去依賴 ,看業務需求 } }
AndroidManifest.xml文件區分
在各個模塊的build.gradle中區分
sourceSets {
main{
if(rootProject.ext.android.isApplication){
manifest.srcFile '/src/main/AndroidManifest.xml'
}else{
manifest.srcFile "src/main/manifest/AndroidManifest.xml"
}
}
}
複製代碼
Application配置
由於咱們會在Application中作一些初始化操做,若是模塊單獨運行的話,那麼這些操做須要放到模塊的Application中,因此這裏須要單獨配置一下,新建module 文件夾,配置好下面文件時,新建自定義的Application類,而後在manifest文件夾下的清單文件內指定Application。這樣做爲依賴庫運行時,module 文件夾下的文件不會進行編譯。
main{
if(rootProject.ext.android.isApplication){
manifest.srcFile '/src/main/AndroidManifest.xml'
}else{
manifest.srcFile "src/main/manifest/AndroidManifest.xml"
java.srcDirs 'src/main/module','src/main/java'
}
}
複製代碼
以上是配置單獨模塊時,Application能夠這樣寫,但這裏還須要考慮Application的初始化問題,殼工程的Application初始化完成後須要分別初始化依賴組件的Application。能夠這樣寫
basic 模塊中定義
public interface IApp{
void init(Application app);
}
而後各個模塊相似這樣寫
public AModuleApplication implements IApp{
public void init(Application app){ 初始化操做 }
}
在殼工程的Application裏維護一個數組 {"com.cv.AModuleApplication.class","xx"}
可是這樣不優雅,建議在basic中建個類專門維護
接下來,做爲一個獨立AP運行P時,只須要在殼工程Application的onCreate方法中對該數組的類所有進行反射構造,
調用init方法便可。
複製代碼
上面這樣寫確實能夠,惟一不足的是須要維護一個包含各個模塊做爲Library時須要初始化的類,**有沒有更好的方法呢?**答案確定是有的,使用註解,對每一個模塊中,須要在Application初始化調用的類,即上述數組中維護的類,加上註解,編譯期收集起來,Application的onCreate方法調用,沒理解的同窗能夠看下面的組件化跳轉分析,道理相似。
在運行時,每一個模塊都會生成一個對應的BuildConfig類,存放包路徑可能不一樣,那咱們怎麼作呢?
在basic模塊的build.gradle中加入如下代碼
buildTypes {
release {
buildConfigField 'boolean', 'isApplication', rootProject.ext.android.isApplication.toString()
}
debug {
buildConfigField 'boolean', 'isApplication', rootProject.ext.android.isApplication.toString()
}
}
複製代碼
爲何要在basic模塊下加入呢?就是由於BuildConfig每一個模塊都會有,總不能在全部模塊都加入這句話吧。在basic模塊加入後,其它模塊依賴這個模塊,而後經過在basic模塊中定義的BaseActivity中,添加獲取該值的方法便可,其餘模塊繼承BaseActivity,就能夠拿到父類方法進行判斷了,這只是一種,具體要看業務進行分析。
按照上述配置後,接下啦第一步就須要解決組件化通訊問題,其中第一類問題就是跳轉相關。由於業務組件之間不能耦合,因此咱們只能經過自定義一個新的 router 模塊,各個業務組件內經過繼承該依賴,而後實現跳轉。
咱們只須要在router模塊中定義一個ARouter容器類,而後各個模塊進行註冊Activity,就可使用了,代碼以下
public class ARouter {
private static ARouter aRouter = new ARouter();
private HashMap<String, Class<? extends Activity>> map = new HashMap<>();
private Context mContext;
private ARouter(){
}
public static ARouter getInstance(){
return aRouter;
}
public void init(Context context){
this.mContext = context;
}
/**
* 將類對象添加到容器中
* @param key
* @param clazz
*/
public void registerActivity(String key,Class<?extends Activity> clazz){
if(key != null && clazz != null && !map.containsKey(key)){
map.put(key,clazz);
}
}
public void navigation(String key){
navigation(key,null);
}
public void navigation(String key, Bundle bundle){
if(mContext == null){
return;
}
Class<?extends Activity > clazz = map.get(key);
if(clazz != null){
Intent intent = new Intent(mContext,clazz);
if(bundle != null){
intent.putExtras(bundle);
}
mContext.startActivity(intent);
}
}
}
複製代碼
經過ARouter.getInstance().navigation("key") 就能跳轉了,可是前提是須要調用registerActivity將每一個Activity和對應路徑註冊進來,那不可能在每一個Activity中都調用該方法將類對象加到ARouter路由表吧?咱們可能會想到在BasicActivity裏面加一個抽象方法,將全部類對象返回,而後你拿到後調用registerActivity方法註冊,可是這個前提是 須要你繼承BasicActivity的類已經建立了,已經實例化了,因此這不可能在沒啓動Activity時進行註冊。那怎麼樣才能在Activity沒啓動時,將全部類對象添加到ARouter容器內呢?有什麼方法能夠在Application建立時候能夠收集到全部未啓動的Activity呢?
可能你們還會想到,在每個模塊裏面新建一個ActivityUtils類,而後定義一個方法,裏面調用ARouter.registerActivity ,註冊該模塊全部須要註冊的類,而後在Application類裏觸發該方法。模塊少還好說,能夠一個個手動敲,模塊一多,每一個模塊都得寫,維護太麻煩了,可不能夠自動生成這樣的方法,自動找到須要註冊的類,收集起來呢?
這就須要使用APT技術來實現了,經過對須要跳轉的Activity進行註解,而後在編譯時生成類文件及類方法,該類方法內利用Map收集對應的註解了的類,在Application建立時,執行這些類文件相關方法,收集到ARouter容器內。
不瞭解如何操做APT的同窗能夠參考
要實現上述說的方案,須要瞭解一下APT(Annotation Processing Tool)技術,即註解處理器,它是Javac的一個工具,主要用來在編譯時掃描和處理註解。
@Target 聲明註解的做用域
@Retention 生命註解的生命週期
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityPath {
String value();
}
複製代碼
@AutoService(Processor.class) 虛擬機在編譯的時候,會經過這個判斷AnnotationCompiler是註解處理器, 是固定的寫法,加個註解便可,經過auto-service中的@AutoService能夠自動生成AutoService註解處理器,用來註冊用來生成 META-INF/services/javax.annotation.processing.Processor 文件
@SupportedSourceVersion(SourceVersion.RELEASE_7) 指定JDK編譯版本
@SupportedAnnotationTypes({Constant.ACTIVITY_PATH}) 指定註解,這裏填寫ActivityPath的類的全限定名稱 包名.ActivityPath
Filer 對象,用來生成Java文件的工具
Element 官方解釋 表示程序元素,如程序包,類或方法,TypeElement表示一個類或接口程序元素,VariableElement表示一個字段、枚舉常量或構造函數參數、局部變量,TypeParameterElement表示通用類、接口、方法、或構造函數元素的正式類型參數,這裏簡單舉個例子
package com.example //PackageElement
public class A{ //TypeElement
private int a;//VariableElement
private A mA;//VariableElement
public A(){} // ExecuteableElement
public void setA(int a){ // ExecuteableElement 參數a是VariableElement
}
}
複製代碼
還須要注意一點,爲了在編譯時不出現GBK編碼錯誤等問題,須要在gradle中添加
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
複製代碼
接下來就開始真正實現了,如今annotation_compile的依賴中添加
implementation'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
implementation 'com.squareup:javapoet:1.11.1'
複製代碼
而後實現註解處理器類
@AutoService(Processor.class)
@SupportedAnnotationTypes({Constant.ACTIVITY_PATH})
// 註解處理器接收的參數
@SupportedOptions(Constant.MODULE_NAME)
public class AnnotationCompiler extends AbstractProcessor {
//生成java文件的工具
private Filer filer;
private String moudleName;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnv.getFiler();
moudleName = processingEnv.getOptions().get(Constant.MODULE_NAME);
}
/**
* 獲得最新的Java版本
*
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}
/**
* 找註解 生成類
*
* @param set
* @param roundEnvironment
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (moudleName == null) {
return false;
}
//獲得模塊中標記了ActivityPath的註解
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ActivityPath.class);
//存放 路徑 類文件名稱
Map<String, String> map = new HashMap<>();
//TypeElement 類節點
for (Element element : elements) {
TypeElement typeElement = (TypeElement) element;
ActivityPath activityPath = typeElement.getAnnotation(ActivityPath.class);
String key = activityPath.value();
String activityName = typeElement.getQualifiedName().toString();//獲得此類型元素的徹底限定名稱
map.put(key, activityName + ".class");
}
//生成文件
if (map.size() > 0) {
createClassFile(map);
}
return false;
}
private void createClassFile(Map<String, String> map) {
//1.建立方法
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("registerActivity")
.addModifiers(Modifier.PUBLIC)
.returns(void.class);
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
String className = map.get(key);
//2.添加方法體
methodBuilder.addStatement(Constant.AROUTER_NAME + ".getInstance().registerActivity(\"" + key + "\"," + className + ")");
}
//3.生成方法
MethodSpec methodSpec = methodBuilder.build();
//4.獲取接口類
ClassName iRouter = ClassName.get(Constant.PACKAGE_NAME, Constant.IROUTER);
//5.建立工具類
TypeSpec typeSpec = TypeSpec.classBuilder(Constant.CLASS_NAME + "?" + moudleName)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(iRouter) //父類
.addMethod(methodSpec) //添加方法
.build();
//6.指定目錄構建
JavaFile javaFile = JavaFile.builder(Constant.PACKAGE_NAME, typeSpec).build();
//7.寫道文件
try {
javaFile.writeTo(filer);
} catch (IOException e) {
}
}
}
複製代碼
生成的文件效果以下
public class RouterGroup$$moduletest implements IRouter {
public void registerActivity() {
com.cv.router.ARouter.getInstance().registerActivity("/main/login",com.cv.moduletest.LoginActivity.class);
}
}
複製代碼
public void init(Context context){
this.mContext = context;
//1.獲得生成的RouterGroup?.. 相關文件 找到這些類
try {
List<String> clazzes = getClassName();
if(clazzes.size() > 0){
for(String className:clazzes){
Class<?> activityClazz = Class.forName(className);
if(IRouter.class.isAssignableFrom(activityClazz)){
//2.是不是IRouter 子類
IRouter router = (IRouter) activityClazz.newInstance();
router.registerActivity();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private List<String> getClassName() throws IOException {
List<String> clazzList = new ArrayList<>();
//加載apk存儲路徑給DexFile
DexFile df = new DexFile(mContext.getPackageCodePath());
Enumeration<String> enumeration = df.entries();
while (enumeration.hasMoreElements()){
String className = enumeration.nextElement();
if(className.contains(Constant.CLASS_NAME)){
clazzList.add(className);
}
}
return clazzList;
}
複製代碼
到此就實現了自動化收集類信息。
當你瞭解了上述方法時,你再去看ARouter的源碼,會輕鬆點,跳轉實現原理,都差很少。固然ARouter也支持攔截等功能,想要查看ARouter源碼,能夠自行在掘金上搜索。這裏給出之前看ARouter時作的筆記,只針對客戶端使用ARouter時的時序圖和文字描述,可能總結寫得不全很差,不喜勿噴
首先在ARouter.getInstance().init()中會調用_ARouter的init()方法,而後回調用after方法,after方法是經過byName形式獲取的攔截器Service。
這裏主要是init()方法,裏面會構建一個Handler,主要用來啓動應用組件跳轉和在debug模式下顯示提示,而後還有一個線程池,主要是用於執行後續攔截器攔截邏輯,而後這個init中,最重要的應該就是LogisticsCenter.init()方法,在這裏面,他會獲取arouter.router包名下的全部類文件名,而後加載到Set集合中,而後遍歷這些class,Root相關的類反射調用loadInto方法加載到groupIndex集合中,Interceptors相關的類加載到interceptorsIndex中,Providers相關的類加載都providersIndex中。這些類文件都是arouter-compile根據註解生成的,文件名規則是ARouter Root ? 模塊名或者是ARouter ?Provider 模塊名,或者是ARouter ? Group ? group名,例如Root相關類的loadInto方法就是把group值和group 相關類匹配放在groupIndex中,而後在須要使用時再去加group相關類的信息。
咱們使用ARouter.getInstance().build().navigation獲取Fragment或者跳轉時,它先是_ARouter的build方法, 這個方法裏,他會bayType形式調用PathReplaceService,對build()方法傳入的路徑path作修改,而後若是使用RouterPath註解時沒有指定group,會獲取path中第一個/後面的字符串做爲group並返回一個Poscard,內部有一個bundle用於接收傳入的參數,而後調用自身的navigation方法,最後仍是回調到了 _ARouter的navigation()方法,這個方法內會按需獲取加載指定path對應的類信息,首先是從groupIndex裏面須要group組名對應的類信息,而後經過反射調用loadInto方法,將該組名下的全部路徑對應關係保存到routes Map中,而後去完善傳入的Path對應的RouteMeta信息,最後根據元信息的類型,構建對應的信息,並指定provider和fragment的開啓綠色通道。而後接下來,就是若是沒有開啓綠色通道,將利用CountDownlaunch和線程池將全部攔截器按需進行處理,而後通行後,會根據元信息類型,構造相應參數,啓動Activity或者反射構建Fragment返回。
@Path(path)
對應於ARouter$$Root$$模塊名.class
,內部生成loadTo
方法,主要用來將 路徑group 和對應類的映射關係存入MAP
中,在ARouter
初始化時,會將這些信息存到靜態的group Map
結構裏。攔截器interceptors
和providers
相似,providers
的話,map
存儲關係是接口名和RouteMeta
的映射關係,RouteMeta
中會包含實現類,path
等信息。
ARouter$$Group$$組名.class
,內部也有loadTo
方法,主要是將路徑group組下的全部路由路徑和對應的RouteMeta
信息存入Map
中
navigation跳轉或者獲得實例過程當中
會先去看跳轉的路徑是不是大於等於2級的,而後看 PathReplaceService
路徑替換接口有沒有實現類,需不須要替換。只有一個
而後會檢查是否須要預處理,也就是跳轉前的處理,例如是不是針對某個路徑,本身處理跳轉
而後就去group map
裏面去找,路徑組名group
對應的類,而後調用該類的方法,將該組下的全部路徑名和包含跳轉目標Activity
或者IProvider
的一些實現類等的信息的RouteMeta
存到新的routes map
裏,而後將以前的group map
下該組信息刪除,節省內存。
若是是Provider
的話,會將Provider
實現類的Class對象
和反射構造的實例,經過providers map
存起來,而後調用init
方法。
Fragment
和Provider
默認開啓綠色通道,不會執行攔截器。
攔截器是在ARouter
初始化時,會默認獲取到系統設置的攔截器
而後在這個攔截器內,會經過線程池 和 CountDownLatch
方式將全部開發者自定義的攔截器,進行執行調用。控制是否放行
這周被問到一個問題,android列表上顯示的全部數據,如何找出最長公共子標籤,我立馬想到動態規劃,可是總感受會有更好的實現方式,畢竟LCS問題大多都是給定兩個字符串,總不能每兩個比較後 (O(n2)),再跟第三個、第四個比較,這樣時間複雜度不是很好。最後回過頭想一想,其實思路應該就是這樣的,經過系統API操做,也要這樣比較。
/**
* str1的長度爲M,str2的長度爲N,生成大小爲M*N的矩陣dp
* dp[i][j] 的含義是str1[0....i] 與 str2[0......j]的公共子序列的長度
* 若是dp[i][0] dp[0][i] 爲1 後面就都爲1
* @author xxx
*
*/
public class DemoFive {
//dp[i][j] 的含義是str1[0....i] 與 str2[0......j]的公共子序列的長度
public static String findLCS(String A,int n,String B,int m) {
char[] arrA = A.toCharArray();
char[] arrB = B.toCharArray();
// n * m 矩陣 A * B
int[][] dp = new int[n][m];
int length = 0;
int start = 0;
for(int i = 1;i<n;i++) {
for(int j = 1;j<m;j++) {
if(arrA[i]== arrB[j] ) {
dp[i][j] = dp[i-1][j-1]+1;
if(dp[i][j] > length) {
length = dp[i][j];
start = i - length+1 ; //注意這裏 下標是從0開始的
}
}
}
}
String result = A.substring(start,start + length);
return result;
}
}
複製代碼
筆記八