動手擼一個ARouter (ARouter原始碼分析)

打不死的小強qz發表於2019-03-04

背景

為什麼要重複造輪子呢?

  • 我認為只有站在作者的角度才能更透徹的理解框架的設計思想
  • 去踩大神們所踩過的坑。
  • 才能深入的理解框架的所提供的功能
  • 學習優秀的作品中從而提高自己

在開始之前我先提出關於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是一樣的):

動手擼一個ARouter  (ARouter原始碼分析)

Why???

讓我們帶著這兩個問題開始RouterManager之旅。

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

我們的目標是根據一個url來開啟指定的頁面,該如何做呢?很簡單,我們把url和對應的頁面做一個對應關係,比如放到map中以url為key,對應的頁面activity為value即可;這樣當我們要開啟這個activity時,根據傳給我們的url去map中找到對應的activity,然後呼叫startActivity就OK了。

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

思路總結

我們可以利用編譯註解的特性,新增一個註解,給每個需要通過url開啟的activity加上此註解。在註解處理器中獲取所有被註解的類,動態生成對映關係表,然後在app啟動時把所生成的對映關係load到記憶體即可。

第二部擼程式碼

0x01

首先我們需要建立三個module,如下圖:

動手擼一個ARouter  (ARouter原始碼分析)

為什麼要三個專案呢?原因如下:

  • 我們需要用到的註解處理器AbstractProcessor是在javax包下,而android專案中是沒有這個包的,因此我們需要建一個java library,也就是router-compiler,它的作用是幫我們動態生成程式碼,只存在於編譯期間

  • 既然router-compiler只存在於編譯期間,那我們的註解是需要在專案中用到的,這個類應該放在那裡呢?這就有了第二個java library,router-annotation,用來專門存放我們定義的註解和一些要被打進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…

相關文章