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