1. 背景
日常游戏渠道对接过程中,会出现一个比较特殊的问题。如渠道A接入的OAID版本为1.0.13,渠道B接入OAID的版本为1.0.23。作为上游渠道,经常会遇到以下问题:
当编译渠道A的代码时候,需要修改自有代码中OAID版本代码匹配上渠道A的OAID;当编译渠道B的代码时候需要切换自有代码中OAID接入代码。 以上操作每次都需要去代码调用地方手动注释掉,然后启用对应版本的代码。
具体代码如下:
# 初始化
class * extends Application{
protected void onCreate(Context context){
try{
//如果版本是 1.0.13
com.bun.miitmdid.core.JLibrary.InitEntry(context);
//如果版本是 1.0.23
// com.bun.miitmdid.core.JLibrary.InitEntry(context);
}catch(Exception ex){}
}
}
#代码调用
class * extends Activity{
protected void onCreate(Bundle bundle){
super.onCreate(bundle);
//如果版本是 1.0.13
MdidSdk mdidSdk = new com.bun.miitmdid.core.MdidSdk();
mdidSdk.InitSdk(this, new com.bun.supplier.IIdentifierListener() {
@Override
public void OnSupport(boolean b, IdSupplier idSupplier) {
// get oaid
}
});
//如果版本是 1.0.23
MdidSdkHelper mdidSdk = new com.bun.miitmdid.core.MdidSdkHelper();
mdidSdk.InitSdk(this, true, new com.bun.miitmdid.interfaces.IIdentifierListener() {
@Override
public void OnSupport(boolean b, IdSupplier idSupplier) {
// get oaid
}
});
}
}
2. 解决方案
基于对接环境,SDK质量参差不齐,使用的OAID版本各不相同。博主不想每次编译代码时候都去做注释的操作,目前尝试过2种方案:基于productFlavors配置不同的接入代码、使用反射+动态代理接入代码
2.1 基于productFlavors的方式
该方法又名配置构建变种,google推荐的方式,为不同的变种代码配置不同的变种维度。具体方式如下:
android {
productFlavors {
v_1_0_13 {
applicationIdSuffix ".v23" // 包名后缀
versionNameSuffix "-v13" //版本名后缀
}
v_1_0_23 {
applicationIdSuffix ".v23"
versionNameSuffix "-v23"
}
}
}
java代码接入部分,不同的代码存放在project/mudule名/src/flavor名/java中。具体不再赘述,详情可参考Android官方文档->配置构建变体
2.2 使用反射+动态代理接入代码
2.2.1 思路如何诞生
博主在阅读Retrofit源码时,发现该框架使用了动态代理来实现调用方只需要输入一个接口class即可得到一个接口class的实现代理对象。具体使用到了Proxy:
// Retrofit部分源码
public <T> T create(final Class<T> service) {
validateServiceInterface(service);
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];
@Override public @Nullable Object invoke(Object proxy, Method method,
@Nullable Object[] args) throws Throwable {
...
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}
Proxy.newProxyInstance()入参有三个,第一个为class的ClassLoader,第二个为待实例化的class,第三个为InvocationHandler接口。
当使用Proxy.newProxyInstance实现的代理对象时候,每一次调用代理对象的方法都会被转发到InvocationHandler接口的invoke方法中。
invoke有三个参数,第一个为代理的对象,第二个为触发的方法,第三个为触发方法的入参
由此引发了一个思考,已经知道了OAID库的调用类及方法,那是否就可以使用动态代理的形式来触发接口调用。
2.2.1 思路实现
Proxy.newProxyInstance的三个参数,其中只有InvocationHandler可以直接实例化。有一个前提,OAID的代码一定会在编译期间和SDK代码打包到一起。那第一个参数ClassLoader获取很简单,读取当前调用方的ClassLoader。
第二个参数class的获取方式不能使用明显的"包名.类名.class",因为这样无法规避代码编译期的检查。可使用的方案是使用Java反射机制。 什么是JAVA反射机制:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
知道了反射机制,接下来如何获取到OAID的调用class就很简单了,直接使用Class.forname(String clz)。
具体代码实现(代码有点长):
class MiitHelperProxy1_0_13{
public void init(Context ctx){
// 反射调用 1.0.13 的初始化
try {
Class<?> aClass = Class.forName("com.bun.miitmdid.core.JLibrary");
Method initEntry = aClass.getDeclaredMethod("InitEntry", Context.class);
Constructor constructor = aClass.getConstructor(new Class[]{});
Object newInstance = constructor.newInstance(new Object[]{});
initEntry.invoke(newInstance, context);
} catch (ClassNotFoundException | NoSuchMethodException e) {
Log.d(TAG, "当前版本JLibrary已经不存在");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
// 读取oaid
public void loadOaid(){
String str2 = "InitSdk";
Class listenerCls = null;
try {
// 反射得到接口class
listenerCls = Class.forName("com.bun.supplier.IIdentifierListener", true, this.classLoader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
boolean z = true;
Class cls = null;
try {
// 反射得到调用接口类class
cls = Class.forName("com.bun.miitmdid.core.MdidSdk", true, this.classLoader);
if (cls == null || listenerCls == null) {
return 1008615;
}
// 得到构造方法
Constructor constructor = cls.getConstructor(new Class[]{Boolean.TYPE});
if (constructor == null) {
logd(z, "not found MdidSdk Constructor");
return 1008615;
}
// 创建实例
Object newInstance = constructor.newInstance(new Object[]{Boolean.valueOf(z)});
if (newInstance == null) {
logd(z, "Create MdidSdk Instance failed");
return 1008615;
}
// 获取声明方法 也就是 InitSdk方法
Method declaredMethod = cls.getDeclaredMethod(str2, new Class[]{Context.class, listenerCls});
if (declaredMethod == null) {
logd(z, "not found MdidSdk " + str2 + " function");
return 1008615;
}
// 动态代理创建com.bun.supplier.IIdentifierListener接口实例
Object identifierListener = createIdentifierListener(this.listenerClsStr, this.classLoader);
if (identifierListener == null) {
logd(z, "not found IdentifierListener " + str2 + " function");
return 1008615;
}
// 执行接口对象的InitSdk
int intValue = ((Integer) declaredMethod.invoke(newInstance, new Object[]{cxt, identifierListener})).intValue();
logd(z, "call and retvalue:" + intValue);
return intValue;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return 1008615;
}
/**
* 通过动态代理创建回调接口
*
* @param listenerClsStr
* @param classLoader
* @return
*/
protected Object createIdentifierListener(String listenerClsStr, ClassLoader classLoader) {
Class cls = null;
try {
cls = Class.forName(listenerClsStr, true, classLoader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if (cls == null) return null;
//动态代理创建对象,当此对象被调用时候会被invoke方法拦截
return Proxy.newProxyInstance(classLoader, new Class[]{cls}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = "OnSupport";
//找到对应的方法,拦截取值
if (name.equals(method.getName())) {
for (Object arg : args) {
// 遍历找到IdSupplier入参数对象
if (!"java.lang.Boolean".equals(arg.getClass().getName())) {
getOaidFromObject(arg);
}
}
return null;
}
return method.invoke(proxy, args);
}
});
}
/**
* 读取IdSupplier对象的值
* @param object
*/
protected void getOaidFromObject(Object object) {
try {
Method oaidMethod = object.getClass().getDeclaredMethod("getOAID");
String oaid = oaidMethod.invoke(object).toString();
Method vaidMethod = object.getClass().getDeclaredMethod("getVAID");
String vaid = vaidMethod.invoke(object).toString();
Method aaidMethod = object.getClass().getDeclaredMethod("getAAID");
String aaid = aaidMethod.invoke(object).toString();
if (getListener() != null) {
getListener().onIdsAvalid(true, oaid, vaid, aaid);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
通过Class.forname动态的获取不同版本的class,再使用动态代理(Proxy的方式)创建回调接口类的代理对象。当从OAID库回调触发代理对象时候,从InvocationHandler类的invoke方法处把IdSupplier对象获取到,最后通过再通过反射触发IdSupplier.getOAID()获取设备的OAID。
3.总结
使用指定类名通过Class.forname的方式得到class。接着使用动态代理的方式创建代理对象,当代理对象的方法被调用时候,拦截invoke得到想要的值。不需要关心渠道的SDK接入的是哪个版本的OAID库,只需要针对不同版本的OAID库做对应的动态代理调用。避免了手动切换注释代码的麻烦,省时省力。但是该方案存在一个缺陷,即是反射牺牲了性能,破坏了代码的结构,降低可读性。
考虑到游戏本身就是一个比较吃手机性能的大型APP,牺牲这点性能对比起每次手动切换带来的麻烦来说,不失为一个好方法。
以下是DEMO项目,文中方案可能不是最好的,但是目前来看是使用得最舒服的。