比特币最新价格

比特币(Bitcoin)的概念最初由中本聪在2008年11月1日提出,并于2009年1月3日正式诞生, 根据中本聪的思路设计发布的开源软件以及建构其上的P2P网络。

Dex(得心应手)

比特币最新价格比特币价格行情2025-09-23 14:36:1793

  版权声明

  作者:所为

  本文为作者投稿,转载请联系作者。

在做 Android 应用研发时,尤其是开发大型应用时,我们很容易遇到 Android 方法超过 65536 的现象。即便进行分 dex 处理,在功能日益增加的今天,主 dex 依然会面临方法数不够用的窘境,然后不得不通过各种压缩、裁剪代码,才得以上线。虽然现在已有广为人知的现成解决方案,然墨子有云:“治于神者,众人不知其功,争于明者,众人知之”,回想起这几年间 Android 程序员和方法数之间林林总总的相爱相杀,发现很多问题既没有事前疏导,也缺乏事后防范总结,所以此刻谈谈方法数这个问题的本源,对达到“治于神”这一境界是存在其必要性的。

  一、引子

方法,对于开发者来说是程序中一段代码的定义,而对于执行方(OS、虚拟机、解释器等)更多是一个存储在可执行对象(C 的 ELF、Windows 的 PE、Java 的 Jar 等)中的符号或指令。方法数并非新奇概念,Java 的 Class 文件中已有定义,ELF 的符号表也有隐含体现,类似的还有变量数等定义。在 Android 平台大行其道之前,对方法数讨论的问题不多,直到 Facebook 2013 年的一篇文章 [1],才提到一些大型应用会遇到的两个方法数问题:

dex 方法数超标

linearAlloc 存储方法数的空间在 Android 2.3 及以下只有 5 MB

当时国内少数巨无霸应用在遇到这类问题后,也根据 Facebook 这篇文章的思想实现了分 dex 的方案(如下图的代码片段);甚至完成对 linearAlloc 的修改,但 Android 2.3 及以下的机器份额日益减少,这个兼容已不再重要。

随着非 BAT 企业对繁荣和需求的进一步诉求,遇到 Android 方法数问题的产品也日益增多。通过对 dex 格式进行分析,会发现 dex 本身并没有对方法数进行限制,而 dex 方法数受约束的真正原因源于 dex 字节码的设计:

The storage unit in the instruction stream is a 16-bit unsigned quantity

由于字节码在调用方法时,必须显示寻址方法在 dex 存储的索引,即 meth@BBBB[2]。BBBB 的含义是每个四位,四个 B 就是十六位,所以最多支持 2^16 个方法。为保护 dex 字节码的执行,所以在生成、合并 dex 时会对方法数、变量等进行检查和保护。Google 在 Android 5.0 已推出分 dex 的 workaround: Multidex,虽然不够完美,但已经使得这类问题的解决开始趋向集中。

  二、 正文

实际上,控制方法数问题的根本要义在于减少打入到 dex 中的方法。Dex 是 dalvik 虚拟机的字节码文件,Class 是 Java 虚拟机的字节码,虽然两者在格式、语法和实现上有一些差别,但本质上还是存在一些映射关系,如图:

与 class 格式类似,dex 用一段连续的空间存放方法的索引集,每个方法被一个 method_id_item 数据结构所描述,由 class_idx、 proto_idx、 name_idx 三个元素组成 [3,4], 它们分别代表方法所在类类型索引、方法声明的索引以及方法名的索引。

如下图所示,Dex 中所有方法均来自 Android 的 Java 代码(也不排除其他语言可以被编译为 dex 格式的情况),通过 Android 打包的 dx 工具,我们能将编译为 class 的 Java 文件转化为 dex。

定位 dex 方法的来源的关键在于找到其所属的 Java 文件,按图索骥可知 Java 文件的来源无非几种情况:

引入的 aidl 文件

参与编译的 Java 源码

根据资源生成的 R 文件

依赖的其他库(会被一同打入到编译结果的)

事实上我们工程中 99% 的方法都来自开发者创建的 Java 文件或者引入的库,那么 Java 这门语言到底会在哪些对方法数产生何种影响?

Dex(得心应手)

  1. 调用的真相

定义方法的根本目的就是要调用它。为了说明调用方法的意义,下图给出一个简单的示例:声明两个类 MainActivity 和 Test,这两个类都有一个 foo 函数,里面执行了 Activity 的 startActivity。

反编译生成的 APK,得到 dex 对应的 smali 文件(smali 是 dex 的汇编器,和 dalvik 一样都是冰岛语,是一脉相承的东西)。可以看到调用 Activity 的 startActivity 的字节码出现在 Test 和 MainActivity 中。

Dex(得心应手)

那么这种方法的调用会不会增加 dex 的方法?先记录下当前的方法数为 24 个。

继续验证,这次只改动一个地方:将 Test 类中 foo 函数的参数类型改为 MainActivity。依旧是调用库方法,不同的是调用者的类型由父类 Activity 变成子类 MainActivity。

经过反编译分析,发现 smali 红框中的方法其所在的类也相应地变为 MainActivity,再计算方法数变为 25,增加 1 个。所以即便是调用方法,也会增加方法数。

导致方法增加的事实是:当类 A 的实例 a 调用了被 invoke-virtual 所修饰的方法 f。在编译期,A 的 字节码中会增加方法 f(如果 f 不在 A 中),即便 f 没被 A 复写或者 f 在 A 的父类中被标记为 final,也阻止不了编译器这样的行为,这是由于虚拟机要实现多态特性而决定的。在运行期,当虚拟机执行到 A 的实例 a 调用 f,如找不到 f 则会出现 NoSuchMethodException。

因为多态和复写是 OO 最常见的编程手段,假如滥用继承且祖先类中的方法很多,那么所有祖先类定义过的方法都会添加到子类中,从而导致方法数膨胀。所以除了进行字面意义上地减少方法,还可以从设计角度来解决这类问题。

综上所述,决定一个方法的三个要素是方法参数列表和返回值、方法名称以及该方法所在的类,修改任何三要素之一都会导致方法数的增加。换一个角度思考,其实不同 class 文件中的相同方法符号会在生成 dex 时被合并,这也是我认为 dex 和 class 两者设计理念的最大区别:dex 格式提供聚合能力。

至于用栈还是寄存器来实现相比顶层设计的意义便没有那么显著。其实这个优化思路更早的痕迹出现在 C 语言的链接器中,如下图所示,链接器通过合并目标文件相似段(ELF 格式)来获取更好的性能和扩展性,这个过程和 dx 将 一系列 class 转化为 dex 如出一辙。

  2. 甜蜜的负担

纵观世界编程语言发展史,java 经常被拿来与 C# 对比,但两者的发展理念早已大相径庭。例如 C# 吸取了很多语言的特点,也更像一个大杂烩,很早就提供了 lambda 表达式、 async 关键字以及丰富的异步 api 接口,看过去的确琳琅满目、功能强大且能帮助快速开发,但实质上如果不清楚其内部原理和实现机制,很容易使用不当且造成隐晦甚至是灾难性的后果。java 在这方面并没有亦步亦趋,更像是一个按着既有计划前进的长者。

为了让使用者更为得心应手,Java 每个版本也持续都引入了不少新特性,例如 1.1 的内部类、1.5 的泛型、1.8 的 lambda 等,满足了开发者不同的诉求。

这里我们来看看语法糖对方法数的影响,下面两个文件分别在类 Test 中定义了 foo 和 toArray 两个方法,类 Test2 继承 Test,并重写了 foo 的返回值。

我们发现 foo 返回值的类型被改写,基类 Test 中的方法 foo 返回的是 Object,而子类 Test2 返回的是 Object 的子类 Long,这种用法的好处在于为多态提供了更多的扩展性,能够让子类的实现更为聚焦,平时一些常见的程序库中就采用了类似用法。分析和对比字节码发现:子类 Test2 中会存在两个 foo 方法,原因是编译器会在子类 Test2 中生成(synthetic)一个与父类一致的方法来做被复写类型的方法的桥接(bridge),从而实现这一便捷的语法。

继续查看泛型方法 toArray 的字节码,其底层是使用 Signature 字节码关键字来标记被擦除前的类型信息,而泛型本身并没有引入额外的方法。

除此之外,java 中最常见的语法就是使用大量的内部类、匿名类,这一块比 C++ 方便不少。在类 Test 中我们使用匿名类和内部类来观察他们对方法数的影响。

类 Test 中的内部类和外部类会相互访问一些具有 private 权限的方法和变量:

继承 Runnable 的匿名类 Test$1 会访问到外部类 Test 的私有变量,

外部类 Test 访问 静态内部类 Test$CS 和内部类 Test&C1 定义的私有方法

对于匿名类访问外部私有变量的情况,可以发现 Test$1 会通过 Test 的 access$000 静态方法来获取其私有变量的值,access$000 是编译期在 Test 中生成。

对于外部类访问内部类私有方法的情况,也会生成相应的静态方法 access$xxx 来帮助突破限制。

值得注意的是 Test 访问内部类的 private 变量却没有增加方法。这是因为由于 Test$CS 和 Test$C1 是常量,编译期就已经确定 c1_.I + CS.I 的值。 同理,如果这些变量不被 final 修饰为常量,那么编译器也会为它们生成 access$xxx 方法来突破访问限制。

综上所述,语法糖的本质是带给开发者以更为便利的使用,这种便捷如果建立在与语言原有设定不一之处,要不就是缺陷,要不就是编译器在背后做了不少。无论是复写返回值还是突破访问权限或者一些类似 lambda 等新语法,它们无一例外地以增加方法、内部类等字节码为代价来实现这种便捷,通过这种手段来屏蔽掉一些不重要的细节,将最令人关心的特性呈现给开发者。

  3. 结构的背后

如果要书写一个 Java 文件,难免要在 abstract class、annotation、class、enum、interface 这五种结构中选取或者组合,它们又在方法数上又有何差异?我们定义这五种结构最简实现,即没有任何方法和成员(用 T_XX.java 命名,XX 表示这些结构前两个字的缩写),来看看不同结构对方法数的影响。

通过反编译 smali 文件分析可得:

这里篇幅问题就不列出字节码了,综上所述:

接口和注解没有引入方法,字节码的大小也是最少

类和抽象类引入了一个方法(会调用 Object 的默认构造函数),大小理论上相同。(除去类名长度等因素,上图 T_AB 与 T_CL 的字节码大小相差 9 个字节是因为抽象类的描述比类的多了 abstract 关键字 加 1 个空格所致)

枚举引入了 1 个默认构造函数和 3 个静态方法,所需的字节码最多,是其他结构的数倍甚至二十倍。枚举有其特性和优雅之处,但使用过多也会对方法数和程序大小产生影响。

  三、 小结

本文简单介绍了方法数的来龙去脉,优化方法数的文章也很多,但最有效的还是要在设计初期就把该问题考虑进去,这里只聊点 the principle underlying。具体的优化和设计方案如对引入依赖的处理、怎样避免方法数膨胀等还需围绕原则,结合项目实际特点来选取和开展。

  四、 资料

[1] https://www.facebook.com/notes/facebook-engineering/under-the-hood-dalvik-patch-for-facebook-for-android/10151345597798920

[2] https://stackoverflow.com/questions/21490382/does-the-android-art-runtime-have-the-same-method-limit-limitations-as-dalvik/21492160#21492160

[3] https://pallergabor.uw.hu/androidblog/dalvik_opcodes.html

[4] https://www.netmite.com/android/mydroid/dalvik/docs/dex-format.html

活动推荐:

由InfoQ主办的第二届GMTC全球移动技术大会即将来袭!大会将于6月9-10日在北京举行。本届大会,我们将探讨智能时代的大前端,2017年都有哪些值得关注的大前端趋势和实践?或点击阅读原文进入大会官网,现在报名享8折优惠!

上一篇:比特币全网算力(比特币全网算力是什么意思)

下一篇:xrp最新价格(Xrp最新价格人民币)

猜你喜欢

网友评论