動手擼一個ARouter (ARouter源碼分析)

背景

爲何要重複造輪子呢?java

  • 我認爲只有站在做者的角度才能更透徹的理解框架的設計思想
  • 去踩大神們所踩過的坑。
  • 才能深刻的理解框架的所提供的功能
  • 學習優秀的做品中從而提升本身

在開始以前我先提出關於ARouter的幾個問題

  • 爲何要在module的build.gradle文件中增長下面配置? 它的做用是什麼?它跟咱們定義的url中的分組有什麼關係?
javaCompileOptions {
    annotationProcessorOptions {
        arguments = [moduleName: project.getName()]
    }
}
複製代碼
  • 有這麼一種業務場景,新建一個業務組件user,user組件中有頁面UserActivity,配置url /user/main;有一個服務接口,其實現類在app中,配置url爲/user/info;代碼以下:
//module:user
@Route(path = "/user/main")
public class UserActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.user_activity);
    }
}

public interface IUserService extends IProvider {
    void test(String s);
}

//module:app
//user服務
@Route(path = "/user/info")
public class UserServiceImpl implements IUserService {

    public void test(String test) {
        Log.d("xxxx->",test);
    }
}
複製代碼

好了開發完成,讓咱們編譯一下項目看看,編譯結果以下圖(ps:這裏編譯的是我本身的項目,但效果和ARouter是同樣的):android

Why???

讓咱們帶着這兩個問題開始RouterManager之旅。git

第一步架構設計思路(處理頁面跳轉)

咱們的目標是根據一個url來打開指定的頁面,該如何作呢?很簡單,咱們把url和對應的頁面作一個對應關係,好比放到map中以url爲key,對應的頁面activity爲value便可;這樣當咱們要打開這個activity時,根據傳給咱們的url去map中找到對應的activity,而後調用startActivity就OK了。github

你可能會問那咱們這個map該如何維護呢?咱們怎麼把這個對應關係存到map中呢?總不能手動去put吧,你別說貌似還真行,咱們在app啓動的時候先把個人映射關係手動初始化好,這樣在打開頁面時直接經過url來獲取就好了。那麼問題來了,大哥你累不累啊?對於一個懶人來講首先會想到的是能不能自動生成這個映射關係表呢?答案是確定的。api

思路總結

咱們能夠利用編譯註解的特性,新增一個註解,給每一個須要經過url打開的activity加上此註解。在註解處理器中獲取全部被註解的類,動態生成映射關係表,而後在app啓動時把所生成的映射關係load到內存便可。數組

第二部擼代碼

0x01

首先咱們須要建立三個module,以下圖:緩存

爲何要三個項目呢?緣由以下:bash

  • 咱們須要用到的註解處理器AbstractProcessor是在javax包下,而android項目中是沒有這個包的,所以咱們須要建一個java library,也就是router-compiler,它的做用是幫咱們動態生成代碼,只存在於編譯期間架構

  • 既然router-compiler只存在於編譯期間,那咱們的註解是須要在項目中用到的,這個類應該放在那裏呢?這就有了第二個java library,router-annotation,用來專門存放咱們定義的註解和一些要被打進app中代碼。app

  • 因爲上述兩個library都是java項目,而咱們最終是要用到android工程中的,所以對外提供api時確定會用到android工程中的類,如Context。因此就有了第三個module router-api用於處理生成產物。如把生成映射關係表load到內存,並提供統一的調用入口。

0x02

咱們先定義咱們本身的註解:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {

    String path();

    String group() default "";

    String name() default "";

    int extras() default Integer.MIN_VALUE;

    int priority() default -1;
}
複製代碼

定義本身的route處理器RouterProcessor

@AutoService(Processor.class)       //自動註冊註解處理器
@SupportedOptions({Consts.KEY_MODULE_NAME})     //參數
@SupportedSourceVersion(SourceVersion.RELEASE_7)        //指定使用的Java版本
@SupportedAnnotationTypes({ANNOTATION_ROUTER_NAME}) //指定要處理的註解類型
public class RouterProcessor extends AbstractProcessor{

    private Map<String,Set<RouteMeta>> groupMap = new HashMap<>();  //收集分組
    private Map<String,String> rootMap = new TreeMap<>();
    private Filer mFiler;
    private Logger logger;
    private Types types;
    private TypeUtils typeUtils;
    private Elements elements;
    private String moduleName = "app"; //默認app
    private TypeMirror iProvider = null; //IProvider類型
    
    //......
複製代碼

其中SupportedAnnotationTypes指定的就是咱們上面定義的註解Route

接下來就是收集全部被註解的類,生成映射關係,代碼以下:

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if(CollectionUtils.isNotEmpty(set)) {
            //獲取到全部被註解的類
            Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(Route.class);
            try {
                logger.info(">>> Found routers,start... <<<");
                parseRoutes(elementsAnnotatedWith);
            } catch (IOException e) {
                logger.error(e);
            }
            return true;
        }
        return false;
    }
複製代碼

獲取完以後交給了parseRoutes方法:

private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
        if(CollectionUtils.isNotEmpty(routeElements)) {

            logger.info(">>> Found routes, size is " + routeElements.size() + " <<<");
            rootMap.clear();
            //.......
            TypeMirror type_activity = elements.getTypeElement(ACTIVITY).asType();

            for (Element element : routeElements) {
                TypeMirror tm = element.asType();
                Route route = element.getAnnotation(Route.class);
                RouteMeta routeMeta;

                if(types.isSubtype(tm,type_activity)) { //activity
                    logger.info(">>> Found activity route: "+ tm.toString() + " <<<");
                    routeMeta = new RouteMeta(route,element,RouteType.ACTIVITY,null);
                } else if(types.isSubtype(tm,iProvider)) { //IProvider
                    logger.info(">>> Found provider route: " + tm.toString() + " <<<");
                    routeMeta = new RouteMeta(route,element,RouteType.PROVIDER,null);
                } else if(types.isSubtype(tm,type_fragment) || types.isSubtype(tm,type_v4_fragment)) { //Fragment
                    logger.info(">>> Found fragment route: " + tm.toString() + " <<< ");
                    routeMeta = new RouteMeta(route,element,RouteType.parse(FRAGMENT),null);
                } else {
                    throw new RuntimeException("ARouter::Compiler >>> Found unsupported class type, type = [" + types.toString() + "].");
                }

                categories(routeMeta);
            }
            
            //.......
複製代碼

這個方法比較長,咱們先看看最主要的處理,遍歷routeElements,判斷當前被註解的類的類型,分別是activity,IProvider,Fragment這三中,也就是說註解Route能夠用來註解activity ,IProvider,和Fragment(注意這裏fragment包括原生包中的和v4包中的fragment)而後根據類型構造出routeMeta對象,構造完以後傳給了categories方法:

private void categories(RouteMeta routeMete) {
        if (routeVerify(routeMete)) {
            logger.info(">>> Start categories, group = " + routeMete.getGroup() + ", path = " + routeMete.getPath() + " <<<");
            //groupMap是一個全局變量,用來按分組存儲routeMeta
            Set<RouteMeta> routeMetas = groupMap.get(routeMete.getGroup());
            if (CollectionUtils.isEmpty(routeMetas)) {
                Set<RouteMeta> routeMetaSet = new TreeSet<>(new Comparator<RouteMeta>() {
                    @Override
                    public int compare(RouteMeta r1, RouteMeta r2) {
                        try {
                            return r1.getPath().compareTo(r2.getPath());
                        } catch (NullPointerException npe) {
                            logger.error(npe.getMessage());
                            return 0;
                        }
                    }
                });
                routeMetaSet.add(routeMete);
                groupMap.put(routeMete.getGroup(), routeMetaSet);
            } else {
                routeMetas.add(routeMete);
            }
        } else {
            logger.warning(">>> Route meta verify error, group is " + routeMete.getGroup() + " <<<");
        }
    }
複製代碼

咱們看到這個方法中首先根據當前url分組去groupMap中查找,也就是看是否有該分組,若是有取出對應的RouterMeta集合,把本次生成的routeMeta放進去;沒有就新存一個集合。

到這裏咱們已經把全部的註解類都獲取到而且已經按分組分類。接下來就是生成java類來存放這些信息:

這裏暫且只看對activity映射關係處理的代碼:

// (1)
for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
                String groupName = entry.getKey();

                // (2)
                MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(groupParamSpec);

                Set<RouteMeta> groupData = entry.getValue();

                for (RouteMeta meta : groupData) {
                    ClassName className = ClassName.get((TypeElement) meta.getRawType());
                    
                   //......   (3)

                    loadIntoMethodOfGroupBuilder.addStatement(
                            "atlas.put($S," +
                                    "$T.build($T." + meta.getType() + ",$T.class,$S,$S," + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + ", " + meta.getPriority() + "," + meta.getExtra() + "))",
                            meta.getPath(),
                            routeMetaCn,
                            routeTypeCn,
                            className,
                            meta.getPath().toLowerCase(),
                            meta.getGroup().toLowerCase());
                }

                //Generate groups   (4)
                String groupFileName = NAME_OF_GROUP + groupName;
                JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                        TypeSpec.classBuilder(groupFileName)
                                .addJavadoc(WARNING_TIPS)
                                .addSuperinterface(ClassName.get(type_IRouteGroup))
                                .addModifiers(Modifier.PUBLIC)
                                .addMethod(loadIntoMethodOfGroupBuilder.build())
                                .build()
                ).build().writeTo(mFiler);

                logger.info(">>> Generated group: " + groupName + "<<<");
                rootMap.put(groupName, groupFileName);
            }

            // (5)
            if(MapUtils.isNotEmpty(rootMap)) {
                for (Map.Entry<String, String> entry : rootMap.entrySet()) {
                    loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue()));
                }
            }

            // ......

            // Write root meta into disk.   (6)
            String rootFileName = NAME_OF_ROOT + moduleName;
            JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                    TypeSpec.classBuilder(rootFileName)
                            .addJavadoc(WARNING_TIPS)
                            .addSuperinterface(ClassName.get(elements.getTypeElement(IROUTE_ROOT)))
                            .addModifiers(PUBLIC)
                            .addMethod(loadIntoMethodOfRootBuilder.build())
                            .build()
            ).build().writeTo(mFiler);

            logger.info(">>> Generated root, name is " + rootFileName + " <<<");
        }

複製代碼

現將上述這段代碼解釋以下:

  • 遍歷咱們以前存儲的groupMap,取出對應的集合,如註釋(1)
  • 生成一個方法體,而且把集合中的全部映射關係都put到參數map中。如 (2)(3)
  • 生成java類,類名爲RouterManager$$Group$$ + moduleName,這裏的moduleName就是在build.gradle文件中配置的,如不配置,活獲取爲null 如(4)
  • 把每一個分組和所生成的類作個映射關係,做用就是爲了實現按分組加載功能 如 (5)(6)

下面咱們看下一輩子成的產物

/**
 DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY ROUTERMANAGER. */
public class RouterManager$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("service", RouterManager$$Group$$service.class);
  }
}

複製代碼

存儲分組對應關係

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY ROUTERMANAGER. */
public class RouterManager$$Group$$service implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/service/test/main",RouteMeta.build(RouteType.ACTIVITY,OtherActivity.class,"/service/test/main","service",null, -1,-2147483648));
  }
}

複製代碼

就這樣映射關係自動生成好了,那麼該如何使用呢?下面就讓我隆重介紹一下咱們Api

0x03

因爲咱們的映射關係表是全局存在的,因此確定須要在Application中作初始化操做,其目的就是把映射關係load到內存,下面讓咱們看看具體實現代碼

首先咱們得須要一個容器來存儲咱們的映射關係,所以就有了Warehouse類

class Warehouse {
    // Cache route and metas
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
    static Map<String, RouteMeta> routes = new HashMap<>();
    
    //......

    static void clear() {
        providers.clear();
        providersIndex.clear();
    }
}

複製代碼

咱們在此類中實例化兩個map用來存儲咱們的分組信息和每一個分組中的對應關係信息

groupIndex:用來存放分組信息,這個會優先load數據 routes:用來存儲對應關係數據

接下來咱們在App初始化時會調用以下代碼來初始化:

RouterManager.init(this);
複製代碼

那麼咱們進去init方法中看看具體幹了什麼?

public static synchronized void init(Application application){
        if(!hasInit) {
            hasInit = true;
            mContext = application;
            mHandler = new Handler(Looper.getMainLooper());
            logger = new DefaultLogger();
            LogisticsCenter.init(mContext,logger);
        }
    }
複製代碼

能夠看到這裏最關鍵的一行代碼是 LogisticsCenter.init(mContext,logger)

那就讓咱們繼續去LogisticsCenter.init(mContext,logger);方法中看看:

public synchronized static void init(Context context, ILogger log) {
        logger = log;
        Set<String> routeMap;
        try {
            if(RouterManager.debuggable() || PackageUtils.isNewVersion(context)) { //開發模式或版本升級時掃描本地件
                logger.info(TAG,"當前環境爲debug模式或者新版本,須要從新生成映射關係表");
                //these class was generated by router-compiler
                routeMap = ClassUtils.getFileNameByPackageName(context, Consts.ROUTE_ROOT_PAKCAGE);
                if(!routeMap.isEmpty()) {
                    PackageUtils.put(context,Consts.ROUTER_SP_KEY_MAP,routeMap);
                }
                PackageUtils.updateVersion(context);
            } else{ //讀取緩存
                logger.info(TAG,"讀取緩存中的router映射表");
                routeMap = PackageUtils.get(context,Consts.ROUTER_SP_KEY_MAP);
            }

            logger.info(TAG,"router map 掃描完成");
            //將分組數據加載到內存
            for (String className : routeMap) {
                //Root
                if(className.startsWith(Consts.ROUTE_ROOT_PAKCAGE + Consts.DOT + Consts.SDK_NAME + Consts.SEPARATOR + Consts.SUFFIX_ROOT)) {
                    ((IRouteRoot)(Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                } 
                //......
            }

            logger.info(TAG,"將映射關係讀到緩存中");

            if(Warehouse.groupsIndex.size() == 0) {
                logger.error(TAG,"No mapping files,check your configuration please!");
            }

            if (RouterManager.debuggable()) {
                logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.providersIndex.size()));
            }

        } catch (Exception e) {
            e.printStackTrace();
            logger.error(TAG,"RouterManager init logistics center exception! [" + e.getMessage() + "]");
        }
    }
複製代碼

具體解釋以下:

1)、首先是根據包名去掃描全部生成的類文件,並放在routeMap中。固然這裏會根據版本判斷而後緩存到本地,目的是爲了不重複掃描 2)、遍歷掃描到的數組,將全部分組信息緩存到Warehouse.groupIndex中

能夠看到初始化時只幹了這兩件事,掃描class文件,讀取分組信息;仔細想一想你會發現這裏並無去讀取咱們的url和activity映射關係信息,這就是所謂的按需加載。

到這裏咱們全部的準備工做都已完成了,那麼該怎麼使用呢?

下面讓咱們看看具體的用法

0x04

咱們先來看一段代碼:

RouterManager.getInstance().build("/user/main").navigation(MainActivity.this);
複製代碼

上述代碼是咱們打開UserActivty頁面所使用的方式,能夠發現這裏只傳了一個url。那就讓咱們看看內部是如何實現的?

首先咱們去build方法中看看具體的代碼:

public Postcard build(String path) {
        if(TextUtils.isEmpty(path)) {
            throw new HandlerException("Parameter is invalid!");
        } else {
            return build(path,extractGroup(path));
        }
    }

    public Postcard build(String path,String group) {
        if(TextUtils.isEmpty(path)) {
            throw new HandlerException("Parameter is invalid!");
        } else {
            return new Postcard(path,group);
        }
    }
複製代碼

發現這裏是一個重載方法,最後返回的是一個Postcard對象,而後調用Postcard的navigation方法。能夠看到這裏Postcard其實只是一個攜帶數據的實體。下面看看navigation方法:

public Object navigation(Context context) {
        return RouterManager.getInstance().navigation(context,this,-1);
    }
複製代碼

能夠發現這裏只是作了一箇中轉,最終調用的是RouterManager的navigation方法:

Object navigation(final Context context,final Postcard postcard,final int requestCode) {
        try {
            LogisticsCenter.completion(postcard);
        } catch (HandlerException e) {
            e.printStackTrace();
            return null;
        }

        final Context currentContext = context == null ? mContext : context;
        switch (postcard.getType()) {
            case ACTIVITY:
                final Intent intent = new Intent(currentContext,postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                int flags = postcard.getFlags();
                if(flags != -1) {
                    intent.setFlags(flags);
                } else if(!(currentContext instanceof Activity)) { //若是當前上下文不是activity,則啓動activity時須要new一個新的棧
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                runInMainThread(new Runnable() {
                    @Override
                    public void run() {
                        startActivity(requestCode,currentContext,intent,postcard);
                    }
                });
                break;
            //......
        }
        return null;
    }
複製代碼

由上述代碼能夠看出首先調用的是LogisticsCenter.completion()方法把postcard對象傳進去,那讓咱們先去這個方法中看個究竟:

/**
     * 填充數據
     * @param postcard
     */
    public synchronized static void completion(Postcard postcard) {
        RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
        if(routeMeta != null) {
            postcard.setDestination(routeMeta.getDestination());
            postcard.setType(routeMeta.getType());
            postcard.setPriority(routeMeta.getPriority());
            postcard.setExtra(routeMeta.getExtra());

            //......
        } else {
            Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
            if(groupMeta == null) {
                throw new NoRouteFoundException("There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
            } else {
                try {
                    //按組加載數據,美其名曰-按需加載
                    IRouteGroup iRouteGroup = groupMeta.getConstructor().newInstance();
                    iRouteGroup.loadInto(Warehouse.routes);
                    Warehouse.groupsIndex.remove(postcard.getGroup());
                } catch (Exception e) {
                    throw new HandlerException("Fatal exception when loading group meta. [" + e.getMessage() + "]");
                }
            }

            completion(postcard); //分組加載完成後從新查找
        }
    }

複製代碼

這裏首先去根據url去Warehouse.routes中查找對應的RouteMeta信息,如何是首次調用的話這裏必定是沒有的,因此會執行else方法,else方法裏先根據分組獲取對應的分組class,而後反射其實例對象並調用loadInfo()方法,把該分組中的全部映射關係讀取到Warehouse.routes中,而後繼續調用當前方法填充相關的信息。

信息填充完成以後繼續回到navigation方法中:

switch (postcard.getType()) {
            case ACTIVITY:
                final Intent intent = new Intent(currentContext,postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                int flags = postcard.getFlags();
                if(flags != -1) {
                    intent.setFlags(flags);
                } else if(!(currentContext instanceof Activity)) { //若是當前上下文不是activity,則啓動activity時須要new一個新的棧
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                runInMainThread(new Runnable() {
                    @Override
                    public void run() {
                        startActivity(requestCode,currentContext,intent,postcard);
                    }
                });
                break;
            //......
        }
複製代碼

能夠看到這裏使用的是常規的啓動方式startActivity去啓動一個新activity。

Ok到此爲止整個流程算是走完了,至於傳遞參數,獲取fragment,以及服務IProvider什麼的套路都同樣,這裏再也不重複贅述。

總結

ARouter的思路很好簡單,就是經過編譯時註解生成url與頁面的映射關係表,而後在程序啓動時將該映射關係表load到內存中,使用時直接去內存中查找而後執行常規的頁面啓動方式。

下面咱們來回答前面提出的兩個問題

第一:爲何要在每一個build.gradle文件中配置一個moduleName呢?

這是由於編譯時註解是以module爲單位去生成代碼的,也就是說咱們須要給每一個module項目都配置該註解生成器的依賴,爲了保證生成java文件的名字不會重複須要加上module爲後綴。此配置和分組沒有任何關係。只是爲了不生成的分組類重複。

第二:爲何會報多個類重名的問題?

咱們知道Router的映射表有兩張表,第一張是用來存儲分組和分組對應的class的,第二張是用來存儲每一個分組中具體url映射關係的。而在第一個問題中咱們根據moduleName來避免存放分組的class重名的問題。那麼每一個分組class自己有沒有重名的可能呢?答案是必定有的。好比:咱們在user組件中配置的url:/user/main分組爲user,這個時候在編譯user組件時就會自動生成一個類名爲 RouterManager$$Group$$user的類,用來存放全部的以user爲分組的頁面映射關係。那麼當咱們在app的中也配置分組名爲user的分組後,編譯app時就會在app中生成類名爲RouterManager$$Group$$user的類。而咱們app項目是依賴的user組件的,這就致使有兩個類名同樣的文件。編譯時天然就會報錯。

對RouterManager的幾點思考

  • RouterManager可否用於誇進程調用:

我認爲是能夠的,RouterManager的關係映射表是存在一個全局靜態變量中的,當咱們須要在其餘進程訪問時只須要提供一個接口來獲得映射關係便可。

  • RouterManager可否在RePlugin中的使用:

答案也是能夠的,因爲RePlugin採用的是多個classloader機制,這就致使咱們在主項目的classloader獲取的對象和在插件classloader中獲取的是兩個獨立的對象,若是想在插件中使用RouterManager去打開一個宿主的頁面,直接調用的話確定是沒有對應的映射關係的,由於在插件裏獲取的RouterManager對象並非宿主的單例對象,而是建立了一個新的對象。那怎麼辦呢?答案很簡單,咱們在插件中使用反射獲取到宿主的RouterManager實例便可正常使用。

注:RouterManager框架的思路來源與ARouter,這裏只實現了頁面跳轉,fragment獲取和服務Provider的獲取功能。至於其餘的降級策略,依賴注入功能就不在一一實現了

項目源碼請移駕到本人的github倉庫查看:github.com/qiangzier/R…

相關文章
相關標籤/搜索