Duang~ Android堆栈慘遭毁容?精神哥揭露毁容真相!
最近精神哥接到多个小伙伴的投诉,说无法看懂Bugly崩溃克星页面上显示的堆栈信息!精神哥赶紧把正研究的Top Crash崩溃和心爱的鸡爪放下,开始着手跟进。经分析发现,大家误会Bugly崩溃克星了,这一切都是Proguard搞的鬼!
下面请容精神哥一一道来!
问题描述
先看看图一中显示的正常堆栈内容,正常堆栈中每一个调用帧(Frame),都会有3个元素组成:
- 类名(Full Class Name,红线标注);
- 方法名(Method,蓝线标注);
- 源文件及行号(SourceFile:LineNum,绿线标注)。
我们再看看图二显示的异常堆栈内容,可以发现栈中的某一调用帧(Frame),一个类名下会存在多个方法名,而且第一个方法尾部的源文件及行号是(Unknown Source),根本看不出源文件和行号。
但,有经验的同学童鞋应该能看出来,这个堆栈是被Proguard还原过的!
怎么看?很简单,出错时JVM生成的堆栈中每一个"方法描述",只有“方法名”而没有“方法返回类型”及“方法参数”。但被Proguard还原过的堆栈,应该有“方法返回类型”及“方法参数”。
那么这里有两个问题:
- 为什么堆栈会是Unknown Source?是Bugly崩溃克星没有上报吗?
- 为什么com.xx.a.a会被还原成多个方法(图2示例中就被还原了5个方法)?
精神哥继续给大家分析分析!
为什么堆栈会是Unknown Source?
是Bugly崩溃克星忘记上报了吗?不是!那是因为,你代码编译的姿势不对!
有经验的童鞋应该会发现,开发阶段上报Bugly的Crash崩溃堆栈都是有源码及行号的,但发布后就变成了Unknown Source,为什么?
如图3所示,我们发布时源码信息会先经过javac编译,再经过proguard混淆,才被打包进发布的apk中,最终Crash崩溃后Bugly获取到的堆栈中有木有源码及行号就要看这两步了。
所以想让Crash崩溃堆栈不再Unknown Source,需要两个保证:
保证一:javac编译保留源文件名及行号
【TODO】javac编译保留源文件名及行号 源文件名、行号、变量名称,都存在class文件的debug信息中,javac编译时可以选择是否保留debug信息,那么我们肯定是要保留的!下面是使用不同编译方法时保存源文件名及行号的解决方法示例。
- 命令行javac编译的解决方法
javac -g:{lines,source} XXX.java
- 使用Ant编译的解决方法
- 使用Eclipse编译的解决方法
保证二:Proguard混淆中保留原文件名及行号
【TODO】Proguard中keep住源文件及行号
-keepattributes SourceFile,LineNumberTable
为什么com.xx.a.a被还原成多个方法了?
如图六,大家看到Class1_Promoted
类下面的两个方法都被混淆成了a方法。
那么问题来了:假如堆栈中有 proguard.retrace.a.a(Unknown Source)
,应该被还原成神马呢?
因为它可以是proguard.retrace.Class_Promoted
类下的方法void doClass1(int)
,也可以是void doClass1()
。所以Proguard还原工具直接把这两个都给你列出来都作为你的还原结果,所以:
proguard.retrace.a.a(Unknown Source)
被还原成
proguard.retrace.Class_Promoted.
void doClass1(int)
void doClass1()
图二中的5个方法也就是这么来的。
真不能怪Proguard还原工具,谁让Java堆栈中不给出方法的完整描述或签名(返回值,方法名,方法参数),而只有方法名,但Java语法又允许方法名相同。但这是有解决办法的!
Proguard还原工具其实还能根据行号进行区分,如果你的堆栈已经解决了UnknownSource问题,那么你的还原Mapping文件就不一样了。如图7,大家看到mapping文件中多了“数字:数字”这类内容,这个就是行号,也就是说proguard.retrace.a
类中24行到26行属于void doClass(int)
方法 ,29到31行属于void doClass1()
方法。
假如堆栈中有proguard.retrace.a.a(Demo.java:25)
,就知道要被还原成proguard.retrace.Class_Promoted.void doClass1(int)
了。
精神哥总结
Proguard作为非常优秀的工具,让我们安装包体积变小了,代码混淆更安全了,代码裁剪优化速度更快了,但确实也埋下了一些坑,增加了我们定位崩溃时的成本。跟着精神哥的这篇文章把原文件名和行号补上后,绝大部分的堆栈问题都可以被解决了!
那么堆栈问题就没了吗?有人曾问我:堆栈里显示A方法调用了D方法,跟着D方法崩溃了,但实际查看代码A并没有调用D方法啊?
精神哥汗了,不知道Progurad会做代码优化的么?认真看看是不是有A->B->C->D ?如果有!那是因为Proguard大神很喜欢把你没用的->B->C给干掉,直接A->D了!
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 微信小程序——轮播图、组件传值、下拉刷新、导航 实战开发
- 从微信网页授权到OAuth 2.0
- 微信小程序常用组件
- React进阶篇(七)React 同构
- 商业数据分析从入门到入职(5)Python基本语法和数据类型
- JavaScript 同步和异步的执行机制问题
- Vue 与小程序:父组件给子组件传值的区别
- Canvas悟空推箱子
- Spring Cloud 之服务网关 Zuul (一)
- React进阶篇(八)react redux
- 通过一个简单例子理解JavaScript闭包和this对象
- Spring Cloud 之服务网关 Gateway (一)
- Hacking with iOS: SwiftUI Edition - Hot Prospects项目(二)
- HarmonyOS-对Android开发者也太友好了吧
- 你还在使用复杂的 zkclient 开发 zookeeper 么?是时候用 Curator 了 !