Ryan Cheng Android Develop Blog

掌握ProGuard代码混淆,构建轻量级安卓代码

ProGuard,一个代码优化和混淆的工具,作为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应用或者库,为宇宙的发展做出贡献。

外部资源

原文地址