# 基於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地址

天星技術交流羣

相關文章
相關標籤/搜索