1. 程式人生 > >Android 一種在Dalvik虛擬機器上多Dex載入優化的方案

Android 一種在Dalvik虛擬機器上多Dex載入優化的方案

在Android原始碼中,DexFile中有一個方法,其函式原型為:

 native private static int openDexFile(byte[] fileContents);

也就是通過byte陣列載入一個Dex,可以達到秒級載入,親自測了下,如果一個使用Multidex載入的App,第二個Dex如果需要載入耗時2s+,則使用這個函式去載入,只需要300ms以內即可完成載入。

因此可以做的優化就是,app安裝後首次載入使用該函式去載入,同時開一個程序使用Multidex載入,當第二次啟動的時候,則使用原始的Multidex去載入。這樣可以做到:
- 首次載入由2s+的耗時降低到300ms以內
- 首次載入多程序完成Multidex,後續載入通過Multidex載入,耗時10ms以內。

但是這個函式在Android 4.4中java層被刪除了,而Native層中的函式還是存在的。因此我們不從Java層中去動手,而是直接從NDK入手。且這種方式不支援art虛擬機器。

這裡就簡單介紹一下原理

  • 在jni的JNI_OnLoad方法中查詢openDexFile函式,獲取其指標
  • 在jni的JNI_OnLoad方法中註冊動態函式,關聯java層的native函式。

我們要查詢的openDexFile函式在libdvm中,通過dlopen函式獲取其指標,然後通過dlsym函式,獲取openDexFile的指標。

 //定義
JNINativeMethod *dvm_dalvik_system_DexFile;
void
(*openDexFile)(const u4 *args, union JValue *pResult); JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = NULL; jint result = -1; void *ldvm = (void *) dlopen("libdvm.so", RTLD_LAZY); dvm_dalvik_system_DexFile = (JNINativeMethod *) dlsym(ldvm, "dvm_dalvik_system_DexFile"
); if (0 == lookup(dvm_dalvik_system_DexFile, "openDexFile", "([B)I", &openDexFile)) { openDexFile = NULL; return result; } if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) { return result; } return JNI_VERSION_1_6; } int lookup(JNINativeMethod *table, const char *name, const char *sig, void (**fnPtrout)(u4 const *, union JValue *)) { int i = 0; while (table[i].name != NULL) { LOGD("lookup %d %s", i, table[i].name); if ((strcmp(name, table[i].name) == 0) && (strcmp(sig, table[i].signature) == 0)) { *fnPtrout = table[i].fnPtr; return 1; } i++; } return 0; }

關於第二步,java函式和jni函式的關聯,你可以使用靜態註冊,即遵守jni的標準即可。這裡使用了動態註冊方式,即在JNI_OnLoad完成java和jni函式的關聯。

首先宣告java函式:

public class Multidex {
    static {
        System.loadLibrary("multidex");
    }

    public static int openDexFile(byte[] dexBytes) throws Exception {
        return openDexFile(dexBytes, dexBytes.length);
    }

    /*
     * Open a DEX file based on a {@code byte[]}. The value returned
     * is a magic VM cookie. On failure, a RuntimeException is thrown.
     */
    private native static int openDexFile(byte[] fileContents, long length);
}

進行註冊

JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
    //....省略n行程式碼
    if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return result;
    }
    if (registerNatives(env) != JNI_TRUE) {
        return result;
    }
    return JNI_VERSION_1_6;
}

registerNatives函式的實現如下:

static JNINativeMethod methods[] = {
        {"openDexFile", "([BJ)I", (void *) Multidex_openDexFile}
};
static const char *classPathName = "com/android/quickmultidex/Multidex";

static int registerNativeMethods(JNIEnv *env, const char *className,
                                 JNINativeMethod *gMethods, int numMethods) {
    jclass clazz;

    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

static int registerNatives(JNIEnv *env) {
    if (!registerNativeMethods(env, classPathName,
                               methods, sizeof(methods) / sizeof(methods[0]))) {
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

最終和java關聯的函式為Multidex_openDexFile函式,其函式原型如下:


JNIEXPORT jint JNICALL Multidex_openDexFile(
        JNIEnv *env, jclass jv, jbyteArray dexArray, jlong dexLen) {

}

在這個函式中,我們就需要呼叫系統的openDexFile函式,獲取載入Dex的cookie。這個函式中比較關鍵的一個問題就是如何構造入參,即ArrayObject相關引數的構造,關於這個構造,請參考一下幾篇文章:

看完了上面幾篇文章,還有一個問題,就是ArrayObject物件中的contents的偏移,該偏移在arm上是16,在x86上是12,因此需要巨集來輔助定義,如下:

#if defined(__i386__)
#define array_object_contents_offset 12
#else
#define array_object_contents_offset 16
#endif

然後就是大小端的判斷,大小端也是通過巨集來定義

#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
#define HAVE_LITTLE_ENDIAN

int getEndian() {
    return 1;
}

#else
#define HAVE_BIG_ENDIAN
int getEndian(){
    return 0;
}
#endif

最後就是所需入參的資料結構構造


#if defined(HAVE_ENDIAN_H)
# include <endian.h>

#else /*not HAVE_ENDIAN_H*/
# define __BIG_ENDIAN 4321
# define __LITTLE_ENDIAN 1234
# if defined(HAVE_LITTLE_ENDIAN)
#  define __BYTE_ORDER __LITTLE_ENDIAN
# else
#  define __BYTE_ORDER __BIG_ENDIAN
# endif
#endif /*not HAVE_ENDIAN_H*/


//資料結構構造定義
typedef uint8_t u1;
typedef uint16_t u2;
typedef uint32_t u4;
typedef uint64_t u8;
typedef int8_t s1;
typedef int16_t s2;
typedef int32_t s4;
typedef int64_t s8;

union JValue {
#if defined(HAVE_LITTLE_ENDIAN)
    u1 z;
    s1 b;
    u2 c;
    s2 s;
    s4 i;
    s8 j;
    float f;
    double d;
    void *l;
#endif
#if defined(HAVE_BIG_ENDIAN)
    struct {
        u1 _z[3];
        u1 z;
    };
    struct {
        s1 _b[3];
        s1 b;
    };
    struct {
        u2 _c;
        u2 c;
    };
    struct {
        s2 _s;
        s2 s;
    };
    s4 i;
    s8 j;
    float f;
    double d;
    void *l;
#endif
};

typedef struct {
    void *clazz;
    u4 lock;
    u4 length;
    u1 *contents;
} ArrayObject;

最關鍵的地方就是Multidex_openDexFile函式的實現,具體如何構造可參考上面的文章

JNIEXPORT jint JNICALL Multidex_openDexFile(
        JNIEnv *env, jclass jv, jbyteArray dexArray, jlong dexLen) {
    LOGD("array_object_contents_offset: %d", array_object_contents_offset);
    u1 *dexData = (u1 *) (*env)->GetByteArrayElements(env, dexArray, NULL);
    char *arr;
    arr = (char *) malloc((size_t) (array_object_contents_offset + dexLen));
    ArrayObject *ao = (ArrayObject *) arr;
    ao->length = (u4) dexLen;
    memcpy(arr + array_object_contents_offset, dexData, dexLen);
    u4 args[] = {(u4) ao};
    union JValue pResult;
    jint result = -1;
    if (openDexFile != NULL) {
        openDexFile(args, &pResult);
        result = (jint) pResult.l;
    }
    return result;
}

這樣,我們就獲取到了通過byte陣列載入Dex後返回的cookie,通過這個cookie我們就可以去查詢Dex中的類。

  • 首先獲取到Dex的位元組陣列
  • 其次呼叫native方法將位元組陣列傳入返回cookie
  • 利用cookie構造DexFile
  • 將DexFile插入到Classloader中

    第一步,可改造Multidex程式碼,將其解壓Dex的程式碼進行改造,返回byte陣列,改造後的程式碼如下:

    private static final String DEX_PREFIX = "classes";
    private static final String DEX_SUFFIX = ".dex";
    private static final int MAX_EXTRACT_ATTEMPTS = 3;

    private static List<byte[]> performExtractions(String sourceApk)
            throws IOException {
        List<byte[]> dexDatas = new ArrayList<byte[]>();

        final ZipFile apk = new ZipFile(sourceApk);
        try {
            int secondaryNumber = 2;
            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
            while (dexFile != null) {
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;
                while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
                    numAttempts++;
                    byte[] extract = extract(apk, dexFile);
                    if (extract == null) {
                        isExtractionSuccessful = false;
                    } else {
                        dexDatas.add(extract);
                        isExtractionSuccessful = true;
                    }
                }
                if (!isExtractionSuccessful) {
                    throw new IOException("Could not create extra file " +
                            " for secondary dex (" +
                            secondaryNumber + ")");
                }
                secondaryNumber++;
                dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
            }
            return dexDatas;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                apk.close();
            } catch (IOException e) {
                e.printStackTrace();
                Log.e(TAG, "Failed to close resource", e);
            }
        }
        return null;
    }

    private static byte[] extract(ZipFile apk, ZipEntry dexFile) throws IOException {
        InputStream input = apk.getInputStream(dexFile);
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int n = 0;
        while (-1 != (n = input.read(buffer))) {
            output.write(buffer, 0, n);
        }
        closeQuietly(output);
        closeQuietly(input);
        return output.toByteArray();
    }

    private static void closeQuietly(Closeable closeable) {
        try {
            closeable.close();
        } catch (IOException e) {
            Log.w(TAG, "Failed to close resource", e);
        }
    }

第二步,將byte陣列轉換為cookie

    private static List<Integer> loadDex(Context context) throws Exception {

        ArrayList<Integer> list = new ArrayList<>();

        ApplicationInfo applicationInfo = context.getApplicationInfo();
        String sourceDir = applicationInfo.sourceDir;

        List<byte[]> dexByteslist = performExtractions(sourceDir);
        if (dexByteslist != null && dexByteslist.size() > 0) {
            for (byte[] dexBytes : dexByteslist) {
                int i = openDexFile(dexBytes);
                Log.e(TAG, "loadDex openDexFile cookie:" + i);
                list.add(i);
            }
        } else {
            Log.e(TAG, "loadDex performExtractions null");

        }
        return list;
    }

第三、四步,構造DexFile,這一步比較繞,主要原理就是通過DexPathList的makeDexElements函式,傳引數為app的apk包路徑,構造一個dexElements出來,然後將該dexElements的所有引數設定為null(除了dexFile),然後將dexFile獲取到,設定沒有用的引數為null,設定cookie為獲取到的cookie,然後插入到classloader中去,怎麼插入,和multidex是一樣的。

    public static boolean inject(Context base, List<Integer> cookies) {
        try {
            ApplicationInfo applicationInfo = base.getApplicationInfo();
            String sourceDir = applicationInfo.sourceDir;

            Field pathListField = findField(base.getClassLoader(), "pathList");
            Object pathList = pathListField.get(base.getClassLoader());

            Method makeDexElements = null;
            if (Build.VERSION.SDK_INT < 19) {
                makeDexElements =
                        findMethod(pathList, "makeDexElements", ArrayList.class, File.class);

            } else {
                makeDexElements =
                        findMethod(pathList, "makeDexElements", ArrayList.class, File.class,
                                ArrayList.class);

            }
            Object[] invokeElements = null;
            ArrayList<File> files = new ArrayList<>();
            for (int i = 0; i < cookies.size(); i++) {
                files.add(new File(sourceDir));
            }
            if (Build.VERSION.SDK_INT < 19) {
                invokeElements = (Object[]) makeDexElements.invoke(pathList, files, null);
            } else {
                invokeElements = (Object[]) makeDexElements.invoke(pathList, files, null, null);
            }

            Field dexElementsFiled = Multidex.findField(pathList, "dexElements");
            Object[] originalDexElements = (Object[]) dexElementsFiled.get(pathList);
            Object[] resultDexElements = (Object[]) Array.newInstance(originalDexElements.getClass().getComponentType(), originalDexElements.length + invokeElements.length);

            System.arraycopy(originalDexElements, 0, resultDexElements, 0, originalDexElements.length);
            System.arraycopy(invokeElements, 0, resultDexElements, originalDexElements.length, invokeElements.length);

            int length = originalDexElements.length;
            for (int i = 0; i < cookies.size(); i++) {
                Object dexElements = resultDexElements[length + i];
                Field fileField = Multidex.findField(dexElements, "file");
                fileField.set(dexElements, null);
                Field zipField = Multidex.findField(dexElements, "zip");
                zipField.set(dexElements, null);
                Field zipFileField = Multidex.findField(dexElements, "zipFile");
                zipFileField.set(dexElements, null);
                Field dexFileField = Multidex.findField(dexElements, "dexFile");
                Object o = dexFileField.get(dexElements);
                Field mCookieField = Multidex.findField(o, "mCookie");
                mCookieField.set(o, cookies.get(i));
                Field mFileNameFiled = Multidex.findField(o, "mFileName");
                mFileNameFiled.set(o, null);
            }

            dexElementsFiled.set(pathList, resultDexElements);
            return true;
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return false;
    }

    public static Field findField(Object instance, String name) throws NoSuchFieldException {
        Class clazz = instance.getClass();
        while (clazz != null) {
            try {
                Field e = clazz.getDeclaredField(name);
                if (!e.isAccessible()) {
                    e.setAccessible(true);
                }

                return e;
            } catch (NoSuchFieldException var4) {
                clazz = clazz.getSuperclass();
            }
        }

        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }

    public static Method findMethod(Object instance, String name, Class... parameterTypes) throws NoSuchMethodException {
        Class clazz = instance.getClass();

        while (clazz != null) {
            try {
                Method e = clazz.getDeclaredMethod(name, parameterTypes);
                if (!e.isAccessible()) {
                    e.setAccessible(true);
                }

                return e;
            } catch (NoSuchMethodException var5) {
                clazz = clazz.getSuperclass();
            }
        }

        throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
    }

最後就是一個對外暴露的函式install

    private static final String TAG = "Multidex";

    public static boolean install(Context context) {
        try {
            long start = System.nanoTime();
            boolean ret = false;
            long startLoadDexData = System.nanoTime();
            List<Integer> cookies = loadDex(context);
            long endLoadDexData = System.nanoTime();
            Log.e(TAG, "loadDexData time:" + (endLoadDexData - startLoadDexData) + " ns");
            Log.e(TAG, "loadDexData time:" + (endLoadDexData - startLoadDexData) / 1000000 + " ms");

            if (cookies != null && cookies.size() > 0) {
                long startInject = System.nanoTime();
                boolean result = inject(context, cookies);
                long endInject = System.nanoTime();
                Log.e(TAG, "inject time:" + (endInject - startInject) + " ns");
                Log.e(TAG, "inject time:" + (endInject - startInject) / 1000000 + " ms");
                ret = result;
            } else {
                ret = false;
            }
            Log.e(TAG, "install result:" + ret);
            long end = System.nanoTime();
            Log.e(TAG, "install time:" + (end - start) + " ns");
            Log.e(TAG, "install time:" + (end - start) / 1000000 + " ms");
            return ret;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

但是事實並沒有那麼完美,當你的專案中使用了java.lang.Class.getTypeParameters()等函式時,就會在Android 4.4上crash掉,這個原因可以見

因此本篇文章的適用範圍為Android 4.1~4.3