引子
这篇文章会告诉你
- 什么是路由,是为了解决什么问题才产生的
- 业界现状是怎么样的,我们可以做什么来优化当前的问题
- 路由设计思路是怎么样的,该怎么设计比较好
- 如何用注解实现路由表
- URL的参数如何依赖注入到Activity、Fragement
- 如何Hook
OnActivityResult
,不需要再进行requstCode判断 - 如何异步拦截路由,实现线程切换,不阻塞页面跳转
- 如何用Apt实现
Retrofit
接口式调用 - 如何找到
Activity
的调用方 - 如何实现路由的
安全
调用 - 如何避开
Apt
不能汇总所有Module路由的问题
前言
当前Android的路由库实在太多了,刚开始的时候想为什么要用路由表的库,用Android原生的Scheme码不就好了,又不像iOS只能类依赖,后面越深入就越发现当时想的太简单了,后面看到Retrofit和OkHttp,才想到页面请求本质和网络请求不是一样吗,终于业界最简单高效的路由方案1.0出来了
开源的库后面会放在公司github地址上面
背景
什么是路由
根据路由表
将页面请求
分发到指定页面
使用场景
- App接收到一个通知,点击通知打开App的某个页面
- 浏览器App中点击某个链接打开App的某个页面
- 运营活动需求,动态把原生的页面替换成H5页面
- 打开页面需要某些条件,先验证完条件,再去打开那个页面
- 不合法的打开App的页面被屏蔽掉
- H5打开链接在所有平台都一样,方便统一跳转
- App存在就打开页面,不存在就去下载页面下载,只有Google的App Link支持
为什么要有路由
Android原生已经支持AndroidManifest
去管理App跳转,为什么要有路由库,这可能是大部分人接触到Android各种Router库不太明白的地方,这里我讲一下我的理解
- 显示Intent:项目庞大以后,类依赖耦合太大,不适合组件化拆分
- 隐式Intent:协作困难,调用时候不知道调什么参数
- 每个注册了Scheme的Activity都可以直接打开,有安全风险
- AndroidMainfest集中式管理比较臃肿
- 无法动态修改路由,如果页面出错,无法动态降级
- 无法动态拦截跳转,譬如未登录的情况下,打开登录页面,登录成功后接着打开刚才想打开的页面
- H5、Android、iOS地址不一样,不利于统一跳转
怎么样的路由才算好路由
路由说到底还是为了解决开发者遇到的各种奇葩需求,使用简单、侵入性低、维护方便是首要条件,不影响你原来的代码,写入代码也很少,这里就要说说我的OkDeepLink
的五大功能了,五大功能瞬间击中你的各种痛点,早点下班不是梦。
- 编译时注解,实现静态路由表,不再需要在臃肿的
AndroidManifest
中找到那个Actvity写Scheme和Intent Filter - 异步拦截器,实现动态路由,安全拦截、动态降级难不倒你
- 模仿
Retrofit
接口式调用,实现方式用apt
,不耗性能,参数调用不再是问题 - Hook
OnActivityResult
,支持RxJava响应式调用,不再需要进行requestCode判断 -
参数依赖注入,自动保存,不再需要手动写
onSaveInstance
、onCreate(SaveInstace)
、onNewIntent(Intent)
、getQueryParamer
详细比较
大部分路由库都用Apt(编译时注解)生成路由表,然后用路由表转发到指定页面
方案对比 | OkDeepLink | Airbnb DeepLinkDispatch | 阿里 ARouter | 天猫 统跳协议 | ActivityRouter |
---|---|---|---|---|---|
路由注册 | 注解式接口注册 | 每个module都要手动注册 | 每个module的路由表都要类查找 | AndroidManiFest配置 | 每个module都要手动注册 |
路由查找 | 路由表 | 路由表 | 路由表 | 系统Intent | 路由表 |
路由分发 | Activity转发 | Activity转发 | Activity转发 | Activity转发 | Activity转发 |
动态替换 | Rxjava实现异步拦截器 | 不支持 | 线程等待 | 不支持 | 不支持 |
动态拦截 | Rxjava实现异步拦截器 | 不支持 | 线程等待 | 不支持 | 主线程 |
安全拦截 | Rxjava实现异步拦截器 | 不支持 | 线程等待 | 不支持 | 主线程 |
方法调用 | 接口 | 手动拼装 | 手动拼装 | 手动拼装 | 手动拼装 |
参数获取 | Apt依赖注入,支持所有类型,不需要在Activity的onCreate 中手动调用get方法 |
参数定义在path,不利于多人协作 | Apt依赖注入,但是要手动调用get方法 | 手动调用 | 手动调用 |
结果返回 | Rxjava回调 | onActivityResult | onActivityResult | onActivityResult | onActivityResult |
Module接入不同App | 支持 | 不支持 | 支持 | 不支持 | 支持 |
其实说到底,路由的本质就是注册再转发,围绕着转发可以进行各种操作,拦截,替换,参数获取等等,其他Apt、Rxjava说到底都只是为了方便使用出现的,这里你会发现各种路由库反而为了修复各种工具带来的问题,出现了原来没有的问题,譬如DeepLinkDispatch为了解决Apt没法汇总所有Module路由,每个module都要手动注册,ARouter为了解决Apt没法汇总所有Module路由,通过类操作耗时,才出现分组的概念。
原理分析
定义路由
对应路由的定义,业界有两种做法
- 参数放在path里面
- 参数放在query里面
参数定义在path里面的做法,有不需要额外传参数的好处,但是没有那么灵活,调试起来也没有那么方便。
路由注册
AndroidManifest
里面的acitivity
声明scheme码是不安全的,所有App都可以打开这个页面,这里就产生有三种方式去注册,
- 注解产生路由表,通过
DispatchActivity
转发 -
AndroidManifest
注册,将其export=fasle
,但是再通过DispatchActivity转发Intent,天猫就是这么做的,比上面的方法的好处是路由查找都是系统调用,省掉了维护路由表的过程,但是AndroidManifest配置还是比较不方便的 - 注解自动修改AndroidManifest,这种方式可以避免路由表汇总的问题,方案是这样的,用自定义
Lint
扫描出注解相关的Activity,然后在processManifestTask后面
修改Manifest
我现在还是采用了注解,第三种不稳定
生成路由表
思路都是用Apt生成URL和activity的对应关系
Airbnb
@DeepLink("foo://example.com/deepLink/{id}")
public class MainActivity extends Activity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
生成
public final class SampleModuleLoader implements Parser {
public static final List<DeepLinkEntry> REGISTRY = Collections.unmodifiableList(Arrays.asList(
new DeepLinkEntry("foo://example.com/deepLink/{id}", DeepLinkEntry.Type.METHOD, MainActivity.class, null)
));
@Override
public DeepLinkEntry parseUri(String uri) {
for (DeepLinkEntry entry : REGISTRY) {
if (entry.matches(uri)) {
return entry;
}
}
return null;
}
}
阿里Arouter
@Route(path = "/deepLink")
public class MainActivity extends Activity {
@Autowired
String id;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
生成
public class ARouter$$Group$$m2 implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/deepLink", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/deepLink", null, null, -1, -2147483648));
}
}
Activity Router
@Router("deeplink")
public class ModuleActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
生成
public final class RouterMapping_sdk {
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("deeplink", ModuleActivity.class, null, extraTypes);
}
}
OkDeepLink
public interface SampleService {
@Path("/main")
@Activity(MainActivity.class)
void startMainActivity(@Query("key") String key);
}
生成
@After("execution(* okdeeplink.DeepLinkClient.init(..))")
public void init() {
DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
}
初始化路由表
汇总路由表
这里就要提一下使用Apt会造成每个module都要手动注册,因为APT是在javacompile任务前插入了一个task,所以只对自己的moudle处理注解
DeepLinkDispatch是这么做的
@DeepLinkModule
public class SampleModule {
}
@DeepLinkHandler({ SampleModule.class, LibraryDeepLinkModule.class })
public class DeepLinkActivity extends Activity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DeepLinkDelegate deepLinkDelegate = new DeepLinkDelegate(
new SampleModuleLoader(), new LibraryDeepLinkModuleLoader());
deepLinkDelegate.dispatchFrom(this);
finish();
}
}
ARouter是通过类查找,就比较耗时了,所以他又加入了分组的概念,按需加载
/**
* 通过指定包名,扫描包下面包含的所有的ClassName
*
* @param context U know
* @param packageName 包名
* @return 所有class的集合
*/
public static List<String> getFileNameByPackageName(Context context, String packageName) throws PackageManager.NameNotFoundException, IOException {
List<String> classNames = new ArrayList<>();
for (String path : getSourcePaths(context)) {
DexFile dexfile = null;
try {
if (path.endsWith(EXTRACTED_SUFFIX)) {
//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
dexfile = DexFile.loadDex(path, path + ".tmp", 0);
} else {
dexfile = new DexFile(path);
}
Enumeration<String> dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements()) {
String className = dexEntries.nextElement();
if (className.contains(packageName)) {
classNames.add(className);
}
}
} catch (Throwable ignore) {
Log.e("ARouter", "Scan map file in dex files made error.", ignore);
} finally {
if (null != dexfile) {
try {
dexfile.close();
} catch (Throwable ignore) {
}
}
}
}
Log.d("ARouter", "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
return classNames;
}
ActivityRouter就比较巧妙了,通过Stub项目,其他地方都是provide的,只有主工程里面用Apt生成RouterInit类,虽然还是要写module
的注解
// RouterInit
if (hasModules) {
debug("generate modules RouterInit");
generateModulesRouterInit(moduleNames);
} else if (!hasModule) {
debug("generate default RouterInit");
generateDefaultRouterInit();
}
美柚路由是通过生成每个module的路由表,然后复制到app的assets目录,运行的时候遍历asset目录,反射对应的activity
//拷贝生成的 assets/目录到打包目录
android.applicationVariants.all { variant ->
def variantName = variant.name
def variantNameCapitalized = variantName.capitalize()
def copyMetaInf = tasks.create "copyMetaInf$variantNameCapitalized", Copy
copyMetaInf.from project.fileTree(javaCompile.destinationDir)
copyMetaInf.include "assets/**"
copyMetaInf.into "build/intermediates/sourceFolderJavaResources/$variantName"
tasks.findByName("transformResourcesWithMergeJavaResFor$variantNameCapitalized").dependsOn copyMetaInf
}
Metis是一个android中解决服务发现的库,他是这么解决的,在app主工程中transfomer的时候去扫描所有modlue和jar带注解的文件去生成路由表,然后把这个java文件编译,但是这种方式需要扫描整个app会慢一点,而且手动去编译java感觉不太稳定的感觉
def destDir
List<String> classpaths = new ArrayList<>()
transformInvocation.inputs.each { input ->
input.jarInputs.each { jarInput ->
def jarName = jarInput.name
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = transformInvocation.outputProvider.getContentLocation(jarName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
classpaths.add(dest)
mAction.loadJar(new JarFile(jarInput.file), jarInput.status)
FileUtils.copyFile(jarInput.file, dest)
mProject.logger.info("scan file:\t ${jarInput.file} status:${jarInput.status}")
}
input.directoryInputs.each { dirInput ->
// 测试发现: 如果目录下的文件没有任何改变,不会进入到这个 transform
Map<File, Status> changedFiles = dirInput.changedFiles
if (changedFiles == null || changedFiles.isEmpty()) {
// clean 后进入, changed 为空
mAction.loadDirectory(dirInput.file)
mProject.logger.info("scan dir:\t ${dirInput.file}")
} else {
mAction.loadChangedFiles(changedFiles)
}
destDir = transformInvocation.outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)
classpaths.add(destDir)
FileUtils.copyDirectory(dirInput.file, destDir)
}
}
天猫 统跳协议 是最简单的,转发一下Intent就可以,但是这样就没法享受注解的好处了。
而OkDeepLink用aspectj
解决了这个问题,会自动汇总所有module的路由省略了这些多余的代码。
@After("execution(* okdeeplink.DeepLinkClient.init(..))")
public void init() {
DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
}
路由查找
路由查找就是查找路由表对应的页面,值得提起的就是因为要适应Module接入不同App,Scheme要自动适应,路由表其实是Path---》Activity,这样的话内部跳转的时候ARouterUri是没有的。而我这边是有的,我组装了一个内部的Uri,这样拦截器不会有影响。
public Request buildRequest(Intent sourceIntent) {
if (sourceIntent == null) {
return null;
}
Intent newIntent = new Intent(sourceIntent);
Uri uri = newIntent.getData();
addNewTaskFlag(newIntent);
if (uri != null) {
addBundleQuery(newIntent, uri);
Address entry = new DeepLinkClient(context).matchUrl(uri.toString());
if (entry == null || entry.getActivityClass() == null) {
return new Request(newIntent, this).setDeepLink(false);
}
newIntent.setComponent(new ComponentName(context, entry.getActivityClass()));
return new Request(newIntent, this);
}
return new Request(newIntent, this).setDeepLink(false);
}
路由分发
现在所有路由方案分发都是用Activity
做分发的,这样做会有这几个缺点
- 每次都要启动一个Activity,而Activity就算不写任何代码启动都要0.1秒
- 如果是异步等待的话,Activiy要在合适时间
finish
,不然会有一层透明的页面阻挡操作
对于第一个问题,有两个方法
- QQ音乐是把
DispatchActivity
设为SingleInstacne
,但是这样的话,动画会奇怪,堆栈也会乱掉,后退会有一层透明的页面阻挡操作 -
DispatchActivity
只在外部打开的时候调用
我选择了第二种
对于第二个问题,有两个方法
-
DispatchActivity
再把Intent转发到Service
,再finish,这种方法唯一的缺陷是拦截器里面的context是Servcie的activity,就没发再拦截器里面弹出对话框了。 -
DispatchActivity
在打开和错误的时候finish
,如果activity
已经finish了,就用application的context去转发路由
我选择了第二种
public void dispatchFrom(Intent intent) {
new DeepLinkClient(this)
.buildRequest(intent)
.dispatch()
.subscribe(new Subscriber<Request>() {
@Override
public void onCompleted() {
finish();
}
@Override
public void onError(Throwable e) {
finish();
}
@Override
public void onNext(Request request) {
Intent dispatchIntent = request.getIntent();
startActivity(dispatchIntent);
}
});
}
其实处理透明Activity阻挡操作可以采用取消所有事件变成无感页面的方法
我找到一种方式解决这个问题解决透明Activity点击不影响用户操作
结果返回
这里我封装了一个库RxActivityResult
去捕获onActivityResult
,这样能保正流式调用
譬如拍照可以这样写,先定义一个接口
public interface ImageCaptureService {
@Action(MediaStore.ACTION_IMAGE_CAPTURE)
Observable<Response> startImageCapture();
}
然后这样调用
public class MainActivity extends AppCompatActivity {
@Service
ImageCaptureService imageCaptureService;
public void captureImage(){
imageCaptureService
.startImageCapture()
.subscribe(new Action1<Response>() {
@Override
public void call(Response response) {
Intent data = response.getData();
int resultCode = response.getResultCode();
if (resultCode == RESULT_OK) {
Bitmap imageBitmap = (Bitmap) data.getExtras().get("data");
}
}
});
}
}
}
是不是很简单,原理是这样的,通过封装一个RxResultHoldFragment去处理onActivityResult
private IActivityObservable buildActivityObservable() {
T target = targetWeak.get();
if (target instanceof FragmentActivity) {
FragmentActivity activity = (FragmentActivity) target;
android.support.v4.app.FragmentManager fragmentManager = activity.getSupportFragmentManager();
IActivityObservable activityObservable = RxResultHoldFragmentV4.getHoldFragment(fragmentManager);
return activityObservable;
}
if (target instanceof Activity) {
Activity activity = (Activity) target;
FragmentManager fragmentManager = activity.getFragmentManager();
IActivityObservable activityObservable = RxResultHoldFragment.getHoldFragment(fragmentManager);
return activityObservable;
}
if (target instanceof Context) {
final Context context = (Context) target;
IActivityObservable activityObservable = new RxResultHoldContext(context);
return activityObservable;
}
if (target instanceof Fragment) {
Fragment fragment = (Fragment) target;
FragmentManager fragmentManager = fragment.getFragmentManager();
if (fragmentManager != null) {
IActivityObservable activityObservable = RxResultHoldFragment.getHoldFragment(fragmentManager);
return activityObservable;
}
}
if (target instanceof android.support.v4.app.Fragment) {
android.support.v4.app.Fragment fragment = (android.support.v4.app.Fragment) target;
android.support.v4.app.FragmentManager fragmentManager = fragment.getFragmentManager();
if (fragmentManager != null) {
IActivityObservable activityObservable = RxResultHoldFragmentV4.getHoldFragment(fragmentManager);
return activityObservable;
}
}
return new RxResultHoldEmpty();
}
动态拦截
拦截器是重中之重,有了拦截器可以做好多事情,可以说之所以要做页面路由,就是为了要实现拦截器。ARouter是用线程等待实现的,但是现在有Rxjava了,可以实现更优美的方式。
先来看一下我做的拦截器的效果.
@Intercept(path = "/second")
public class SecondInterceptor extends Interceptor {
@Override
public void intercept(final Call call) {
Request request = call.getRequest();
final Intent intent = request.getIntent();
Context context = request.getContext();
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("Intercept\n");
stringBuffer.append("URL: " + request.getUrl() + "\n");
AlertDialog.Builder builder = new AlertDialog.Builder(context,R.style.Theme_AppCompat_Dialog_Alert);
builder.setTitle("Notice");
builder.setMessage(stringBuffer);
builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
call.cancel();
}
});
builder.setPositiveButton("ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
intent.putExtra("key1", "value3");
call.proceed();
}
});
builder.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
call.cancel();
}
});
builder.show();
}
}
是不是很简单,参考了部分OkHttp
的实现思路,加入Rxjava,实现异步拦截。
首先将请求转换成责任链模式RealCallChain
,RealCallChain的call方法实际不会执行路由跳转,只有Interceptor
里面调用了call.proceed或者call.cancel才会执行.
private Observable<Request> buildRequest() {
RealCallChain chain = new RealCallChain(interceptors, 0, request);
chain.setTimeout(interceptTimeOut);
chain.call();
return chain
.getRequestObservable()
.map(new Func1<Request, Request>() {
@Override
public Request call(Request request) {
if (interceptors != null) {
for (Interceptor interceptor : interceptors) {
interceptor.onCall(request);
}
}
return request;
}
});
}
接着处理异步的问题,这里用到了Rxjava的AsyncSubject和BehaviorSubject,
- AsyncSubject具有仅释放Observable释放的最后一个数据的特性,作为路由请求的发送器
- BehaviorSubject具有一开始就会释放最近释放的数据的特性,作为路由拦截器的发送器
具体实现看核心代码
@Override
public void proceed() {
if (index >= interceptors.size()) {
realCall();
return;
}
final Interceptor interceptor = interceptors.get(index);
Observable
.just(1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<Integer>() {
@Override
public void call(Integer integer) {
interceptor.intercept(RealCallChain.this);
}
});
interceptorSubject.onNext(interceptor);
index = index + 1;
}
方法调用
大部分路由库都是手动拼参数调用路由的,这里模仿了Retrofit
接口式调用,受了LiteRouter的启发,不过Retrofit
使用了动态代理,我使用的Apt
没有性能损耗。
通过Apt生成每个接口的实际方法
譬如把SecondService
接口
public interface SecondService {
@Path("/second")
@Activity(SecondActivity.class)
void startSecondActivity();
}
生成
@Aspect
public final class SecondService$$Provider implements SecondService {
public DeepLinkClient deepLinkClient;
public SecondService$$Provider(DeepLinkClient deepLinkClient) {
this.deepLinkClient= deepLinkClient;
}
@Override
public void startSecondActivity() {
Intent intent = new Intent();
intent.setData(Uri.parse("app://deeplink/second"));
Request request = deepLinkClient.buildRequest(intent);
if (request != null) {
request.start();
}
}
@Around("execution(* okdeeplink.DeepLinkClient.build(..))")
public Object aroundBuildMethod(ProceedingJoinPoint joinPoint) throws Throwable {
DeepLinkClient target = (DeepLinkClient)joinPoint.getTarget();
if (joinPoint.getArgs() == null || joinPoint.getArgs().length != 1) {
return joinPoint.proceed();
}
Object arg = joinPoint.getArgs()[0];
if (arg instanceof Class) {
Class buildClass = (Class) arg;
if (buildClass.isAssignableFrom(getClass())) {
return new SecondService$$Provider(target);
}
}
return joinPoint.proceed();
}
}
然后调用
SecondService secondServicenew = DeepLinkClient(target).build(SecondService.class);
SecondService就生成了。
为了调用方便,直接在Activity
或者fragement
写这段代码,sampleServive就自动生成了
@Service
SampleService sampleService;
但是如果用到MVP
模式,不是在Activity
里面调用路由,后面会支持在这些类里面自动注入SampleService,现在先用java代码build
参数获取
大部分路由库都是手动获取参数的,这样还要传入参数key比较麻烦,有三种做法
- Hook掉
Instrumentation
的newActivity
方法,注入参数 - 注册
ActivityLifecycleCallbacks
方法,注入参数 -
Apt
生成注入代码,onCreate
的时候bind一下
Hook掉Instrumentation
的newActivity
方法是这么实现的
@Deprecated
public class InstrumentationHook extends Instrumentation {
/**
* Hook the instrumentation's newActivity, inject
* <p>
* Perform instantiation of the process's {@link Activity} object. The
* default implementation provides the normal system behavior.
*
* @param cl The ClassLoader with which to instantiate the object.
* @param className The name of the class implementing the Activity
* object.
* @param intent The Intent object that specified the activity class being
* instantiated.
* @return The newly instantiated Activity object.
*/
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
// return (Activity)cl.loadClass(className).newInstance();
Class<?> targetActivity = cl.loadClass(className);
Object instanceOfTarget = targetActivity.newInstance();
if (ARouter.canAutoInject()) {
String[] autoInjectParams = intent.getStringArrayExtra(ARouter.AUTO_INJECT);
if (null != autoInjectParams && autoInjectParams.length > 0) {
for (String paramsName : autoInjectParams) {
Object value = intent.getExtras().get(TextUtils.getLeft(paramsName));
if (null != value) {
try {
Field injectField = targetActivity.getDeclaredField(TextUtils.getLeft(paramsName));
injectField.setAccessible(true);
injectField.set(instanceOfTarget, value);
} catch (Exception e) {
ARouter.logger.error(Consts.TAG, "Inject values for activity error! [" + e.getMessage() + "]");
}
}
}
}
}
return (Activity) instanceOfTarget;
}
}
业界的统一做法都是用apt,其他方式不稳定,ARouter、androidannotations、Jet, 思路都是一样的,这里拿ARouter的代码说明一下是怎么实现的
用Autowired
生成Test1Activity$$ARouter$$Autowired类,用inject方法找到AutowiredServiceImpl
方法,AutowiredServiceImpl
调用到Test1Activity$$ARouter$$Autowired
@Route(path = "/test/activity1")
public class Test1Activity extends AppCompatActivity {
@Autowired
String name;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test1);
ARouter.getInstance().inject(this);
}
}
@Route(path = "/arouter/service/autowired")
public class AutowiredServiceImpl implements AutowiredService {
private LruCache<String, ISyringe> classCache;
private List<String> blackList;
@Override
public void init(Context context) {
classCache = new LruCache<>(66);
blackList = new ArrayList<>();
}
@Override
public void autowire(Object instance) {
String className = instance.getClass().getName();
try {
if (!blackList.contains(className)) {
ISyringe autowiredHelper = classCache.get(className);
if (null == autowiredHelper) { // No cache.
autowiredHelper = (ISyringe) Class.forName(instance.getClass().getName() + SUFFIX_AUTOWIRED).getConstructor().newInstance();
}
autowiredHelper.inject(instance);
classCache.put(className, autowiredHelper);
}
} catch (Exception ex) {
blackList.add(className); // This instance need not autowired.
}
}
}
public class Test1Activity$$ARouter$$Autowired implements ISyringe {
@Override
public void inject(Object target) {
Test1Activity substitute = (Test1Activity)target;
substitute.name = substitute.getIntent().getStringExtra("name");
}
}
OkDeepLink这里模仿了ARouter,不过支持类型更全一些,支持Bundle支持的所有类型,而且不需要在Acitivty的onCreate
调用获取代码。
通过Apt把这段代码
public class MainActivity extends AppCompatActivity {
@Query("key")
String key;
}
生成
@Aspect
public class MainActivity$$Injector {
@Around("execution(* okdeeplink.sample.MainActivity.onCreate(..))")
public void onCreate(ProceedingJoinPoint joinPoint) throws Throwable {
MainActivity target = (MainActivity)joinPoint.getTarget();
Bundle dataBundle = new Bundle();
Bundle saveBundle = (Bundle)joinPoint.getArgs()[0];
Bundle targetBundle = BundleCompact.getSupportBundle(target);
if(targetBundle != null) {
dataBundle.putAll(targetBundle);
}
if(saveBundle != null) {
dataBundle.putAll(saveBundle);
}
try {
target.key= BundleCompact.getValue(dataBundle,"key",String.class);
} catch (Exception e) {
e.printStackTrace();
}
joinPoint.proceed();
}
@After("execution(* okdeeplink.sample.MainActivity.onSaveInstanceState(..))")
public void onSaveInstanceState(JoinPoint joinPoint) throws Throwable {
MainActivity target = (MainActivity)joinPoint.getTarget();
Bundle saveBundle = (Bundle)joinPoint.getArgs()[0];
Intent intent = new Intent();
intent.putExtra("key",target.key);
saveBundle.putAll(intent.getExtras());
}
@Around("execution(* okdeeplink.sample.MainActivity.onNewIntent(..))")
public void onNewIntent(ProceedingJoinPoint joinPoint) throws Throwable {
MainActivity target = (MainActivity)joinPoint.getTarget();
Intent targetIntent = (Intent)joinPoint.getArgs()[0];
Bundle dataBundle = targetIntent.getExtras();
try {
target.key= BundleCompact.getValue(dataBundle,"key",String.class);
} catch (Exception e) {
e.printStackTrace();
}
joinPoint.proceed();
}
}
Module接入不同App
这里是参考ARouter把path作为key对应activity
,这样接入到其他app中,就自动替换了scheme码
了
DeepLinkClient.addAddress(new Address("/main", MainActivity.class));
安全
现在有好多人用脚本来打开App,然后干坏事,其实时可以用路由来屏蔽掉.
有三种方法供君选择,不同方法适合不同场景
签名屏蔽
就是把所有参数加密成一个数据作为sign参数,然后比对校验,但是这要求加密方法不变,要不然升级了以前的app就打不开了
adb打开屏蔽
在android5.1手机上,用adb打开的app它的mReferrer为空
public boolean isStartByAdb(android.app.Activity activity){
if (Build.VERSION.SDK_INT >= 22) {
android.net.Uri uri = ActivityCompat.getReferrer(activity);
return uri == null | TextUtils.isEmpty(uri.toString()) ;
}
return false;
}
包名过滤
在Android 4.4手机上, 写了android:ssp的组件,只有特定应用可以打开
<activity
android:name="okdeeplink.DeepLinkActivity"
android:noHistory="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:ssp="com.app.test"
android:host="app"
android:scheme="odl" />
</intent-filter>
</activity>
这三种方法,比较适合的还是签名校验为主,adb过滤为副
如何解决路由造成的Activity堆栈错乱的问题
activity的launchMode使用不当会照成闪屏页面打开多次的问题,可以参考我这篇文章。
未来展望
路由是一个基础模块,技术难度虽然不是很大,但是如果每个开发都重新踩一遍,性价比就比较低,我希望能把路由相关的所有链路都替你弄好,你可以留着时间去干其他更重要的事情,譬如陪陪家人,逗逗狗什么的。
接下来我会在这几个方面努力,把整条链路补全。
- 做一个像
Swagger
的平台,支持一键导出所有路由、二维码打开路由 - 注解修改AndroidManifest,不再需要路由表
-
支持路由方法接收器,Url直接打开某个方法,不再局限已实现Activity
如果大家有意见,欢迎联系我kingofzqj@gmail.com
参考文献
业界做法
设计方案
- UrlRouter路由框架的设计
- 移动端路由层设计
- 客户端路由动态配置
- 移动端基于动态路由的架构设计
- Android组件化通信(多进程)
- iOS 组件化 —— 路由设计思路分析
- QQ音乐首页Activity的单例实现
个人开发
- LiteRouter 模仿retrofit,各个业务分根据需求约定好接口,就像一份接口文档一样
- ActivityRouter
- ActivityRouter2
- AndRouter
- Router
- Router2
- router-android
安全讨论
- 如何在Activity中获取调用者 讨论了android里面原生支持找到路由来源的可能性,分析了referrer是如何产生的
-
LauncherFrom
提供了一种hook activitythread找到launchedFromPackage的方法,不过也只支持5.0以上 -
高效过滤Intents
只有包含特定Package URL的 intent 才会唤起页面