在 ProGuard 优化期间删除未使用的字符串

Posted

技术标签:

【中文标题】在 ProGuard 优化期间删除未使用的字符串【英文标题】:Removing unused strings during ProGuard optimisation 【发布时间】:2011-08-25 22:18:26 【问题描述】:

我在发布 android 应用程序时包含 this ProGuard configuration 以去除调试日志语句:

-assumenosideeffects class android.util.Log 
    public static *** d(...);
    public static *** v(...);

这按预期工作 - 我可以从 ProGuard 日志和 Android 日志输出中看到,诸如 Log.d("This is a debug statement"); 之类的调用已被删除。

但是,如果我在这个阶段反编译应用程序,我仍然可以看到所有使用过的 String 字面量——即本例中的 This is a debug statement

有没有办法同时从字节码中删除不再需要的每个 String

【问题讨论】:

这是否与 -dontoptimize 一起工作,因为 Android 文档说 “添加优化会带来一定的风险,例如,并非所有由 ProGuard 执行的优化都适用于所有版本的 Dalvik。” @codingcrow 可能不会,因为据我所知,这是一种优化。但我相信默认的 Android ProGuard 配置通常启用了优化,但禁用了一些他们认为无法可靠运行的特定优化。所以你应该可以毫无问题地将它添加到默认配置中。 如果您查看 project.properties,您会发现 proguard.config=$sdk.dir/tools/proguard/proguard-android.txt:proguard-project.txt 行,在 progaurd-android.txt 中您会发现 默认情况下优化是关闭的。 Dex 不喜欢代码运行 # 通过 ProGuard 优化和预验证步骤。所以这是一个 catch-22 的情况。 好的,所以它不是默认值。但是您可以改用捆绑的 proguard-android-optimize.txt 配置。 【参考方案1】:

ProGuard 可以删除简单的常量参数(字符串、整数等)。所以在这种情况下,代码和字符串常量应该完全消失:

Log.d("This is a debug statement");

但是,您可能已经观察到以下代码的问题:

Log.d("The answer is "+answer);

编译后,这个其实对应:

Log.d(new StringBuilder().append("The answer is ").append(answer).toString());

ProGuard 4.6 版可以将其简化为:

new StringBuilder().append("The answer is ").append(answer).toString();

所以日志记录消失了,但优化步骤仍然留下一些绒毛。如果没有对 StringBuilder 类有更深入的了解,要简化它是非常棘手的。就 ProGuard 而言,它可以说:

new DatabaseBuilder().setup("MyDatabase").initialize(table).close();

对于人类来说,StringBuilder 代码显然可以删除,但 DatabaseBuilder 代码可能不能。 ProGuard 需要逃逸分析和其他一些技术,这些技术还没有在这个版本中。

至于解决方案:您可以创建其他带有简单参数的调试方法,并让 ProGuard 删除这些:

MyLog.d("The answer is ", answer);

或者,您可以尝试在每个调试语句前加上 ProGuard 以后可以评估为假的条件。这个选项可能有点复杂,需要在调试标志的初始化方法上添加一些额外的 -assumenosideeffects 选项。

【讨论】:

感谢您的详细回答。添加-assumenosideeffects StringBuilder append(String); append(...); toString(); 似乎可以有效地删除现在未引用的StringBuilders。我错过了一些为什么这会不好的原因吗? :) @Christopher append 方法确实有副作用:它们扩展了 StringBuilder 实例,即使没有使用返回值。因此,这样的配置可以消除所需的事件。它可能有效,但风险很大。 啊好吧..例如我可以调用sb.append("x"),不使用返回的StringBuilder,然后调用foo(sb)——但"x"不会被附加,因为该方法调用被删除了? 这真是个好办法,我不明白为什么框架不提供带字符串格式的日志方法。实际上,构建一个字符串(有时与 String.format 复杂)最终删除它是没有意义的,因为它没有达到最低日志级别 @rds 使用SLF4J 它具有非常灵活的API,例如:LOG.warn(" did ", this, "something", ex),注意ex 没有对应的,因此将打印堆栈跟踪。您也不必担心使用正确的 % 模式或 StringBuilder。【参考方案2】:

这是我们的做法 - 使用 ant 任务

<target name="base.removelogs">
    <replaceregexp byline="true">
        <regexp pattern="Log.d\s*\(\s*\)\s*;"/>
        <substitution expression=";"/>
        <fileset dir="src/"><include name="**/*.java"/></fileset>
    </replaceregexp>
</target>

【讨论】:

很有趣,但我在这里问的是更一般的 ProGuard 问题 :) 好方法。但是如果 Log.d 方法调用中有断行呢? @qiuping345,试试加"(?s)Log.d... @nir,\(\s*\) 如何匹配任何日志记录?它的意思是“括号之间的空格”【参考方案3】:

由于我没有足够的代表来直接评论 ant 任务的答案,因此这里对其进行了一些更正,因为事实证明它与可以执行发布构建的 Jenkins 等 CI-Server 结合使用非常有帮助:

<target name="removelogs">
    <replaceregexp byline="true">
        <regexp pattern="\s*Log\.d\s*\(.*\)\s*;"/>
        <substitution expression=";"/>
        <fileset dir="src">
            <include name="**/*.java"/>
        </fileset>
    </replaceregexp>
</target>

'.'在 Log 之后必须转义和一个 '.'括号内的目标是任何日志记录语句,而不仅仅是像 '\s*' 那样的空格。

由于我没有太多使用 RegEx 的经验,我希望这将帮助一些处于相同情况的人完成这项 ant 任务(例如在 Jenkins 上)。

【讨论】:

再次,与实际问题无关。如果您的日志记录语句跨越多行,大概这不起作用? @ChristopherOrr,你是对的,但如果这是一个真正的 Java 模式,这是可能的:"(?s)Log\.d...【参考方案4】:

如果你想支持多行日志调用,你可以改用这个正则表达式:

(android\.util\.)*Log\.@([ewidv]|wtf)\s*\([\S\s]*?\)\s*;

您应该可以在 ant replaceregexp 任务中使用它,如下所示:

<replaceregexp>
    <regexp pattern="((android\.util\.)*Log\.([ewidv]|wtf)\s*\([\S\s]*?\)\s*;)"/>
    <substitution expression="if(false)\1"/>
    <fileset dir="src/">
        <include name="**/*.java"/>
    </fileset>
</replaceregexp>

注意:这围绕着带有if(false) 的Log 调用,因此保留了原始调用,供参考和在检查中间构建文件时保留行号,让Java 编译器在编译期间剥离调用。

如果您希望完全删除日志调用,您可以这样做:

<replaceregexp>
    <regexp pattern="(android\.util\.)*Log\.([ewidv]|wtf)\s*\([\S\s]*?\)\s*;"/>
    <substitution expression=""/>
    <fileset dir="src/">
        <include name="**/*.java"/>
    </fileset>
</replaceregexp>

您还可以将正则表达式用作&lt;copy&gt; 任务中的过滤器,如下所示:

<copy ...>
    <fileset ... />
    <filterchain>
        <tokenfilter if:true="$strip.log.calls">
            <stringtokenizer delims=";" includeDelims="true"/>
            <replaceregex pattern="((android\.util\.)*Log\.([ewidv]|wtf)\s*\([\S\s]*?\)\s*;)" replace="if(false)\1"/>
        </tokenfilter>
    </filterchain>
    <!-- other-filters-etc -->
</copy>

【讨论】:

以上是关于在 ProGuard 优化期间删除未使用的字符串的主要内容,如果未能解决你的问题,请参考以下文章

Android 代码混淆规则

使用混淆ProGuard压缩代码和资源/减少方法数量

使用 Proguard 删除 Google Play Services 库中未使用的类

Android Proguard 未删除所有日志消息

Proguard:如何避免缩小(和混淆)整个包以避免删除(和混淆)“未使用的方法”?

Android 安装包优化开启 ProGuard 混淆 ( 压缩 Shrink | 优化 Optimize | 混淆 Obfuscate | 预检 | 混淆文件编写 | 混淆前后对比 )