游戏SDK如何实现无代码接入OAID库服务

2,636 阅读6分钟

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项目,文中方案可能不是最好的,但是目前来看是使用得最舒服的。