掌握ProGuard代码混淆,构建轻量级安卓代码
18 Jan 2014ProGuard,一个代码优化和混淆的工具,作为Android SDK的一部分,它可以说是一把双刃剑——它上手有很大的挑战,但是如果被正确地使用,可以获得极大的好处!在Crashlytics我们花了很多时间借助ProGuard的能力,开发了一些轻量级的库,来帮助APP开发者开发出很棒的产品——我们在日常开发中主要使用以下四个功能。
Shrinking
随着你的代码库不断壮大、功能不断完善,记住一点是很重要的,减小APK的体积是极其有益的,因为在一个欠佳的网络环境或一台老旧的设备上,很大的二进制文件不一定能安装成功。
在开发者社区中,Dalvik虚拟机的内存限制64K已经是广为人知了。由于这个限制,ProGuard可以提供一个缓冲区,给你来考虑采取何种措施去减小代码体积。删除未使用的代码(很可能存在于一个64K的项目中),将使你的开发团队在开发一些功能时,在进行重构或者考虑加载外部的类的技术限制上面畅通无阻。
Shrinking是有益的,简单的找出未使用的代码然后去除也是一个很好的方式。通过在Proguard.cfg(你的配置文件)中使用printusage
标记,ProGuard会列出未使用的代码,以进行适当代码维护和清除。
# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-verbose
-printseeds seeds.txt
-printusage unused.txt
-printmapping mapping.txt
Obfuscating
由于有工具可以提取你的APK的内容、反编译、读取类文件,进行混淆处理,以保护你对代码的专有权,是很重要的。ProGuard 生成一个映射文件,允许你匹配混淆后的代码堆栈到实际的函数方法。
原始代码:
package com.example.app;
public class Data {
public final static int RESULT_ERROR = -1;
public final static int RESULT_UNKNOWN = 0;
public final static int RESULT_SUCCESS = 1;
private final int mId;
private final int mResult;
private final String mMessage;
public Data(int id, int result, String message) {
mId = id;
mResult = result;
mMessage = message;
}
public int getId() {
return mId;
}
public int getResult() {
return mResult;
}
public String getMessage() {
return mMessage;
}
}
ProGuard混淆后的代码:
package com.example.app;
public class a {
private final int a;
private final int b;
private final String c;
public a(int paramInt1, int paramInt2, String paramString) {
this.a = paramInt1;
this.b = paramInt2;
this.c = paramString;
}
public int a() {
return this.a;
}
public int b() {
return this.b;
}
public String c() {
return this.c;
}
}
通过自动收集生成的映射文件,Crashlytics简化你的代码的反混淆处理和智能优先堆栈跟踪,使你的调试过程毫不费力。
Repackaging
Repackaging允许ProGuard将外部jar包和类文件移到一个单独的拥有一个共同的java包位置的容器:
# com.example.networking contains the networking level
# com.example.database contains the persistence level
# repackage low level services into common package for simplicity -repackageclasses "com.example.internal"
# com.example.public contains public interfaces
# ignore these in repacking
-keep public class com.example.public.* { public *; }
对于你的那些构建中的库来说,如果你选择对第三方开发者只暴露一个简单的接口,并保持一个可维护的结构良好的工程层次在你的代码仓库中,repackaging是非常有帮助的。同时在组织一系列低层次的包并暴露一个设计良好的接口时,这也是很有用的!
Optimizing
Optimizing作用于编译后的类,基于java版本,引入许多小的优化。通常,Android工具附带的proguard-android.txt优化是默认关闭的,但是如果需要的话proguard-android-optimize.txt有预设。
# Optimizations: If you don't want to optimize, use the
# proguard-android.txt configuration file instead of this one, which
# turns off the optimization flags. Adding optimization introduces
# certain risks, since for example not all optimizations performed by
# ProGuard works on all versions of Dalvik. The following flags turn
# off various optimizations known to have issues, but the list may not
# be complete or up to date. (The "arithmetic" optimization can be
# used if you are only targeting Android 2.0 or later.) Make sure you
# test thoroughly if you go this route.
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-optimizationpasses 5
-allowaccessmodification
-dontpreverify
Optimizations提供针对语言操作的性能改善。然而,针对不同的Dalvik版本,已知有不兼容问题。因此我们建议,在启用前,对代码和目标设备做一个彻底的review。 除了利用ProGuard的这四个核心功能,对于那些希望构建轻量级应用或库的人,我们还制定了几个策略,跟ProGuard的优化一起使用。
改善编译时间
加入ProGuard到编译过程中会延长编译时间,因此最小化ProGuard要检查的代码总数是很重要的。当考虑第三方库的时候,这是至关重要的,如Crashlytics,已经被ProGuard处理过了——通过ProGuard重新处理一遍只是浪费CPU,而且会更慢! 我们认为,在预处理的时候估算编译时间的改善是很重要的(第三方库在ProGuard中忽略)。拿Crashlytics举个例子,我们在各种尺寸的设备上,对内部测试程序进行了许多测试运行。我们发现,当忽略掉Crashlytics包,编译时间改善可达5%。这仅仅是一个已经超轻量化的包而已啊。想象下依赖许多额外库的应用——这个编译时间的改善肯定是巨大的。
避免对一个已经预处理过的库再次进行处理,只需将下面两句话添加到Proguard.cfg:
-libraryjars libs
-keep class com.crashlytics.** { *; }
由于混淆一般是用于安全性方面,对于一个开源库来说,没有理由对它进行混淆。只需按照上面的列出的方式,处理时间可以进一步减少,最终改善编译时间,Android support library就是一个很好的例子:
-libraryjars libs
-keep class android.support.v4.app.** { *; }
-keep interface android.support.v4.app.** { *; }
反射
由于各种原因,在Android中使用反射可以说是非常令人沮丧的,包括性能和API改变的不稳定。然而,对单元测试来说,却是非常有用的。通常的用法包括改变方法的作用域来设置测试数据和模拟对象。如果你在开发过程中使用ProGuard来混淆,理解这一点是很重要的:当一个方法或类名改变时,它们的字符串表示形式不会。在设计可测试的接口时,如果测试程序运行在一个使用ProGuard处理过的设备上时,这会导致method not found异常。
库开发
在开发库通过ProGuard进行处理的时候,额外的复杂性被引入,既因为它们是分布式的,又因为app开发者也运行处理。当代码被混淆两次的时候,追踪bug会变得更加挑战,因为需要两份mapping.txt文件反混淆堆栈trace。为了避免对这些库处理两次,确保按照我们上面的步骤改善编译时间。
对于你的那些库,你可能已经遇到ProGuard的更多挑战,因为在一个重复复杂的工程,可能存在自定义的ProGuard规则。我们建议不要需求自定义ProGuard规则,因为在自定义的设置之后应用一个不同的规则设置,库会被破坏。如果需求自定义规则,确保那些使用你的库的在他们自己的配置文件中引入自定义ProGuard,以确保你的库和他的app的兼容性!
# Custom Rules
-keep class com.example.mylibrary.** { *; }
从Crashlytics诞生以来,让开发者易于上手已经成为我们的使命。我们希望这些策略会帮助你构建下一个开创性的Android应用或者库,为宇宙的发展做出贡献。
外部资源
- Android ProGuard Docs – Complete documentations to ProGuard
- Dalvik Limits Script – Specialty script to detects how close the method limit is — a gift from Crashlytics