本周开发安卓时,遇到一个诡异的问题,如下图所示,在调试时部分语句无法执行到,更具体讲,就是method中前几个return语句无法执行到,每次在前面几个return语句要执行时,代码直接就跳到了方法的最后一个return处了,而且这几个return都被打了叉叉,表示无法断点

A

如何排查?

第一反应自然是安装到真机上的APK包代码与IDEA里面看到的源码不一致

这个思路显然是对的,事实上,开发中经常会遇到APK安装代码与IDEA中代码不一致的情况

怎么验证这个猜想?很简单,在源码里面加上一些特定的调试代码,然后重新打包安装APK包试一下就行了。事实证明,问题没那么简单,病状依旧

但是我相信大方向没有错,一定是有其他地方的配置会导致APK实际安装代码与源码出现了差异,顺着这个思路,谷歌一把,我找到了第二把钥匙:

安卓打包参数minifyEnabled

请看安卓官网文档:压缩代码和资源

代码压缩通过 ProGuard 提供,ProGuard 会检测和移除封装应用中未使用的类、字段、方法和属性,包括自带代码库中的未使用项(这使其成为以变通方式解决 64k 引用限制的有用工具)。ProGuard 还可优化字节码,移除未使用的代码指令,以及用短名称混淆其余的类、字段和方法。混淆过的代码可令您的 APK 难以被逆向工程,这在应用使用许可验证等安全敏感性功能时特别有用。

要启用通过 ProGuard 实现的代码压缩,请在 build.gradle 文件相应的构建类型中添加 minifyEnabled true。

简单说,就是启用minifyEnabled参数可以简化优化实际安装的安卓代码,OK,咱们再试一试,把minifyEnabled设置为false

android {
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile(‘proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}

很遗憾,失败Again…

这个时候,经同事 @晓东 点拨(为老司机点赞!)

会不会是编译成.dex文件时做了优化?

我们知道安卓虽然是基于Java开发,但运行在安卓机器上的并不是.class文件,而是经过进一步转码的.dex文件(安卓也不使用JVM虚拟机,而是针对移动平台特设的Dalvik虚拟机),而这个转码过程,显然也是可以加入优化的

这里插一段Dalvik科普,有请维基君:Dalvik虚拟机

Dalvik虚拟机,是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。它可以支持已转换为.dex(即“Dalvik Executable”)格式的Java应用程序的运行。.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik由Dan Bornstein编写的,名字来源于他的祖先曾经居住过的小渔村达尔维克(Dalvík),位于冰岛Eyjafjörður。

这里还提到了dx工具,用于把class文件转码成dex文件,中间涉及到一些优化,嗯哼,说不定就是你了!

dx工具是一种用来转换Java class成为DEX格式的工具。多个类被包含在一个dex文件之中。各个类中重复的字符串和其他常数只在DEX中存放一次,以节省空间。Java字节码(bytecode)被转换成Dalvik虚拟机所使用的替代指令集。一个未压缩dex文件通常稍小于一个已经压缩的.jar档。
安装到行动设备之时,Dalvik可执行文件可能会被修改。为了获得进一步优化,虚拟机可能会调整文件内部分数据的端序、内联一些函数和简单的结构体、并短路掉一些不必要的操作。

能不能去掉这个优化呢?当然是可以的,使用gradle的安卓插件2.2版本(老版本不支持),里面就可以配置这个参数:DexOptions

如下所示,关闭优化,再来一发打包调试,5·4·3·2·1·go~

android {
    compileSdkVersion 23
    buildToolsVersion "25.0.0"

    dexOptions {
        optimize false
        threadCount 2
    }

很遗憾,继续失败

问题进行到这,似乎已经可以GG了,折腾很久了,然而宝宝并不弃疗!因为我相信大方向是正确的,而且在一步一步接近真相,最后一搏,走起!

考虑到dx有重大嫌疑,我调整了谷歌关键词,果然,终于,一条潜伏在安卓官网的Issue浮出水面,这条Issue创建于2012年,病状与我高度一致,而且

矛头直指Dalvikdx

yeah, if i run this on the RI i don’t see the apparent step to “return -1” but on dalvik i do. looking at the dex file, it’s obvious that dx has done this to us by compiling all three returns into one (instruction 0x7):
i’d guess the debug information for that instruction gets overwritten each time we compile a return, leaving us reporting the final return to the debugger. this is shit, and should be fixed, but sadly this isn’t something i can fix in jdwp.

(多个return语句会被dx编译成为一个return)

嗯哼,就是你了!

Issue的最后一条记录发布于2015年,是一位安卓的项目成员发布的,提示这个bug依旧没有fix,但是可以使用另外一种编译器Experimental New Android Tool Chain – Jack and Jill作为workaround

dx still has this bug, but its replacement jack does not:
http://tools.android.com/tech-docs/jackandjill
as work on dx is effectively stopped, marking this obsolete.

Jack(全称Java Android Compiler Kit)是下一代的安卓编译工具,目前还在实验阶段,按照如下配置以后,我最后一次打包编译调试

android {
    ...
    buildToolsVersion ‘21.1.2’
    defaultConfig {
      // Enable the experimental Jack build tools.
       jackOptions {
         enabled true
       }
    }
    ...
}

由于Jack还是个实验工具,我依次升级了JDK至1.8,升级了Gradle,升级了Gradle安卓插件,按照提示一步步踩坑,才构建成功

嗯,这次终于成功了!如图所示,所有return语句都打上了小勾勾,表示可以正常执行断点了,而且实测确实OK!

B

尾声及题外话

为什么我会遇到这个二逼问题?因为我自测时习惯在method的每个出口处打上断点,用以验证程序是否按照我的预设进入指定分支,而这个「Bug」导致调试时method的出口出现了混淆

问题虽小,但还是要打破沙锅问到底,因为说不定别人也会遇到同样的问题呢?

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注