Android 路由设计最佳实践详解手机开发
当前Android的路由库实在太多了,刚开始的时候想为什么要用路由表的库,用Android原生的Scheme码不就好了,又不像iOS只能类依赖,后面越深入就越发现当时想的太简单了,后面看到Retrofit和OKHttp,才想到页面请求本质和网络请求不是一样吗,终于业界最简单高效的路由方案1.0出来了
OkDeepLink
什么是路由根据路由表将页面请求分发到指定页面
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,不耗性能,参数调用不再是问题 HookOnActivityResult,支持RxJava响应式调用,不再需要进行requestCode判断 参数依赖注入,自动保存,不再需要手动写onSaveInstance、onCreate(SaveInstace)、onNewIntent(Intent)、getQueryParamerApt依赖注入,支持所有类型,不需要在Activity的onCreate中手动调用get方法 参数定义在path,不利于多人协作 Apt依赖注入,但是要手动调用get方法
其实说到底,路由的本质就是注册再转发,围绕着转发可以进行各种操作,拦截,替换,参数获取等等,其他Apt、Rxjava说到底都只是为了方便使用出现的,这里你会发现各种路由库反而为了修复各种工具带来的问题,出现了原来没有的问题,譬如DeepLinkDispatch为了解决Apt没法汇总所有Module路由,每个module都要手动注册,ARouter为了解决Apt没法汇总所有Module路由,通过类操作耗时,才出现分组的概念。
原理流程图我这边是完全按照URL规范了,这里要说一下,现在好多方法是把参数定义在path里面的,虽然这样做,有不需要额外传参数的好处,但是这样路由就没有那么灵活,调试起来就没有那么方便了。
建议有好几款app的公司,host都一样,只有scheme不一样,这样只要替换Scheme就能实现降级,维护也简单。
AndroidManifest里面的acitivity声明scheme码是不安全的,所有App都可以打开这个页面,这里就产生有两种方式去注册,
注解产生路由表,通过DispatchActivity转发 AndroidManifest注册,将其export=fasle,但是再通过DispatchActivity转发Intent,天猫就是这么做的,比上面的方法的好处是路由查找都是系统调用,省掉了维护路由表的过程,但是AndroidManifest配置还是比较不方便的我现在还是采用了注解,后面我会结合两种方法,将注解自动修改AndroidManifest,对于接入方是没有变动的,方法已经找到了,用自定义Lint扫描出注解相关的Activity,然后用processManifestTask修改Manifest,有个demo了,后面会接入。
生成路由表譬如通过Apt把这段代码
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都要手动注册
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(); }
天猫 统跳协议 是最简单的,转发一下Intent就可以,但是这样就没法享受注解的好处了。
而我用aspectj解决了这个问题,会自动汇总所有module的路由省略了这些多余的代码,或者有谁知道用Apt自生怎么解决,请联系我一下。
@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堆栈没有采用这种方案
getwindow().addflags( windowmanager.layoutparams.flag_not_focusable | windowmanager.layoutparams.flag_not_touch_modal | windowmanager.layoutparams.flag_not_touchable);
这里我封装了一个库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比较麻烦,这里模仿了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使用不当会照成闪屏页面打开多次的问题,可以参考我这篇文章。
路由是一个基础模块,技术难度虽然不是很大,但是如果每个开发都重新踩一遍,性价比就比较低,我希望能把路由相关的所有链路都替你弄好,你可以留着时间去干其他更重要的事情,譬如陪陪家人,逗逗狗什么的。
接下来我会在这几个方面努力,把整条链路补全。
如果大家有意见,[email protected]
相关文章
- Android 代码混淆规则
- 代码加密 android,Android 开发怎样做代码加密或混淆「建议收藏」
- 史上最全的Java&Android面试题搜集整理
- android开发笔记之 Android代码混淆打包
- android进阶之了解Android系统与开机过程
- Android抽象布局——include、merge 、ViewStub[通俗易懂]
- android vlc 中文字幕,解决Android版vlc中文乱码问题
- 为应用提供 64 位原生支持 | Android 中文教学视频
- Android NDK 开发 | CMake 使用手册 - 初见篇
- android toast显示时间,Android Toast自定义显示时间「建议收藏」
- android登录注册跳转的代码_Android开发代码
- Android 编译_android线程
- android点击按钮打开相册,打开相机的代码[通俗易懂]
- eclipse中android开发_Android开发教程
- Android 数据库加密 android-database-sqlcipher 开源版本编译过程
- Android进程间通信(一):基础介绍
- Android studio更新后出现警告:Warning:The `android.dexOptions.incremental` property is deprecated and it has
- 【错误记录】Flutter / Android 报错 ( AAPT: error: attribute android:requestLegacyExternalStorage not found )
- android Universal Image Loader for Android 说明文档 (1)详解手机开发
- Android Studio 入门级教程详解手机开发
- Android中的ProgressBar的android:indeterminate详解手机开发
- Rowhammer:针对物理内存的攻击可以取得 Android 设备的 root 权限
- 【硬创邦】跟hoowa学做智能路由(十三):网络音箱之Android篇
- Android程序退出完美解决方案兼容所有SDK
- 解析Android中如何做到Service被关闭后又自动启动的实现方法
- Android编程中避免内存泄露的方法总结