阅读 75

Android进阶宝典 -- IOC依赖注入框架原理

在# 动态代理设计模式实现Retrofit框架这篇文章当中,主要是介绍了动态代理的使用,那么动态代理使用的场景还有哪些呢?

(1)利用动态代理,能够实现在方法执行前后加入额外的逻辑处理;例如Hook Activity的启动流程,常用在插件化的框架中,详情可见Android进阶宝典 -- 插件化2(Hook启动插件中四大组件)

(2)利用动态代理,能够实现解耦,使得调用层与实现层分离,例如Retrofit框架;

(3)动态代理不需要接口的实现类,常用于IPC进程间通信;

(4)动态代理可以解决程序的执行流程,例如反射调用某个方法,需要传入一个接口实现类,就会使用到动态代理;也是本篇文章着重介绍的。

1 动态代理深入

首先简单看下一个动态代理的例子

private fun testProxy() {     val proxy = Proxy.newProxyInstance(         classLoader,         arrayOf(IProxyInterface::class.java)     ) { obj, method, args ->         Log.e("TAG", "方法调用前------")         return@newProxyInstance handleMethod()     } as IProxyInterface     /**调用方法*/     val result = proxy.getName()     Log.e("TAG", "result==>$result") } private fun handleMethod(): Any? {     Log.e("TAG", "开始执行方法--")     return "小明~" } 复制代码

当通过Proxy的newProxyInstance方法创建一个IProxyInterface的代理对象的时候,其实这个接口并没有任何实现类

interface IProxyInterface {     fun getName(): String } 复制代码

只有一个getName方法,那么当这个代理对象调用getName()方法的时候,就会先走到InvocationHandler的方法体内部,handleMethod方法我们可以认为是接口方法的实现,所以在方法实现之前,可以做一些前置的操作。

2022-11-26 20:25:07.960 403-403/com.lay.mvi E/TAG: 方法调用前------ 2022-11-26 20:25:07.960 403-403/com.lay.mvi E/TAG: 开始执行方法-- 2022-11-26 20:25:07.960 403-403/com.lay.mvi E/TAG: result==>小明~ 复制代码

1.1 $Proxy0

所以,当我们创建一个接口之后,并不需要实例化该接口,而是采用动态代理的方式生成一个代理对象,从而实现调用层与实现层的分离,这样也是解耦的一种方式。

那么生成的IProxyInterface代理对象是接口吗?肯定不是,因为接口不可实例化,那么生成的对象是什么呢?

image.png

通过断点,我们发现这个对象是$Proxy0,那么这个对象是怎么生成的呢?

public static Object newProxyInstance(ClassLoader loader,                                       Class<?>[] interfaces,                                       InvocationHandler h)     throws IllegalArgumentException {     Objects.requireNonNull(h);     final Class<?>[] intfs = interfaces.clone();         /*      * Look up or generate the designated proxy class.      */     Class<?> cl = getProxyClass0(loader, intfs);     /*      * Invoke its constructor with the designated invocation handler.      */     try {             final Constructor<?> cons = cl.getConstructor(constructorParams);         final InvocationHandler ih = h;         if (!Modifier.isPublic(cl.getModifiers())) {             // BEGIN Android-removed: Excluded AccessController.doPrivileged call.             /*             AccessController.doPrivileged(new PrivilegedAction<Void>() {                 public Void run() {                     cons.setAccessible(true);                     return null;                 }             });             */             cons.setAccessible(true);             // END Android-removed: Excluded AccessController.doPrivileged call.         }         return cons.newInstance(new Object[]{h});     } catch (IllegalAccessException|InstantiationException e) {         throw new InternalError(e.toString(), e);     } catch (InvocationTargetException e) {         Throwable t = e.getCause();         if (t instanceof RuntimeException) {             throw (RuntimeException) t;         } else {             throw new InternalError(t.toString(), t);         }     } catch (NoSuchMethodException e) {         throw new InternalError(e.toString(), e);     } } 复制代码

其实我们也能够看到,通过getProxyClass0方法目的就是查找或者生成一个代理的Class对象,并通过反射创建一个实体类,其实就是$Proxy0

那么调用getName方法,其实就是调用$Proxy0的getName方法,最终内部就是调用了InvocationHandler的invoke方法。

2 动态代理实现Xutils

如果没有使用过ViewBinding的伙伴,可能在项目中大多都是用ButterKnife这些注入框架,那么对于这类依赖注入工具,我们该如何亲自实现呢?这就使用到了注解配合动态代理,这里我们先忘记ViewBinding。

2.1 Android属性注入

在日常的开发过程中,我们经常需要通过findViewById获取组件,并设置点击事件;或者为页面设置一个layout布局,每个页面几乎都需要设置一番,那么通过事件注入,就可以大大简化我们的流程。

/**运行时注解,放在类上使用*/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface setContentView {     /**布局id*/     int value(); } 复制代码

那么我们以布局注入为例,介绍一下事件是如何被注入进去的。

@RequiresApi(api = Build.VERSION_CODES.N) public class InjectUtils2 {     public static void inject(Context context) {         injectContentView(context);     }     private static void injectContentView(Context context) {         /**获取布局id*/         Class<?> aClass = context.getClass();         try {             setContentView setContentView = aClass.getDeclaredAnnotation(setContentView.class);             if (setContentView == null) {                 return;             }             int layoutId = setContentView.value();             /**反射获取Activity的setContentView方法*/             Method setContentViewMethod = aClass.getMethod("setContentView", int.class);             setContentViewMethod.setAccessible(true);             setContentViewMethod.invoke(context, layoutId);         } catch (Exception e) {         }     } } 复制代码

这里我们采用反射的方式,判断类上方是否存在setContentView注解,如果存在,那么就反射调用Activity的setContentView方法。

这里为什么使用Java,是因为在反射的时候,如果反射的源码为Java代码,最好使用Java,否则与Kotlin的类型不匹配会导致反射失败。

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface viewId {     int value(); } 复制代码

对于控件的注入,类似于ViewBinding

private static void injectView(Context context) {     Class<?> aClass = context.getClass();     try {         Field[] declaredFields = aClass.getDeclaredFields();         if (declaredFields.length == 0) {             return;         }         for (Field field : declaredFields) {             /**判断当前属性是否包含viewId注解*/             viewId viewId = field.getDeclaredAnnotation(viewId.class);             if (viewId != null) {                 /**获取id值*/                 int id = viewId.value();                 /**执行findViewById操作*/                 Method findViewById = aClass.getMethod("findViewById", int.class);                 findViewById.setAccessible(true);                 field.setAccessible(true);                 field.set(context, findViewById.invoke(context, id));             }         }     } catch (Exception e) {         Log.e("TAG","exp===>"+e.getMessage());     } } 复制代码

具体的使用如下

@setContentView(R.layout.activity_splash) class SplashActivity : BaseActivity() {     @viewId(R.id.tv_music)     private var tv_music: TextView? = null     override fun initView() {         JUCTest.test()         Singleton.getInstance().increment()         testProxy()         tv_music?.setOnClickListener {             Toast.makeText(this, "点击了", Toast.LENGTH_SHORT).show()         }     } 复制代码

2.2 动态代理实现事件注入

前面我们介绍了布局的注入以及属性的注入,其实这两个事件还是很简单的,通过反射赋值即可。但是如果是一个点击事件,就不是单纯的赋值了,就需要使用到动态代理了。

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface OnClick {     int[] value(); } 复制代码

对于Android的事件来说有很多种,像点击事件、长按事件、滑动事件等等,如果只是像上面的注解一样,只有一个id,显然是不够的。

拿点击事件来说,需要三要素:setOnClickListener、OnClickListener对象、回调onClick

tv_music?.setOnClickListener {     Toast.makeText(this, "点击了", Toast.LENGTH_SHORT).show() } 复制代码

那么这些可以放在注解中,在调用的时候传入,但是对于用户来说,肯定只需要传入id就可以了,而不需要在外层传一堆乱七八糟的东西

@OnClick(value = [R.id.tv_music],function="setOnClickListener",......) private fun clickButton() { } 复制代码

那么这些操作就需要在注解内部处理。

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface EventBase {     /**设置监听的类型,例如setOnClickListener、setOnTouchListener......*/     String listenerSetter();     /**匿名内部类类型,例如OnClickListener.class*/     Class<?> listenerType();     /**回调方法*/     String callbackMethod(); } 复制代码

这里首先定义了一个注解的基类,里面定义了事件的三要素,目的就是给上层注解提供实现类似于继承的方式

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @EventBase(listenerSetter = "setOnClickListener", listenerType = View.OnClickListener.class, callbackMethod = "onClick") public @interface OnClick {     int[] value(); } 复制代码

接下来就可以通过反射获取方法上的注解

private static void injectClick(Context context) {     Class<?> aClass = context.getClass();     try {         Method[] methods = aClass.getDeclaredMethods();         if (methods.length == 0) {             return;         }         /**处理单击事件*/         for (Method method : methods) {             Annotation[] annotations = method.getDeclaredAnnotations();             if (annotations.length > 0) {                 for (Annotation annotation : annotations) {                     EventBase eventBase = annotation.annotationType().getAnnotation(EventBase.class);                     if (eventBase == null) {                         continue;                     }                     /**拿到事件三要素*/                     String listenerSetter = eventBase.listenerSetter();                     Class<?> listenerType = eventBase.listenerType();                     String callbackMethod = eventBase.callbackMethod();                     /**拿到注解中传入的id*/                     Method values = annotation.getClass().getDeclaredMethod("values");                     values.setAccessible(true);                     int[] componentIds = (int[]) values.invoke(annotation);                     for (int id : componentIds) {                         /**反射获取到这个id对应的组件*/                         Method findViewById = aClass.getMethod("findViewById", int.class);                         findViewById.setAccessible(true);                         View view = (View) findViewById.invoke(context, id);                         /**反射获取事件方法,注意这里类型是动态的*/                         Method setListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);                         /**执行这个事件*/                         setListenerMethod.setAccessible(true);                         setListenerMethod.invoke(view, buildProxyInstance(listenerType, context, method));                     }                 }             }         }     } catch (Exception e) {         Log.e("TAG", "injectClick exp===>" + e.getMessage());     } } /**  * 根据listener类型创建动态代理对象  *   */ private static Object buildProxyInstance(Class<?> listenerType, Context context, Method callbackMethod) {     return Proxy.newProxyInstance(listenerType.getClassLoader(), new Class<?>[]{listenerType}, new InvocationHandler() {         @Override         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {             Log.e("TAG", "调用前处理--");             callbackMethod.setAccessible(true);             return callbackMethod.invoke(context);         }     }); } 复制代码

这里通过反射获取时,完全是根据listenerSetter属性动态查找,而不是写死一个方法,这种方式使用起来具备扩展性。

public interface OnClickListener {     /**      * Called when a view has been clicked.      *      * @param v The view that was clicked.      */     void onClick(View v); } 复制代码

因为这里采用的是动态代理的方式,动态创建一个OnClickListener对象,并作为setOnclickListener方法的参数传入进去,所以当onClick执行的时候,会走到InvocationHandler的invoke方法中,在这里执行了应用层的方法。

@OnClick(values = [R.id.tv_music]) private fun clickButton() {     Toast.makeText(this, "点击了", Toast.LENGTH_SHORT).show() } 复制代码

2.3 组件化依赖注入

如果在项目中使用到组件化的伙伴可能有遇到这样的问题,两个模块需要通信,通常采用的是模块依赖直接通信

image.png

这种方式其实是不可行的,因为不管是模块化还是组件化,这种方式会使得两个模块间耦合非常严重,两个模块应该相对独立,并向下继承,所以在下层需要有一个module专门负责依赖注入。

image.png

因为所有的业务模块会向下依赖,因此在:base:ioc库中会创建与业务相关的代理接口。

# :base:ioc module interface ILoginDelegate {     fun openLoginActivity(context: Context, src: (Intent.() -> Unit)? = null) } 复制代码

既然有接口出现,那么就会有对应的实现类,该实现类是在登录模块中实现的。

# login module class LoginDelegateImpl : ILoginDelegate{     override fun openLoginActivity(context: Context, src: (Intent.() -> Unit)?) {         val intent = Intent()         if (src != null){             intent.src()         }         intent.setClass(context,LoginActivity::class.java)         context.startActivity(intent)     } } 复制代码

所以登录模块需要向ioc模块注入这个实现类,其中比较简单的方式就是通过接口名与实现类名存储在一个Map中,当任意一个模块想要调用时,只需要拿到接口名就可以得到注入的实现类。

object InjectUtils {     /**接口名与实现类名一一对应的map*/     private val routerMap: MutableMap<String, String> by lazy {         mutableMapOf()     }     /**接口名与实现类的一一对应*/     private val implMap: MutableMap<String, WeakReference<*>> by lazy {         mutableMapOf()     }     /**注册*/     fun inject(interfaceName: String, implName: String) {         if (routerMap.containsKey(interfaceName) || routerMap.containsValue(interfaceName)) {             return         }         routerMap[interfaceName] = implName     }     /**获取实现类*/     fun <T> getApiService(clazz: Class<T>): T? {         try {             val weakInstance = implMap[clazz.name]             if (weakInstance != null) {                 val instance = weakInstance.get()                 if (instance != null) {                     return instance as T                 }             }             /**如果实例为空,需要新建一个实现类*/             val implName = routerMap[clazz.name]             val instance = Class.forName(implName).newInstance()             implMap[clazz.name] = WeakReference(instance)             return instance as T         } catch (e: Exception) {             Log.i("InjectUtils", "error==>${e.message}")             return null         }     } } 复制代码

例如在news模块想要跳转到登录,首先需要全局注入

InjectUtils.inject(ILoginDelegate::class.java.name, LoginDelegateImpl::class.java.name) 复制代码

然后在任何一个模块中都能够拿到这个实例。

InjectUtils.getApiService(ILoginDelegate::class.java)?.openLoginActivity(this) 复制代码

其实想要实现这种注入方式有很多,像通过注解修饰这个实现类,配合注解处理器全局扫描就可以少一部自己手动存储的这一步,就是APT的思路;还有就是Dagger2或者Hilt实现的隔离层架构,同样也是一种方式。总之想要实现模块解耦,依赖注入是必须的。


作者:Vector7
链接:https://juejin.cn/post/7170541066532323364


文章分类
ANDROID
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐