在# 动态署理设计形式完成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署理目标是接口吗?必定不是,由于接口不可实例化,那么生成的目标是什么呢?

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

经过断点,咱们发现这个目标是$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 组件化依靠注入

如果在项目中运用到组件化的同伴可能有遇到这样的问题,两个模块需求通讯,一般选用的是模块依靠直接通讯

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

这种办法其实是不可行的,由于不管是模块化仍是组件化,这种办法会使得两个模块间耦合十分严重,两个模块应该相对独立,并向下承继,所以在下层需求有一个module专门负责依靠注入。

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

由于一切的事务模块会向下依靠,因此在: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完成的隔离层架构,相同也是一种办法。总归想要完成模块解耦,依靠注入是必须的。