crash with NoSuchMethodError after proguard with m

2020-04-04 04:00发布

问题:

the source code before compile&proguard :

public class IntentSession extends BaseIntentSession {
    @Override
    public void onResume() {
        super.onResume();
        mExecutor.exec(getIntent(), this::finish);
    }
}

the decompiled code after compile&proguard : (Decompiled with CFR 0_118)

public class a extends superA {

    public void e() {
        super.e();
        this.c.a(this.j(), b.a((a)this)); // the problematic code here
    }
}

now is the key code after compile&proguard, the b class's decompiled code :

final class b implements c.a {
    private a a;

    b (a a1) {
        this.a = a1;
    }

    static /* synthetic */ b a(final a a) {
        return new b(a);
    }

    @LambdaForm.Hidden
    public void a() {
        this.a.finish();
    }
}

it still referenced the finish() method which was already obfuscated as m() by proguard.

I expect the reference finish() method to be obfuscated as m(), but this is not what is happening, and that's my question.

Proguard didn't warn me, it only crashes with NoSuchMethodError on runtime once it hits the wrong code. So don't tell me to add a proguard configuration like -dontwarn java.lang.invoke.* which i tried but it did not work.

Maybe the handling sequence of the involved classes were wrong during obfuscation, who know?

I don't want to add the @Keep annotation on the finish() method, it's a bad solution and I would have to worry about it and carefully use method references in the future, so I'm looking for the best solution.

below are my gradle configurations :

dependencies {
    classpath 'com.android.tools.build:gradle:2.2.3'
    classpath 'me.tatarka:gradle-retrolambda:3.4.0'
    classpath "com.fernandocejas.frodo:frodo-plugin:0.8.3"
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}

Below is my proguard-rules.pro:

-optimizationpasses 5
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify
-verbose
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
-ignorewarnings

-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService

-keepclasseswithmembernames class * {
    native <methods>;
}

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
}

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}

-dontwarn java.util.**
-keep class java.util.** {*; }

-dontwarn com.android.**
-keep class com.android.** { *; }

-dontwarn android.support.**
-keep class android.support.** { *; }

-keepattributes SourceFile, LineNumberTable

# end common config

##---------------Begin: proguard configuration for Gson  ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature

# Gson specific classes
-keep class sun.misc.Unsafe { *; }
#-keep class com.google.gson.stream.** { *; }

-dontwarn com.google.gson.**
-keep class com.google.gson.** { *; }

-dontwarn com.baidu.util.audiocore.**
-keep class com.baidu.util.audiocore.** { *; }

# Application classes that will be serialized/deserialized over Gson
##---------------End: proguard configuration for Gson  ----------

# Explicitly preserve all serialization members. The Serializable interface
# is only a marker interface, so it wouldn't save them.
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

-keep public class * implements java.io.Serializable {*;}
# end Serializable

#  ----------------------------
-dontnote
-dontwarn com.xiaomi.push.service.XMPushService

#for speech sdk
-keep class com.orion.speech.** {*;}
-keep class com.orion.speech.audio.** {*;}
#end for speech sdk

#for xiaomi
-keep class PushReceiver {*;}
-keep class com.xiaomi.push.**{*;}
#end for xiaomi

#for retrofit
-dontwarn sun.misc.Unsafe
-dontwarn okio.**
# Platform calls Class.forName on types which do not exist on Android to determine platform.
-dontnote retrofit2.Platform
# Platform used when running on RoboVM on iOS. Will not be used at runtime.
-dontnote retrofit2.Platform$IOS$MainThreadExecutor
# Platform used when running on Java 8 VMs. Will not be used at runtime.
-dontwarn retrofit2.Platform$Java8
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions
#end for retrofit

#for lambda
-dontwarn java.lang.invoke.*
#end for lambda

#for okhttp
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
#end for okhttp

#for RxJava
-keep class rx.schedulers.Schedulers {
    public static <methods>;
}
-keep class rx.schedulers.ImmediateScheduler {
    public <methods>;
}
-keep class rx.schedulers.TestScheduler {
    public <methods>;
}
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
   long producerIndex;
   long consumerIndex;
}

-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
    rx.internal.util.atomic.LinkedQueueNode producerNode;
}

-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
    rx.internal.util.atomic.LinkedQueueNode consumerNode;
}
# end for RxJava

#for bugly
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
#end for bugly

#----------------android

# this indicate the case of using APIs higher than minSDK (API 8)
-dontwarn android.**

# ---------------------------------------

# TODO: can be reduce if we have more understanding about Service and AIDL
-keep public class android.service.notification.** {*;}

-keepattributes *Annotation*,EnclosingMethod
-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet, int);
}


-keepclasseswithmembernames class * {
    native <methods>;
}

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
}

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

-keepattributes *Annotation*,EnclosingMethod,Signature

-keep interface android.content.pm.**{*;}
-keep class android.content.pm.**{*;}
-keep class android.os.Process{*;}

-dontwarn com.android.internal.os.*

-keep class android.support.v4.os.**{*;}

-keepclassmembers class * {
    @android.support.v4 *;
}

# cmcm support
-keep class com.cmcm.support.jni.** { *; }

回答1:

Working through these bugs that -keep doesn't fix is a REAL pain and the only way I've ever made headway on them is by following this strategy:

  1. Figure out at what phase of the proguard cycle the bug is being introduced (shrinking, optimizing or obfuscating)
  2. Add/remove exceptions for that step, working your way from the broadest scope of exclusion to the narrowest until the problem resurfaces

E.G.

Verify that this is an optimization issue or not

  1. Add -dontoptimize instead of the -optimizations string rebuild and test
  2. If the crash is mitigated, work backwards through the classes of optimization exclusions at the highest level !method/*, !code/*, !class/*, !field/* until you determine which exclusion makes the issue go away
  3. Determine what the minimum exclusion is in that class of exclusion (assuming it was !method/*, go from it to !method/marking/* and then if that works too try !method/marking/final. If that works, then you've found the minimum exclusion)

This could very well be a bug in the Proguard rules of one of the libs you're using or in the version of Proguard you're using itself (I've seen both) so try to update both as well.



回答2:

Most of the time, the website/github of your library provides the necessary proguard rules like retrolamda:

-dontwarn java.lang.invoke.*
-dontwarn **$$Lambda$*

Proguarding is a trail-error story. Check your logging to see which library, class or component causes a problem and add them carefully to the rules :).

Your error NoSuchMethod specifically:

Your code is probably calling something like myClass.getMethod, trying to find some method dynamically. Since ProGuard can't always detect this automatically, you have to keep the missing method in using the appropriate -keep option:

-keepclassmembers class mypackage.MyClass { void myMethod(); }



回答3:

This happens when a class is looking for, or directly invoking a method of a given argument via reflection in runtime. Proguard can't warn you about this, because there is no link between the obfuscated class and the consumer class in compile time. You can have something like

public class AbstractbaseSomething{

    public abstract void doStuff();

}

public class iDoStuff{

    public void letsGo(Object o){
        Method method = o.getClass().getDeclaredMethod("doStuff");
        // do stuff with the method
    }

}

Since the method is referenced used a string with the name, proguard does not detect it, and in runtime, you get a crash. the only solution is, assuming you can't modify the code, is to avoid obfuscating the method and class.

(You can check a more realistic example in Ormlite-Android)



回答4:

after re-check carefully, i concluded it probably not a bug of proguard, only gradle.

first, i let the source code using general interface coding style :

mExecutor.exec(getIntent(), new MyInterface() {
    @Override
    public void execute() {
        finish();
    }
});

then i clean the build cache and rebuild :

./gradlew clean
./gradlew :app:assembleRelease

I perform the output release app and make it reach the problematic code, it work without crash.

this time i turn to method references :

mExecutor.exec(getIntent(), this::finish);

but i didn't clean the build cache before re-building :

./gradlew :app:assembleRelease

now re-perform with the crash happen :

05-22 11:35:33.870 D/AndroidRuntime(  631): Shutting down VM
05-22 11:35:37.470 E/AndroidRuntime(  631): FATAL EXCEPTION: main
05-22 11:35:37.470 E/AndroidRuntime(  631): Process: com.cmrobot.assistant, PID: 631
05-22 11:35:37.470 E/AndroidRuntime(  631): java.lang.NoSuchMethodError: com.session.a.finish
05-22 11:35:37.470 E/AndroidRuntime(  631):     at com.newsessions.b.executeDone(Unknown Source)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at com.newsessions.base.a.a(BaseIntentExecutor.java:99)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at com.newsessions.base.a.e(BaseIntentExecutor.java:76)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at com.newsessions.base.a.a(BaseIntentExecutor.java:67)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at com.newsessions.cmd.general.volume.VolumeChangeExecutor.b(VolumeChangeExecutor.java:28)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at com.newsessions.cmd.general.volume.a.a(LowerVolumeExecutor.java:63)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at com.newsessions.base.a.d(BaseIntentExecutor.java:44)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at com.newsessions.base.b.run(Unknown Source)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at android.os.Handler.handleCallback(Handler.java:733)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at android.os.Handler.dispatchMessage(Handler.java:95)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at android.os.Looper.loop(Looper.java:136)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at android.app.ActivityThread.main(ActivityThread.java:5001)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at java.lang.reflect.Method.invokeNative(Native Method)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at java.lang.reflect.Method.invoke(Method.java:515)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:829)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:645)
05-22 11:35:37.470 E/AndroidRuntime(  631):     at dalvik.system.NativeStart.main(Native Method)

in order to confirm it is the build cache reason, i clean then re-build on the changed code basically :

./gradlew clean
./gradlew :app:assembleRelease

that crash gone in the afterword app.

i attempt to create a demonstrating project to prove this problem, but that project doesn't popup the crash, only in my productive project.