Shrinking your app with R8 (Android Dev Summit '19)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[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]
Info
Channel: Android Developers
Views: 15,187
Rating: undefined out of 5
Keywords: type: Conference Talk (Full production);, pr_pr: Android, purpose: Educate
Id: uQ_yK8kRCaA
Channel Id: undefined
Length: 18min 4sec (1084 seconds)
Published: Thu Oct 24 2019
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.