Android Router 從 0 到 1

在Android中啓動Activity通常使用startActivity或者startActivityForResult,經過這種方法啓動Activity的缺點是寫代碼時Activity必須已經存在,這不利於多人協同工做,並且這樣硬編碼啓動Activity也不夠靈活, 如須要在H5界面中啓動本地Activity,或者在server端配置客戶端行爲時,這樣的啓動方式顯得比較笨重。javascript

若是能夠經過相似url的方式打開Activity,即經過解析一個url字符串就能夠打開相應的界面,不只很是酷,並且以上提到問題也能夠獲得解決。java

思路

google Android router 能夠發現其實已經有了很多可用的輪子:
android


其中最後一個 ActivityRouter在以前的文章 Android組件化開發實踐中提到過,這裏咱們來分析一下如何來實現一個Android Router,並拆一下 ActivityRouter,分析它的思路。

路由的目的就是把不一樣的請求交給不一樣的控制器,路由做爲一箇中間層,把頁面請求和請求處理進行了解耦,並且還能夠增長一些自定義功能,在靈活性和擴展性上作一些事情。git

Android中咱們的目的是創建url到Activity的一個映射,創建的過程要解決幾個問題:github

  1. url的定義和解析
    一個合理的url結構,不只要方便理解,並且要方便快速解析查找。web

  2. 路由表的創建
    路由表是路由中很是重要的一環,路由表一方面要能夠快速查找,一方面要方便創建和維護。瀏覽器

  3. 數據傳遞
    啓動Activity時,常常須要給新的Activity傳遞一些數據,使用路由後,須要設計必定的策略在Activiy之間傳遞數據。app

下面就從以上幾個方面來看一下ActivityRouter的實現。ide

url 的設計

url定義

url通常主要由Schema、Host、Path以及QueryParameter等構成。組件化

咱們在路由中使用自定義的Schema以和普通的http進行區分,Host能夠在應用中使用贊成的字符串或者能夠省略,path用來設置Activity請求路徑,QueryParameter能夠作他用,完成數據傳遞的任務。

咱們看一下ActivityRouter中url的使用:

mzule://main/0xff878798

上例中mzule是自定義的Schema,main是path,

0xff878798是自定義的Parameter 。

url 解析

url解析就是拿到字符串中的Schema、host、path、queryParameters。

public static Path create(Uri uri) {
        Path path = new Path(uri.getScheme().concat("://"));
        String urlPath = uri.getPath();
        if (urlPath == null) {
            urlPath = "";
        }
        if (urlPath.endsWith("/")) {
            urlPath = urlPath.substring(0, urlPath.length() - 1);
        }
        parse(path, uri.getHost() + urlPath);
        return path;
    }
    private static void parse(Path scheme, String s) {
        String[] components = s.split("/");
        Path curPath = scheme;
        for (String component : components) {
            Path temp = new Path(component);
            curPath.next = temp;
            curPath = temp;
        }
    }複製代碼

按照以上url的規範設計咱們Android路由中的url,能夠很是方便地使用Java的Api,以上代碼是ActivityRouter對於url的解析,很是清晰易懂。

路由表的實現

在路由表中增長一條記錄都須要那些東西呢?首先確定須要一個url,其次須要知道跳轉的Activity 的名字,最好再有能夠傳遞的一些數據,咱們來看一下ActivityRouter的實現:

//Mapping.java
    private final String format;
    private final Class<? extends Activity> activity;
    private final MethodInvoker method;
    private final ExtraTypes extraTypes;
    private Path formatPath;複製代碼

能夠看到format其實就是咱們須要的url,activity就是跳轉的Activity,extraTypes是能夠傳遞的數據,徹底符合咱們的需求。

如何根據url打開相應的Activity呢:

private static boolean doOpen(Context context, Uri uri, int requestCode) {
        initIfNeed();
        Path path = Path.create(uri);
        for (Mapping mapping : mappings) {
            if (mapping.match(path)) {
                //activity router 不只能夠打開Activity,還能夠執行一些方法
                if (mapping.getActivity() == null) {
                    mapping.getMethod().invoke(context, mapping.parseExtras(uri));
                    return true;
                }
                Intent intent = new Intent(context, mapping.getActivity());
                intent.putExtras(mapping.parseExtras(uri));
                intent.putExtra(KEY_RAW_URL, uri.toString());
                //若是context不是activity的實例(如是Application的實例),則須要添加Intent.FLAG_ACTIVITY_NEW_TASK,才能夠正確打開Activity
                if (!(context instanceof Activity)) {
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }
                if (requestCode >= 0) {
                    if (context instanceof Activity) {
                        ((Activity) context).startActivityForResult(intent, requestCode);
                    } else {
                        throw new RuntimeException("can not startActivityForResult context " + context);
                    }
                } else {
                    context.startActivity(intent);
                }
                return true;
            }
        }
        return false;
    }複製代碼

能夠參考註釋理解代碼,沒有什麼難度。

如何在路由表中插入這樣一條條記錄呢,若是每次增長一個功能就手動增長一條記錄並非很是明智的作法,擴展性和維護性都很差。ActivityRouter採用了Apt的方式,這是ActivityRouter相比於其餘android router的一個亮點,這裏重點介紹一下。

annotation

annotation其實在不少經常使用的第三方庫中都會用到,如EventBus三、butterknife、dagger等。
annotation根據其做用能夠分爲三種:

  • 標記
    僅僅在源碼中起做用,用於標示,功能相似於註釋,如@Override

  • 編譯時annotation
    在編譯時起做用,能夠在代碼進行編譯時對註解部分進行處理,好比根據annotation的部分自動生成代碼的等,butterknife其實就到了annotation的這個功能

  • 運行時annotation
    能夠在運行時根據annotation 經過反射實現一些功能

看一個ActivityRouter中的例子:

@Retention(RetentionPolicy.CLASS)
public @interface Module {
    String value();
}複製代碼

這裏定義了一個annotation,在使用時能夠@Module("sdk")這樣使用。

能夠看到定義annotation時也使用了annotation,它們是元註解:

元註解共有四種@Retention, @Target, @Inherited, @Documented

@Retention 保留的範圍,默認值爲CLASS. 可選值有三種

  • SOURCE, 只在源碼中可用
  • CLASS, 在源碼和字節碼中可用
  • RUNTIME, 在源碼,字節碼,運行時都可用

@Target 能夠用來修飾哪些程序元素,如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER等,未標註則表示可修飾全部

@Inherited 是否能夠被繼承,默認爲false

@Documented 是否會保存到 Javadoc 文檔中

Apt

Android-apt實際是一個插件,能夠處理annotation processors,在編譯階段對annotation進行處理。這裏ActivityRouter就是使用annotation經過Apt的方式自動生成咱們的路由表的。
關於Apt的更多介紹和使用能夠參考android-apt

使用apt以後經過繼承AbstractProcessor來對annotation來進行處理:

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        debug("process apt with " + annotations.toString());
        if (annotations.isEmpty()) {
            return false;
        }
        boolean hasModule = false;
        boolean hasModules = false;
        // module
        String moduleName = "RouterMapping";
        Set<? extends Element> moduleList = roundEnv.getElementsAnnotatedWith(Module.class);
        if (moduleList != null && moduleList.size() > 0) {
            Module annotation = moduleList.iterator().next().getAnnotation(Module.class);
            moduleName = moduleName + "_" + annotation.value();
            hasModule = true;
        }
        // modules
        String[] moduleNames = null;
        Set<? extends Element> modulesList = roundEnv.getElementsAnnotatedWith(Modules.class);
        if (modulesList != null && modulesList.size() > 0) {
            Element modules = modulesList.iterator().next();
            moduleNames = modules.getAnnotation(Modules.class).value();
            hasModules = true;
        }
        // RouterInit
        if (hasModules) {
            debug("generate modules RouterInit");
            generateModulesRouterInit(moduleNames);
        } else if (!hasModule) {
            debug("generate default RouterInit");
            generateDefaultRouterInit();
        }
        // RouterMapping
        return handleRouter(moduleName, roundEnv);
    }複製代碼

ActivityRouter對project中是否有多個module分別進行了處理。

javapoet是很是好用的一個java代碼生成庫,ActivityRouter使用javapoet處理annotation,在編譯時生成路由映射表。

private void generateDefaultRouterInit() {
        MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);
        initMethod.addStatement("RouterMapping.map()");
        TypeSpec routerInit = TypeSpec.classBuilder("RouterInit")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(initMethod.build())
                .build();
        try {
            JavaFile.builder("com.github.mzule.activityrouter.router", routerInit)
                    .build()
                    .writeTo(filer);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }複製代碼

以上代碼生成RouterInit.java,並生成init方法。

private boolean handleRouter(String genClassName, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Router.class);

        MethodSpec.Builder mapMethod = MethodSpec.methodBuilder("map")
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC)
                .addStatement("java.util.Map<String,String> transfer = null")
                .addStatement("com.github.mzule.activityrouter.router.ExtraTypes extraTypes")
                .addCode("\n");

        for (Element element : elements) {
            Router router = element.getAnnotation(Router.class);
            String[] transfer = router.transfer();
            if (transfer.length > 0 && !"".equals(transfer[0])) {
                mapMethod.addStatement("transfer = new java.util.HashMap<String, String>()");
                for (String s : transfer) {
                    String[] components = s.split("=>");
                    if (components.length != 2) {
                        error("transfer `" + s + "` not match a=>b format");
                        break;
                    }
                    mapMethod.addStatement("transfer.put($S, $S)", components[0], components[1]);
                }
            } else {
                mapMethod.addStatement("transfer = null");
            }

            mapMethod.addStatement("extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes()");
            mapMethod.addStatement("extraTypes.setTransfer(transfer)");

            addStatement(mapMethod, int.class, router.intParams());
            addStatement(mapMethod, long.class, router.longParams());
            addStatement(mapMethod, boolean.class, router.booleanParams());
            addStatement(mapMethod, short.class, router.shortParams());
            addStatement(mapMethod, float.class, router.floatParams());
            addStatement(mapMethod, double.class, router.doubleParams());
            addStatement(mapMethod, byte.class, router.byteParams());
            addStatement(mapMethod, char.class, router.charParams());

            for (String format : router.value()) {
                ClassName className;
                Name methodName = null;
                if (element.getKind() == ElementKind.CLASS) {
                    className = ClassName.get((TypeElement) element);
                } else if (element.getKind() == ElementKind.METHOD) {
                    className = ClassName.get((TypeElement) element.getEnclosingElement());
                    methodName = element.getSimpleName();
                } else {
                    throw new IllegalArgumentException("unknow type");
                }
                if (format.startsWith("/")) {
                    error("Router#value can not start with '/'. at [" + className + "]@Router(\"" + format + "\")");
                    return false;
                }
                if (format.endsWith("/")) {
                    error("Router#value can not end with '/'. at [" + className + "]@Router(\"" + format + "\")");
                    return false;
                }
                if (element.getKind() == ElementKind.CLASS) {
                    mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, $T.class, null, extraTypes)", format, className);
                } else {
                    mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, null, " +
                            "new MethodInvoker() {\n" +
                            " public void invoke(android.content.Context context, android.os.Bundle bundle) {\n" +
                            " $T.$N(context, bundle);\n" +
                            " }\n" +
                            "}, " +
                            "extraTypes)", format, className, methodName);
                }
            }
            mapMethod.addCode("\n");
        }
        TypeSpec routerMapping = TypeSpec.classBuilder(genClassName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(mapMethod.build())
                .build();
        try {
            JavaFile.builder("com.github.mzule.activityrouter.router", routerMapping)
                    .build()
                    .writeTo(filer);
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return true;
    }複製代碼

以上代碼用於根據註解生成一條條路由映射。咱們能夠看一下最終生成的文件:

public final class RouterMapping_app {
  public static final void map() {
    java.util.Map<String,String> transfer = null;
    com.github.mzule.activityrouter.router.ExtraTypes extraTypes;

    transfer = null;
    extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
    extraTypes.setTransfer(transfer);
    com.github.mzule.activityrouter.router.Routers.map("home/:homeName", HomeActivity.class, null, extraTypes);

    transfer = null;
    extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
    extraTypes.setTransfer(transfer);
    com.github.mzule.activityrouter.router.Routers.map("with_host", HostActivity.class, null, extraTypes);

    transfer = new java.util.HashMap<String, String>();
    transfer.put("web", "fromWeb");
    extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
    extraTypes.setTransfer(transfer);
    extraTypes.setLongExtra("id,updateTime".split(","));
    extraTypes.setBooleanExtra("web".split(","));
    com.github.mzule.activityrouter.router.Routers.map("http://mzule.com/main", MainActivity.class, null, extraTypes);
    com.github.mzule.activityrouter.router.Routers.map("main", MainActivity.class, null, extraTypes);
    com.github.mzule.activityrouter.router.Routers.map("home", MainActivity.class, null, extraTypes);

    transfer = null;
    extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
    extraTypes.setTransfer(transfer);
    com.github.mzule.activityrouter.router.Routers.map("logout", null, new MethodInvoker() {
           public void invoke(android.content.Context context, android.os.Bundle bundle) {
               NonUIActions.logout(context, bundle);
           }
        }, extraTypes);

    transfer = null;
    extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
    extraTypes.setTransfer(transfer);
    com.github.mzule.activityrouter.router.Routers.map("upload", null, new MethodInvoker() {
           public void invoke(android.content.Context context, android.os.Bundle bundle) {
               NonUIActions.uploadLog(context, bundle);
           }
        }, extraTypes);

    transfer = null;
    extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
    extraTypes.setTransfer(transfer);
    com.github.mzule.activityrouter.router.Routers.map("user/:userId", UserActivity.class, null, extraTypes);
    com.github.mzule.activityrouter.router.Routers.map("user/:nickname/city/:city/gender/:gender/age/:age", UserActivity.class, null, extraTypes);

    transfer = null;
    extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes();
    extraTypes.setTransfer(transfer);
    com.github.mzule.activityrouter.router.Routers.map("user/collection", UserCollectionActivity.class, null, extraTypes);

  }
}複製代碼

這裏有個小問題是,RouterInit 和 RouterMapping兩個文件實際上是在編譯期生成的,而很明顯咱們在其餘地方須要用到這兩個文件,也就是在build以前就須要存在着兩個文件,怎麼處理呢?這裏在gradle中的dependencies中使用provided,即咱們能夠提早寫好空殼RouterInit 和 RouterMapping,而後經過provided的方式使得代碼經過編譯,可是在執行時實際使用的是以後生成的文件:

dependencies {
    provided project(':stub')
    compile 'com.github.mzule.activityrouter:annotation:1.1.5'
}複製代碼

數據傳遞

在web中咱們能夠經過在url後添加參數訪問提交數據,在Android router中咱們一樣能夠把數據保存在url中,只有能夠正確解析出數據便可。

咱們看下ActivityRouter中實現:

public Bundle parseExtras(Uri uri) {
        Bundle bundle = new Bundle();
        // path segments // ignore scheme
        Path p = formatPath.next();
        Path y = Path.create(uri).next();
        while (p != null) {
            if (p.isArgument()) {
                put(bundle, p.argument(), y.value());
            }
            p = p.next();
            y = y.next();
        }
        // parameter
        Set<String> names = UriCompact.getQueryParameterNames(uri);
        for (String name : names) {
            String value = uri.getQueryParameter(name);
            put(bundle, name, value);
        }
        return bundle;
    }複製代碼

合理解析url,能夠把數據打包爲bundle,在啓動activity時傳遞過去。

ActivityRouter代碼結構

  • activityrouter 路由實現的主義邏輯
  • annotation 定義用到的annotation
  • app demo
  • app_module demo
  • compiler 處理annotation,實現apt
  • stub 提供編譯期的RouterInit Router Mapping文件

##總結

其實ActivityRouter中還實現了在瀏覽器中啓動應用的界面,主要思路是啓動一個透明activity,而後在activity中解析url,再啓動目標activity。ActivityRouter也支持直接解析http,打開web界面。本文中再也不進行分析,感興趣的同窗能夠去看看源碼。

歡迎關注公衆號wutongke,天天推送移動開發前沿技術文章:

wutongke

推薦閱讀:

尋找卓越的(Android)軟件工程師

想在Android中使用java8?你可能再也不須要retrolambda了

你不知道一些神奇Android Api

Android增量編譯3~5秒的背後

相關文章
相關標籤/搜索