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