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

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

引言

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

核心架构

基于VirtualApk插件化android

目录

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

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

若是你是一个历来不使用第三方库的程序员,你能够跳过阅读本章节。git

首先我来讲一下,动态加载第三方库到底有没有必要,对于这个问题我也考虑了好久,最后总结了一点,若是你喜欢用程序员

okhttp,rxjava,retrofit2,gsongithub

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

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

目前基本上绝大部分都会自带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地址

天星技术交流群

相关文章
相关标签/搜索