Android统计方法耗时之:ASM插桩编译问题"stack overflow"

3,054 阅读2分钟

背景

最早在腾讯Matrix中遇到这个编译问题, 当时认为是首包方法数超过了65536.
然而, 不断精简首包方法数依然不能解决. 所以确认和方法数无关.无法解决, 只得放弃使用腾讯Matrix

使用ASM开发自研的插件, 也遇到同样的"stack overflow"问题, 导致编译失败. 问题报错如下:





问题跟踪

既然是自研的代码, 有足够的自由空间, 那一定可以着手解决这个问题.

根据异常堆栈提示, 在"ExecutionStack.java:168"位置抛出异常:


看起来, 是因为某个计算的结果值超过了stack.length. 就主动抛出了异常"overflow"

那么这个stack是什么? 其length是从哪里来的?
继续跟代码可知是ExecutionStack初始化是给到的:

继续翻代码, 发现这段:

是Frame类初始化时传入的值. 而这个(int maxLocals, int maxStack)有点眼熟:
ASM里注入方法时的这个visitMaxs方法, 参数名一致, 似乎问题就是出在这里. 但是还需要验证.



摸索修改

首先, visitMaxs()是在ASM中的, 而ExecutionStack是gradle中的, 两者来源其实并不同.
顺代码梳理, 应该很花时间. 简单网搜了下, 都和jvm的==方法栈长度==有关, 看起来有点联系.

其次, gradle的源码是不能改的, 深入的学习jvm得花点时间.
只有ASM的这个visitMaxs()方法, 我们可以重写. 那就从visitMaxs()入手吧
重写visitMaxs()的关键就是传入正确的maxStack, 和maxLocals. 那么==问题的关键就是如何正确传入这两个值==
使用AS的ASM Bytecode Outline插件可以查看java文件的字节码, 以此确认正确的值:

确认值之后重写visitMaxs()方法:

尝试编译, 成功!





根本原因

解决后总结根本原因:

  1. 利用ASM插桩更改了方法的实际栈深度, 但是没有同步更新正确的深度值
  2. ASM提供了ClassReader.EXPAND_FRAMES 和 ClassWriter.COMPUTE_MAXS来计算方法栈深度,
    但是在某些情况下, ==ASM没有计算出正确的方法栈深度==, 还很消耗编译时间.
  3. 这个问题, 和方法数超过65536没有任何关系.

PS: 另外还有一种情况, 也是visitMaxs()传入的参数不正确导致的:

小结

经验上看, 利用ASM插桩碰到和数组长度相关的编译异常, 可以优先确认visitMaxs()传入的值是否正确.