前篇回顧
上篇文章揖铜,我們了解到“路由”是如何進行工作的彤委,并嘗試手寫了實現(xiàn)代碼裳食;
本篇,以ARouter為例辨赐,我們將剖析它是如何完成工作的,并通過源碼了解其原理京办。
還是以Activity跳轉(zhuǎn)掀序、Fragment獲取、跨模塊功能調(diào)用三個方向為出發(fā)點惭婿,看看ARouter將如何實現(xiàn)這些功能不恭。
集成ARouter
咱們先快速將ARouter集成到項目中,當然你也可以參照官方文檔來操作财饥。
在第一篇文章中换吧,我們將業(yè)務模塊的依賴關(guān)系集中到了config.gradle文件中,現(xiàn)修改如下:
- 1钥星、config.gradle中管理依賴版本
ext {
// ARouter 版本號
arouter_api_version = "1.5.2"
arouter_compiler_version = "1.5.2"
//各模塊引入的第三方公共庫
dependenciesImport = [
...
...
arouter_api : "com.alibaba:arouter-api:$arouter_api_version"
]
//注解處理器
dependenciesKapt = [
...
...
arouter: "com.alibaba:arouter-compiler:$arouter_compiler_version"
]
}
- 2沾瓦、子模塊的build.gradle中,傳遞APT所需參數(shù)谦炒,并引入依賴
def dependenciesImport = rootProject.ext.dependenciesImport
def dependenciesKapt = rootProject.ext.dependenciesKapt
android {
defaultConfig {
...
...
// ARouter APT 傳參
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":lib_comm")
// 導入依賴庫
dependenciesImport.each { k, v -> implementation(v) }
// 導入依賴的注解處理器
dependenciesKapt.each { k, v -> kapt(v) }
}
- 3贯莺、初始化ARouter
class App : Application() {
override fun onCreate() {
super.onCreate()
initRouter()
}
private fun initRouter() {
if (BuildConfig.DEBUG) {
ARouter.openLog() // 打印日志
ARouter.openDebug() // 開啟調(diào)試模式
}
ARouter.init(this) // 初始化
}
}
- 4、運行查看Log記錄:
2021-09-13 17:47:07.911 10088-10088/com.ljb.component I/ARouter::: ARouter init success![ ]
ARouter - Activity跳轉(zhuǎn)
根據(jù)官方文檔宁改,只需2步便可完成功能:
- 1缕探、注解標記需要跳轉(zhuǎn)的Activity
@Route(path = "/user/UserMainActivity" , group = "user")
class UserMainActivity : AppCompatActivity() {
...
}
- 2、發(fā)起跳轉(zhuǎn)
ARouter.getInstance().build("/user/UserMainActivity").navigation()
很神奇还蹲!與前篇我們實現(xiàn)的路由相比爹耗,ARouter的代碼要簡潔很多,完全不需要手動注冊路由就可完成跳轉(zhuǎn)谜喊,它是怎么做到的呢潭兽?
通過跟進navigation()函數(shù)調(diào)用過程,我們把目光聚焦到兩個容器中:
// ARouter源碼
class Warehouse {
// Cache route and metas
static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
static Map<String, RouteMeta> routes = new HashMap<>();
...
}
- Warehouse.groupsIndex: 用于存儲所有的路由組
public interface IRouteGroup {
/**
* Fill the atlas with routes in group.
* atlas用于存儲當前組里的所有路由锅论,實際傳入的就是Warehouse.routes
*/
void loadInto(Map<String, RouteMeta> atlas);
}
- Warehouse.routes:用于存儲已注冊的所有路由
// 路由包裝類讼溺,路由目標的Class對象就存儲在這里面
public class RouteMeta {
private RouteType type; // Type of route
private Element rawType; // Raw type of route
private Class<?> destination; // Destination
private String path; // Path of route
private String group; // Group of route
private int priority = -1; // The smaller the number, the higher the priority
private int extra; // Extra data
private Map<String, Integer> paramsType; // Param type
private String name;
...
}
ARouter對路由提出了分組概念,上面 UserMainActivity就屬于user組下最易,當路由path存在2級及以上時怒坯,group字段也可以省略炫狱,ARouter默認會使用第一個反斜杠后面的path作為組名。
// group可省略不寫
@Route(path = "/user/UserMainActivity")
class UserMainActivity : AppCompatActivity() {
...
}
一般情況下剔猿,我們會將同一模塊的路由劃分在同一個組下视译,例如App模塊下的所有路由都在“app”這個分組下 , user模塊的路由都在“user”分組下;當然归敬,同一模塊擁有多個分組也是完全可行的酷含,只要保證與其它模塊中的路由分組不重名即可。
通過翻閱源碼汪茧,分析這兩個容器的作用椅亚,大致如下:
- 1、當傳入path進行跳轉(zhuǎn)時舱污,優(yōu)先從Warehouse.routes中直接獲取路由對象呀舔;
- 2、路由對象不存在扩灯,就需要通過Warehouse.groupsIndex路由組來完成注冊功能媚赖;
- 3、注冊成功后珠插,當前path所在組的所有路由都將存儲到Warehouse.routes中惧磺;
- 4、回到第1步捻撑,獲取路由對象磨隘;
- 5、讀取路由對象信息布讹;
- 6琳拭、完成跳轉(zhuǎn)。
下方是ARouter源碼中的實現(xiàn)代碼描验,我把不重要的部分進行了刪減:
//ARouter源碼
public class LogisticsCenter {
...
public synchronized static void completion(Postcard postcard) {
//1白嘁、當傳入path進行跳轉(zhuǎn)時,優(yōu)先從Warehouse.routes中直接獲取路由包裝對象膘流;
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if (null == routeMeta) {
//2絮缅、包裝對象不存在,就需要通過Warehouse.groupsIndex路由組來完成注冊功能呼股;
addRouteGroupDynamic(postcard.getGroup(), null);
//4耕魄、回到第1步,獲取包裝對象彭谁;
completion(postcard);
} else {
// 5吸奴、讀取包裝對象信息;
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
...
}
public synchronized static void addRouteGroupDynamic(String groupName, IRouteGroup group) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
if (Warehouse.groupsIndex.containsKey(groupName)){
//3、注冊成功后,當前path所在組的所有路由都將存儲到Warehouse.routes中则奥;
Warehouse.groupsIndex.get(groupName).getConstructor().newInstance().loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(groupName);
}
if (null != group) {
// 注冊本組路由
group.loadInto(Warehouse.routes);
}
}
...
}
final class _ARouter {
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
...
LogisticsCenter.completion(postcard);
...
// 6考润、完成跳轉(zhuǎn)。
switch (postcard.getType()) {
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
//...
// Navigation in main looper.
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);
}
});
break读处;
...
}
...
}
}
整個跳轉(zhuǎn)的源代碼過程不難理解糊治,但關(guān)鍵點在于:
- Warehouse.groupsIndex容器是何時將路由組加載至內(nèi)存的?
- group.loadInto(Warehouse.routes)又是如何完成注冊的罚舱?
回答這兩個問題井辜,需回過頭來看ARouter初始化都做了什么。
ARouter.init(this) // ARouter初始化
隨著代碼的跟進管闷,我們會發(fā)現(xiàn)下面這幾行代碼:
//ARouter源碼
public final class Consts {
public static final String SDK_NAME = "ARouter";
public static final String SEPARATOR = "$$";
public static final String SUFFIX_ROOT = "Root";
public static final String DOT = ".";
public static final String ROUTE_ROOT_PAKCAGE = "com.alibaba.android.arouter.routes";
}
public class LogisticsCenter {
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
// 獲取com.alibaba.android.arouter.routes下的所有class文件
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
// 將名為ARouter$$Root開頭的Class對象創(chuàng)建出來粥脚,并調(diào)用loadInto(),將分組信息加載進Warehouse.groupsIndex中
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
}
...
}
}
}
源碼中會從com.alibaba.android.arouter.routes包下讀取所有的Class文件包个,并將文件名以ARouter$$Root為開頭的Class文件挑選出來阿逃,最后通過反射創(chuàng)建對象,并調(diào)用其loadInto()函數(shù)赃蛛,注意這里傳入?yún)?shù)正是Warehouse.groupsIndex,我們的路由組容器搀菩,從而將分組信息載入Warehouse.groupsIndex中。
果不其然肪跋,我們在user模塊的build文件夾下找到了這些文件:
- 關(guān)于ARouter$$Root 前綴文件歧蒋,可以看到其后綴是當前模塊名module_user,而其loadInto()函數(shù)主要是將user模塊下涉及到的所有路由組存儲到Warehouse.groupsIndex中州既;
public class ARouter$$Root$$module_user implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("user", ARouter$$Group$$user.class);
}
}
- 關(guān)于ARouter$$Group前綴文件谜洽,其后綴是當前路由所屬的的分組名user,前面在梳理Activity跳轉(zhuǎn)的過程中吴叶,我們知道當路由未注冊時阐虚,會通過分組名獲取到組,并創(chuàng)建組對象調(diào)用其loadInto()函數(shù)完成對Warehouse.routes的注冊蚌卤,此文件中編寫的既是具體的注冊過程实束。
//3、注冊成功后逊彭,當前path所在組的所有路由都將存儲到Warehouse.routes中咸灿;
Warehouse.groupsIndex.get(groupName).getConstructor().newInstance().loadInto(Warehouse.routes);
public class ARouter$$Group$$user implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
// loadInto被調(diào)用時,完成對Warehouse.routes的注冊
atlas.put("/user/AppMainActivity", RouteMeta.build(RouteType.ACTIVITY, AppMainActivity.class, "/user/appmainactivity", "user", null, -1, -2147483648));
}
}
ARouter - APT技術(shù)
通過上述分析侮叮,我們知道路由的注冊離不開ARouter生成的這兩個文件避矢,那這兩文件又是怎么來的呢?
這里不得不提到APT技術(shù)( Annotation Processing Tool),譯:注解處理器审胸。如果對該技術(shù)有所耳聞亥宿,你應該知道在Android中我們常用的EventBus、Butterknife歹嘹、Dagger2等框架都有它的身影箩绍。如果你是第一次聽說也沒關(guān)系,暫時只需知道通過這門技術(shù)可以做到“在編譯期時尺上,可使編譯器為我們生成具有一定規(guī)則的模板性代碼”即可材蛛。
顯然,這兩個文件就是ARouter通過APT生成的模板代碼怎抛,至于具體的實現(xiàn)過程僚饭,本篇中不做過多闡述搓劫,后續(xù)處理自定義路由時,我們會嘗試編寫一個自己的注解處理器,到時再細聊APT技術(shù)的相關(guān)細節(jié)炫欺。
不過我把ARouter注解處理器中生成這兩個文件的核心代碼貼在下方,看懂它的思路即可:
//ARouter 注解處理器源碼
@AutoService(Processor.class)
@SupportedAnnotationTypes({ANNOTATION_TYPE_ROUTE, ANNOTATION_TYPE_AUTOWIRED})
public class RouteProcessor extends BaseProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 獲取所有被 @Route 注解標記的類元素
Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
//解析這些類元素
this.parseRoutes(routeElements);
}
private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
// 解析元素
for (Element element : routeElements) {
if (types.isSubtype(tm, type_Activity)) {
// Activity
routeMeta = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);
Set<RouteMeta> routeMetas = groupMap.get(routeMete.getGroup());
routeMetaSet.add(routeMete);
groupMap.put(routeMete.getGroup(), routeMetaSet);
}
}
// 根據(jù)解析元素生成Java文件
for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
// 生成ARouter$$Group$$前綴文件
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);
}
// 生成ARouter$$Root$$前綴文件
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler);
}
}
至此筒狠,整個Activity跳轉(zhuǎn)涉及到的源碼大致就是這些汪诉,總結(jié)下ARouter都做了什么:
- 1、在編譯期椭赋,ARouter注解處理器將所有@Route注解標記的類查找出來抚岗,并根據(jù)這些類的相關(guān)信息生成ARouter$$相關(guān)文件;
- 2哪怔、ARouter.init()初始化時宣蔚,將ARouter$$Group前綴文件涉及到的路由組存儲到Warehouse.groupsIndex中;
- 3认境、調(diào)用ARouter...navigation()跳轉(zhuǎn)頁面時胚委,通過Warehouse.groupsIndex查詢路由所在組,將路由組中的所有路由注冊到Warehouse.routes中叉信,從Warehouse.routes中取出路由信息(包含Activity.class對象)亩冬,完成跳轉(zhuǎn)。
Fragment獲取
依舊根據(jù)文檔茉盏,獲取Fragment歩奏也只需2步:
- 1鉴未、注解標記需要獲取的Fragemnt
@Route(path = "/user/UserFragment")
class UserFragment : Fragment() {
...
}
- 2、獲取Fragment
val f = ARouter.getInstance().build("/user/UserFragment").navigation() as Fragment
有了對Activity跳轉(zhuǎn)源碼分析的鋪墊鸠姨,F(xiàn)ragment的獲取原理基本是一樣的铜秆,只是在最后一步發(fā)生跳轉(zhuǎn)的代碼,變?yōu)榉祷谾ragment對象:
final class _ARouter {
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
...
// 6讶迁、完成跳轉(zhuǎn)连茧。
switch (postcard.getType()) {
// Activity跳轉(zhuǎn)
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
//...
// Navigation in main looper.
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);
}
});
break;
// Fragment獲取
case FRAGMENT:
Class<?> fragmentMeta = postcard.getDestination();
Object instance = fragmentMeta.getConstructor().newInstance();
if (instance instanceof Fragment) {
((Fragment) instance).setArguments(postcard.getExtras());
} else if (instance instanceof android.support.v4.app.Fragment) {
((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
}
return instance;
...
}
...
}
}
跨模塊功能調(diào)用
ARouter的方案和我們之前的實現(xiàn)的方案原理是一樣的,都需將對外的服務暴露到lib_comm中啸驯,只是如Activity和Fragment一樣注冊的功能通過APT技術(shù)來實現(xiàn)客扎。
- 1、lib_comm中定義暴露的接口
interface IUserService2 : IProvider {
/**
* 是否登錄
*/
fun isLogin(): Boolean
}
- 2罚斗、業(yè)務模塊實現(xiàn)接口徙鱼,并通過@Route注解標記
@Route(path = "/user/UserService")
class IUserServiceImpl2 : IUserService2 {
override fun init(context: Context?) {
}
override fun isLogin(): Boolean {
return UserUtils.isLogin()
}
}
- 3、獲取服務
ARouter.getInstance().build("/user/UserService").navigation() as IUserService2
和Fragment類似针姿,只不過在獲取時袱吆,多了一步緩存操作,緩存相關(guān)的核心代碼我貼在了下方:
class Warehouse {
// Cache provider
static Map<Class, IProvider> providers = new HashMap<>();
}
// 創(chuàng)建服務距淫,并緩存
switch (routeMeta.getType()) {
case PROVIDER: // if the route is provider, should find its instance
// Its provider, so it must implement IProvider
Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
IProvider instance = Warehouse.providers.get(providerMeta);
if (null == instance) { // There's no instance of this provider
IProvider provider;
try {
provider = providerMeta.getConstructor().newInstance();
provider.init(mContext);
// 進行緩存
Warehouse.providers.put(providerMeta, provider);
instance = provider;
} catch (Exception e) {
logger.error(TAG, "Init provider failed!", e);
throw new HandlerException("Init provider failed!");
}
}
postcard.setProvider(instance);
postcard.greenChannel(); // Provider should skip all of interceptors
break;
}
獲取時绞绒,只需從postcard.getProvider()中取即可:
final class _ARouter {
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
...
// 6、完成跳轉(zhuǎn)榕暇。
switch (postcard.getType()) {
// Activity跳轉(zhuǎn)
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras());
//...
// Navigation in main looper.
runInMainThread(new Runnable() {
@Override
public void run() {
startActivity(requestCode, currentContext, intent, postcard, callback);
}
});
break蓬衡;
// Fragment獲取
case FRAGMENT:
Class<?> fragmentMeta = postcard.getDestination();
Object instance = fragmentMeta.getConstructor().newInstance();
if (instance instanceof Fragment) {
((Fragment) instance).setArguments(postcard.getExtras());
} else if (instance instanceof android.support.v4.app.Fragment) {
((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
}
return instance;
// 獲取Service
case PROVIDER:
return postcard.getProvider();
...
}
...
}
}
OK,ARouter路由核心三劍客功能已摸了個底朝天彤枢,與我們自己實現(xiàn)的路由框架相比狰晚,其核心思想是一樣的,但在細節(jié)上ARouter的處理明顯優(yōu)于我們缴啡,例如:
- 通過APT來實現(xiàn)路由注冊家肯,省掉手動注冊的繁瑣,且無需在App啟動時注冊所有路由(與我們startup方案相比)盟猖,僅在使用到某組路由時才進行注冊,避免不必要的內(nèi)存開銷换棚。
- 對外提供功能的Service對象式镐,在使用時進行緩存,避免重復創(chuàng)建固蚤。
小結(jié)
本篇對ARouter的Activity跳轉(zhuǎn)娘汞、Fragment獲取、擴模塊功能調(diào)用花費了大幅篇章來講解源碼夕玩,是希望讀者能了解其基本原理你弦,后續(xù)我在處理自定義路由時,會參考ARouter實現(xiàn)來造輪子燎孟,也方便讀者對后續(xù)篇章的理解禽作。
ARouter還支持很多功能,例如:攔截器揩页、標準Scheme URL跳轉(zhuǎn)旷偿、全局降級策略、動態(tài)路由注冊等,這些功能后續(xù)篇章中一部分會講到萍程,一部分也可能使用不到幢妄,但并不代表這些功能沒用,每個項目或團隊對組件化架構(gòu)的理解都會有所不同茫负,最后落地的項目結(jié)構(gòu)蕉鸳、路由交互方案也就各具特色。無論是怎樣的項目忍法,個人認為:
- 組件化潮尝,它是由實際業(yè)務開發(fā)過程中,開發(fā)人員通過經(jīng)驗積累缔赠,設計出的一套適用于多人協(xié)作開發(fā)的架構(gòu)模式衍锚;
- 具備可復用性、可替代性嗤堰、熱插拔戴质、可獨立調(diào)試特性的模塊,就是組件化模塊(組件)踢匣;
- 由多個組件化模塊組成的項目架構(gòu)告匠,就是組件化架構(gòu)。
下篇离唬,聊聊組件通訊之Scheme URL后专,我們下篇再見!