在Android中啟動Activity一般使用startActivity或者startActivityForResult,通過這種方法啟動Activity的缺點是寫程式碼時Activity必須已經存在,這不利於多人協同工作,而且這樣硬編碼啟動Activity也不夠靈活, 如需要在H5介面中啟動本地Activity,或者在server端配置客戶端行為時,這樣的啟動方式顯得比較笨重。
如果可以通過類似url的方式開啟Activity,即通過解析一個url字串就可以開啟相應的介面,不僅非常酷,而且以上提到問題也可以得到解決。
思路
google Android router 可以發現其實已經有了不少可用的輪子:
其中最後一個ActivityRouter在之前的文章Android元件化開發實踐中提到過,這裡我們來分析一下如何來實現一個Android Router,並拆一下ActivityRouter,分析它的思路。
路由的目的就是把不同的請求交給不同的控制器,路由作為一箇中間層,把頁面請求和請求處理進行了解耦,而且還可以增加一些自定義功能,在靈活性和擴充套件性上做一些事情。
Android中我們的目的是建立url到Activity的一個對映,建立的過程要解決幾個問題:
url的定義和解析
一個合理的url結構,不僅要方便理解,而且要方便快速解析查詢。路由表的建立
路由表是路由中非常重要的一環,路由表一方面要可以快速查詢,一方面要方便建立和維護。資料傳遞
啟動Activity時,經常需要給新的Activity傳遞一些資料,使用路由後,需要設計一定的策略在Activiy之間傳遞資料。
下面就從以上幾個方面來看一下ActivityRouter的實現。
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其實在很多常用的第三方庫中都會用到,如EventBus3、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,每天推送移動開發前沿技術文章:
推薦閱讀: