Proguard not stripping unused generated variables after stripping out Log statements - android

I created a wrapper class MyLog to do logging for my Android app, which essentially just wraps android.util.Log. I want logging to be completely gone in my release app. I have the following rule defined in my proguard file:
-assumenosideeffects class com.myapp.logging.MyLog {
public static void d(...);
}
I am seeing that lines that have log statements as follows:
MyLog.d("Logging a boolean %b and a string %s parameter", isTrue, stringName);
are being shrunk to:
Object[] objArr = new Object[]{Boolean.valueOf(z), str};
and lines with log statements as follows:
MyLog.d("function call result: " + foo() + " end");
are shrunk to:
new Object[1][0] = foo();
In both cases the leftovers from obfuscation are pretty useless and might as well should've been removed.
Question:
Why would proguard leave unused variables in example #1 above?
Is there a better way to tell proguard - "Assume no side effects when you remove this method declaration and any calls to it, along with the parameters passed to it"?
I have read other answers related to the topic and the closest answer is here. I am not looking for solutions with BuildConfig.IS_LOGGING_ENABLED type solution where every log statement should be wrapped with this check.

Related

Storing (partial) Proguard obfuscation mapping in code

In an android Java code like this:
String methodName = "myMethod"
public void myMethod() {
}
Is it possible to figure out the obfuscated name of the method at runtime?
I know I can ask Proguard to not obfuscate that method, but I'm looking for a way to avoid that and still be able to use it through reflection.
I can't find a Proguard configuration for that, but I'm wondering if there could be a gradle task for merging the generated mapping?
If you need the method name from inside the method, you can use
String methodName;
public void myMethod() {
methodName = new Object() {}.getClass().getEnclosingMethod().getName();
}
Or, in newer Java versions
String methodName;
public void myMethod() {
methodName = StackWalker.getInstance(Set.of(), 1)
.walk(s -> s.map(StackWalker.StackFrame::getMethodName).findFirst())
.orElseThrow();
}
When you need the name from the outside, i.e. with calling it, you can mark the method with an annotation and search for it.
#Retention(RetentionPolicy.RUNTIME) #interface Marker {}
#Marker
public void myMethod() {
}
String name = Arrays.stream(ContainingClass.class.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(Marker.class))
.map(Method::getName)
.findFirst().orElseThrow();
As said by others, not to use Reflection might be the better option. E.g., referring to the method using a matching functional interface, is not a reflective access:
Runnable method = this::myMethod;
public void myMethod() {
}
This will keep working after obfuscation.
Code obfuscation usually just renames classes and variables to short and random text, keeping proper dependencies at the same time. There is no way to get the obfuscated name of the function as it is generated randomly and always different per each run.
Here are some options:
Instead, exclude the class from obfuscation and name your function with random text if you want to keep it unreadable.
-keep class <com.package.DontObfuscate>
After the first obfuscation, keep mapping.txt file and add a line to your proguard-rules.pro
-applymapping mapping.txt
That's will keep from generating new names for classes/variables.
-applymapping - Specifies to reuse the given name mapping that was printed out in a previous obfuscation run of ProGuard. Classes and class members that are listed in the mapping file receive the names specified along with them. Classes and class members that are not mentioned receive new names.
Look for a way to not use reflection in your code with class/interface extensions.

What does the R8 maximumremovedandroidloglevel option?

I am currently removing Android Logging by using the common Proguard/R8 configuration:
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
public static int e(...);
}
But I have found this official documentation which recommends to set the following R8 option with the corresponding log level (in the example 4) too:
-maximumremovedandroidloglevel 4
I checked to R8 source code to understand what this additional option does. I only found this comment:
Replace Android logging statements like Log.w(...) and Log.IsLoggable(..., WARNING) at or below a certain logging level by false.
Is this really required and what are the benefit vs only using the
assumenosideeffects rule?
I was curious about this rule as well, so I have researched a bit.
Is this really required and what are the benefit vs only using the assumenosideeffects rule?
This -maximumremovedandroidloglevel rule is intended to have the same effect as the -assumenosideeffects for Log.* methods, therefore it might be used as a complete substitute.
However, as of the latest version (3.3.75), it performs slightly differently than -assumenosideeffects. If you intend to remove all Log.* method invocations - then you shouldn't care (you can use one, or the other). But if you're removing only a portion of those methods and you rely on Log#isLoggable, then I'd suggest sticking to your current configuration, and avoiding adding the -maximumremovedandroidloglevel rule to your proguard configuration file.
A deeper dig
Let's assume we have the following lines of code in our source code, and then I'll show what it looks like after R8 processing with different configurations.
if (Log.isLoggable("FOO_TAG", Log.VERBOSE)) {
Log.v("FOO_TAG", "verbose message");
}
if (Log.isLoggable("FOO_TAG", Log.WARN)) {
Log.w("FOO_TAG", "warn message");
}
<1> R8 v3.3.75, with the following rules (note that I have commented out the w and e methods):
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
# public static int w(...);
public static int d(...);
# public static int e(...);
}
Produces the following output:
if (Log.isLoggable("FOO_TAG", 5)) {
Log.w("FOO_TAG", "warn message");
}
R8 removed the VERBOSE logs as expected. Note that it keeps the Log#isLoggable method invocation where the level (the second parameter) is WARN (5).
<2> R8 v3.3.75, with the following rules (4 means we want to remove all the log methods up to INFO, including):
-maximumremovedandroidloglevel 4
Produces the following output:
Log.w("FOO_TAG", "warn message");
Note that this rule keeps the Log#w method invocation, but removes the Log#isLoggable invocation (this is where the behavior slightly differs).
This means that the latest R8 version in regards to -maximumremovedandroidloglevel doesn't work exactly as advertised (here):
Example: The value of android.util.log.INFO is 4. Therefore,
specifying -maximumremovedandroidloglevel 4 will remove all calls to
Log.v(), Log.d(), and Log.i(), as well as it will replace calls to
Log.isLoggable(X, {2, 3, 4}) by false.
<3> R8 from the tip of the main branch, with the following rules (4 means we want to remove all the log methods up to INFO, including):
-maximumremovedandroidloglevel 4
Produces the following output:
if (Log.isLoggable("FOO_TAG", 5)) {
Log.w("FOO_TAG", "warn message");
}
It seems that main branch includes a fix that reinstates the behavior parity of two approaches (Log#isLoggable with WARN as a parameter was not removed).
The relevant diff between the main and 3.3.75 tag is those two commits: 1, 2.
Why the official documentation includes both -assumenosideeffects and -maximumremovedandroidloglevel?
According to my tests - it seems misleading, as they should have suggested to using one or the other, definitely not both.
I have opened an issue with a request to elaborate on that.

What exactly does the Timber library do?

I heard about Timber and was reading github README, but it's quietly confusing me.
Behavior is added through Tree instances. You can install an instance
by calling Timber.plant. Installation of Trees should be done as early
as possible. The onCreate of your application is the most logical
choice.
What behavior?
This is a logger with a small, extensible API which provides utility
on top of Android's normal Log class.
What more does it provide on top of Android's Log?
The DebugTree implementation will automatically figure out from which
class it's being called and use that class name as its tag. Since the
tags vary, it works really well when coupled with a log reader like
Pidcat.
What is DebugTree?
There are no Tree implementations installed by default because every
time you log in production, a puppy dies.
Again, what is a tree implementation? What does it do? And how do I stop killing puppies?
Two easy steps:
Install any Tree instances you want in the onCreate of your
application class.
Call Timber's static methods everywhere throughout your app.
Two easy steps for accomplishing what?
None of this has been explained in the Readme. It's pretty much a description for someone who already knows what it is :/
Problem :-
We do not want to print logs in Signed application as we may sometimes log sensible information . Generally to overcome this developers tend to write if condition before writing log
Example:-
if(BuildConfig.DEBUG) {
Log.d(TAG,userName);
}
so every time you want to print a log you need to write a if condition and a TAG which most times will be class name
Timber tackels these two problems
You just need to check condition once in application class and initialize Timber.plant
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(DebugTree())
}
}
}
remaining all places we can just write Timber.d("Message") without any tag or if condition .
If you want a different tag then you can use
Timber.tag("Tag").d("message");
Edit :
Also you can plant your own trees in Timber , and do some cool stuff like Log only warnings and error in release , send warnings and error logs to server etc . eg
import timber.log.Timber;
public class ReleaseTree extends Timber.Tree {
#Override
protected void log(int priority, String tag, String message, Throwable t) {
if (priority == ERROR || priority == WARNING){
//Send to server
}
}
}
You can plant different trees for different build flavours .
Check out this article and have a listen to this podcast

Why some package-private classes are not obfuscated by Proguard?

Working with an Android project in Android Studio 3.2, having enabled Proguard and some specific rules, I'm not able to figure out the following:
a specific package (and its subpackages) in a library module, used by client code, is preserved through the rule:
-keep public class com.mylib.mypackage.** {
public protected *;
}
Now, within this package there are also a number of package-private classes, which should not be picked by that rule. Some of those classes are effectively obfuscated, both in their own names and their member names, which is fine.
Instead there are some classes, implementing public interfaces, whose class names are not obfuscated, while I'd expect they should. For completeness, their member names, when not part of interface, are effectively obfuscated.
Example:
/* package */ class InternalComponent implements ExternalInterface {
// ExternalInterface is kept: Ok
// InternalComponent is kept: don't like, I'd like it renamed
#Override
public void ExternalMethod() {
// this is kept: Ok
}
public void InternalMethod() {
// this is renamed: Ok
}
}
I'd like to highlight that InternalComponent is created within some other (kept) class and only returned to client code through the ExternalInterface.
How can I also obfuscate their class names as well, if possible?
Edit #1
After #emandt answer on Proguard output files, I double checked and com.mylib.mypackage.InternalComponent is listed in seeds.txt, which according to this blog post lists all items matched by keep rules. So, for some reason, the rule above also picks package-private classes, which still seems wrong to me.
Edit #2
In the meantime, I ended up doing exactly the same approach proposed by #shizhen. For completeness, in order to extend the exclusion to any package named internal, I modified my proguard rule as:
-keep public class !com.mylib.mypackage.**.internal.*, com.mylib.mypackage.** {
public protected *;
}
(note the first part before the comma, prefixed by !)
I'll mark #shizhen answer, though I'd like to be curious as to why the original rule is also picking package-private components.
Are you working on an Android Library project? Probably YES.
In order to achieve your purpose, I am afraid that you need to re-organise your packages into something like below:
Public interfaces
com.my.package.apiforusers
Private/Internal implementations
com.my.package.apiforusers.internal
Then for your obfuscation rules, you can have it like below:
-keep public class com.my.package.apiforusers.** { public *; }
So that only the public classes/interfaces are kept and all those ones inside com.my.package.apiforusers.internal will be obfuscated.
Please note the double-asterisk at the end so that public classes/interface are also kept for the sub-packages.
In "/build/outputs/mapping/release/" folder there are few files ("usage.txt", "seeds.txt", etc..) that contain the REASONS of why and which classes/variables/methods/etc.. are not-processed/not-shrinked/ot-obfuscated via ProGuard utilities.

Proguard causing NoSuchMethodException

Class<?> c = Class.forName("co.uk.MyApp.dir.TargetClass");
Method main = c.getDeclaredMethod("main", Report_Holder.class);
Throws a 'java.lang.NoSuchMethodException.main[class co.uk.MyApp.classes.Report_Holder]' error once I've prepared the app for release using Proguard.
I spent hours thinking the problem was in 'co.uk.MyApp.dir.TargetClass', commenting out things, re-releasing the app, and re-testing. But it turns out that the error is right at the root, at:
Method main = c.getDeclaredMethod("main", Report_Holder.class);
I then updated proguard-project.txt to include:
-dontobfuscate
-keeppackagenames
(I am using the Lint suggested method which suggested putting code into project.properties and putting the config in a text file), such as:
proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
But adding those 2 lines didn't have any effect.
So now I am wondering if;
a) I should add anything on top of '-keeppackagenames' etc.
b) Is proguard.config set up correctly; should ${sdk.dir} actually be a proper uri to the sdk
The class that it is targeting is like this:
public static void main(Report_Holder args) {
....
}
Edit
Or is it because I have 2 instances of this type of thing, both called 'main' ? But called in different activities.
Method main = c.getDeclaredMethod("main", Report_Holder.class);
Method main = c.getDeclaredMethod("main", OtherReport_Holder.class);
And both targets being like this:
public static void main(Report_Holder args) {
....
}
public static void main(OtherReport_Holder args) {
....
}
Once you know how to use proguard, you should add the option -keepattributes Signature
. this is necesary when using generics (collections).
For all methods beeing called via reflection, you must explictly exclude them from obfsucation. use the option to output the obfuscation map file, to see if your rules had the desired effect.

Categories

Resources