[MUSIC PLAYING]
SOREN GJESSE: Good afternoon, and welcome to this talk on R8
shrinking.
My name is Soren Gjesse.
I'm a software engineer and a tech lead on the R8 project.
So this is the agenda of this short talk.
First, I'll touch briefly upon what shrinking
means from an R8 perspective followed
by giving you an idea of what you can potentially
save by using R8.
Then I'll give an overview of how the shrinking process works
and how it's configured in Android Studio.
Then Christoffer Adamsen will take over and give you
a detailed rundown of one of our more advanced shrinking
algorithms.
So what is R8 shrinking?
Well, in terms of R8, shrinking means optimizing code for size.
It's all about reducing the amount of code in your app
as much as possible.
And for Android, we are talking about the size
of the text files in the app.
Having a small app is a benefit for both you and the ecosystem.
It means more installs, especially in emerging markets,
and also means that more people are more likely to keep
your app on their phone.
So overall, we have three different shrinking techniques.
The first is tree shaking, which removes unused code
and structure.
It use static analysis to get rid of unreachable code
and remove uninstantiated objects.
The second is using traditional compiler techniques
for optimizing for size.
The third is identify renaming, which is also
sometimes referred to as obfuscation, which can shorten
names of classes, methods, and fields,
and thereby reducing size.
It can also squash the package namespace down to the root.
In addition to these code shrinking techniques,
we also have ways to reduce the amount
of debug information, which is also
resident inside the text files.
So why would I gain anything from using R8?
When you write an app, all the code in the app
is there to serve a purpose, some feature of the app.
So how much is there actually to remove?
Well, the answer is quite a lot.
And there are two main reasons for that.
First of all, all apps use third-party libraries.
So there's a lot of code in your app that you have not written.
And also, when you use a third-party library,
typically only a very small part of a third-party library
is used in a particular app.
And second, even after removing the code that's not used,
the remaining code can still be optimized for size
using the other optimization techniques.
So as mentioned, library code counts.
So without any shrinking, all library code
is retained in your app.
Here's a list of some of the more popular
third-party libraries, like the Jetpack libraries or AndroidX
libraries.
They're used by pretty much all applications.
Oh, sorry.
Could I go back one slide, please?
OkHttp is a very popular networking app.
Guava, Gson, and the Google Play Services
are frequently used Google-provided libraries.
And note that the Kotlin Standard Library is also
on this list, because if you're writing a app in Kotlin,
the Kotlin Standard Library becomes
part of your application.
Now, before I go into more details,
I'll just tell you how to enable R8 shrinking in your app.
In your app's main build.gradle file, set minifyEnable to true.
So now it's turned on.
Don't let the minifyEnable name confuse you.
It is actually turning on R8 shrinking.
So now that R8 has been turned on,
how much can you expect to reduce
the size of your application?
Well, just to give you an idea, I'll
try to create a simple one activity app.
I'll build it in two versions, one using
Kotlin and one using Java.
Both versions use the Jetpack libraries,
but no other libraries.
Note that before shrinking, the size of the Kotlin app
is considerably larger than the Java app.
The main reason for this is that the Kotlin Standard
Library is part of the Kotlin app,
but not part of the Java app.
However, after shrinking, you can
see that the size of the two apps
are at a comparable and much smaller size.
So now I try to add the OkHttp 4.2 library to both projects.
And here you can see that the Java version of the app
suddenly doubles in size--
more than doubles in size, actually--
whereas the Kotlin app only grows up 21%.
And the reason for that is the OkHttp 4.2 library
is written in Kotlin.
So suddenly, the Java app gets the Kotlin Standard Library
s a transitive dependency as well.
Now note, after shrinking, the size
is still the same, because in this example,
I haven't actually used OkHttp in the code.
Now you might say that this is cheating to show
numbers for an empty app.
So to give you an idea, I have tried
to take the effect of a real app.
So it took the Google I/O app from this year.
And here you can see that R8 actually reduces the code
to less than half.
And if you look at the number of methods and DEX files,
you can see that shrinking the app
has a lot of other positive side effects.
Now let me just take you through the basic process of how
the shrinking algorithm works.
So the shrinking algorithm traces all reachable code
from the well-known entry points of the program.
So R8 starts with these well-known entry
points of the program.
It walks through all the code that is reachable.
And to define these well-known entry points,
we use what we call keep rules.
And there's no battery.
Well, maybe-- there we go.
Yes.
So take a look at this simple "Hello, world!"
example in Java.
The well-known entry points of a Java application
is the static [INAUDIBLE] of the Application class.
And this is defined using this keep rule.
So this is the entry point.
And when shrinking starts, it starts
by tracing the code at the entry points.
So here we go through the entry point.
It find that greeting is called.
So it goes to greeting.
It traces greeting.
Greeting calls into the runtime.
So here the tracing stops.
And now the tracing has finished tracing all code.
So now the tree shaking can remove the unused code.
There it goes.
And then the identifier renaming can
rename the greeting method to something shorter.
And finally, the optimization phase can actually
inline this method, so that you have
the resulting of the shrinking is quite a lot smaller
than what we started with.
So this demonstrates the three techniques--
tree shaking, optimization, and identifier renaming.
Now just like a standalone Java program,
an Android application also have a number
of well-known entry points.
So these are your activities, your services,
your content providers, and your broadcast receivers.
And the AATP tool will take care of handling these entry points.
So here is a manifest file.
It defines the main activity.
And AATP tool will generate this keep rule
to have this entry points survive.
Now, besides these well-known entry points
generated by AATP tool, there's a number of other things
that is required for the Android platform, which
is provided by Android Studio in this configuration file.
Now, with all the entry points in the app in place,
there's one more area that which needs configuration
for the app to work after shrinking with R8,
and that is reflection in application code.
So reflection can be seen as a entry point
into the code, which is not really recognized
by R8 by just tracing the code.
And one thing to keep in mind here
is that reflection can happen in third-party libraries.
And as third-party libraries are now effectively
part of your app, you also become
responsible for the reflection performed in these applications
as well.
So some of these libraries might come
with their own rules included.
But also keep in mind that quite a few libraries are actually
not written neither with Android nor shrinking in mind,
so they might need additional configuration.
Just to show how this can actually manifest itself
in your program, take a look at this simple code.
We have a class with a field name name.
And we have a main method that will just create an instance
and serialize that to JSON.
So we shrink the code, and we run, and then
you see the output is an empty JSON object.
Now, why is that?
It turns out that the field name is written, but never read,
so R8 removes it.
Actually, this field is supposed to be read by the Gson
serializer, but Gson uses reflection techniques,
so R8 does not see that this field is actually read.
OK.
Now we try to add a method to read the field.
So this method will somehow read the field.
What happens when we run the code?
Now the output is this.
We have a JSON with a field named A. And why is that?
Well, now the field is both written and read,
so it's not [INAUDIBLE] by R8, but renaming can still change
the name to a shorter name.
And as Gson uses reflection, it'll
just find the name that's currently
on the field and not the original shorter name
from source.
Now, how do I fix this?
I add this keep rule.
It says that in the class Person,
keep the field named name.
So we're running the code.
The output is the expectated JSON object.
It works because R8 is instructed to not touch
the field named name.
Yes.
And these additional keep rules goes into an additional file
that you reference from your application's
build.gradle file.
And with that, I'll give it to Christoffer.
CHRISTOFFER ADAMSEN: Thank you, Soren.
So now let's try to take a closer look
at some of the optimizations that R8 performs.
R8 consists of a lot of different optimizations.
It turns out that a few of these optimizations
are responsible for the majority of code size reductions.
So one important optimization is minification, which Soren also
mentioned, which gives shorter names to identifiers
such as class names.
Another important optimization is method inlining,
which often ends up removing a lot of method definitions
in apps, because it turns out that many methods are only
used in a few places.
And then there's tree shaking, which
is responsible for removing unused code.
R8 also has more specialized optimizations
that are important for removing code structure that would not
be removed by standard techniques as these ones.
So one example of that is what we call class inlining.
Class inlining is an optimization
that attempts to remove classes that are only used locally.
So as programmers, we often introduce
abstractions and utilities, helper methods,
and so on, in order to make the code more maintainable and also
easier to read.
So one good example of that is Builder classes.
So with builders, you would typically
create a new instance of a builder,
invoke a few methods on that to modify
the state of the builder, and then invoke
a method to create a new object based
on the state of the builder.
However, at runtime, these builders
are not strictly needed, because it would be possible just
to write the code without actually using the Builder.
So it's mostly there for the maintainability of the code.
So class inlining will actually try
to rewrite the code in such a way
that the Builder ends up being unused,
so that it can be removed by [INAUDIBLE] optimizations.
So let's try to look at an example.
So here's a few lines of code taken from the Google I/O app
from this year.
So I should mention that I've slightly
simplified the code here.
So if you go and look in the I/O app,
it will be slightly different from this.
So here on the first line, we invoke
a method called databaseBuilder, which
will basically just return a new instance of a builder.
Then we invoke two methods on that.
And the build method here is going to return
a new instance of a database.
So the issue, so to speak, with this piece of code
is that it uses this builder here,
which has last time I checked around 15 or 17 fields
and a bunch of methods.
And these few lines of code actually
end up using the entire builder.
So we can't get rid of it.
So let's try to look at how R8 optimizes this code.
So the first thing that R8 will do
is to look at this field read here.
And R8 will recognize that this field has a constant value.
So R8 will inline the value of the field.
And now since the field is never read anymore,
we can just get rid of the field entirely.
The next thing R8 will do is to inline this databaseBuilder
method.
So this method here just returns a new instance of a builder
as I mentioned before, and then it
performs a few sanity checks on the database name argument.
So if we inline that into the code,
then we'll just move the code out into the outer context.
And now if we look at the if condition here,
then R8 will recognize that the name is definitely not null.
And if you trim that and take the length,
then you don't get zero.
So since this condition here is always false,
R8 will just remove the code.
So this leaves us with this piece of code here.
And that's where the class inlining optimization kicks in.
So this optimization works in four phases.
And the first thing that it's going to do
is to run an escape analysis for the builder here.
So it will basically try intuitively
to determine that the responsible is only locally
used in this piece of code.
So say that the responsible was being written into a field,
well, then there would be no hope for us
that we could get rid of the instance
entirely, because we actually need to get an instance
and store it into a field.
So in this case, R8 will analyze the code
and determine that the builder is in fact local.
And then it will move on to the next phase, where
it will try to inline each of the methods that
are being used on the builder.
So we will start with the constructor here.
It just assigns a bunch of fields on the builder instance.
So when we inline that, we'll move these field assignments
into the outer context, where the assignments will now
be on the builder instance.
Then R8 moves on to inlining the next method.
This just happens to set a field to true.
So when we inline, that will move that field assignment out
in the outer context as well.
And then, finally, there's the build method.
So it's not particularly important for the purpose
of this presentation what this build method is actually doing.
One thing that's worth noting here
is that it reads a bunch of fields
on this, which is the builder, so when we inline this code,
then these field accesses will be
on the builder instead of this.
OK, so that leaves us with this piece of code here.
So remember that we want to get rid of the builder instance,
but we still have quite a few accesses to the builder.
If we look more closely at the code here,
you'll see that we have a field read right after a field write.
So we can just take the value that was stored into the field
and use that instead of reading the field, like this.
And if we do that for the other field reads as well,
then we would also propagate these values down.
Now if we look at these field assignments here,
we just removed all of the reads of these fields,
so now we can get rid of all of these field assignments.
And that leaves us with this piece of code.
So now we've pretty much achieved
what we were hoping for, because the builder is no longer being
used in this part of the code.
So now we can also remove that one.
And that means that we've now basically optimized the code
and gotten completely rid of the builder.
So now when we run tree shaking, the builder
will actually just be removed.
So on the Google I/O app, so this optimization alone
is responsible for removing exactly 170 classes, around
350 methods, and then it reduces the DEX size of the app
by around 38K.
So I think that's quite impressive for a very
specialized optimization.
And remember that this is just one specialized optimization
out of many [INAUDIBLE].
So with that, that concludes the example.
And I'll give the [INAUDIBLE] back to you, Soren.
SOREN GJESSE: Thank you.
So the takeaway from this should be use R8
to make your apps smaller, as app size does matter.
It's easy to turn on.
All the Android entry points are handled.
You only need to worry about reflection.
And some of the libraries which are normally
used for [INAUDIBLE] things like serialization frameworks
or maybe object-relational mappers.
Yes.
And now I actually want to talk about something
completely different.
So are you missing the Java 8 APIs and java.util.streams
in Android?
Are you interested in using more Java 8 APIs
on lower SDK versions?
Well, many developers have asked.
And at Google I/O earlier this year,
we promised to work on this.
So our team is not only responsible for shrinking,
but we're also responsible for Java 8 desugaring.
And we've been working on this.
And as you maybe have heard already yesterday,
this is now coming.
And I'm happy to announce there's
a preview of this available already in Android Studio
4.0 alpha 1, which was released yesterday.
[APPLAUSE]
So with that, I'll just close and say, please use R8
if you're not already doing so, try out the new Java 8
desugaring.
Don't hesitate to file bugs.
We actually value your feedback.
We want to have bugs.
We want to have feature requests, especially around
the new desugaring.
And that was all we had for you.
Thank you very much for attending this talk.
[APPLAUSE]
[MUSIC PLAYING]