Android滴滴路由框架DRouter原理解析

作者:linversion

前言

最近的一個新項目使用了Clean Architecture+模塊化+MVVM架構(gòu),將首頁每個tab對應(yīng)的功能都放到單獨的模塊且不相互依賴,這時就有了模塊間頁面跳轉(zhuǎn)的問題,經(jīng)過一番研究選擇了滴滴的DRouter,因為其出色的性能、靈活的組件拆分,更重要的是生成路由表時支持插件增量編譯、多線程掃描,運行時異步加載路由表,支持回調(diào)式ActivityResult,比ARouter好太多。本著用一個新框架,只會用還不夠的原則,我決定去了解一下框架的原理,并給自己制定了以下幾個問題:

1、框架的設(shè)計分層是什么樣的?
2、它是如何生成路由表的?
3、它是如何加載路由表的?
4、相比于ARouter如何提高了性能?

閱讀官方文檔

相比于直接一頭扎進(jìn)源碼,先閱讀官方的文檔總是沒錯的,官方給了一篇介紹的文章,寫得非常好,基本回答了我以上的所有問題。

首先在介紹DRouter的亮點部分得到了問題2、3、4的答案。

路由表在編譯期通過插件動態(tài)生成。插件會啟動多線程同時異步處理所有的組件;增量掃描功能可以幫助開發(fā)者在第二次編譯時,只對修改過的代碼進(jìn)行處理,極大地縮短路由表生成的時間。

在編譯器使用gradle插件配合transform掃描所有的類,生成路由表,并且支持增量掃描,回答了問題2。

另外框架初始化的時候啟動子線程去加載路由表,不阻塞主線程的執(zhí)行,盡其所能提高效率。

回答了問題3。

加載路由表、實例化路由、以及跨進(jìn)程命令到達(dá)服務(wù)端后的分發(fā)這些常規(guī)應(yīng)該使用反射的場景,使用預(yù)占位或動態(tài)生成代碼來替換成java的new創(chuàng)建和顯式方式執(zhí)行,最大限度的去避免反射執(zhí)行,提高性能。

回答了問題4,通過減少使用反射提升了性能。

在原理和架構(gòu)章節(jié)處給了一張架構(gòu)的設(shè)計圖:

整體架構(gòu)分三層,自下而上是數(shù)據(jù)流層、組件層、開放接口層。

數(shù)據(jù)流層是DRouter最重要的核心模塊,這里承載著插件生成的路由表、路由元素、動態(tài)注冊、以及跨進(jìn)程功能相關(guān)的序列化數(shù)據(jù)流。所有的路由流轉(zhuǎn)都會從這里取得對應(yīng)的數(shù)據(jù),進(jìn)而流向正確的目標(biāo)。

RouterPlugin和MetaLoader負(fù)責(zé)生成路由表,路由元素指的是RouterMeta,存放scheme/host/path等信息。

組件層,核心的路由分發(fā)、攔截器、生命周期、異步暫存和監(jiān)控、ServiceLoader、多維過濾、Fragment路由,以及跨進(jìn)程命令打包等。

開放接口層則是使用時接觸到的一些類,API設(shè)計得也很簡單易用,DRouter類和Request類分別只有75和121行代碼。

問題1得到解答,到此處也對整個框架有了一個整體的認(rèn)識。

閱讀源碼

1.初始化流程

調(diào)用DRouter.init(app)后的時序圖如下:

默認(rèn)是在子線程實現(xiàn)路由表加載,不影響主線程。

    public static void checkAndLoad(final String app, boolean async) {
        if (!loadRecord.contains(app)) {
            // 雙重校驗鎖
            synchronized (RouterStore.class) {
                if (!loadRecord.contains(app)) {
                    loadRecord.add(app);
                    if (!async) {
                        Log.d(RouterLogger.CORE_TAG, "DRouter start load router table sync");
                        load(app);
                    } else {
                        new Thread("drouter-table-thread") {
                            @Override
                            public void run() {
                                Log.d(RouterLogger.CORE_TAG, "DRouter start load router table in drouter-table-thread");
                                load(app);
                            }
                        }.start();
                    }
                }
            }
        }
    }

最終走到了RouterLoader的load方法來加載路由表到一個map中,仔細(xì)看它的引入路徑是com.didi.drouter.loader.host.RouterLoader,是不存在于源碼中的,因為它是編譯的時候生成的,位置位于app/build/intermediates/transforms/DRouter/dev/debug/../com/didi/drouter/loader/host/RouterLoader。

public class RouterLoader extends MetaLoader {
    @Override
    public void load(Map var1) {
        var1.put("@@$$/browse/BrowseActivity", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("", "", "/browse/BrowseActivity", "com.example.demo.browse.BrowseActivity", (IRouterProxy)null, (Class[])null, (String[])null, 0, 0, false));
    }

    public RouterLoader() {
    }
}

public abstract class MetaLoader {

    public abstract void load(Map<?, ?> data);

    // for regex router
    protected void put(String uri, RouterMeta meta, Map<String, Map<String, RouterMeta>> data) {
        Map<String, RouterMeta> map = data.get(RouterStore.REGEX_ROUTER);
        if (map == null) {
            map = new ConcurrentHashMap<>();
            data.put(RouterStore.REGEX_ROUTER, map);
        }
        map.put(uri, meta);
    }

    // for service
    protected void put(Class<?> clz, RouterMeta meta, Map<Class<?>, Set<RouterMeta>> data) {
        Set<RouterMeta> set = data.get(clz);
        if (set == null) {
            set = Collections.newSetFromMap(new ConcurrentHashMap<RouterMeta, Boolean>());
            data.put(clz, set);
        }
        set.add(meta);
    }
}

不難猜出其是在編譯期加了一個transform,生成RouterLoader類時加入了load方法的具體實現(xiàn),具體來說是javaassit API+Gradle Transform,所以去看看drouter-plugin在編譯期做了什么。

2.編譯期transform

直接看時序圖。

創(chuàng)建了一個RouterPlugin,并且注冊了一個Gradle Transform。

class RouterPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        ...
        project.android.registerTransform(new TransformProxy(project))
    }
}

class TransformProxy extends Transform {
        @Override
    void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
        String pluginVersion = ProxyUtil.getPluginVersion(invocation)
        if (pluginVersion != null) {
            ...

            if (pluginJar.exists()) {
                URLClassLoader newLoader = new URLClassLoader([pluginJar.toURI().toURL()] as URL[], getClass().classLoader)
                Class<?> transformClass = newLoader.loadClass("com.didi.drouter.plugin.RouterTransform")
                ClassLoader threadLoader = Thread.currentThread().getContextClassLoader()
                // 1.設(shè)置URLClassLoader
                Thread.currentThread().setContextClassLoader(newLoader)
                Constructor constructor = transformClass.getConstructor(Project.class)
                // 2.反射創(chuàng)建一個RouterTransform
                Transform transform = (Transform) constructor.newInstance(project)
                transform.transform(invocation)
                Thread.currentThread().setContextClassLoader(threadLoader)
                return
            } else {
                ProxyUtil.Logger.e("Error: there is no drouter-plugin jar")
            }
        }
    }
}

注釋2處反射創(chuàng)建一個com.didi.drouter.plugin.RouterTransform對象,并執(zhí)行其transform方法,此處真正處理transform邏輯,它的位置位于drouter-plugin模塊。

class RouterTransform extends Transform {
    @Override
    void transform(TransformInvocation invocation) throws TransformException, InterruptedException, IOException {
        ...
        // 1.創(chuàng)建一個DRouterTable目錄
        File dest = invocation.outputProvider.getContentLocation("DRouterTable", TransformManager.CONTENT_CLASS,
                ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY)
        // 2.執(zhí)行RouterTask
        (new RouterTask(project, compilePath, cachePathSet, useCache, dest, tmpDir, setting, isWindow)).run()
        FileUtils.writeLines(cacheFile, cachePathSet)
        Logger.v("Link: https://github.com/didi/DRouter")
        Logger.v("DRouterTask done, time used: " + (System.currentTimeMillis() - timeStart) / 1000f  + "s")
    }
}

注釋2處new了一個RouterTask對象,并執(zhí)行其run方法,之后的log輸出就是平時編譯能看到的信息,表示transform的耗時。

public class RouterTask {
    void run() {
        StoreUtil.clear();
        JarUtils.printVersion(project, compileClassPath);
        pool = new ClassPool();
        // 1.創(chuàng)建ClassClassify
        classClassify = new ClassClassify(pool, setting);
        startExecute();
    }

    private void startExecute() {
        try {
            ...
            // 2.執(zhí)行ClassClassify的generatorRouter
            classClassify.generatorRouter(routerDir);
            Logger.d("generator router table used: " + (System.currentTimeMillis() - timeStart) + "ms");
            Logger.v("scan class size: " + count.get() + " | router class size: " + cachePathSet.size());
        } catch (Exception e) {
            JarUtils.check(e);
            throw new GradleException("Could not generate d_router table\n" + e.getMessage(), e);
        } finally {
            executor.shutdown();
            FileUtils.deleteQuietly(wTmpDir);
        }
    }
}

重點在于ClassClassify這個類,其generatorRouter方法便是最終處理生成路由表的邏輯。

public class ClassClassify {
    private List<AbsRouterCollect> classifies = new ArrayList<>();

    public ClassClassify(ClassPool pool, RouterSetting.Parse setting) {
        classifies.add(new RouterCollect(pool, setting));
        classifies.add(new ServiceCollect(pool, setting));
        classifies.add(new InterceptorCollect(pool, setting));
    }

    public void generatorRouter(File routerDir) throws Exception {
        for (int i = 0; i < classifies.size(); i++) {
            AbsRouterCollect cf = classifies.get(i);
            cf.generate(routerDir);
        }
    }
}

構(gòu)造函數(shù)處添加了RouterCollect/ServiceCollect/InterceptorCollect,最終執(zhí)行的是他們的generate方法,分別處理路由表、service、攔截器,我們只看路由表的。

class RouterCollect extends AbsRouterCollect {
    @Override
    public void generate(File routerDir) throws Exception {
        // 1.創(chuàng)建RouterLoader類
        CtClass ctClass = pool.makeClass(getPackageName() + ".RouterLoader");
        CtClass superClass = pool.get("com.didi.drouter.store.MetaLoader");
        ctClass.setSuperclass(superClass);

        StringBuilder builder = new StringBuilder();
        builder.append("public void load(java.util.Map data) {\n");
        for (CtClass routerCc : routerClass.values()) {
            try {
                // 處理注解、class類型等邏輯
                ...
                StringBuilder metaBuilder = new StringBuilder();
                metaBuilder.append("com.didi.drouter.store.RouterMeta.build(");
                metaBuilder.append(type);
                metaBuilder.append(").assembleRouter(");
                metaBuilder.append("\"").append(schemeValue).append("\"");
                metaBuilder.append(",");
                metaBuilder.append("\"").append(hostValue).append("\"");
                metaBuilder.append(",");
                metaBuilder.append("\"").append(pathValue).append("\"");
                metaBuilder.append(",");
                if ("com.didi.drouter.store.RouterMeta.ACTIVITY".equals(type)) {
                    if (!setting.isUseActivityRouterClass()) {
                        metaBuilder.append("\"").append(routerCc.getName()).append("\"");
                    } else {
                        metaBuilder.append(routerCc.getName()).append(".class");
                    }
                } else {
                    metaBuilder.append(routerCc.getName()).append(".class");
                }
                metaBuilder.append(", ");
                ...
                metaBuilder.append(proxyCc != null ? "new " + proxyCc.getName() + "()" : "null");
                metaBuilder.append(", ");
                metaBuilder.append(interceptorClass != null ? interceptorClass.toString() : "null");
                metaBuilder.append(", ");
                metaBuilder.append(interceptorName != null ? interceptorName.toString() : "null");
                metaBuilder.append(", ");
                metaBuilder.append(thread);
                metaBuilder.append(", ");
                metaBuilder.append(priority);
                metaBuilder.append(", ");
                metaBuilder.append(hold);
                metaBuilder.append(")");
                ...
                if (isAnyRegex) {
                    // 2. 插入路由表
                    items.add("    put(\"" + uri + "\", " + metaBuilder + ", data); \n");
                    //builder.append("    put(\"").append(uri).append("\", ").append(metaBuilder).append(", data); \n");
                } else {
                    items.add("    data.put(\"" + uri + "\", " + metaBuilder + "); \n");
                    //builder.append("    data.put(\"").append(uri).append("\", ").append(metaBuilder).append("); \n");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            Collections.sort(items);
            for (String item : items) {
                builder.append(item);
            }
            builder.append("}");

            Logger.d("\nclass RouterLoader" + "\n" + builder.toString());
            // 3.生成代碼
            generatorClass(routerDir, ctClass, builder.toString());
        }
    }
}

此處邏輯比較多,但總體是清晰的,處理完注解和類型的判斷,獲取路由的信息,構(gòu)造將要插入的代碼,最后統(tǒng)一在父類AbsRouterCollect的generatorClass處理load方法的生成,此時編譯器的工作就完成了。

ARouter也提供了arouter-register插件,同是在編譯期生成路由表,不同的是在生成代碼時,ARouter使用的是ASM,DRouter使用Javassist,查了一下資料,ASM性能比Javassist更好,但更難上手,需要懂字節(jié)碼知識,Javassist在復(fù)雜的字節(jié)碼級操作上提供了更高級別的抽象層,因此實現(xiàn)起來更容易、更快,只需要懂很少的字節(jié)碼知識,它使用反射機制。

3.運行期加載路由表

重新貼一下加載路由表的load方法。

public class RouterLoader extends MetaLoader {
    @Override
    public void load(Map var1) {
        var1.put("@@$$/browse/BrowseActivity", RouterMeta.build(RouterMeta.ACTIVITY).assembleRouter("", "", "/browse/BrowseActivity", "com.example.demo.browse.BrowseActivity", (IRouterProxy)null, (Class[])null, (String[])null, 0, 0, false));
    }

    public RouterLoader() {
    }
}

看下RouteMeta的build方法。

public static RouterMeta build(int routerType) {
    return new RouterMeta(routerType);
}

可見是直接new的一個路由類,這與ARouter直接通過反射創(chuàng)建路由類不同,性能更好。

4.總結(jié)

本文分析了DRouter路由部分的原理,其在編譯器使用Gradle Transform和Javassist生成路由表,運行時new路由類,異步初始化加載路由表,實現(xiàn)了高性能。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內(nèi)容