在 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>
您还可以将正则表达式用作<copy>
任务中的过滤器,如下所示:
<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 优化期间删除未使用的字符串的主要内容,如果未能解决你的问题,请参考以下文章
使用 Proguard 删除 Google Play Services 库中未使用的类
Proguard:如何避免缩小(和混淆)整个包以避免删除(和混淆)“未使用的方法”?
Android 安装包优化开启 ProGuard 混淆 ( 压缩 Shrink | 优化 Optimize | 混淆 Obfuscate | 预检 | 混淆文件编写 | 混淆前后对比 )