前言

米娜桑,是时候揭开DEX的面纱了!我们都知道multidex,都知道65535方法数超标,那DEX到底是个什么东西呢?或许又有些同学知道DEX会优化为ODEX,那ODEX又是什么鬼,优化了什么呢?为什么ClassLoader热补丁方案插入构造函数导致CLASS_ISPREVERIFIED为false后,会对性能造成影响,和ODEX又有什么关系呢?

我们又知道5.0以上Android虚拟机变成了Art,那DEX在art上变成了什么呢?为什么安装特别耗时间?有时候我看着我的Nexus6安装一个应用在那进度条读啊读的好像卡住了,有一种想砸了它的想法,所以当我拿到Nexus 5测试机的时候,第一件事就是刷到4.4,不然每次安装的效率实在不能忍(捂脸)。

DEX是什么

直接把apk当成zip打开后,第一级目录你就会看见有classes.dex,这就是我们要揭开面纱的东西了。

Why DEX

为什么需要DEX,jar不行吗?相应地,为什么需要Dalvik虚拟机,JVM不行吗?

Dalvik虚拟机是专门为了Android移动平台设计的。目标系统的RAM有限,数据存储在缓慢的内部闪存上,而且性能和上个世纪的周免系统相当。它们运行Linux,来提供虚拟内存,进程和线程,以及基于UID的安全机制。

这些特征和限制使我们聚焦在这些目标上:类数据,尤其是字节码,必须被多个进程共享,以最小化系统内存使用。

启动一个新app的开销必须最小化,来保证设备的可响应。

在独立的文件存储类数据可能导致很多冗余,尤其是字符串。为了保证磁盘空间,我们需要把这些因子提出来。

解析类数据的fields在类加载的时候增加了很多不必要的开销。把数据值直接当成C类型(比如整数或字符串)使用会更好。

字节码验证是必要的,却也是缓慢的。所以我们想在app执行外尽量验证更多,以便不要影响app本身体验。

字节码优化(加速指令,精简方法)对速度和电池生命很重要。

为了安全原因,进程不能编辑共享代码。

典型的虚拟机执行从压缩文件解压独立的类,然后把它们存到heap上。这就导致了每个类可能在每个进程有独立的拷贝,从而使得应用启动变慢,因为代码必须被解压(或者至少需要从磁盘的很多小片段去读取)。另一方面,在本地heap放置字节码简化了首次使用时的指令重写,从而可能导致一些不同的优化。

这些目标指引了一些基本决定:多个类被聚集到一个单个的DEX文件。

DEX文件被映射为只读,并且在进程间共享。

针对本地系统调整字节码顺序和词对齐。

字节码验证对所有类都是强制的,但我们想要对一切可能的进行”预验证(pre-verify)”。

需要重写字节码的优化必须提前执行。

而Dalvik虚拟机和DEX也就应运而生。

Hello DEX

让我们手动来生成一个java,编译成javac,然后转换为dex看看:

1

2

3

4

5

6

7echo 'class Foo {'\

'public static void main(String[] args) {'\

'System.out.println("Hello, world"); }}' > Foo.java

javac Foo.java

dx --dex --output=foo.jar Foo.class

adb push foo.jar /sdcard/

adb shell dalvikvm -cp /sdcard/foo.jar Foo

当我们在dx命令的output中指定输出文件后缀为.jar,.zip,或者.apk,名为classes.dex的文件就会被创建并保存在压缩包内。解开Foo.jar你就会看到classes.dex和META-INF文件夹(里面只有一个MANIFEST.MF文件)。

我们创建完该jar后直接push到设备上,并通过shell直接让dalvik虚拟机去运行它,如果操作无误,会看到命令行的反馈 - Hello, world。

DEX in file system

这次我打算多画点图,所以看图说话吧:

DEX in memory

为什么DEX不能被内存映射,或者说,不能直接从zip去执行呢?因为数据是压缩的,文件头也不保证是词对齐的。这些问题可以通过不压缩直接保存为classes.dex和填充zip文件来解决,但会导致数据网络间传输的包体积变大。

我们需要在使用前把zip包里的classes.dex解压。当我们拿到文件的时候,我们可能还会做些之前提到的其他操作(对齐、优化、验证)。这又引出了另一个问题:谁去负责做这些,我们又该把输出放在哪儿?

ODEX是什么

ODEX,全名Optimized DEX,即优化过的DEX。

有至少3种方法去创建一个“准备好的”DEX文件,即ODEX:虚拟机“即时(just in time)”执行。输出会跑到一个特殊的dalvik-cache目录。这只在一些特殊的桌面和工程机的设备上使用(这些机器的build中,dalvik-cache目录的权限不是严格的)。在生产机器上这是不被允许的。

系统的安装器在程序首次安装时候执行,它有写dalvik-cache的权限。

构建(build)系统预先执行。相关的 jar / apk 文件还在,但classes.dex被剥离出来了。ODEX和原来的zip包保存在一起,不在dalvik-cache,而是系统镜像的一部分。

dalvik-cache目录更准确地说是$ANDROID_DATA/data/dalvik-cache。里面的文件的名字来源于源DEX的完整路径。在设备上该目录被system所拥有,而system拥有0771权限,保存在那里的ODEX被系统和应用的组所拥有,权限为0644。数字权限保护的应用会使用640权限来防止其他应用去检测它们。底线是你可以读取自己的与其他大部分应用的DEX文件,但你不能创建、修改,或删除它们。

前两种方法的执行分为以下三个步骤:

首先,dalvik-cache文件被创建。这必须在一个有恰当权限的进程进行,所以在“系统安装器”的场景,是在运行为root的installd进程执行的。

接着,classes.dex从zip包中解压出来。文件头部留出一小块空间给ODEX header。

最后,文件被内存映射以便访问,并被为当前系统使用进行调整。这包括了字节交换(byte-swapping),结构重新排列(structure realigning),但并没有对DEX文件做有意义的改变。还做了一些其他的基本结构检查,比如确保文件偏移量和数据索引落在有效范围内。

构建系统不在桌面上运行工具,而宁愿去启动模拟器,强制所有相关DEX文件的即时优化,然后从dalvik-cache把结果提取出来。这样做的原因,在解释完优化后会变得更显而易见。

一旦代码被字节替换和对齐,我们就可以继续了。我们添加了一些预计算的数据,在文件头填写ODEX header,然后开始执行。然而,如果我们对验证和优化有兴趣,就需要在初始准备后再插入一个步骤。

dexopt的魔法

在Android 2.3版本以前,系统源码中提供了生成odex的工具dexopt-wrapper,位于Android 2.2系统源码的 build/tools/dexpreopt/dexopt-wrapper/ 目录下,查看DexOptWrapper.cpp文件会发现实际调用的是 /system/bin/dexopt 程序。在5.0及以上版本的设备上,你可能已经再也找不到dexopt了,取而代之的是dex2oat。

我们想要验证和优化DEX文件里的所有类。最简单和安全的方法就是把所有类加载到虚拟机,然后跑一遍。任何加载失败的就是验证/优化失败的。不幸的是,这可能导致一些资源的分配难以释放(比如native共享库的加载),所以我们不想执行在应用运行的虚拟机里。

解决方案就是起一个叫做dexopt的程序(事实上就是虚拟机的后门)。它会执行一个简短的虚拟机初始化,从引导的类路径加载0个或多个DEX文件,然后开始做一切从目标DEX可以做的验证和优化。结束后,进程退出,释放所有资源。

因为多个虚拟机可能同时需求同一个DEX文件,文件锁被用来确保dexopt仅被执行一次。

验证

字节码验证过程包含了扫描DEX文件中每一个类每个方法的指令。目的是为了识别非法指令序列以便不会在运行时才发现它们。涉及到的很多运算对“准确的”GC也是必要的。更多信息见Dalvik字节码验证器笔记。

为了性能原因,(下节描述的)优化器假设验证器已经运行成功,还会做一些其他可能不安全的假设。默认地,Dalvik会坚持验证所有类,并只优化那些被验证过的类。可以使用命令行flags去禁用验证器。怎么在Android应用框架中控制这些功能的指令见控制嵌入式虚拟机。

验证失败的报告是一个复杂的问题。例如,在不同的package中,调用一个package内可见的方法是非法的,会被验证器捕捉到。但我们未必想要在验证期报告它 —— 事实上我们想要在试图调用方法的时候抛出异常。在每个方法调用上检查这些访问flags也是很昂贵的,Dalvik字节码验证器笔记提到了这个问题。

成功被验证的类在ODEX有一个flag被设置了,在加载的时候就不会被重新验证。ODEX文件有一个32位的checksum,但那是主要是用来快速检查数据损坏的。

优化

虚拟机解释器通常会在一段代码被首次使用的时候执行某些优化。常量池引用被指向内部数据结构的指针所替代,总是成功的操作或是那些总会以某种方式工作的,会被更简单的形式所替代。这些的一部分需要仅在运行时可用的信息,另一部分在某些特定假设下可以被静态推论出。

Dalvik优化器做了这些:对于虚方法调用,把方法索引替换为vtable索引。

对于实例变量(field)的get/put,把变量索引替换为字节偏移。另外,把 boolean / byte / char / short 基本变量(variants)合并到单个的32位形式(解释器里更少的代码意味着CPU I-cache里更少的空间)。

替换一些高频次调用,比如把 String.length() 替换成”内联“的。这可以跳过一些常见的方法调用消耗,直接从解释器切换到native实现。

删除空方法。最简单的例子就是Object.,啥都没干,但却必须在任何对象被分配的时候执行。指令会被替换为一个新版本的空指令(no-op)形式,除非调试器被attach上去了。

附加预计算数据。例如,虚拟机想要一个类名的哈希表以便查找。不同于在加载DEX文件时候去计算这个,我们可以先计算,以节省堆(heap)空间和所有加载该DEX文件的虚拟机的计算时间。

大部分的优化显然都会更好。

Hello ODEX

我们继续玩耍之前生成的dex,来做一个odex:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20adb push dexopt-wrapper /sdcard/

adb shell

# 不然没权限去/data/local

su

chmod 777 dexopt-wrapper

# 直接在sdcard执行会提示权限错误

cp dexopt-wrapper /data/local/

cp foo.jar /data/local/

cd /data/local

/dexopt-wrapper foo.jar foo.odex

--- BEGIN 'foo.jar' (bootstrap=0) ---

--- waiting for verify+opt, pid=5220

--- would reduce privs here

--- END 'foo.jar' (success) ---

cp foo.odex /sdcard

exit

exit

adb pull /sdcard/foo.odex .

这样子就拿到了优化后的odex,赶紧把手机还给同事。

打开App,阅读手记

android odex版本调试_Android可执行文件之谜 - DEX与ODEX, OAT与ELF相关推荐

  1. android odex版本调试_Android编译odex版本的控制开关 | 学步园

    build\core\package.mk 中156行改为 LOCAL_DEX_PREOPT := false. true为odex版本,false为非odex版本 默认编译odex版本,如果需要非o ...

  2. android odex版本调试_android 基础-Dalvik ,ART,JIT,AOT,Dex,Odex

    Dalvik 和 ART Dalvik:Dalvik 虚拟机,android 5.0 以前所使用的虚拟机,可执行文件为 dex 格式,基于寄存器的虚拟机(jvm 基于堆栈).通过 dx 工具将 .cl ...

  3. android odex版本调试_Android开启odex开关和开机速度优化

    开odex优化首次开机速度,是牺牲空间换取时间的做法,仅限于空间足够的设备.开了odex之后,在编译的时候,整个system image就会被预先优化.由于在启动时不再需要进行app的dex文件进行优 ...

  4. linux apk 拆分 odex,android平台下,apk、jar、dex、odex、vdex、art文件相互转换,编译和反编译...

    apk.jar.dex.odex.vdex相互转换java 1. ? to jar 1.1 odex to jar 1.2 dex to jar 1.3 apk to jar 输出结果可能有损 1). ...

  5. android odex版本调试_[ROM开发工具]Android 8.0 9.0合并ODEX工具

    编译过程: Clone this repository Install Android NDK if you want to cross-compile for Android devices Inv ...

  6. 【Android 屏幕适配】异形屏适配 ② ( 需要异形屏适配情况 | 需要异形屏适配的 Android 系统版本 | 刘海屏状态判定 | 异形屏适配调试 - 华为云调试 )

    文章目录 一.需要异形屏适配情况 1.需要异形屏适配的 Android 系统版本 8.0 2.正常有状态栏的界面竖屏不需要适配 3.正常有状态栏的界面横屏需要适配 4.刘海屏状态判定 二.异形屏适配调 ...

  7. Android高版本P/Q/R源码编译指南

           Android高版本P/Q/R源码编译指南 Android源码编译系列博客: Android.bp你真的了解吗 Android.bp入门指南之Android.mk转换成Android.b ...

  8. Android .dex、.odex、Dalvik、ART、AOT、OAT

    文章来源:https://www.jianshu.com/p/e52b7e460748 https://zhuanlan.zhihu.com/p/53723652 目的 理清 .dex..odex.A ...

  9. Android开发和调试

    Android开发和调试 分类: Andriod 研究2010-11-03 16:04 13499人阅读 评论(0) 收藏 举报 androideclipse工具antjunitide 目录(?)[+ ...

最新文章

  1. Kubelet源码分析(一):启动流程分析
  2. C# 用tabcontrol实现窗体类似网页排版的显示
  3. java 注解的几大作用及使用方法详解(转载)
  4. ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
  5. 禅道项目管理_推荐一个项目管理工具,落地基于Scrum的敏捷开发!
  6. GridView中不能用If?
  7. [zsh] restart a zsh process
  8. C#在控制台工程中嵌入winform窗体
  9. shiro简单配置教程
  10. 计算机蓝屏代码0x0000007b,开机出现蓝屏代码0X0000007B原因分析及解决方法
  11. Android——TextView实现真正的跑马灯效果
  12. 思路分享——hdu 3233
  13. 关于paypal账户限制的话题
  14. 目标框选之单阶段与两阶段目标检测区别
  15. java assist_Java-Javaassist(一)
  16. 百度/Google等搜索引擎的信息检索搜索技巧总结
  17. 升级合作伙伴计划,实现全面赋能
  18. docker 根据镜像名称查找容器并删除
  19. springboot大学生兼职网站毕业设计源码311734
  20. 微信公众号发送消息 Java

热门文章

  1. SAP C4C OData服务的filter,客户端分页和排序的使用方式
  2. SAP OData schema version and backend version
  3. SAP UI5 group function调试
  4. SAP云平台CloudFoundry上部署应用的log分析
  5. jMeter 线程启动时间
  6. ubuntu上wordpress安装的前置工作
  7. for-each keyword
  8. Cookie test
  9. SAP OData实现错误消息:OPPORTUNITIES_GET_ENTITY not implemented in data provider class
  10. Require Busy dialog