Android10 dex2oat实践
最近看到一篇博客:Android性能优化之Android 10+ dex2oat实践,对这个优化很感兴趣,打算研究研究能否接入到项目中。不过该博客只讲述了思路,没有给完整源码。本项目参考该博客的思路,实现了该方案。
源码地址:https://github.com/carverZhong/DexOpt
一、dex2oat 详解
以下是官方对于dex2oat的解释:
ART 使用预先 (AOT) 编译,并且从 Android 7.0(代号 Nougat,简称 N)开始结合使用 AOT、即时 (JIT) 编译和配置文件引导型编译。所有这些编译模式的组合均可配置,我们将在本部分中对此进行介绍。例如,Pixel 设备配置了以下编译流程:
最初安装应用时不进行任何 AOT 编译。应用前几次运行时,系统会对其进行解译,并对经常执行的方法进行 JIT 编译。
当设备闲置和充电时,编译守护程序会运行,以便根据在应用前几次运行期间生成的配置文件对常用代码进行 AOT 编译。
下一次重新启动应用时将会使用配置文件引导型代码,并避免在运行时对已经过编译的方法进行 JIT 编译。在应用后续运行期间经过 JIT 编译的方法将会添加到配置文件中,然后编译守护程序将会对这些方法进行 AOT 编译。
ART 包括一个编译器(dex2oat 工具)和一个为启动 Zygote 而加载的运行时 (libart.so)。dex2oat 工具接受一个 APK 文件,并生成一个或多个编译工件文件,然后运行时将会加载这些文件。文件的个数、扩展名和名称因版本而异,但在 Android 8 版本中,将会生成以下文件:
.vdex:其中包含 APK 的未压缩 DEX 代码,以及一些旨在加快验证速度的元数据。
.odex:其中包含 APK 中已经过 AOT 编译的方法代码。
.art (optional):其中包含 APK 中列出的某些字符串和类的 ART 内部表示,用于加快应用启动速度。(配置 ART)
也就是说,dex2oat可以触发APK的AOT编译,并生成对应的产物,APP运行时会加载这些文件。执行过AOT编译的产物能加快启动速度、代码执行效率。
二、代码实现
具体原理还是参考博客:Android性能优化之Android 10+ dex2oat实践。这里说下实现上的细节。
博客的思路是通过一些手段触发系统来进行dex2oat。
1.整体思路
PackageManagerShellCommand.runCompile方法可以触发Secondary Apk进行dex2oat,但是Secondary Apk需要先注册。注册的逻辑在
IPackageManagerImpl.registerDexModule,其中IPackageManagerImpl是PackageManagerService的内部类,并继承了IPackageManager.Stub。最后,再执行
PackageManagerShellCommand.runreconcileSecondaryDexFiles反注册,就大功告成了。
所以整体分三步走:
注册Secondary Apk
执行dex2oat
反注册Secondary Apk
2.注册Secondary Apk
IPackageManager是个AIDL接口,而应用中的ApplicationPackageManage刚好持有这个AIDL接口,因此可以通过其调用registerDexModule方法。
为此,可以通过反射调用registerDexModule方法。以下是核心实现:
// 注册Secondary Apk
private fun registerDexModule(apkFilePath: String): Boolean {
try {
val callbackClazz = ReflectUtil.findClass("android.content.pm.PackageManager\$DexModuleRegisterCallback")
ReflectUtil.callMethod(
getCustomPM(),
"registerDexModule",
arrayOf(apkFilePath, null),
arrayOf(String::class.java, callbackClazz)
)
return true
} catch (thr: Throwable) {
Log.e(TAG, "registerDexModule: thr.", thr)
}
return false
}
/**
* 创建一个自定义的 PackageManager,避免影响正常的 PackageManager
*/
private fun getCustomPM(): PackageManager {
val customPM = cacheCustomPM
if (customPM != null && cachePMBinder?.isBinderAlive == true) {
return customPM
}
val pmBinder = getPMBinder()
val pmBinderDynamicProxy = Proxy.newProxyInstance(
context.classLoader, ReflectUtil.getInterfaces(pmBinder::class.java)
) { _, method, args ->
if ("transact" == method.name) {
// FLAG_ONEWAY => NONE.
args[3] = 0
}
method.invoke(pmBinder, *args)
}
val pmStubClass = ReflectUtil.findClass("android.content.pm.IPackageManager\$Stub")
val pmStubProxy = ReflectUtil.callStaticMethod(pmStubClass,
"asInterface",
arrayOf(pmBinderDynamicProxy),
arrayOf(IBinder::class.java))
val contextImpl = if (context is ContextWrapper) context.baseContext else context
val appPM = createAppPM(contextImpl, pmStubProxy!!)
cacheCustomPM = appPM
return appPM
}
3.执行dex2oat
这里有个难点就是,如何才能调用到PackageManagerShellCommand.runCompile?看下调用逻辑:
// 代码位于PackageManagerService.java。
// IPackageManagerImpl是PackageManagerService的内部类。
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
ResultReceiver resultReceiver) {
(new PackageManagerShellCommand(this, mContext, mDomainVerificationManager.getShell()))
.exec(this, in, out, err, args, callback, resultReceiver);
}
IPackageManager.Stub继承了Binder,而这个方法是Binder中的,调用逻辑如下:
// Binder.java
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
int flags) throws RemoteException {
if (code == INTERFACE_TRANSACTION) {
reply.writeString(getInterfaceDescriptor());
return true;
} else if (code == DUMP_TRANSACTION) {
// 省略部分代码...
return true;
} else if (code == SHELL_COMMAND_TRANSACTION) {
ParcelFileDescriptor in = data.readFileDescriptor();
ParcelFileDescriptor out = data.readFileDescriptor();
ParcelFileDescriptor err = data.readFileDescriptor();
String[] args = data.readStringArray();
ShellCallback shellCallback = ShellCallback.CREATOR.createFromParcel(data);
ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(data);
try {
if (out != null) {
// 重点!!!调用了 shellCommand 方法
shellCommand(in != null ? in.getFileDescriptor() : null,
out.getFileDescriptor(),
err != null ? err.getFileDescriptor() : out.getFileDescriptor(),
args, shellCallback, resultReceiver);
}
} finally {
// 省略部分代码...
}
return true;
}
return false;
}
public void shellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
@Nullable FileDescriptor err,
@NonNull String[] args, @Nullable ShellCallback callback,
@NonNull ResultReceiver resultReceiver) throws RemoteException {
// 这里调用的!!!
onShellCommand(in, out, err, args, callback, resultReceiver);
}
所以这里逻辑清晰了,再次整理下逻辑:
Binder.onTransact收到 SHELL_COMMAND_TRANSACTION 命令会执行 shellCommand方法
shellCommand方法又调用了onShellCommand方法
IPackageManager.Stub继承了Binder
IPackageManagerImpl继承了IPackageManager.Stub并重写了onShellCommand方法
IPackageManagerImpl的onShellCommand执行了PackageManagerShellCommand相关逻辑
所以我们的核心是找到IPackageManager.aidl,并向其发送 SHELL_COMMAND_TRANSACTION 命令。得益于Android Binder机制,我们可以在应用进程拿到IPackageManger的Binder,并通过它来发送命令。
代码实现如下:
// 执行dex2oat
private fun performDexOpt() {
val args = arrayOf(
"compile", "-f", "--secondary-dex", "-m",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) "verify" else "speed-profile",
context.packageName
)
executeShellCommand(args)
}
// IPackageManager.aidl 发送 SHELL_COMMAND_TRANSACTION 命令
private fun executeShellCommand(args: Array<String>) {
val lastIdentity = Binder.clearCallingIdentity()
var data: Parcel? = null
var reply: Parcel? = null
try {
data = Parcel.obtain()
reply = Parcel.obtain()
data.writeFileDescriptor(FileDescriptor.`in`)
data.writeFileDescriptor(FileDescriptor.out)
data.writeFileDescriptor(FileDescriptor.err)
data.writeStringArray(args)
data.writeStrongBinder(null)
resultReceiver.writeToParcel(data, 0)
getPMBinder().transact(SHELL_COMMAND_TRANSACTION, data, reply, 0)
reply.readException()
} catch (t: Throwable) {
Log.e(TAG, "executeShellCommand error.", t)
} finally {
data?.recycle()
reply?.recycle()
}
Binder.restoreCallingIdentity(lastIdentity)
}
4.反注册Secondary Apk
反注册也是执行PackageManagerShellCommand相关方法,只不过给的参数不一样。所以大部分逻辑跟第三步是一样的。代码实现如下:
private fun reconcileSecondaryDexFiles() {
val args = arrayOf("reconcile-secondary-dex-files", context.packageName)
executeShellCommand(args)
}
最后,本项目的代码组织情况如下:
DexOpt:外部调用接口,执行DexOpt.dexOpt即可开启dex2oat。
ApkOptimizerN:负责Android7-Android9的dex2oat逻辑。
ApkOptimizerQ:负责Android10的dex2oat逻辑。也是本文的讲解重点。
三、优缺点
把这项技术应用到了一个插件化项目中,对插件APK进行dex2oat优化,总结下其优缺点。
1.优点
- 插件的加载速度大大增加(实测可以达到90%以上),对插件化框架的冷启动有很大的意义。
- 代码运行的速度有微小的提升。测试了跳转Activity、Service这些场景,能够提升20-80ms左右,跟机型有很大的关系。
2.缺点
- dex2oat产物也会占用一定的存储空间。所以如果插件更新记得及时删除老的oat文件。
- dex2oat 执行时间较长,首次还是建议直接加载插件,在后台执行dex2oat优化。
- 部分手机执行后没有成功生成oat文件,还是存在机型兼容问题。
Android10 dex2oat实践的更多相关文章
- Android性能优化之Android 10+ dex2oat实践
作者:字节跳动终端技术--郭海洋 背景 对于Android App的性能优化来说,方式方法以及工具都有很多,而dex2oat作为其中的一员,却可能不被大众所熟知.它是Android官方应用于运行时,针 ...
- 【腾讯Bugly干货分享】微信热补丁Tinker的实践演进之路
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57ad7a70eaed47bb2699e68e Dev Club 是一个交流移动 ...
- 【腾讯bugly干货分享】微信Android热补丁实践演进之路
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1264& ...
- 微信Android热补丁实践演进之路
版权声明:本文由张绍文原创文章,转载请注明出处: 文章原文链接:https://www.qcloud.com/community/article/81 来源:腾云阁 https://www.qclou ...
- ART模式下基于dex2oat脱壳的原理分析
本文博客地址:http://blog.csdn.net/qq1084283172/article/details/78513483 一般情况下,Android Dex文件在加载到内存之前需要先对dex ...
- webp图片实践之路
最近,我们在项目中实践了webp图片,并且抽离出了工具模块,整合到了项目的基础模板中.传闻IOS10也将要支持webp,那么使用webp带来的性能提升将更加明显.估计在不久的将来,webp会成为标配. ...
- Hangfire项目实践分享
Hangfire项目实践分享 目录 Hangfire项目实践分享 目录 什么是Hangfire Hangfire基础 基于队列的任务处理(Fire-and-forget jobs) 延迟任务执行(De ...
- TDD在Unity3D游戏项目开发中的实践
0x00 前言 关于TDD测试驱动开发的文章已经有很多了,但是在游戏开发尤其是使用Unity3D开发游戏时,却听不到特别多关于TDD的声音.那么本文就来简单聊一聊TDD如何在U3D项目中使用以及如何使 ...
- Logstash实践: 分布式系统的日志监控
文/赵杰 2015.11.04 1. 前言 服务端日志你有多重视? 我们没有日志 有日志,但基本不去控制需要输出的内容 经常微调日志,只输出我们想看和有用的 经常监控日志,一方面帮助日志微调,一方面及 ...
随机推荐
- Web 前端实战:JQ 实现下拉菜单
实现过程 实现一个简易的鼠标悬停菜单项显示其子项的下拉框控件.将用到 CSS 绝对定位.流式布局.动画等:JQuery 鼠标移入和移出事件.DOM 查找.效果图如下: HTML 结构: <div ...
- 超实用在线工具!能将文字加密为Emoji表情
试想一下,如果你需要将一段比较敏感的内容发送给你的好友. 但如果这段内容不小心外泄,被别人看到了,可能会带来很多麻烦. 那么,有什么方法能够让传输的文本内容不那么容易被"看破"呢? ...
- [SDOI2012]走迷宫 (强连通分量缩点,动态规划,高斯消元)
题面 Morenan被困在了一个迷宫里.迷宫可以视为N个点M条边的有向图,其中Morenan处于起点S,迷宫的终点设为T.可惜的是,Morenan非常的脑小,他只会从一个点出发随机沿着一条从该点出发的 ...
- HandInDevil 的头发 (分 块)
题面 H a n d I n D e v i l \rm HandInDevil HandInDevil 的头发很油,因此随时有跳蚤跳上 H a n d I n D e v i l \rm HandI ...
- python 数据挖掘模块学习
项目中用到的模块 API # 模块: import pandas as pd import numpy as np from scipy.optimize import curve_fit numpy ...
- MySQL 在 Kubernetes IPVS 模式下引发的 TCP 超时问题
文章转载自:https://mp.weixin.qq.com/s/XQ2SlCYxvXPY0rRRO-CURA
- RDS MySQL内存管理
官方文档地址:https://help.aliyun.com/product/26090.html?spm=5176.7920929.1290474.7.2c6f4f7bACaToi 官方文档地址:h ...
- Node.js躬行记(23)——Worker threads
Node.js 官方提供了 Cluster 和 Child process 创建子进程,通过 Worker threads 模块创建子线程.但前者无法共享内存,通信必须使用 JSON 格式,有一定的局 ...
- .NET6 JWT(生成Token令牌)
一.Net 6环境下的.net core项目里如何使用JWT. 第一步,在Nuget引入JWT.Microsoft.AspNetCore.Authentication.JwtBearer这两个NuGe ...
- 文本数据挖掘---课后作业shuffle函数洗牌C++
题目: 代码如下:#include <iostream> #include <random> #include <algorithm> #include <v ...