Android疑难杂症——因内联优化导致9.0机型Native Crash

一、起因

        近期我们APP发现线上一小部分用户出现了“诡异”的Native Crash,奇怪的是这些用户的机型都是Android 9.0。分析log发现有这样一段信息:“Inlined method resolution crossed dex file boundary” ,直译过来就是说“内联方法解析越过了dex文件的边界”。我的内心不禁充满了疑惑,什么是内联方法?为什么会出现内联方法越界的这种情况?为了解决这个莫名奇妙的Crash,下面就来分析下为什么会出现这种情况。

二、什么是内联优化

        对于Java程序来说,除了开发者本身对代码优化之外,还有一个“人”也在背后默默的优化我们的代码,这个"人"就是JVM。JVM会帮我们分析出热点代码,优化代码逻辑。其中JVM最常做的优化之一就是:方法内联优化

        什么是方法内联?又可以叫做函数内联,Java中方法可等同于其它语言中的函数。关于方法内联维基百科上面解释是:

计算机科学中, 内联函数(有时称作 在线函数编译时期展开函数)是一种 编程语言结构,用来建议 编译器对一些特殊 函数进行 内联扩展(有时称作 在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方( 上下文),从而节省了每次调用函数带来的额外时间开支。

        简单通俗的讲就是把方法内部调用的其它方法的逻辑,嵌入到自身的方法中去,变成自身的一部分,之后不再调用该方法,从而节省调用函数带来的额外开支。

        举例来说,若有这样的调用链:method1->method2->method3->method4,则在四个方法都满足内联条件的情况下,最终内联的结果将是method1包含method2,method3,method4的代码,method2包含method3,method4的代码。

        而对于Android平台来说,内联优化的策略并不是一开始就被引入的。在Android 4.4之前的Dalvik VM是没有内联优化的,ART取代Dalvik之后,由于为了提升APP的运行效率,于是有了Dex文件预编译成目标平台的机器码的dex2oat的过程,所以新增了方法的内联优化。

        这里你可能会疑惑,不是ART就已经引入内联优化了吗?为什么只在Android 9.0机型会有内联优化的问题?这是因为Google在Android P上做了一个“骚操作”:

Android P内联优化新增检测项
Google在Android P中添加了新的检测项,对国内大多数应用造成了严重影响:在调用resolve inline method时,如果检测到caller与callee处于不同的dex file,会主动发起abort(inline不允许跨dex文件),导致应用出现闪退等异常问题。

        至此,我们终于搞清楚了什么是内联优化,也知道了为什么Android 9.0机型会出现内联优化的问题。

三、解决因内联优化所引起的Crash

(一)内联优化的条件

        要想解决这个问题,我们首先还要知道,什么情况下会触发内联优化。由于Android生成oat文件时Compiler有多种实现,因此内联条件也有所不同:

        对于Quick Compiler,当以下条件均满足时被调用的方法将被inline:

  1.  App不是Debug版本的;
  2. 被调用方法的实现满足下列条件之一:
  • 2.1. 空方法; 
  • 2.2. 仅返回方法参数; 
  • 2.3. 仅返回一个方法内声明的常量或null; 
  • 2.4. 从被调用方法所在类的非静态成员获取并返回获取的值;(注意,static final成员会被优化成常量,此时要参照2.3) 
  • 2.5. 仅设置了被调用方法所在类的非静态成员的值; 
  • 2.6. 仅设置了被调用方法所在类的非静态成员的值,并返回一个方法内声明的常量或null。

注:条件2隐含了一个条件,就是被调用的方法的字节码不超过2条。

        对于Optimizing Compiler,当以下条件均满足时被调用的方法将被inline:

  1. App不是Debug版本的;
  2. 被调用的方法所在的类与调用者所在的类位于同一个Dex;(注意,符合Class N命名规则的多个Dex要看成同一个Dex)(很坑!!这里应该这样理解,首先满足Class N情况,其次被调用的方法所在类在classes1, 调用的方法所在类在classes2)
  3. 被调用的方法的字节码条数不超过dex2oat通过--inline-max-code-units指定的值,6.x默认为100,7.x默认为32;
  4. 被调用的方法不含try块;
  5. 被调用的方法不含非法字节码;
  6. 对于7.x版本,被调用方法还不能包含对接口方法的调用。(invoke-interface指令)

(二)内联触发abort的条件

        了解了内联优化的条件之后,接下来我们还要清楚一点就是,内联优化是ART为了提升效率所做的策略,它时时刻刻都有可能发生,但是并不是说发生了内联就一定会导致Crash。内联触发abort信号也需要满足一定的条件。

        从报错信息我们看到:“This must bo due to duplicate classes or playing wrongly with class loaders”,直译过来就是说“一定是由于重复类或者错误地使用类加载器造成的”。那么也就是说造成abort的原因有两种

  1. dex A 和 dex B中有重复类。这种情况常见于热修复,应用原始apk中的dex A和从应用服务端下载的热修复dex B存在重复类,触发热修复且系统后台优化inline编译后,便会出现这样的问题。
  2. 错误地使用ClassLoader。由 classloader A 加载的 class1 调用一个由 classloader B 加载的 class2里的某个 inline 方法,将导致应用闪退。

(三)解决方案

        根据上面的分析,我们可以从破坏内联优化条件破坏内联触发abort条件两方面入手去解决这个问题。

  1. 破坏内联条件:

(1)把APP改成debug版本??显然不可能,pass

(2)也就是说我们要追整个调用链,总之要保证classes1和classes2相互独立。最终Tinker好像采用的是这个方案,这个方法待定。

(3)被调用方法的字节码不可控,pass

(4)这个貌似是最简单的方法,可行

(5)非法字节码。。啥玩意??pass

(6)还要加个接口方法?pass

  1. 破坏内联触发abort条件:

(1)等同于1.1

(2)也就是说只要是调用的方法所在的class和被调用的方法所在的class都是由同一个ClassLoader加载,即时发生了内联优化,也OK。这个方法待定。

 

        综上所述,1(2)、1(4)、2(2)都是比较靠谱的方法。实际的效果还待验证。。

 

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值