基于VirtualApk的Android手游SDK插件化架构
引言
一个独立开发android手游SDK发行系统两年的菜鸡,学习过,反编译过九游SDK,在此将我开发中遇到的一些问题和解决方案讲述一下。欢迎大家关注留言投币丢香蕉。
核心架构
基于
目录
动态加载SDK中使用的第三方库
为什么要动态加载使用的第三方库?
如果你是一个从来不使用第三方库的程序员,你可以跳过阅读本章节。
首先我来说一下,动态加载第三方库到底有没有必要,对于这个问题我也考虑了很久,最后总结了一点,如果你喜欢用
okhttp,rxjava,retrofit2,gson
等第三方库的话,就很有必要了。
为什么这么说了,我来分析说一下吧,根据我2年游戏sdk开发经验来分析下。
目前基本上绝大部分都会自带support-v4包
我们可以清楚的看到,v4-23.0.1包的方法数量已经这么多了
可能你会说,在android studio里面配置一下multidexEnabled true就可以解决了,但是我想说的是,大部分游戏开发厂商都是用自己的打包脚本打包,所以为了避免65535方法,最好还是做动态加载,在游戏运行后加载自己使用过的第三方库。
方式 | 优点 | 缺点 |
---|---|---|
动态加载第三方库 | 有效的减少了主APP的dex的方法数量 | 第一次安装需要会卡一下UI,插件释放和加载需要一定的时间,还必须是同步操作 |
传统方式 | 如果主dex方法数量没有超过65535方法,将不耗费时间 | 如果方法数量超过65535,和动态加载第三方库一样会卡UI |
注意
插件化加载第三方库只能用于不包含res资源的工程,如果你想做的插件化第三方库有res等android资源的话,请跳过阅读本章,之后在第三章会将包含资源的插件库怎么编写。
其实 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 ListnativeLibraryDirectories = (List ) 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 Mapmap = 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来实现本章代码。