给 Retrofit 嵌套动态代理,高效处理运营打点难题
本文由 黄光华 授权发表
原文链接:
https://www.jianshu.com/p/145542aedd78
需求背景
相信大部分朋友都经历过,运营突然来要求,要给某部分接口带上某个参数(这个参数可能是from,表示当前在哪个页面;或者是duration,表示当前界面停留了多久)。这个时候,最直接的做法就是,直接加呗~ 有些接口还被多个界面调用,要改代码的界面可能是十多个,也可能是大几十个。
//举例子,帖子点赞,原本的请求调用: ApiService.getInstance().likePost(likeType, postId); //直接在调用方法时加 from 参数: ApiService.getInstance().likePost(likeType, postId, from);
而我收到的需求则是要带上当前页面和上一级页面。。。这个需求,按常规做法,在各个Activity间的intent都要传入上一级Activity的信息。这个代码量就更大了,而且代码会很累赘。
这时候,我的上级给了我提示,可以试下多重动态代理。之前我也考虑过这个需求适合用动态代理做,但是我知道Retrofit本身已经用了,我没想到还可以多重动态代理。接着就试了一下,还真的OK,果然还是大佬牛啊~!
给Retrofit嵌套一层动态代理后,我们项目中调用请求接口的地方不需要修改代码了,不用每处请求都手动添加上 from 参数,因为在这个自定义的动态代理工作时,已经帮我们统一加上了这个 from 参数。
相关知识
动态代理:方便的对被代理类的方法进行统一处理。
反射:一种能够在程序运行时动态访问、修改某个类中任意属性(状态)和方法(行为)的机制(包括private实例和方法)。
阅读本文需要你对动态代理和反射有一定的理解,不然建议先熟悉一下相关知识点。
Retrofit嵌套动态代理步骤
1. 给 Retrofit.create (final Class
service)方法的返回值,再加上自定义的动态代理:
/** * 获取对应的 Service */ T create(Class service) { // Retrofit 的代理 T retrofitProxy = retrofit.create(service); //再添加一层自定义的代理。 T customProxy = addCustomProxy(retrofitProxy); //返回这个嵌套的代理 return customProxy; }
/** * 嵌套添加动态代理 * @param target 被代理的对象 * @return 返回嵌套动态代理之后的对象 */ public T addCustomProxy(T target) { CustomProxy customProxy = new CustomProxy(); return (T) customProxy.newProxy(target); }
2. 在原来的请求接口的基础上,加上带运营打点所需要的参数(在本例就是 from 参数),如果请求参数是每个值分开传的才需要这一步( 例如这里的 likePost 接口),对于请求参数是一个bean类或者Map,不需要这一步( 例如这里的 savePost 接口)。
/** * 广场发帖 */ @FormUrlEncoded @POST("square/post/save") Call<RootBean> savePost(@Body EditPostRequest editPostRequest); /** * 帖子点赞 */ @FormUrlEncoded @POST("square/post/like") Call<RootBean> likePost(@Field("likeType") int likeType, @Field("postId") long postId); /** * 帖子点赞 * 带"from"参数的版本 * 不要删除,动态代理会调用{@link CustomProxy} */ @FormUrlEncoded @POST("square/post/like") Call<RootBean> likePost(@Field("likeType") int likeType, @Field("postId") long postId, @Field("from") String from);
PS:注释说明“不要删除,动态代理会调用”建议一定不能省~~因为动态代理的方法在IDE中是索引不到的,同事甚至自己很容易删掉,编译是不会报错的。
3. 在自定义的代理类里,真正执行统一加参数的操作
(这里还是以加 from 做例子)
/** * 嵌套添加动态代理 * 简例:https://blog.csdn.net/zhenghuangyu/article/details/102808338 */ public static class CustomProxy implements InvocationHandler { //被代理对象,在这里就是 Retrofit.create(service) 的返回值 private Object mTarget; @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String from = "testFrom"; final String methodName = method.getName(); switch (methodName) { case "savePost": { //形参是一个bean类,用这种方式 //获取第一个请求参数args[0],这是我们定义该接口形参时的bean类 EditPostRequest editPostRequest = (EditPostRequest) args[0]; //以变量形式设置 editPostRequest.setFrom(); break; } case "likePost": { //形参是一个个值的形式,用这种方式 //将参数长度+1,作为新的参数数组 args = Arrays.copyOf(args, (args.length + 1)); //在新的参数数组末端加上 from args[args.length - 1] = from; //为了调用带 from 版本的方法,构造新的形参 Class[] newParams = Arrays.copyOf(method.getParameterTypes(), (method.getParameterTypes().length + 1)); //新的形参里,最后一个参数 from 是String类型的,这个必须声明,才能准确调用反射 newParams[newParams.length - 1] = String.class; //找出新method对象,就是带 from 版本的那个方法 method = mTarget.getClass().getDeclaredMethod(method.getName(), newParams); break; } } //正式执行方法 return method.invoke(mTarget, args); } //在这里嵌套外层的动态代理 public Object newProxy(Object target) { this.mTarget = target; return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } }
嗯,这样就完成了为多个接口添加参数的需求。本来少说也要修改几十个地方,现在简单优雅的解决了。更重要的是,不需要机械地添加累赘的代码,用工程化的方案解决问题。
文章重点是多重动态代理。至于我的需求里,怎么优雅地处理当前和上一级Activity的路径,我想到的方法有两种:1.用AMS获取Activity栈 2.用ActivityLifecycle。
我用的是第二种,并通过一个Stack对象,自行记录Activity的入栈出栈。不过这个不是文章重点,不详细展开了。放上简单代码:
/** * 要记录最新的两个页面,用栈操作 */ private Stack tagsRecords = new Stack(); /** * 标签入栈 */ public void pushTagRecord(String tag) { tagsRecords.push(tag); } /** * 标签出栈 */ public void popTagRecord() { tagsRecords.pop(); } //注册LifeCycle监听,在这里完成界面对应tag的出栈入栈 Application.ActivityLifecycleCallbacks lifecycleCallbacks = new Application.ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { //新建界面,入栈 pushTagRecord(activity.getLocalClassName()); } @Override public void onActivityDestroyed(Activity activity) { //界面销毁,出栈 popTagRecord(); } }
然后在项目Application类的初始化方法中注册lifecycle
registerActivityLifecycleCallbacks(lifecycleCallbacks)
嗯,通过这种用lifecycle配合栈结构的方式,记录页面访问路径,就避免了在每处 startActivity()的intent里传递参数。而且这种方法比AMS获取Activity栈的方式更灵活。例如我的实际需求就是,特定的几个Activity才算有效路径,在Activity入栈出栈时,我可以做一层判断过滤,而AMS我是控制不了的。