5 种优化 Android 5.0 Lollipop 代码的方法





0/5 (0投票)
在本文中,我们将介绍 ART 中的一些新功能,将其与之前的 Android Dalvik* 运行时进行基准测试,并分享五条开发人员可以进一步提高应用程序性能的技巧。
英特尔® 开发人员专区提供跨平台应用开发的工具和操作信息、平台和技术信息、代码示例以及同行专业知识,帮助开发人员创新和成功。加入我们的Android、物联网、英特尔® 实感™ 技术和Windows社区,下载工具、访问开发套件、与志同道合的开发人员分享想法,并参与黑客马拉松、竞赛、路演和当地活动。
引言
随着 Android 5.0 Lollipop* 的发布,引入了一个创新的默认运行时环境,名为 ART*(Android RunTime 的缩写)。它包含许多增强功能,可以提高性能。在本文中,我们将介绍 ART 中的一些新功能,将其与之前的 Android Dalvik* 运行时进行基准测试,并分享五条开发人员可以进一步提高应用程序性能的技巧。
ART 中有什么新功能?
在 Dalvik 运行时对许多 Android 应用程序进行分析后,发现最终用户面临两个关键痛点:应用程序启动时间长和卡顿量大。卡顿是指应用程序因无法跟上屏幕刷新率而出现停顿、抖动或完全停止的情况,这是由于帧设置时间过长造成的。当一帧比前一帧快得多或慢得多时,它就被定义为卡顿。用户会将卡顿视为不流畅的运动,这使得用户体验不如用户和开发人员所希望的那样流畅。为了解决这些问题,ART 中有几个新功能:
- 提前编译:在安装时,ART 使用设备上的 dex2oat 工具编译应用程序,并为目标设备生成编译后的应用程序可执行文件。相比之下,Dalvik 使用解释器和即时编译,在安装时将 APK 转换为优化的 dex 字节码,并在应用程序运行时进一步将优化的 dex 字节码编译为热路径的本机机器代码。结果是应用程序在 ART 下启动更快,但代价是安装时间更长。应用程序在 ART 下也会使用设备上更多的闪存空间,因为安装时编译的代码会占用额外的空间。
- 改进的内存分配:需要大量分配内存的应用程序在 Dalvik 上可能会遇到性能迟缓的问题。单独的大对象空间和内存分配器的改进有助于缓解这个问题。
- 改进的垃圾回收:ART 具有更快、更并行的垃圾回收功能,从而减少了内存碎片并更好地利用了内存。
- 改进的 JNI 性能。 优化的 JNI 调用和返回代码序列减少了进行 JNI 调用所需的指令数量。
- 64 位支持:ART 充分利用 64 位架构,提高了许多应用程序在 64 位硬件上运行时的性能。
这些功能共同改善了仅使用 Android SDK 编写的应用程序以及进行大量 JNI 调用的应用程序的用户体验。用户还可以受益于更长的电池续航时间,因为应用程序只编译一次并执行更快,因此在日常使用中消耗的电量更少。
比较 ART 和 Dalvik 的性能
当 ART 作为 Android KitKat 4.4 的预览版首次发布时,其性能受到了一些批评。这不是一个公平的比较,因为早期预览版的 ART 与完全成熟和优化的 Dalvik 进行了比较,结果是一些应用程序在 ART 下运行比 Dalvik 下运行更慢。
我们现在有机会将消费者就绪的 ART 版本与 Dalvik 进行比较。由于 ART 是 Android 5.0 中唯一的运行时,因此只有将最近从 Android KitKat 4.4 更新到 Android Lollipop 5.0 的设备进行比较,才能进行 Dalvik 和 ART 的并行比较。为此,我们使用 TrekStor SurfTab xintron i7.0* 平板电脑(搭载 Intel® AtomTM 处理器)进行了测试,最初运行 Android 4.4.4 和 Dalvik,然后更新到 Android 5.0 和 ART。
由于我们正在比较不同版本的 Android,我们看到的一些改进可能来自 ART 之外的 Android 5.0 改进,但根据我们的内部性能分析,我们发现 ART 是大多数改进的原因。
我们运行了基准测试,其中 Dalvik 积极优化重复执行代码的能力有望为其带来优势,以及英特尔自己的游戏模拟。
我们的数据显示,ART 在我们测试的五个基准测试中表现优于 Dalvik,在某些情况下显著。
有关这些基准测试的更多信息,请参阅以下链接
- Quadrant 2.1.1*
- CaffeineMark 3.0*
- Smartbench 2012 productivity*
- Antutu 4.4 总体*(请注意,我们测试的版本已不再提供下载)
- CF-Bench 1.3 总体*
IcyRocks 1.0 版本是英特尔开发的一种工作负载,旨在模拟真实世界的游戏应用程序。它使用开源 Cocos2d* 库以及 JBox2D*(一个 Java 物理引擎)进行大部分计算。它测量在各种负载级别下每秒可以渲染的平均动画数(帧),然后通过取这些各种负载级别下 FPS 的几何平均值来计算最终指标。它还测量卡顿程度(每秒卡顿),即在各种负载级别下每秒卡顿帧的平均值。它显示 ART 比 Dalvik 性能有所提高
IcyRocks 1.0 版本还表明,ART 比 Dalvik 更一致地渲染帧,卡顿更少,因此用户体验更流畅。
根据这项性能评估,很明显 ART 已经提供了比 Dalvik 更好的用户体验和性能。
将代码从 Dalvik 迁移到 ART
从 Dalvik 到 ART 的过渡是透明的,大多数在 Dalvik 上运行的应用程序应该可以在 ART 上运行,无需修改。因此,当用户升级到新的运行时时,许多应用程序将看到性能提升。仍然建议使用 ART 测试您的应用程序,特别是如果它使用 Java 本机接口,因为ART 的 JNI 错误处理比 Dalvik 更严格,如本文所述。
优化代码的五个技巧
大多数应用程序将因上述 ART 的改进而获得性能提升。此外,您可以采用一些实践来进一步优化您的应用程序以适应 ART。对于下面的每种技术,我们都提供了一些简化代码来演示其工作原理。
由于所有应用程序都不同,并且由此产生的性能在很大程度上取决于周围的代码和上下文,因此无法提供您可以预期的性能提升指示。但是,我们将解释为什么这些技术可以提高性能,我们建议您在自己的代码上下文中测试它们,以查看它们如何影响您的性能。
我们在此提供的技巧广泛适用,但在 ART 的情况下,从 dex 文件生成二进制可执行代码的 dex2oat 编译器将实现这些优化。
技巧 #1 – 尽可能使用局部变量而不是公共类字段
通过限制变量的范围,您不仅可以使代码更具可读性且不易出错,还可以使其更易于优化。
在下面的未优化代码中,v 的值在应用程序运行时计算。这是因为 v
可以从方法外部访问,并且可以被任何代码更改,因此其值在编译时未知。编译器不知道 some_global_call() 操作是否更改了 v
,因为 v
可以被方法外部的任何代码更改。
在优化代码中,v
是一个局部变量,其值可以在编译时计算。结果,编译器可以直接将结果放入代码中,避免在运行时进行计算。
未优化代码
class A { public int v = 0; public int m(){ v = 42; some_global_call(); return v*3; } }
未优化代码
class A { public int m(){ int v = 42; some_global_call(); return v*3; } }
技巧 #2 – 使用 final 关键字提示值是常量
final
关键字可以用来保护您的代码免受意外修改应该为常量的变量,但也可以通过向编译器提示值是常量来提高性能。
在下面的未优化代码中,v*v*v
的值必须在运行时计算,因为 v
的值可能会改变。在优化代码中,在为 v
赋值时使用 final
关键字告诉编译器这个值不会改变,因此可以在编译期间执行计算,并将结果添加到代码中,从而无需在运行时计算它。
未优化代码
class A { int v = 42; public int m(){ return v*v*v; } }
优化代码
class A { final int v = 42; public int m(){ return v*v*v; } }
技巧 #3 – 对类和方法定义使用 final 关键字
由于 Java 中的所有方法都可能是多态的,因此将方法或类声明为 final 告诉编译器该方法未在任何子类中重新定义。
在下面的未优化代码中,在进行调用之前必须解析 m()
。
在优化代码中,因为方法 m()
被声明为 final,编译器知道将调用哪个版本的 m()
。因此,它可以避免方法查找并内联调用,将对 m()
的调用替换为其方法的内容。这会带来性能提升。
未优化代码
class A { public int m(){ return 42; } public int f(){ int sum = 0; for (int i = 0; i < 1000; i++) sum += m(); // m must be resolved before making a call return sum; } }
优化代码
class A { public final int m(){ return 42; } public int f(){ int sum = 0; for (int i = 0; i < 1000; i++) sum += m(); return sum; } }
技巧 #4 – 避免对小方法进行 JNI 调用
使用 JNI 调用有很好的理由,例如当您有 C/C++ 代码库或库可以重用、您需要跨平台实现或您需要提高性能时。但重要的是要尽量减少 JNI 调用的数量,因为每个调用都会带来显著的开销。当 JNI 调用用于优化性能时,这种开销可能导致无法实现预期的收益。特别是,频繁调用短 JNI 方法可能会适得其反,并且将 JNI 调用放入循环中可能会放大开销。
代码示例
class A { public final int factorial(int x){ int f = 1; for (int i =2; i <= x; i++) f *= i; return f; } public int compute (){ int sum = 0; for (int i = 0; i < 1000; i++) sum += factorial(i % 5); // if we used the JNI version of factorial() here // it would be noticeably slower, because it is in a loop // and the loop amplifies the overhead of the JNI call return sum; } }
技巧 #5 – 使用标准库而不是在您自己的代码中实现相同的功能
标准 Java 库经过高度优化,并且通常使用内部 Java 机制来获得最佳性能。它们可能比在您自己的应用程序代码中实现相同功能时工作得快得多。试图避免调用标准库的开销实际上可能会导致性能降低。在下面的未优化代码中,存在自定义代码以避免调用 Math.abs()
。但是,使用 Math.abs()
的代码工作得更快,因为 Math.abs()
在编译时被 ART 中的优化内部实现替换。
未优化代码
class A { public static final int abs(int a){ int b; if (a < 0) b = a; else b = -a; return b; } }
优化代码
class A { public static final int abs (int a){ return Math.abs(a); } }
ART 中的英特尔优化
英特尔与原始设备制造商合作,提供了一个优化的 Dalvik 版本,可在英特尔处理器上提供更好的性能。英特尔正在 ART 中进行相同的投资,因此在新运行时上性能将进一步提高。优化将通过 Android 开源项目 (AOSP) 和/或直接通过设备制造商提供。与以前一样,这些优化对开发人员和用户都是透明的,因此无需更新应用程序即可受益。
了解更多
要了解有关优化英特尔处理器 Android 应用程序的更多信息,以及发现英特尔® 编译器,请访问英特尔开发人员专区:https://software.intel.com。
关于作者
Anil Kumar 在英特尔公司工作了超过 15 年,在软件和服务部门担任过各种职务。他目前是高级员工软件性能架构师,并通过为标准组织、多个基准测试(SPECjbb*、SPECjvm2008、SPECjEnterprise2010 等)、通过实现更好的用户体验和资源利用率的客户应用程序以及硬件和软件配置的默认性能做出贡献,在 Java 生态系统中发挥积极作用。
Daniil Sokolov 是英特尔软件和服务部门的高级软件工程师。Daniil 在过去 7 年中一直专注于 Java 性能的各个方面。他目前致力于改善英特尔 Android 设备上的用户体验和 Java 应用程序性能。
Xavier Hallade 是英特尔软件和服务部门驻法国巴黎的开发人员布道师,他在那里从事各种 Android 框架、库和应用程序的工作,帮助开发人员改进对新硬件和技术的支持。
他还是 Android 领域的 Google Developer Expert,专注于 Android NDK 和 Android TV。