• Android10 dex2oat实践


    最近看到一篇博客:Android性能优化之Android 10+ dex2oat实践,对这个优化很感兴趣,打算研究研究能否接入到项目中。不过该博客只讲述了思路,没有给完整源码。本项目参考该博客的思路,实现了该方案。

    源码地址:https://github.com/carverZhong/DexOpt

    一、dex2oat 详解

    以下是官方对于dex2oat的解释:

    ART 使用预先 (AOT) 编译,并且从 Android 7.0(代号 Nougat,简称 N)开始结合使用 AOT、即时 (JIT) 编译和配置文件引导型编译。所有这些编译模式的组合均可配置,我们将在本部分中对此进行介绍。例如,Pixel 设备配置了以下编译流程:

    1. 最初安装应用时不进行任何 AOT 编译。应用前几次运行时,系统会对其进行解译,并对经常执行的方法进行 JIT 编译。

    2. 当设备闲置和充电时,编译守护程序会运行,以便根据在应用前几次运行期间生成的配置文件对常用代码进行 AOT 编译。

    3. 下一次重新启动应用时将会使用配置文件引导型代码,并避免在运行时对已经过编译的方法进行 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.整体思路

    1. PackageManagerShellCommand.runCompile方法可以触发Secondary Apk进行dex2oat,但是Secondary Apk需要先注册。

    2. 注册的逻辑在IPackageManagerImpl.registerDexModule,其中IPackageManagerImplPackageManagerService的内部类,并继承了IPackageManager.Stub

    3. 最后,再执行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文件,还是存在机型兼容问题。
  • 相关阅读:
    GPT learning
    Android发布依赖到 Jitpack
    DSPE-PEG-DBCO,DBCO-PEG-DSPE,磷脂-聚乙二醇-二苯并环辛炔科研试剂
    网络刷卡器开发,刷新移动物联新生活
    网络攻防中黑客常用技术跨站脚本技术:html, js 的自解码机制,解码顺序,浏览器urlencode 的影响,测试样例
    shell脚本的函数
    《ON JAVA》学习笔记2:Java如何谈数据类型
    MobileViT
    计算机设计大赛 疲劳驾驶检测系统 python
    Vue3:proxy数据取值proxy[Target]取值
  • 原文地址:https://www.cnblogs.com/carvergit/p/16854150.html