在# 动态署理设计形式完成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署理目标是接口吗?必定不是,由于接口不可实例化,那么生成的目标是什么呢?
经过断点,咱们发现这个目标是$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 组件化依靠注入
如果在项目中运用到组件化的同伴可能有遇到这样的问题,两个模块需求通讯,一般选用的是模块依靠直接通讯
这种办法其实是不可行的,由于不管是模块化仍是组件化,这种办法会使得两个模块间耦合十分严重,两个模块应该相对独立,并向下承继,所以在下层需求有一个module专门负责依靠注入。
由于一切的事务模块会向下依靠,因此在: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完成的隔离层架构,相同也是一种办法。总归想要完成模块解耦,依靠注入是必须的。