谨以此文,记我在公司实习时,所接到的第一个正式的、真正有意义的任务——将公司即将发布的APK进行代码混淆。
什么是代码混淆
混淆就是对发布出去的程序进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能,而混淆后的代码很难被反编译,即使反编译成功也很难得出程序的真正语义。被混淆过的程序代码,仍然遵照原来的档案格式和指令集,执行结果也与混淆前一样,只是混淆器将代码中的所有变量、函数、类的名称变为简短的英文字母代号,在缺乏相应的函数名和程序注释的况下,即使被反编译,也将难以阅读。同时混淆是不可逆的,在混淆的过程中一些不影响正常运行的信息将永久丢失,这些信息的丢失使程序变得更加难以理解。
为什么要进行代码混淆
>Java 是一种跨平台的、解释型语言,Java 源代码编译成中间”字节码”存储于 class 文件中。由于跨平台的需要,Java 字节码中包括了很多源代码信息,如变量名、方法名,并且通过这些名称来访问变量和方法,这些符号带有许多语义信息,很容易被反编译成 Java 源代码。为了防止这种现象,我们可以使用
Java 混淆器对 Java 字节码进行混淆。
>混淆器的作用不仅仅是保护代码,它也有精简编译后程序大小的作用。以“ProGuard”为例,“ProGuard”的主要作用就是混淆,同时它还能对字节码进行缩减体积、优化等。由于对变量名和方法名进行缩减,以及前面所说过的部分不影响正常运行的信息会被丢失,使得编译后的jar文件的体积减少,文件精简。
通过什么方式进行代码混淆
目前,主流的代码混淆的方式,就是使用“Proguard”,同时,你也可以在网上搜代码混淆,代码加固等等服务,可以轻易的搜索到诸如“360加固”、“爱加密”之类第三方的服务。那么他们的区别在哪里:
1>使用工具,就是在你自己的电脑上,通过配置“Proguard”之类的工具,自己进行代码混淆,自己编译,自己调试。而使用第三方服务,就是将你自己的APK,上传到他们的网站,他们帮你进行混淆,混淆/加固完成,再将混淆后的APK发回来给你。
2>使用工具,APK自始至终都在你自己手里,如果使用第三方的服务,原始APK就需要传给别人,这样增加了不安全性,当然你有可能会说,别人那么大间公司怎么觊觎你的APK?对于这个问题的看法就因人而异,像我实习那时处理我公司的那个项目,我的“leader”明确要求自己使用工具手动混淆,不能使用第三方的服务,不能将APK传给别人。
什么是“ProGuard”
“ProGuard”是一个混淆代码的开源项目。它的主要作用就是混淆,当然它还能对字节码进行缩减体积、优化等,但是对于我们来说,体积压缩以及优化功能,还不是最重要的。我们真正在乎的,就是他的混淆功能。这是对“ProGuard”简介,如果你想看详细的,这里附上官网地址:http://proguard.sourceforge.net/
“ProGuard”可以进行哪些优化?
以下资料,来自网络:
除了在压缩操作删除的无用类,字段和方法外,“ProGuard”也能在字节码级提供性能优化,内部方法有:
>常量表达式求值。
>删除不必要的字段存取。
>删除不必要的方法调用。
>删除不必要的分支。
>删除不必要的比较和instanceof验证。
>删除未使用的代码。
>删除只写字段。
>删除未使用的方法参数。
>像push/pop简化一样的各种各样的peephole优化。
>在可能的情况下为类添加static和final修饰符。
>在可能的情况下为方法添加private, static和final修饰符。
>在可能的情况下使get/set方法成为内联的。
>当接口只有一个实现类的时候,就取代它。
>选择性的删除日志代码。
实际的优化效果是依赖于你的代码和执行代码的虚拟机的。简单的虚拟机比有复杂JIT编译器的高级虚拟机更有效。无论如何,你的字节码会变得更小。
仍有一些明显需要优化的技术不被支持:
>使非final的常量字段成为内联。
>像get/set方法一样使其他方法成为内联。
>将常量表达式移到循环之外。
如何使用“ProGuard”
前面说了混淆代码的起因和意义,也介绍了“ProGuard”各种好处,现在说说怎么使用这个工具。首先,以下说明全部基于"Eclipse"开发环境,Android2.3以后版本。
在Android 2.3以前,混淆Android代码只能手动添加proguard来实现代码混淆,非常不方便。而2.3以后,Google已经将这个工具加入到了SDK的工具集里。该工具的具体路径:SDK\tools\proguard。当创建一个新的Android工程时,在工程目录的根路径下,会出现一个proguard的配置文件proguard.cfg。也就是说,我们可以通过简单的配置,在我们的elipse工程中直接使用ProGuard混淆Android工程。
如何启动“ProGuard”
在工程的根路径下,找到”project-properties.txt”文件,源码如下:
# This file is automaticallygenerated by Android Tools.
# Do not modify this file -- YOURCHANGES WILL BE ERASED!
#
# This file must be checked inVersion Control Systems.
#
# To customize properties used bythe Ant build system edit
# "ant.properties", andoverride values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink andobfuscate your code,uncomment this (available properties: sdk.dir,user.home):
# proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-22
你只要将蓝色那段代码,前面那个“#”号去掉,就能启动“ProGuard”工具,当然蓝色不是这个文件里面这段代码原有颜色,是我在文章里面为了说明加上的。现在进行导包操作,在导包的过程当中,“Eclipse”会自动使用“ProGuard”工具。另外需要说明的一点是,你直接“run”到设备的应用是没有经过混淆的,即便你已经启动了“ProGuard”工具,只有手动导包之后所获得的APK,才是经过混淆工具处理过的。
如何配置“ProGuard”混淆文件
前面说了如何启动“ProGuard”工具,对于一些极简单的工程(没有引用第三方库,没有自定义View,没有调用“native”层的方法),比如你的一个测试“demo”,可能不用配置什么,只要启动工具就好,混淆工作就这样完成了。但是对于较复杂的工程,我们需要手动配置一些东西才能保证混淆过程正常完成,以及混淆之后的APK正常运行。那么为什么复杂的工程就一定要手动配置?究其根本原因就是:不是什么东西都能被混淆的,有些代码混淆之后,它所对应的功能就没法使用。这主要体现在两个地方:
1>所有第三方引用库不能混淆,只要混淆,基本都是要出错的。
2>诸如自定义“View”,“native”层的方法,等等都不能被混淆,混淆之后其对应的功能都要出错,这类代码比较多,我在这里只列举了两个,后面我会详细记录一些。配置混淆文件就是为了告诉系统,某些东西不能混淆,以免我的APP会出错,现在详细介绍一下如何配置“ProGuard”混淆文件。
两个与混淆相关的配置文件
1>默认配置:工程刚创建的时候,开发环境其实已经默认配置好了混淆设置,你可以在“Eclipse路径\sdk\tools\proguard\”路径下面,找到一个“proguard-android.txt”文件,该文件是开发环境自动配置,混淆设置,这也是为什么对于极简单的项目而言,自己不用另外配置,系统默认配置就能满足。对于默认配置这里只做路径介绍,接下来是重点问题,如何根据项目具体状况,自行配置混淆文件。
2>自定义配置:在项目的根路径下,有个文件:proguard-project.txt。该文件就是手动进行配置的地方。我们需要按照一定语法规则,根据项目实际状况,编写该文件,这样才能正常通过混淆过程。
自定义配置的具体规则
在“proguard-project.txt”文件里面编写混淆规则,其实只有一个目的:如果某个类混淆后,将会导致应用无法正常运行(或者部分功能无法正常运行)那么就要在配置文件里,声明不要混淆这些类。我不关心为什么这些类混淆之后会出错,只关心哪些类在混淆后出错,然后在文件里面声明不要去混淆这些类。当然如果你非得要刨根问底,你也可以在网上找更详细地资料。
通用规则
以下,是通用的,几乎所有工程都要避开的类(所谓避开,就是声明这些类不要被混淆):
>四大组件以及系统基本的API不要混淆。
语法规则:-keep public class * extends xxxx
代码示例:
#所有“Activity”及其子类不要混淆,同理,所有“Service”、“BroadcastReceiver”等等系统级别的类,不要混淆。 -keep public class * extendsandroid.app.Activity -keep public class * extendsandroid.app.Application -keep public class * extendsandroid.app.Service -keep public class * extendsandroid.content.BroadcastReceiver -keep public class * extendsandroid.content.ContentProvider -keep public class * extendsandroid.app.backup.BackupAgentHelper -keep public class * extendsandroid.preference.Preference -keep public classcom.android.vending.licensing.ILicensingService
>保持”native”层的方法不要混淆。
-keepclasseswithmembernamesclass * { native<methods>; }
>保持自定义控件,以及指定格式构造方法不要混淆。
-keepclasseswithmembers class * { public <init>(android.content.Context, android.util.AttributeSet); #保持自定义控件类不被混淆,指定格式的构造方法不去混淆 } -keepclasseswithmembers class * { public <init>(android.content.Context, android.util.AttributeSet, int); }
>保持指定规则的方法不被混淆(Android layout 布局文件中为控件配置的onClick方法不能混淆)
-keepclassmembersclass * extends android.app.Activity { public void *(android.view.View); }
>保持自定义控件指定规则的方法不被混淆
-keeppublic class * extends android.view.View { public<init>(android.content.Context); public<init>(android.content.Context, android.util.AttributeSet); public<init>(android.content.Context, android.util.AttributeSet, int); public void set*(...); }
>所有枚举类型不要混淆
-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); }
>需要序列化和反序列化的类不能被混淆(注:Java反射用到的类也不能被混淆)
#保持实现"Serializable"接口的类不被混淆 -keepnamesclass * implements java.io.Serializable #保护实现接口Serializable的类中,指定规则的类成员不被混淆 -keepclassmembersclass * implements java.io.Serializable { static final long serialVersionUID; private static finaljava.io.ObjectStreamField[] serialPersistentFields; !static !transient <fields>; private voidwriteObject(java.io.ObjectOutputStream); private voidreadObject(java.io.ObjectInputStream); java.lang.Object writeReplace(); java.lang.Object readResolve(); }
>保持实现"Parcelable"接口的类不被混淆
-keepclass * implements android.os.Parcelable { public static finalandroid.os.Parcelable$Creator *; }
>所有泛型不能混淆
-keepattributes Signature
>假如项目中有用到注解,应加入这行配置
-keepattributes *Annotation*
>保持R文件不被混淆,否则,你的反射是获取不到资源id的
-keep class **.R$*{*;}
>保护WebView对HTML页面的API不被混淆
-keep class **.Webview2JsInterface {*; }
>如果你的项目中用到了webview的复杂操作 ,最好加入
-keepclassmembers class * extends android.webkit.WebViewClient { public void *(android.webkit.WebView,java.lang.String,android.graphics.Bitmap); public boolean *(android.webkit.WebView,java.lang.String); } -keepclassmembers class * extends android.webkit.WebChromeClient { public void *(android.webkit.WebView,java.lang.String); }
至此,主要通用规则已经介绍完毕,你可以直接拷贝到“ProGuard”配置文件里面去,这些规则都通用的。
保持第三方引用库(第三方Jar包)不被混淆
前文已经讨论混淆通用规则,如果一个项目里面,没有引用第三方库,基本上你只要将前面的规则根据情况给抄上去,混淆基本都不会有什么问题。不过现在绝大多数项目(尤其是公司的项目)都会或多或少引入第三方库,前文也已经有提到,所有第三方引用库都不能够混淆,所以接下来我们就聊聊,如何保持第三方库不被混淆。
其实保持第三方库不被混淆并不复杂,最关键的就是要细心+耐心。为什么呢?保持第三方库不被混淆,是要将你所引用的所有第三方库,按照一定语法格式,写在混淆配置文件里面,简单地说,就是将你"Eclipse"工程里的"Android Private Libraries"目录下面所有的第三方的引用包,按照给定语法格式,全部(注意是全部)写到你的"proguard.project.txt"文件里面。来让我们看图说话。
一图胜千言,然后我们总结一下,对于每一个第三方的导入包,我们只要:
>-libraryjars libs/xxxx.jar
>-dontwarn 包名.**
>-keep class 包名.** { *;}
大部分的第三方包都能按照这个规则配置,有些第三方引用包,在其官方网站上面会有混淆代码配置说明,比如高德地图就有,这个时候你抄上去就可以了。
好了现在让我们把所有(对是所有)第三方包全都写上,写到手软~
运行程序,查漏补缺
一般来说,按照前面我们说讨论的规则配置之后,混淆过程都能正常通过(不会报错)。不过非常遗憾的是,混淆通过并不代表你APP就能运行,我们之前有提到过,有些东西原来不能混淆,当你混淆之后他的功能就会出错。当你完成了混淆后,只要运行你APP,每个功能都按一按,多玩一下,就有可能发生一些奇怪的事。所以我们才需要做查漏补缺。
首先执行混淆之后,我们能够在路径"proguard"下面发现新出现了四个文件:
>mapping.txt:表示混淆前后代码的对照表,这个文件非常重要。如果你的代码混淆后会产生bug的话,log提示中是混淆后的代码,希望定位到源代码的话就可以根据mapping.txt反推。每次发布都要保留它方便该版本出现问题时调出日志进行排查,它可以根据版本号或是发布时间命名来保存或是放进代码版本控制中。
>dump.txt:描述apk内所有class文件的内部结构。
>seeds.txt:列出了没有被混淆的类和成员。
>usage.txt:列出了源代码中被删除在apk中不存在的代码。
在我自己这个项目完成混淆代码之后,发生两件奇怪的事,现在说说怎么利用这些文件进行解决。
>混淆过后的APP,所有列表(ListView)里的数据,都不显示,在确认了数据确实已经收到,就是没有显示之后,在usage.txt文件里面,发现了所有"ListView"的适配器,也就是说,混淆过后的Apk,代码里面已经没有适配器了,所以造成显示失败,而我当时所做的事,就是在混淆配置文件(proguard.project)文件里面,添加了如下代码:
#保持所有适配器类不被混淆,本应用中,不加这个将会导致适配器类加载失败,所有列表项没办法显示 -keep public class * extends android.widget.BaseAdapter
不过这个不算是混淆的配置规则,因为我的另外一个同时,跟我类似项目结构,但是他没有加这句,他的列表显示正常。这是使用"usage.txt"文件进行查漏补缺的例子了。
>混淆过后的APP,越用越卡(其实就是内存泄露),用着用着手机就会莫名其妙的死机了,不单单是应用卡死,整台手机都不动了。不论在APP里面进行什么操作,都会导致APP将手机给弄死了。这个异常最终不是通过前面四个文件来解决的,而是通过对APP功能进行考虑。基于所发生的现象,可以看出,一定有什么全局性东西,混淆之后发生错误,导致这个全局功能没法运行,但是却又不断请求,最终耗尽系统资源。最终确定的原因是,我们APP的推送以及IM功能,混淆之后没法工作,最终耗尽系统资源。那么解决的办法是,声明他不要被混淆。
总结
我们聊了那么多的东西,先介绍了什么叫做代码混淆,然后介绍混淆方式选择,接着介绍如何启动以及配置混淆,最重要的当然就是配置混淆。其中包括通用规则,第三方包,以及根据混淆后的文件进行查漏补缺。至此,APP的代码混淆基础,介绍完毕。