求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
Android 4.0 中由ProGuard引发的一场血案
 

作者:yihongyuelan,发布于2012-11-9,来源:CSDN

 

案件还原:

修改Android 4.0源码中的Setting,添加一项功能之后,在eng模式下编译,一切正常,遂提交代码到服务器。第二天,传来噩耗,Setting上新添加的功能无法使用,一点击则报错。

案件分析:

上传代码之前,已经在本地编译测试过,咋会有错呢??!!管它三七二十一,操起adb logcat抓取log进行分析。不看不知道,一看吓一跳,log中显示的错误信息竟然是ClassNotFound,也就是找不到相应的类,难道我上传的时候遗漏了吗(有这种可能)?马上查看提交记录,逐个检查提交的文件,全部提交了啊!咋会出现ClassNotFound呢?这时候咋办呢?冷静下来分析后可以知道,本地测试一般是在eng模式下编译的,而这次报错的是服务器在usr模式下编译处出来的。难道eng编译和usr编译出来的结果不一致??!!

案件进一步分析:

在本地选择usr编译模式,编译代码进行测试,果然出现了与服务器一样的错误,即ClassNotFound。

1.既然eng模式编译的代码能够正常运行,证明代码逻辑以及功能都是OK的。

2.usr编译出来出现ClassNotFound的问题,证明usr和eng编译模式不同导致编译结果差异。

3.比较eng模式和usr模式编译出来的Settings.apk,发现大小竟然不一致,用apktool反解后对比发现,usr模式下编译的Settings.apk中的确少了一个类(后文用lostClass.java代替)。

案件调查:

假设一:

难道是编译过程中没有将lostClass.java编译进去?

验证一:

查看编译过程中是否有对lostClass.java进行编译。

1.第一种查看方法。

打开源码中Settings文件夹下的Android.mk文件我们可以发现以下代码:

LOCAL_SRC_FILES := $(call all-java-files-under, src)

我们在这句代码后添加以下内容:

$(warning SevenTest LOCAL_SRC_FILES : $(LOCAL_SRC_FILES))

保存后终端中执行编译,我们可以看到src文件信息输出在终端上。复制到文件中查找lostClass.java,可以找到,因此可以推翻4的假设。

2.第二种查看方法。

查看APK的编译打包流程,如图1:

根据以上流程,我们可以找到每一步对应的mk文件,这里我们找到AndroidSouceCode/build/core/definitons.mk中注释以下代码:

define compile-java
$(hide) rm -f $@
$(hide) rm -rf $(PRIVATE_CLASS_INTERMEDIATES_DIR)
$(hide) mkdir -p $(dir $@)
$(hide) mkdir -p $(PRIVATE_CLASS_INTERMEDIATES_DIR)
$(call unzip-jar-files,$(PRIVATE_STATIC_JAVA_LIBRARIES),$(PRIVATE_CLASS_INTERMEDIATES_DIR))
$(call dump-words-to-file,$(PRIVATE_JAVA_SOURCES),$(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-list)
$(hide) if [ -d "$(PRIVATE_SOURCE_INTERMEDIATES_DIR)" ]; then \
	   find $(PRIVATE_SOURCE_INTERMEDIATES_DIR) -name '*.java' >> $(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-lis; \
fi
$(hide) tr ' ' '\n' < $(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-list \
    | sort -u > $(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-list-uniq
$(hide) $(1) -encoding UTF-8 \
    $(strip $(PRIVATE_JAVAC_DEBUG_FLAGS)) \
    $(if $(findstring true,$(LOCAL_WARNINGS_ENABLE)),$(xlint_unchecked),) \
    $(2) \
    $(addprefix -classpath ,$(strip \
        $(call normalize-path-list,$(PRIVATE_ALL_JAVA_LIBRARIES)))) \
    $(if $(findstring true,$(LOCAL_WARNINGS_ENABLE)),$(xlint_unchecked),) \
    -extdirs "" -d $(PRIVATE_CLASS_INTERMEDIATES_DIR) \
    $(PRIVATE_JAVACFLAGS) \
    \@$(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-list-uniq \
    || ( rm -rf $(PRIVATE_CLASS_INTERMEDIATES_DIR) ; exit 41 )
#Seven注释
#$(hide) rm -f $(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-list
#$(hide) rm -f $(PRIVATE_CLASS_INTERMEDIATES_DIR)/java-source-list-uniq
$(hide) jar $(if $(strip $(PRIVATE_JAR_MANIFEST)),-cfm,-cf) \
    $@ $(PRIVATE_JAR_MANIFEST) -C $(PRIVATE_CLASS_INTERMEDIATES_DIR) .
endef

define transform-java-to-classes.jar
@echo "target Java: $(PRIVATE_MODULE) ($(PRIVATE_CLASS_INTERMEDIATES_DIR))"
$(call compile-java,$(TARGET_JAVAC),$(PRIVATE_BOOTCLASSPATH))
#Seven注释
#$(hide) rm -rf $(PRIVATE_CLASS_INTERMEDIATES_DIR)
endef 

这样我们在终端中执行编译命令后,可以在路径:

AndroidSourceCode/out/target/common/obj/APPS/Settings_intermediates/classes找到以下两个文件:

java-source-list和java-source-list-uniq,在其中搜索lostClass.java即可以找到。

通过以上查看可以知道,lostClass.java在是被编译了的,最后为了验证,我们可以通过jd-gui这个工具来查看AndroidSourceCode/out/target/common/obj/APPS/Settings_intermediates/classes.jar,通过查看该jar文件中包含了lostClass.java,从而假设一排除。

假设二:

Settings.apk在打包的时候出现异常。

验证二:

对比eng模式以及usr模式下编译的输出信息后发现,二者copy的dex文件不同。如下:

usr模式:

Copying: out/target/common/obj/APPS/Settings_intermediates/proguard.classes.dex

eng模式:

Copying: out/target/common/obj/APPS/Settings_intermediates/noproguard.classes.dex

通过比较可以知道:proguard.classes.dex比noproguard.classes.dex小,这里的proguard就是混淆。也就是说usr模式对代码进行了混淆,从而使得usr模式编译出的Settings.apk与eng模式编译出的大小不同。是否会是usr模式对代码进行ProGuard导致问题的呢?这里暂时不知道假设二正确与否。

假设三:

usr模式下编译系统对代码进行ProGuard时出现异常。

验证三:

先了解一下ProGuard这个工具;

ProGuard简介:

ProGuard是一个免费的java类文件压缩,优化,混淆器。它探测并删除没有使用的类,字段,方法和属性。它删除没有用的说明并使用字节码得到最大优化。它使用无意义的名字来重命名类,字段和方法。

ProGuard作用:

1.创建紧凑的代码,快速装载和更小的内存占用。

2.增加程序和程序使用的库被反编译的难度

3.删除来自源文件中的没有调用的代码 。

在大致了解了ProGuard的作用之后,回过头来看看lostClass.java,该类主要在Settings_heads.xml中调用,并没有通过其他方式调用,难道这被ProGuard误认为是没有使用的代码而给优化掉了?

打开源码Settings下的Android.mk文件,可以看到以下代码:

LOCAL_PROGUARD_FLAG_FILES := proguard.flags

继续查看当前路径下的proguard.flags,代码如下:

# Keep all Fragments in this package, which are used by reflection.
-keep class com.android.settings.*Fragment
-keep class com.android.settings.*Picker
-keep class com.android.settings.*Settings
-keep class com.android.settings.wifi.*Settings
-keep class com.android.settings.deviceinfo.*
-keep class com.android.settings.bluetooth.*
-keep class com.android.settings.applications.*
-keep class com.android.settings.inputmethod.*
-keep class com.android.settings.MasterClear
-keep class com.android.settings.MasterClearConfirm
-keep class com.android.settings.accounts.*
-keep class com.android.settings.fuelgauge.*

稍稍查看ProGuard的语法后知道,这样表示哪些类不会完全被ProGuard处理。先在这里加上一句代码:

-keep class com.android.settings.xxxx.*

这里的xxxx表示lostClass.java的包名。

再次进入终端,切换到usr模式下编译代码,测试成功,但有几个小的功能还是不能正常使用。通过以上至少证明假设三是正确的,即在ProGuard过程中产生的问题。

这几个不能使用的小功能,都有一个共同的特点,即点击Button后报错,提示找不到对应的方法。这几个Button是在xml文件中添加onClick方法的,如:

  <Button
    android:id="@+id/JumpButton"
    android:onClick="JumpFuction"
    android:layout_width="0dip"
    android:layout_height="wrap_content"
    android:layout_weight="1"/>

通过查看ProGuard官网的Example——A complete Android Application可以知道,还需要在proguard.flags中添加以下代码:

-keepclassmembers class * extends android.content.Context {
   public void *(android.view.View);
   public void *(android.view.MenuItem);
}

官网给出的解释是:We're also keeping possible onClick handlers in custom Context extensions, since they might be referenced from XML layout files.

也就是说:有的onClick事件是在xml中定义的,所以在proguard.flags文件中添加以上内容从而保证这些onClick触发方法不会被ProGuard优化掉。加上以上代码,编译,测试,通过!!

案件回顾:

整个案件看似扑朔迷离,但背后隐藏的是APK整个编译流程。一开始看似不可能的事情(usr模式和eng模式编译结果不一致),经过小白一步一步的跟踪后竟然发现了背后的“惊天秘密”。ProGuard是一个免费的开源项目,用以压缩、优化、混淆代码,Android 源码集成了proguard,默认会在usr以及usrdebug模式下开启,这一点可以在AndroidSouceCode4.0/build/core/package.mk中找到:

ifndef LOCAL_PROGUARD_ENABLED

ifneq ($(filter user userdebug, $(TARGET_BUILD_VARIANT)),)

# turn on Proguard by default for user & userdebug build

LOCAL_PROGUARD_ENABLED :=full

endif

endif

(PS:高通的源码与google原生的差不多,而MTK则注释掉了这里的LOCAL_PROGUARD_ENABLED :=full,即usr、usrdebug、eng模式编译出来的结果都没经过混淆。)

当然除了ProGuard还有一个DexGuard可以实现对Android应用的优化与保护,不过这货是收费的(最便宜都要350 欧元啊。。。换算过来要将近3000快RMB。。。o(╯□╰)o)。

最后在大人的明察秋毫下,真相终于大白。


 
分享到
 
 


android人机界面指南
Android手机开发(一)
Android手机开发(二)
Android手机开发(三)
Android手机开发(四)
iPhone消息推送机制实现探讨
手机软件测试用例设计实践
手机客户端UI测试分析
手机软件自动化测试研究报告
更多...   


Android高级移动应用程序
Android应用开发
Android系统开发
手机软件测试
嵌入式软件测试
Android软、硬、云整合


领先IT公司 android开发平台最佳实践
北京 Android开发技术进阶
某新能源领域企业 Android开发技术
某航天公司 Android、IOS应用软件开发
阿尔卡特 Linux内核驱动
艾默生 嵌入式软件架构设计
西门子 嵌入式架构设计
更多...