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

com.bytedance.app.boost_multidex-1.apk.classes2.zip

com.bytedance.app.boost_multidex-1.apk.classes3.dex

com.bytedance.app.boost_multidex-1.apk.classes3.zip

com.bytedance.app.boost_multidex-1.apk.classes4.dex

com.bytedance.app.boost_multidex-1.apk.classes4.zip

这一步是通过DexFile.loadDex方法实现的,只需要指定原始 ZIP 文件和 ODEX 文件的路径,就能够根据 ZIP 中的 DEX 生成相应的 ODEX 产物,这个方法会最终返回一个DexFile对象。

最后,APP 把这些DexFile对象都添加到PathClassLoaderpathList里面,就可以让 APP 在运行期间,通过ClassLoader加载使用到这些 DEX 中的类。

在这整个过程中,生成 ZIP 和 ODEX 文件的过程都是比较耗时的,如果一个 APP 中有很多个 Secondary DEX 文件,就会加剧这一问题。尤其是生成 ODEX 的过程,Dalvik 虚拟机会把 DEX 格式的文件进行遍历扫描和优化重写处理,从而转换为 ODEX 文件,这就是其中最大的耗时瓶颈。

普遍采用的优化方式


目前业界已经有了一些对 MultiDex 进行优化的方法,我们先来看下大家通常是怎么优化这一过程的。

异步化加载

把启动阶段要使用的类尽可能多地打包到主 Dex 里面,尽量多地不依赖 Secondary DEX 来跑业务代码。然后异步调用MultiDex.install,而在后续某个时间点需要用到 Secondary DEX 的时候,如果 MultiDex 还没执行完,就停下来同步等待它完成再继续执行后续的代码。

这样确实可以在 install 的同时往下执行部分代码,而不至于被完全堵住。然而要做到这点,必须首先梳理好启动逻辑的代码,明确知道哪些是可以并行执行的。另外,由于主 Dex 能放的代码本身就比较有限,业务在启动阶段如果有太多依赖,就不能完全放入主 Dex 里面,因此就需要合理地剥离依赖。

因此现实情况下这个方案效果比较有限,如果启动阶段牵扯了太多业务逻辑,很可能并行执行不了太多代码,就很快又被 install 堵住了。

模块懒加载

这个方案最早见于美团的文章,可以说是前一个方案的升级版。

它也是做异步 DEX 加载,不过不同之处在于,在编译期间就需要对 DEX 按模块进行拆分。

一般是把一级界面的 Activity、Service、Receiver、Provider 涉及到的代码都放到第一个 DEX 中,而把二级、三级页面的 Activity 以及非高频界面的代码放到了 Secondary DEX 中。

当后面需要执行某个模块的时候,先判断这个模块的 Class 是否已经加载完成,如果没有完成,就等待 install 完成后再继续执行。

可见,这个方案对业务的改造程度相当巨大,而且已经有了一些插件化框架的雏形。另外,想要做到能对模块的 Class 的加载情况进行判断,还得通过反射 ActivityThread 注入自己的 Instrumentation,在执行 Activity 之前插入自己的判断逻辑。这也会相应地引入机型兼容性问题。

多线程加载

原生的 MultiDex 是顺序依次对每个 DEX 文件做 ODEX 优化的。而多线程的思路是,把每个 DEX 分别用各自线程做 OPT。

这么乍看起来,似乎是能够并行地做 ODEX 来起到优化效果。然而我们项目中一共有 6 个 Secondary DEX 文件,实测发现,这种方式几乎没有优化效果。原因可能是 ODEX 本身其实是重度 I/O 类型的操作,对于并发而言,多个线程同时进行 I/O 操作并不能带来明显收益,并且多线程切换本身也会带来一定损耗。

后台进程加载

这个方案主要是防止主进程做 ODEX 太久导致 ANR。当点击 APP 的时候,先单独启动了一个非主进程来先做 ODEX,等非主进程做完 ODEX 后再叫起主进程,这样主进程起来直接取得做好的 ODEX 就可以直接执行。不过,这只是规避了主进程 ANR 的问题,第一次启动的整体等待时间并没有减少。

一个更彻底的优化方案


上述几个方案,在各个层面都尝试做了优化,然而仔细分析便会发现,它们都没有触及这个问题中根本,也就是就MultiDex.install操作本身。

MultiDex.install生成 ODEX 文件的过程,调用的方法是DexFile.loadDex,它会启动一个 dexopt 进程对输入的 DEX 文件进行 ODEX 转化。那么,这个 ODEX 优化的时间是否可以避免呢?

我们的 BoostMultiDex 方案,正是从这一点入手,从本质上优化 install 的耗时。

我们的做法是,在第一次启动的时候,直接加载没有经过 OPT 优化的原始 DEX,先使得 APP 能够正常启动。然后在后台启动一个单独进程,慢慢地做完 DEX 的 OPT 工作,尽可能避免影响到前台 APP 的正常使用。

突破口

这里的难点,自然是——如何做到可以直接加载原始 DEX,避免 ODEX 优化带来的耗时阻塞。

如果要避免 ODEX 优化,又想要 APP 能够正常运行,就意味着 Dalvik 虚拟机需要直接执行没有做过 OPT 的、原始的 DEX 文件。虚拟机是否支持直接执行 DEX 文件呢?毕竟 Dalvik 虚拟机是可以直接执行原始 DEX 字节码的,ODEX 相比 DEX 只是做了一些额外的分析优化。因此即使 DEX 不通过优化,理论上应该是可以正常执行的。

功夫不负有心人,经过我们的一番挖掘,在系统的 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);

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

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

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

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

资源分享

一线互联网面试专题

379页的Android进阶知识大全

379页的Android进阶知识大全

点击:

**《Android架构视频+BAT面试专题PDF+学习笔记​》**即可免费获取

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

C-1710508310412)]

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

资源分享

[外链图片转存中…(img-xGajs82D-1710508310413)]

[外链图片转存中…(img-1NseSeMG-1710508310413)]

[外链图片转存中…(img-jAY1MaIJ-1710508310413)]

点击:

**《Android架构视频+BAT面试专题PDF+学习笔记​》**即可免费获取

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

2020年虽然路途坎坷,都在说Android要没落,但是,不要慌,做自己的计划,学自己的习,竞争无处不在,每个行业都是如此。相信自己,没有做不到的,只有想不到的。祝大家2021年万事大吉。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值