31

I include this ProGuard configuration to strip out debug log statements when I release an Android application:

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

This works as expected — I can see from the ProGuard logs and Android log output that calls such as Log.d("This is a debug statement"); are removed.

However, if I decompile the app at this stage, I can still see all the String literals that were used — i.e. This is a debug statement in this example.

Is there a way to also remove each String that's no longer needed from the bytecode?

Community
  • 1
  • 1
Christopher Orr
  • 110,418
  • 27
  • 198
  • 193
  • Does this work along with `-dontoptimize` because Android documentation says _"Adding optimization introduces certain risks, since for example not all optimizations performed by ProGuard works on all versions of Dalvik."_ – Gaurav Agarwal Sep 09 '12 at 02:59
  • @codingcrow Probably not since, as far as I'm aware, this is an optimisation. But I believe the default Android ProGuard config has optimisation enabled in general, but disables some specific optimisations that they believe don't work reliably. So you should be able to add this to the default config without issue. – Christopher Orr Sep 09 '12 at 21:16
  • 1
    If you look at project.properties you will find the line `proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt` and in progaurd-android.txt you will find _Optimization is turned off by default. Dex does not like code run # through the ProGuard optimize and preverify steps_. So it is a catch-22 situation. – Gaurav Agarwal Sep 09 '12 at 22:55
  • 2
    Ok, so it's not the default. But you can use the bundled `proguard-android-optimize.txt` config instead. – Christopher Orr Sep 10 '12 at 10:59

4 Answers4

49

ProGuard can remove simple constant arguments (Strings, integers, etc). So in this case, the code and the string constant should disappear completely:

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

However, you may have observed the issue with some code like this:

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

After compilation, this actually corresponds to:

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

ProGuard version 4.6 can simplify this to something like:

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

So the logging is gone, but the optimization step still leaves some fluff behind. It's surprisingly tricky to simplify this without some deeper knowledge about the StringBuilder class. As far as ProGuard is concerned, it could say:

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

For a human, the StringBuilder code can obviously be removed, but the DatabaseBuilder code probably can't. ProGuard requires escape analysis and a few other techniques, which aren't in this version yet.

As for a solution: you can create additional debug methods that take simple arguments, and let ProGuard remove those:

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

Alternatively, you can try prefixing every debug statement with a condition that ProGuard can later evaluate as false. This option may be a bit more convoluted, requiring some additional -assumenosideeffects option on an initialization method for the debug flag.

Eric Lafortune
  • 45,150
  • 8
  • 114
  • 106
  • 1
    thanks for the detailed answer. Adding `-assumenosideeffects StringBuilder { append(String); append(...); toString(); }` seems to be effective at removing the now-unreferenced StringBuilders. Am I missing some reason why this would be bad? :) – Christopher Orr May 17 '11 at 08:49
  • 1
    @Christopher The append methods do have side-effects: they extend the StringBuilder instances, even if the return values are not used. Such a configuration may therefore remove occurrences that are required. It might work, but it's pretty risky. – Eric Lafortune May 17 '11 at 22:17
  • Ah ok.. for example I could call `sb.append("x")`, not use the returned `StringBuilder`, then call `foo(sb)` -- but `"x"` wouldn't have been appended because that method invocation was removed? – Christopher Orr May 18 '11 at 20:53
  • 1
    This is really a good approach, and I don't understand why the framework doesn't provide log methods with string formatting. It actually doesn't make sense to build a String (sometimes complex with String.format) to eventually drop it because it does not reach the minimum log level – rds Aug 31 '12 at 22:59
  • @rds use [SLF4J](http://www.slf4j.org/android/) it has very flexible API like: `LOG.warn("{} did {}", this, "something", ex)`, notice that `ex` does not have a corresponding `{}` so the stacktrace will be printed. You also don't have to worry about using the proper % pattern or StringBuilder. – TWiStErRob Apr 29 '15 at 10:45
  • ProGuard docs now have samples of removing unused StringBuilders with `-assumenoexternalsideeffects`. – Miha_x64 Apr 25 '18 at 16:24
  • @Miha_x64 It works with the [proguard official usage](https://www.guardsquare.com/manual/configuration/examples#logging), but when I use r8, `-assumenoexternalsideeffects` and `-assumenoexternalreturnvalues` are ignored. – DysaniazzZ May 20 '21 at 03:58
  • @DysaniazzZ, thanks for noting. Shame that there's no official way of running BOTH ProGuard and R8. – Miha_x64 May 20 '21 at 13:33
6

here is how we do it - using ant task

<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>
nir
  • 161
  • 2
  • 7
5

As I don't have enough rep to comment the ant task answer directly, here some corrections for it as it proves to be very helpful in combination with a CI-Server like Jenkins who can execute it for a release build:

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

The '.' after Log must be escaped and a '.' inside of the brackets targets any logging statement, not just whitespaces as '\s*' does.

As I don't have much experience with RegEx I hope this will help some people in the same situation to get this ant task working (e.g. on Jenkins).

utopius
  • 59
  • 1
  • 2
  • 1
    Again, not related to the actual question. Presumably this doesn't work if your logging statement spans more than one line? – Christopher Orr Sep 12 '12 at 13:46
  • @ChristopherOrr, you are right, but it's possible if that's a real Java pattern: [`"(?s)Log\.d...`](http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html#DOTALL). – TWiStErRob Apr 29 '15 at 10:52
0

If you want to support multiline log calls, you can use this regexp instead:

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

You should be able to use this within an ant replaceregexp task like so:

<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>

Note: this surrounds the Log calls with if(false){ and } so the original call is preserved, both for reference and to preserve line numbers when inspecting the intermediate build files, letting the java compiler strip the calls during compilation.

If you prefer to remove the log calls completely, you could do so like this:

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

You could also apply the regular expression as a filter within a <copy> task like so:

<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>
Lorne Laliberte
  • 6,261
  • 2
  • 35
  • 36