安卓 TV 端应用的更新比较困难,一方面是受限于各个设备厂商的规则,应用更新策略比较慢,另一方面是 TV 用户主动更新的意愿比较低。因此插件化热更新在安卓 TV 端就成为了有效更新应用业务能力的必要技术手段,使用插件化热更新技术能在不安装新版 apk 的前提下,自动地更新整个应用的业务能力,不受系统本身的限制,不需要用户选择,极大地提升新版本的覆盖率。
插件化技术本质上是深度挖掘安卓系统的私有能力,需要对各个版本的安卓系统进行适配。不但要保证低版本的低性能设备运行顺畅,也要保证高版本的系统是可以使用插件化升级的。随着 TV 端高版本系统的比例逐渐增大,对高版本系统的适配成为了插件化技术的主要挑战。为支持低性能设备的首次安装运行顺畅,我们的插件化框架将插件加载功能与 TV 业务逻辑放在了同一个 dex 中,但这样的架构在高版本系统上又会产生内联崩溃的问题,在既要保证低端设备性能的同时,又要覆盖到高版本设备的前提下,内联崩溃是一个无法简单绕过的一个问题。
本文从内联崩溃的背景开始介绍,再结合 TV 端的插件化特点深入分析产生的原因,最后给出 TV 端的解决方案。
01
背景
什么是内联
内联(inline)是一种编译优化方法。编译器自动地将函数体的代码插入到这个函数的调用处,将函数调用展开为函数体的代码。这种优化能消除函数调用的开销,但会使指令数量膨胀。
下面用 Kotlin 语言中的内联举例:
Kotlin 的内联使用 inline 关键字定义内联,将内联的选择权完全交给了使用者,只要写了 inline 关键字就一定会内联。另外,Kotlin 的内联触发时机是编译,在 Kotlin 编译为字节码的时候就会将 inline 函数调用展开,生成的字节码不会调用内联方法,而是直接将指令插入到调用处。
Kotlin 语言的内联可以说是最简单的一种内联,而本文要分析的内联崩溃的内联是 ART 虚拟机在将字节码编译为机器指令时进行的内联。这种内联是完全自动的,由 ART 虚拟机自己决定,没有语言层面的机制去触发它,而且根据不同版本的 ART 虚拟机,内联的规则和细节都可能不同。它的触发时机是运行时,也就是发生在安卓设备运行应用时,而不是在开发期间。具体来说,是发生在 JIT 和 AOT 的过程中的。
内联崩溃的场景
1、特点和条件
内联崩溃是一种 native 崩溃,有明确的 abort message,容易辨认,发生在以下场景:
崩溃发生时一定在运行 epg 插件(或者叫宿主插件),one 版本时不可能发生。
只有 epg 插件有这个问题。投屏、设置、体育等所有功能插件没有这个问题。
只发生在 Android 9.0(P) 系统上。
触发的概率较低,但在大量用户的基础上有一定量级,奇异果的各个版本都能在 APM 中搜到这种崩溃。在 APM 上一直存在,但没有针对性地统计过具体在 9.0 系统的崩溃率。
这个内联崩溃问题本地不容易复现,需要长时间运行 one 版本才有可能触发内联,进而在运行插件版时发生崩溃。
这个内联崩溃是安卓插件化领域已知的问题,tinker 有公开分享的解决方案,但对于我们并不适用,后文会详细说明。
2、崩溃现场
Native 崩溃 abort message:
entrypoint_utils-inl.h:94] Inlined method resolution crossed dex file boundary: from void com.gala.video.plugincenter.download.downloader.DownloadManager$TaskHolder.progress(com.gala.video.module.plugincenter.bean.download.DownloadItem, long, long, long, b...
<