-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update ProGuard default shrinking rules #2448
Conversation
…s without a no-args constructor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like ShrinkingIT
needs to be updated?
Yes, and that makes sense. However, I cannot reproduce locally. If I run |
I was seeing the same thing with JDK 11 on Google's internal Linux. #2450 should fix it. With that PR, I am able to reproduce the test failure in that environment. |
This case of using Out of curiosity, could you please explain a bit why you think it is needed? In the end it is still Éamonn's decission whether this is included, but I am a bit afraid that with this change we contribute to people unwittingly relying on |
Just to be sure, you run this in the parent directory, right? Edit: Nevermind, sorry the reason is the other test failures both of you mentioned. Probably due to that the Maven build already fails for the |
After testing with the new configuration I realized that with just Java
Kotlin
To get no-args constructors requires effectively assigning default values to all fields for these patterns (either directly for the field or assigning in an explicit no-args constructor), which I don't think is feasible to require, and then the provided rules will not work for many developers. I is still an option for developers to add no-args constructors to not use |
Tests updated, please review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Three other issues stand out to me here:
- There are two rules about classes with
@SerializedName
fields, which I think could be consolidated into one-keepclasseswithmembers
rule? - More of these
-keep
/-keepclassmembers
declarations could useallowobfuscation
, which should work fine with reflection that refers to things by symbol instead of string name (including these annotations). - Possibly this should have
-keep,allowobfuscation class
rules for the annotations used here. I think that can matter when there are multiple optimization passes? Not 100% sure, basing this on other examples that follow that pattern. Not sure if that would be best to do with a rule that matchescom.google.gson.annotations.*
or justJsonAdapter
andSerializedName
specifically.
@@ -65,6 +65,6 @@ | |||
# See also https://github.com/google/gson/pull/2420#discussion_r1241813541 for a more detailed explanation | |||
-if class * |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this if class *
, -classeswithmembers... class <1>
doesn't do anything relative to -classeswithmembers... class *
, after reading some of the follow-up threads on that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least for R8 it does seem to make a difference: -keepclasseswithmembers ... class *
always keeps the class, even if it isn't used. Whereas -if class * ... -keepclasseswithmembers ... class <1>
only keeps the class if it is actually used.
Unfortunately this is not covered by the tests yet; have created #2455 to add a test for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks also for the other points you raised, but unless sgjesse wants to address them here as well could you please create a separate GitHub issue or pull request? Otherwise the discussion on this pull request here might drift too far away from the original topic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed the rules to be two -if
rules
- Keep all @SerializedName fields for classes which are present after shrinking
- If a class with a @SerializedName field is present keep its no-args constructor
Realized this bit probably doesn't matter, I was confused by examples for annotations that are just referenced by proguard config rules, and not actually otherwise referenced in the code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sgjesse, hmmm ok, let's hope then that those users don't encounter the pitfalls of JDK Unsafe (see documentation of GsonBuilder.disableJdkUnsafe()
) (and also let's hope that neither the JDK nor Android remove the needed JDK Unsafe functionality any time soon).
Personal opinion on this (click to show)
Personally I am a bit indecisive on these changes; on one hand I can definitely see that it would be convenient for users if it just worked automatically. On the other hand, the problem is that JDK Unsafe usage is opt-out and I assume most users are not aware of it being used at all, nor what the implications of it are (if it was opt-in I wouldn't have a problem with that). If as user you want to use your classes with Gson, then you should (in my opinion) adjust them to follow Gson's requirements (or if necessary differentiate between application logic classes and DTO classes, or however you want to call them), and from that perspective I would appreciate any changes which make users less dependent on JDK Unsafe.
Though I am not a direct member of this project, so it is not up to me to decide this.
Thanks for the test changes, could you please adjust the comments which are now partially obsolete?
Also, ideally there would be a new test in shrinker-test
's com.example.Main
which covers the successful case of a class without no-args constructor but with fields annotated with @SerializedName
, which is now supported with your changes.
If you don't want to do that I can do it in a follow-up PR.
Technically Troubleshooting.md
is now also a bit outdated (e.g. it says "has a no-args constructor"), but I would leave it that way for now to encourage usage of a no-args constructor to avoid relying on JDK Unsafe.
@@ -65,6 +65,6 @@ | |||
# See also https://github.com/google/gson/pull/2420#discussion_r1241813541 for a more detailed explanation | |||
-if class * | |||
-keepclasseswithmembers,allowobfuscation,allowoptimization class <1> { | |||
<init>(); | |||
<init>(...); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please adjust the comment in lines 62 and 63 (GitHub does not let me comment there); for example reword it to:
# If a class is used in some way by the application and has fields annotated with @SerializedName,
# keep those fields and the constructors of the class
# For convenience this also matches classes without no-args constructor, in which case Gson uses JDK Unsafe
// making class abstract); other constructors are ignored to suggest to user adding default | ||
// constructor instead of implicitly relying on JDK Unsafe |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please adjust this comment (starting in line 19), for example:
// Specify explicit constructor with args to remove implicit no-args default constructor
The current comment mentioning ProGuard rules and JDK Unsafe is now obsolete.
@@ -20,7 +20,7 @@ static class TestClassNotAbstract { | |||
// making class abstract); other constructors are ignored to suggest to user adding default | |||
// constructor instead of implicitly relying on JDK Unsafe | |||
static class TestClassWithoutDefaultConstructor { | |||
@SerializedName("s") | |||
// No @SerializedName annotation to avoid matching the consumer rules from gson.pro. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good addition; could you please copy it to the other classes TestClass
and TestClassNotAbstract
above as well?
Though I think the trailing period should be omitted, it just looks a bit weird after the file name:
// No @SerializedName annotation to avoid matching the consumer rules from gson.pro. | |
// No @SerializedName annotation to avoid matching the consumer rules from gson.pro |
I'll file a PR for whatever @sgjesse doesn't want to handle here. We were just discussing that since I was trying to solve some obstacles to adopting these rules downstream, and we're colleagues, we share a downstream. |
…-changes Adjust shrinker tests
shrinker-test/proguard.pro
Outdated
#-keep class com.example.ClassWithSerializedName { | ||
# <init>(...); | ||
#} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, no removed the rules in comments. Also removed the rule above, as that was also not needed. I probably added it while testing different rules.
# Keep fields annotated with @SerializedName for classes which are present. | ||
# If classes with fields annotated with @SerializedName have a no-args | ||
# constructor keep that as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you remove the link to the pull request discussion?
At least in my opinion this -if class * ...
is pretty cryptic and its intended behavior is not directly obvious, so it would be good to have a reference explaining what this actually does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, added those references back.
-if class * | ||
-keepclasseswithmembers,allowobfuscation,allowoptimization class <1> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you remove allowoptimization
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having allowoptimization
can allow R8 to propagate field values, so if you have an explicit construction of a serialized class always setting a field to a specific value, then that single value can become the default value for the created instances instead of 0
/null
.
-if class * | ||
-keepclasseswithmembers,allowobfuscation,allowoptimization class <1> { | ||
<init>(...); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it intentional that you removed <init>(...)
again and used <init>()
below? My personal opinion in #2448 (review) was not blocking the merge of this pull request (sorry if I did not make that clear enough).
This change to use <init>()
again might render some or all of the shrinker test changes of this pull request obsolete.
Edit: Maybe not, see comment below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or at least I am a bit surprised that no exception occurs for ClassWithoutDefaultConstructor
; I assume it might be related to your newly added -keepclasseswithmembers
.
So, do I understand it correctly that your new approach of using -keepclasseswithmembers
achieves the same thing as your initial <init>(...)
approach (i.e. Gson can instantiate classes without no-args constructor using Unsafe; instead of failing because R8 made the class abstract). But this new approach is better than the <init>(...)
rule because that would have unnecessarily kept the constructors with args around, even though they are not actually used by Gson?
# Keep fields with @SerializedName annotation, but allow obfuscation of their names | ||
-keepclassmembers,allowobfuscation class * { | ||
@com.google.gson.annotations.SerializedName <fields>; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, why did you remove this here and add it below guarded by a -if class *
? Does that make a difference for the end result?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in R8 full mode we have a distinction between referenced and instantiated classes. For classes which are not seen by R8 as instantiated (e.g. if only referenced statically using .class
as one will do in fromJson
then -keepclassmembers
will only keep static members and not instance members. For ProGuard and R8 in compatability mode the two will do the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm thanks, ok that sounds like the previous rules would not have worked properly then because -keepclassmembers
would not have kepts the non-static @SerializedName
fields. Though I think the shrinker-test showed that it did work as expected?
I am bit confused, or am I misunderstanding your comment or are the shrinker-tests flawed and not close enough to real code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One way to see the difference is to have an instance field annotated with @SerializedName
which is not used in the code at all, then the field will be removed with the previous rule in R8 full mode (which is default from AGP 8.0.0). Then in a pass through from fromJson
to toJson
the field value will be lost. Of course it is a matter of taste if unused fields should be kept this way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed all comments.
@@ -65,6 +65,6 @@ | |||
# See also https://github.com/google/gson/pull/2420#discussion_r1241813541 for a more detailed explanation | |||
-if class * |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed the rules to be two -if
rules
- Keep all @SerializedName fields for classes which are present after shrinking
- If a class with a @SerializedName field is present keep its no-args constructor
# Keep fields with @SerializedName annotation, but allow obfuscation of their names | ||
-keepclassmembers,allowobfuscation class * { | ||
@com.google.gson.annotations.SerializedName <fields>; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in R8 full mode we have a distinction between referenced and instantiated classes. For classes which are not seen by R8 as instantiated (e.g. if only referenced statically using .class
as one will do in fromJson
then -keepclassmembers
will only keep static members and not instance members. For ProGuard and R8 in compatability mode the two will do the same.
# Keep fields annotated with @SerializedName for classes which are present. | ||
# If classes with fields annotated with @SerializedName have a no-args | ||
# constructor keep that as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, added those references back.
-if class * | ||
-keepclasseswithmembers,allowobfuscation,allowoptimization class <1> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having allowoptimization
can allow R8 to propagate field values, so if you have an explicit construction of a serialized class always setting a field to a specific value, then that single value can become the default value for the created instances instead of 0
/null
.
shrinker-test/proguard.pro
Outdated
#-keep class com.example.ClassWithSerializedName { | ||
# <init>(...); | ||
#} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, no removed the rules in comments. Also removed the rule above, as that was also not needed. I probably added it while testing different rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your update and also for your work on this!
Though it looks like some of the changes in your last commit might not have been intentional (see comments)?
Also, I think #2448 (comment) is still open; just to clarify that nothing was overlooked.
See also #2448 (comment) (unfortunately GitHub does not show that comment down here as well, so it might be easy to miss).
@@ -22,6 +22,7 @@ | |||
# Keep class TypeToken (respectively its generic signature) if present | |||
-if class com.google.gson.reflect.TypeToken | |||
-keep,allowobfuscation class com.google.gson.reflect.TypeToken | |||
#-keep class com.google.gson.reflect.TypeToken { *; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this commented line intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No removed.
-if class * | ||
-keepclasseswithmembers,allowobfuscation class <1> { | ||
@com.google.gson.annotations.SerializedName <fields>; | ||
} | ||
-if class * { | ||
@com.google.gson.annotations.SerializedName <fields>; | ||
} | ||
-keepclassmembers,allowobfuscation class <1> { | ||
-keepclassmembers,allowobfuscation,allowoptimization class <1> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought in #2448 (comment) you just explained that you intentionally removed allowoptimization
, but now you are adding it back? Is that intentional?
(Or am I misunderstanding this?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This rule is only covering the no-args constructor (<init>()
), so here there is no issue with allowoptimization
. For the rule above for the annotated fields it should not be there as otherwise R8 can do value propagation.
shrinker-test/common.pro
Outdated
@@ -38,3 +38,5 @@ | |||
-keepclassmembernames class com.example.ClassWithVersionAnnotations { | |||
<fields>; | |||
} | |||
|
|||
-dontobfuscate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this intentional? I think this should not be necessary and there were explicit rules which preserved names where needed for the tests.
Though it looks like you (accidentally?) removed one in proguard.pro
:
- -keepclassmembernames class com.example.NoSerializedNameMain$TestClassWithoutDefaultConstructor {
- <fields>;
- }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this was all for some testing. These changes should not have been there.
@Marcono1234 Thank you for being such an observant reviewer! Addressed my mistakes in my previous upload. Please take another look. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is close enough now. If there are further changes needed, we can make them in a follow-up PR.
Thank you for all the work you have put into this, and all the detailed explanations! That was helpful for me to understand at least a bit better how R8 works. And hopefully it will also be useful for future reference. The changes look good, so I don't think there is any direct follow-up PR needed. |
@Marcono1234, @sgjesse, @eamonnmcmanus: Thank you all for your work on this, I really appreciate it! |
* Update ProGuard default shrinking rules to correctly deal with classes without a no-args constructor * Update test after changing default shrinking rules * Adjust shrinker tests * Update rules * Addressed review comments * Addressed more review comments * Addressed more review comments --------- Co-authored-by: Marcono1234 <[email protected]>
This change deals correctly with classes without a no-args constructor. If a no-args constructor was not present the conditional keep rule would not trigger.
See https://issuetracker.google.com/150189783#comment11.