# 基于VirtualApk的Android手游SDK插件化架构(一)

基于VirtualApk的Android手游SDK插件化架构

引言

一个独立开发android手游SDK发行系统两年的菜鸡,学习过U8SDK,反编译过九游SDK,在此将我开发中遇到的一些问题和解决方案讲述一下。欢迎大家关注留言投币丢香蕉。

核心架构

基于VirtualApk插件化

目录

动态加载SDK中使用的第三方库

为什么要动态加载使用的第三方库?

如果你是一个从来不使用第三方库的程序员,你可以跳过阅读本章节。

首先我来说一下,动态加载第三方库到底有没有必要,对于这个问题我也考虑了很久,最后总结了一点,如果你喜欢用

okhttp,rxjava,retrofit2,gson

等第三方库的话,就很有必要了。

为什么这么说了,我来分析说一下吧,根据我2年游戏sdk开发经验来分析下。

目前基本上绝大部分都会自带support-v4

我们可以清楚的看到,v4-23.0.1包的方法数量已经这么多了

再加上我们其他第三方库的话,30000个方法数量肯定绰绰有余了,然而google爸爸的短视,一个dex最大的方法数量只能少于65535方法数,我们这样已经占用了一半方法数量,再加上开发商也会使用一些第三方库,所以65535方法很容易爆棚。

可能你会说,在android studio里面配置一下multidexEnabled true就可以解决了,但是我想说的是,大部分游戏开发厂商都是用自己的打包脚本打包,所以为了避免65535方法,最好还是做动态加载,在游戏运行后加载自己使用过的第三方库。

方式优点缺点
动态加载第三方库有效的减少了主APP的dex的方法数量第一次安装需要会卡一下UI,插件释放和加载需要一定的时间,还必须是同步操作
传统方式如果主dex方法数量没有超过65535方法,将不耗费时间如果方法数量超过65535,和动态加载第三方库一样会卡UI

注意

插件化加载第三方库只能用于不包含res资源的工程,如果你想做的插件化第三方库有resandroid资源的话,请跳过阅读本章,之后在第三章会将包含资源的插件库怎么编写。

其实 virtualApk 中已经实现了第三方库的插件化加载,但是如果你想要用 virtualApk 直接加载插件库的话,也不是不行,只是 virtualApk 的框架一开始就 hook 了很多系统方法,然而我们只是需要仅仅是动态加载一些第三方库,所以为了避免和app开发厂商的冲突,我们还是单独将 virtualApk 中动态加载第三方库的核心代码提出来封装好一点。

现在将 virtualApk 加载插件的方法提出来如下。

只需要这一个类,你就可以动态加载一些第三方库,代码过长,你如果只是想用的话,可以直接跳过遇到代码,直接复制到你的工程即可使用。


import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

/**
 * Created by ollyice on 2018/8/9.
 */

public class MultiApk {
    private static final String FILE_NAME = "YouGameSdk_Settings";//自己根据你们SDK名称修改吧
    private static final String DEX_RELEASE_DIR = "dex_cache";//dex释放路径
    private static final String JNI_RELEASE_DIR = "jni_cache";//jni加载与释放路径

    //是否设置了jni加载路径
    private static boolean sHasInsertedNativeLibrary = false;

    //判断app里面是否已经加载了当前插件
    public static boolean isInstalled(Context context, File apk) {
        try {
            Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
            int length = Array.getLength(baseDexElements);
            for (int i = 0; i < length; i++) {
                Object object = Array.get(baseDexElements, i);
                File src = null;
                if (Build.VERSION.SDK_INT >= 26) {//这里可能会有点问题,主要是没有很多手机来测试api 26以上版本的这个name
                    src = getInjectedApk(object,"path");
                } else {
                    src = getInjectedApk(object,"zip");
                }
                if (src != null && src.getAbsolutePath().equals(apk.getAbsolutePath())) {
                    Log.d("MultiApk","插件已经加载过:" + apk.getAbsolutePath());
                    return true;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
    * 反射获取PathClassLoader里面dexElements 的文件路径
    */
    private static File getInjectedApk(Object object, String name) {
        try {
            Field field = object.getClass().getDeclaredField(name);
            field.setAccessible(true);
            return (File) field.get(object);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 安装一个插件
     * @param context  app的 application
     * @param apk  插件app文件路径
     */
    public static void install(Context context, File apk) {
        ClassLoader parent = MultiApk.class.getClassLoader();//获取 app classloader

        String dexDir = getDexReleaseDir(context)
                .getAbsolutePath();//获取dex释放路径
        String jniDir = getJniReleaseDir(context)
                .getAbsolutePath();//获取jni加载与释放路径

        //利用DexClassLoader加载外部插件apk文件
        DexClassLoader dexClassLoader = new DexClassLoader(
                apk.getAbsolutePath(),
                dexDir,
                jniDir,
                parent
        );

        try {
            //获取app中的dexElements
            Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
            //获取plugin中的dexElements
            Object newDexElements = getDexElements(getPathList(dexClassLoader));
            //合并app与plugin中的dexElements
            Object allDexElements = combineArray(baseDexElements, newDexElements);

            Object pathList = getPathList(getPathClassLoader());
            //将新的dexElements反射设置到app中替换原来的dexElements
            setField(pathList.getClass(), pathList, "dexElements", allDexElements);

            //设置so文件加载目录
            insertNativeLibrary(context,dexClassLoader);

            //从插件中查找符合cpu架构的so文件释放到so库加载目录
            tryToCopyNativeLib(context,apk);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取jni加载与释放路径
     */
    private static File getJniReleaseDir(Context context) {
        return context.getDir(JNI_RELEASE_DIR,Context.MODE_PRIVATE);
    }

    /**
     * 获取dex缓存路径
     */
    private static File getDexReleaseDir(Context context) {
        return context.getDir(DEX_RELEASE_DIR,Context.MODE_PRIVATE);
    }

    /**
     * 设置so加载目录
     */
    private static void insertNativeLibrary(Context context,DexClassLoader dexClassLoader) throws Exception {
        //jni加载目录只需要设置一次
        if (sHasInsertedNativeLibrary) {
            return;
        }
        sHasInsertedNativeLibrary = true;

        Object basePathList = getPathList(getPathClassLoader());
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {//5.1
            List<File> nativeLibraryDirectories = (List<File>) getField(basePathList.getClass(),
                    basePathList, "nativeLibraryDirectories"); //获取pathList里面的jni加载目录
            nativeLibraryDirectories.add(getJniReleaseDir(context));//将我们插件so加载目录写到里面去

            //5.1以上新增的需要反射设置so库的路径
            Object baseNativeLibraryPathElements = getField(basePathList.getClass(), basePathList, "nativeLibraryPathElements");
            final int baseArrayLength = Array.getLength(baseNativeLibraryPathElements);

            Object newPathList = getPathList(dexClassLoader);
            Object newNativeLibraryPathElements = getField(newPathList.getClass(), newPathList, "nativeLibraryPathElements");
            Class<?> elementClass = newNativeLibraryPathElements.getClass().getComponentType();
            Object allNativeLibraryPathElements = Array.newInstance(elementClass, baseArrayLength + 1);
            System.arraycopy(baseNativeLibraryPathElements, 0, allNativeLibraryPathElements, 0, baseArrayLength);

            Field soPathField;
            if (Build.VERSION.SDK_INT >= 26) {
                soPathField = elementClass.getDeclaredField("path");
            } else {
                soPathField = elementClass.getDeclaredField("dir");
            }
            soPathField.setAccessible(true);
            final int newArrayLength = Array.getLength(newNativeLibraryPathElements);
            for (int i = 0; i < newArrayLength; i++) {
                Object element = Array.get(newNativeLibraryPathElements, i);
                String dir = ((File) soPathField.get(element)).getAbsolutePath();
                if (dir.contains(DEX_RELEASE_DIR)) {
                    Array.set(allNativeLibraryPathElements, baseArrayLength, element);
                    break;
                }
            }

            setField(basePathList.getClass(), basePathList, "nativeLibraryPathElements", allNativeLibraryPathElements);
        } else {
            File[] nativeLibraryDirectories = (File[]) getFieldNoException(basePathList.getClass(),
                    basePathList, "nativeLibraryDirectories");
            final int N = nativeLibraryDirectories.length;
            File[] newNativeLibraryDirectories = new File[N + 1];
            System.arraycopy(nativeLibraryDirectories, 0, newNativeLibraryDirectories, 0, N);
            newNativeLibraryDirectories[N] = getJniReleaseDir(context);
            setField(basePathList.getClass(), basePathList, "nativeLibraryDirectories", newNativeLibraryDirectories);
        }
    }

    /**
     * 获取PathList里面的dexElements对象
     */
    private static Object getDexElements(Object pathList) throws Exception {
        return getField(pathList.getClass(), pathList, "dexElements");
    }

    /**
     * 获取ClassLoader里面的pathList对象
     */
    private static Object getPathList(Object baseDexClassLoader) throws Exception {
        return getField(Class.forName("dalvik.system.BaseDexClassLoader"), baseDexClassLoader, "pathList");
    }

    /**
     * 获取PathClassLoader
     */
    private static PathClassLoader getPathClassLoader() {
        PathClassLoader pathClassLoader = (PathClassLoader) MultiApk.class.getClassLoader();
        return pathClassLoader;
    }

    /**
     * 合并数组
     */
    private static Object combineArray(Object firstArray, Object secondArray) {
        Class<?> localClass = firstArray.getClass().getComponentType();

        //modify to sure plugin jar class is first use
        int firstArrayLength = Array.getLength(secondArray);
        int allLength = firstArrayLength + Array.getLength(firstArray);
        Object result = Array.newInstance(localClass, allLength);
        for (int k = 0; k < allLength; ++k) {
            if (k < firstArrayLength) {
                Array.set(result, k, Array.get(secondArray, k));
            } else {
                Array.set(result, k, Array.get(firstArray, k - firstArrayLength));
            }
        }
        return result;
    }

    /**
     * 释放so
     */
    private static void tryToCopyNativeLib(Context context,File apk) throws Exception {
        long startTime = System.currentTimeMillis();
        ZipFile zipfile = new ZipFile(apk.getAbsolutePath());//apk就是一个zip文件

        String packageName = getPackageName(context,apk);
        int versionCode = getPackageVersion(context,apk);
        File nativeLibDir = getJniReleaseDir(context);

        try {
            //查找插件zip的文件目录
            //根据手机cpu架构释放对应目录的so文件
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                for (String cpuArch : Build.SUPPORTED_ABIS) {
                    if (findAndCopyNativeLib(zipfile, context, cpuArch, packageName, versionCode, nativeLibDir)) {
                        return;
                    }
                }

            } else {
                if (findAndCopyNativeLib(zipfile, context, Build.CPU_ABI, packageName, versionCode, nativeLibDir)) {
                    return;
                }
            }

            findAndCopyNativeLib(zipfile, context, "armeabi", packageName, versionCode, nativeLibDir);

        } finally {
            zipfile.close();
            Log.d("NativeLib", "Done! +" + (System.currentTimeMillis() - startTime) + "ms");
        }
    }

    /**
     * 获取插件app版本号
     */
    private static int getPackageVersion(Context context, File apk) {
        String apkPath = apk.getAbsolutePath();
        PackageManager pm = context.getPackageManager();
        PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkPath,PackageManager.GET_ACTIVITIES);
        if (pkgInfo != null) {
            ApplicationInfo appInfo = pkgInfo.applicationInfo;
            appInfo.sourceDir = apkPath;
            appInfo.publicSourceDir = apkPath;
            return pkgInfo.versionCode; // 得到版本信息
        }
        return 0;
    }

    /**
     * 获取插件app的包名
     */
    private static String getPackageName(Context context, File apk) {
        String apkPath = apk.getAbsolutePath();
        PackageManager pm = context.getPackageManager();
        PackageInfo pkgInfo = pm.getPackageArchiveInfo(apkPath,PackageManager.GET_ACTIVITIES);
        if (pkgInfo != null) {
            ApplicationInfo appInfo = pkgInfo.applicationInfo;
            appInfo.sourceDir = apkPath;
            appInfo.publicSourceDir = apkPath;
            return pkgInfo.packageName; // 得到版本信息
        }
        return null;
    }

    /**
     * 遍历插件app的lib/xxxx文件夹  释放对应的so库
     */
    private static boolean findAndCopyNativeLib(ZipFile zipfile, Context context, String cpuArch, String packageName, int versionCode, File nativeLibDir) throws Exception {
        Log.d("NativeLib", "Try to copy plugin's cup arch: " + cpuArch);
        boolean findLib = false;
        boolean findSo = false;
        byte buffer[] = null;
        String libPrefix = "lib/" + cpuArch + "/";
        ZipEntry entry;
        Enumeration e = zipfile.entries();

        //遍历zip文件
        while (e.hasMoreElements()) {
            entry = (ZipEntry) e.nextElement();
            String entryName = entry.getName();

            if (entryName.charAt(0) < 'l') {
                continue;
            }
            if (entryName.charAt(0) > 'l') {
                break;
            }
            if (!findLib && !entryName.startsWith("lib/")) {
                continue;
            }
            findLib = true;
            if (!entryName.endsWith(".so") || !entryName.startsWith(libPrefix)) {
                continue;
            }

            if (buffer == null) {
                findSo = true;
                Log.d("NativeLib", "Found plugin's cup arch dir: " + cpuArch);
                buffer = new byte[8192];
            }

            String libName = entryName.substring(entryName.lastIndexOf('/') + 1);
            Log.d("NativeLib", "verify so " + libName);
            File libFile = new File(nativeLibDir, libName);
            String key = packageName + "_" + libName;
            if (libFile.exists()) {
                int VersionCode = getSoVersion(context, key);
                if (VersionCode == versionCode) {
                    Log.d("NativeLib", "skip existing so : " + entry.getName());
                    continue;
                }
            }
            FileOutputStream fos = new FileOutputStream(libFile);
            Log.d("NativeLib", "copy so " + entry.getName() + " of " + cpuArch);
            copySo(buffer, zipfile.getInputStream(entry), fos);
            setSoVersion(context, key, versionCode);
        }

        if (!findLib) {
            Log.d("NativeLib", "Fast skip all!");
            return true;
        }

        return findSo;
    }

    /**
     * 缓存so库版本信息
     */
    private static void setSoVersion(Context context, String name, int version) {
        SharedPreferences preferences = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = preferences.edit();
        editor.putInt(name, version);
        editor.commit();
    }

    /**
     * 获取缓存的so库版本信息
     */
    private static int getSoVersion(Context context, String name) {
        SharedPreferences preferences = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE);
        return preferences.getInt(name, 0);
    }

    /**
     * 从插件apk文件中释放so文件
     */
    private static void copySo(byte[] buffer, InputStream input, OutputStream output) throws IOException {
        BufferedInputStream bufferedInput = new BufferedInputStream(input);
        BufferedOutputStream bufferedOutput = new BufferedOutputStream(output);
        int count;

        while ((count = bufferedInput.read(buffer)) > 0) {
            bufferedOutput.write(buffer, 0, count);
        }
        bufferedOutput.flush();
        bufferedOutput.close();
        output.close();
        bufferedInput.close();
        input.close();
    }

    /**
     * 反射获取field的值
     */
    private static Object getField(Class clazz, Object target, String name) throws Exception {
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        return field.get(target);
    }

    /**
     * 反射设置field的值
     */
    private static void setField(Class clazz, Object target, String name, Object value) throws Exception {
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        field.set(target, value);
    }

    /**
     * 反射获取field的值
     */
    private static Object getFieldNoException(Class clazz, Object target, String name) {
        try {
            return getField(clazz, target, name);
        } catch (Exception e) {
            //ignored.
        }

        return null;
    }
}

复制代码

使用方法

在App的application中

public class App extends Application{

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        File gson = AssetsUtils.releaseFile(this, "plugins/", "gson.apk");
        MultiApk.install(this,gson);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        testGson1();
    }

    /**
     * 5.0以下会被error捕获 提示找不到这个类 这个应该是系统加载class的时机问题吧  具体需要去问google爸爸吧
     * 5.1  7.0  9.0测试全部可以log  
     */
    void testGson1(){
        try {
            Json json = new Json();

            for (int i = 1; i < 20; i++) {
                json.put("APP GSON1:" + i, (i + 10000) + "");
            }
            Log.d("APPJSON1", new Gson().toJson(json));
        }catch (Exception e){
            e.printStackTrace();
        }catch (Error e){
            e.printStackTrace();
        }
    }

    public class Json {
        private Map<String,String> map = new HashMap<>();

        public Json put(String key, String value){
            map.put(key,value);
            return this;
        }
    }
}

复制代码

之后在其他地方都可以调用插件中的类,部分低版本手机在App这个类中无法调用,具体原因要问google爸爸吧。在其他类中使用就不会有这个问题了。

使用场景

比如在app中集成一些第三方统计的情况,我们可以通过在服务器下载的方式来使用。

在host中添加一个统计管理类,然后编写统计接口,在插件加载完成后通过接口初始化统计。当你的业务需求改动后也可以动态修改业务逻辑。详情参考Demo中MainActivity中加载统计插件代码。

对于游戏SDK开发者来说,推荐将第三方库全部下载源码后手动修改包名后编译打包成第三方插件APK,这样错可以避免类冲突问题。

如果你只准备做插件化加载不含res等android资源的第三方插件库加载的话,只需观看本章内容,在下一期我会通过修改virtualApk来实现本章代码。

Demo地址

天星技术交流群

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值