最近看到一篇博客: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文件,还是存在机型兼容问题。