Android低版本上APP首次启动时间减少80%(一(1)

功夫不负有心人,经过我们的一番挖掘,在系统的 dalvik 源码里面果然找到了这一隐藏入口:

/*

  • private static int openDexFile(byte[] fileContents) throws IOException

  • Open a DEX file represented in a byte[], returning a pointer to our

  • internal data structure.

  • The system will only perform “essential” optimizations on the given file.

*/

static void Dalvik_dalvik_system_DexFile_openDexFile_bytearray(const u4* args,

JValue* pResult)

{

ArrayObject* fileContentsObj = (ArrayObject*) args[0];

u4 length;

u1* pBytes;

RawDexFile* pRawDexFile;

DexOrJar* pDexOrJar = NULL;

if (fileContentsObj == NULL) {

dvmThrowNullPointerException(“fileContents == null”);

RETURN_VOID();

}

/* TODO: Avoid making a copy of the array. (note array is modified) */

length = fileContentsObj->length;

pBytes = (u1*) malloc(length);

if (pBytes == NULL) {

dvmThrowRuntimeException(“unable to allocate DEX memory”);

RETURN_VOID();

}

memcpy(pBytes, fileContentsObj->contents, length);

if (dvmRawDexFileOpenArray(pBytes, length, &pRawDexFile) != 0) {

ALOGV(“Unable to open in-memory DEX file”);

free(pBytes);

dvmThrowRuntimeException(“unable to open in-memory DEX file”);

RETURN_VOID();

}

ALOGV(“Opening in-memory DEX”);

pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));

pDexOrJar->isDex = true;

pDexOrJar->pRawDexFile = pRawDexFile;

pDexOrJar->pDexMemory = pBytes;

pDexOrJar->fileName = strdup(“”); // Needs to be free()able.

addToDexFileTable(pDexOrJar);

RETURN_PTR(pDexOrJar);

}

这个方法可以做到对原始 DEX 文件做加载,而不依赖 ODEX 文件,它其实就做了这么几件事:

  1. 接受一个byte[]参数,也就是原始 DEX 文件的字节码。

  2. 调用dvmRawDexFileOpenArray函数来处理byte[],生成RawDexFile对象

  3. RawDexFile对象生成一个DexOrJar,通过addToDexFileTable添加到虚拟机内部,这样后续就可以正常使用它了

  4. 返回这个DexOrJar的地址给上层,让上层用它作为 cookie 来构造一个合法的DexFile对象

这样,上层在取得所有 Seconary DEX 的DexFile对象后,调用 makeDexElements 插入到 ClassLoader 里面,就完成 install 操作了。如此一来,我们就能完美地避过 ODEX 优化,让 APP 正常执行下去了。

寻找入口

看起来似乎很顺利,然而在我们却遇到了一个意外状况。

我们从Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个函数的名字可以明显看出,这是一个 JNI 方法,从 4.0 到 4.3 版本都能找到它的 Java 原型:

/*

  • Open a DEX file based on a {@code byte[]}. The value returned

  • is a magic VM cookie. On failure, a RuntimeException is thrown.

*/

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

然而我们在 4.4 版本上,Java 层它并没有对应的 native 方法。这样我们便无法直接在上层调用了。

当然,我们很容易想到,可以用 dlsym 来直接搜寻这个函数的符号来调用。但是可惜的是,Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个方法是static的,因此它并没有被导出。我们实际去解析libdvm.so的时候,也确实没有找到Dalvik_dalvik_system_DexFile_openDexFile_bytearray这个符号。

不过,由于它是 JNI 函数,也是通过正常方式注册到虚拟机里面的。因此,我们可以找到它对应的函数注册表:

const DalvikNativeMethod dvm_dalvik_system_DexFile[] = {

{ “openDexFileNative”, “(Ljava/lang/String;Ljava/lang/String;I)I”,

Dalvik_dalvik_system_DexFile_openDexFileNative },

{ “openDexFile”, “([B)I”,

Dalvik_dalvik_system_DexFile_openDexFile_bytearray },

{ “closeDexFile”, “(I)V”,

Dalvik_dalvik_system_DexFile_closeDexFile },

{ “defineClassNative”, “(Ljava/lang/String;Ljava/lang/ClassLoader;I)Ljava/lang/Class;”,

Dalvik_dalvik_system_DexFile_defineClassNative },

{ “getClassNameList”, “(I)[Ljava/lang/String;”,

Dalvik_dalvik_system_DexFile_getClassNameList },

{ “isDexOptNeeded”, “(Ljava/lang/String;)Z”,

Dalvik_dalvik_system_DexFile_isDexOptNeeded },

{ NULL, NULL, NULL },

};

dvm_dalvik_system_DexFile这个数组需要被虚拟机在运行时动态地注册进去,因此,这个符号是一定会被导出的。

这么一来,我们也就可以通过 dlsym 取得这个数组,按照逐个元素字符串匹配的方式来搜寻openDexFile对应的Dalvik_dalvik_system_DexFile_openDexFile_bytearray方法了。

具体代码实现如下:

const char *name = “openDexFile”;

JNINativeMethod* func = (JNINativeMethod*) dlsym(handler, “dvm_dalvik_system_DexFile”);;

size_t len_name = strlen(name);

while (func->name != nullptr) {

if ((strncmp(name, func->name, len_name) == 0)

&& (strncmp(“([B)I”, func->signature, len_name) == 0)) {

return reinterpret_cast<func_openDexFileBytes>(func->fnPtr);

}

func++;

}

捋清步骤

小结一下,绕过 ODEX 直接加载 DEX 的方案,主要有以下步骤:

  1. 从 APK 中解压获取原始 Secondary DEX 文件的字节码

  2. 通过 dlsym 获取dvm_dalvik_system_DexFile数组

  3. 在数组中查询得到Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数

  4. 调用该函数,逐个传入之前从 APK 获取的 DEX 字节码,完成 DEX 加载,得到合法的DexFile对象

  5. DexFile对象都添加到 APP 的PathClassLoader的 pathList 里

完成了上述几步操作,我们就可以正常访问到 Secondary DEX 里面的类了

getDex 问题


然而,正当我们顺利注入原始 DEX 往下执行的时候,却在 4.4 的机型上马上遇到了一个必现的崩溃:

JNI WARNING: JNI function NewGlobalRef called with exception pending

in Ljava/lang/Class;.getDex:()Lcom/android/dex/Dex; (NewGlobalRef)

Pending exception is:

java.lang.IndexOutOfBoundsException: index=0, limit=0

at java.nio.Buffer.checkIndex(Buffer.java:156)

at java.nio.DirectByteBuffer.get(DirectByteBuffer.java:157)

at com.android.dex.Dex.create(Dex.java:129)

at java.lang.Class.getDex(Native Method)

at libcore.reflect.AnnotationAccess.getSignature(AnnotationAccess.java:447)

at java.lang.Class.getGenericSuperclass(Class.java:824)

at com.google.gson.reflect.TypeToken.getSuperclassTypeParameter(TypeToken.java:82)

at com.google.gson.reflect.TypeToken.(TypeToken.java:62)

at com.google.gson.Gson$1.(Gson.java:112)

at com.google.gson.Gson.(Gson.java:112)

… …

可以看到,Gson 里面使用到了Class.getGenericSuperclass方法,而它最终调用了Class.getDex,它是一个 native 方法,对应实现如下:

JNIEXPORT jobject JNICALL Java_java_lang_Class_getDex(JNIEnv* env, jclass javaClass) {

Thread* self = dvmThreadSelf();

ClassObject* c = (ClassObject*) dvmDecodeIndirectRef(self, javaClass);

DvmDex* dvm_dex = c->pDvmDex;

if (dvm_dex == NULL) {

return NULL;

}

// Already cached?

if (dvm_dex->dex_object != NULL) {

return dvm_dex->dex_object;

}

jobject byte_buffer = env->NewDirectByteBuffer(dvm_dex->memMap.addr, dvm_dex->memMap.length);

if (byte_buffer == NULL) {

return NULL;

}

jclass com_android_dex_Dex = env->FindClass(“com/android/dex/Dex”);

if (com_android_dex_Dex == NULL) {

return NULL;

}

jmethodID com_android_dex_Dex_create =

env->GetStaticMethodID(com_android_dex_Dex,

“create”, “(Ljava/nio/ByteBuffer;)Lcom/android/dex/Dex;”);

if (com_android_dex_Dex_create == NULL) {

return NULL;

}

jvalue args[1];

args[0].l = byte_buffer;

jobject local_ref = env->CallStaticObjectMethodA(com_android_dex_Dex,

com_android_dex_Dex_create,

args);

if (local_ref == NULL) {

return NULL;

}

// Check another thread didn’t cache an object, if we’ve won install the object.

ScopedPthreadMutexLock lock(&dvm_dex->modLock);

if (dvm_dex->dex_object == NULL) {

dvm_dex->dex_object = env->NewGlobalRef(local_ref);

}

return dvm_dex->dex_object;

}

结合堆栈和代码来看,崩溃的点是在 JNI 里面执行com.android.dex.Dex.create的时候:

jobject local_ref = env->CallStaticObjectMethodA(com_android_dex_Dex,

com_android_dex_Dex_create,

args);

由于是 JNI 方法,这个调用发生异常后如果没有 check,在后续执行到env->NewGlobalRef调用的时候会检查到前面发生了异常,从而抛出。

com.android.dex.Dex.create之所以会执行失败,主要原因是入参有问题,这里的参数是dvm_dex->memMap取到的一块 map 内存。dvm_dex 是从这个 Class 里面取得的。虚拟机代码里面,每个 Class 对应是结构是ClassObject中,其中有这个字段:

struct ClassObject : Object {

… …

/* DexFile from which we came; needed to resolve constant pool entries */

/* (will be NULL for VM-generated, e.g. arrays and primitive classes) */

DvmDex* pDvmDex;

… …

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

学习分享

在当下这个信息共享的时代,很多资源都可以在网络上找到,只取决于你愿不愿意找或是找的方法对不对了

很多朋友不是没有资料,大多都是有几十上百个G,但是杂乱无章,不知道怎么看从哪看起,甚至是看后就忘

如果大家觉得自己在网上找的资料非常杂乱、不成体系的话,我也分享一套给大家,比较系统,我平常自己也会经常研读。

2021最新上万页的大厂面试真题

七大模块学习资料:如NDK模块开发、Android框架体系架构…

只有系统,有方向的学习,才能在段时间内迅速提高自己的技术。

这份体系学习笔记,适应人群:
**第一,**学习知识比较碎片化,没有合理的学习路线与进阶方向。
**第二,**开发几年,不知道如何进阶更进一步,比较迷茫。
第三,到了合适的年纪,后续不知道该如何发展,转型管理,还是加强技术研究。如果你有需要,我这里恰好有为什么,不来领取!说不定能改变你现在的状态呢!
由于文章内容比较多,篇幅不允许,部分未展示内容以截图方式展示 。如有需要获取完整的资料文档的朋友点击我的GitHub免费获取。

料非常杂乱、不成体系的话,我也分享一套给大家,比较系统,我平常自己也会经常研读。

2021最新上万页的大厂面试真题

[外链图片转存中…(img-EyzAsiWE-1710818058723)]

七大模块学习资料:如NDK模块开发、Android框架体系架构…

[外链图片转存中…(img-wzvQRZSI-1710818058723)]

只有系统,有方向的学习,才能在段时间内迅速提高自己的技术。

这份体系学习笔记,适应人群:
**第一,**学习知识比较碎片化,没有合理的学习路线与进阶方向。
**第二,**开发几年,不知道如何进阶更进一步,比较迷茫。
第三,到了合适的年纪,后续不知道该如何发展,转型管理,还是加强技术研究。如果你有需要,我这里恰好有为什么,不来领取!说不定能改变你现在的状态呢!
由于文章内容比较多,篇幅不允许,部分未展示内容以截图方式展示 。如有需要获取完整的资料文档的朋友点击我的GitHub免费获取。

  • 18
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值