ProtoBuf 2.0 method count optimization for android development
The Protocol Buffer library was not initially built for Android, it just turned out to have a Java version to be used in Android, but it’s not optimized for Android apps, for example: it doesn’t consider the method count limit in Android.
So, in this article, I’d like to share some work I found specifically on Protocol Buffer Version 2 that can reduce the method count. If your app is also heavily relying on Protocol Buffer, I hope these approaches are useful for you too.
General Approaches
1. Use Protocol Buffer Java Lite Runtime
Just as the name indicates, the dependency library is much smaller than the regular Protocol Buffer Java runtime, the generated code is also much slimmer. However, the APIs are compatible between those two versions, so the call sites would not be affected when changing the library.
2. Don’t use <Message>OrBuilder
interface
For each Protocol Buffer message definition in .proto file, the Protocol Buffer compiler generates an interface named <Message>OrBuilder
(<Message>
is the name defined in .proto file). This interface would be implemented by the concrete <Message> class and the corresponding Builder.
It might be attractive to use it as a variable type thereby you can depend on abstractions to not concrete classes. But calling methods on Java interface would make Dex take count of those methods.
In reality, every place can directly use either <Message>
or Builder
, then the optimization tool (like R8, ProGuard) can safely remove the methods declared on the interface.
Special Tricks
Protocol Buffer Java Lite is very good, but if I open the magic box of generated Java classes, I notice it’s still not optimal for Android. There are a couple places I could modify to make it more effective for Android applications.
Instead of copying the Java files and making the change, which is error prone when engineers update the .proto file, I created a script to automate the job. Just add the execution of this script at the end of the Protocol Buffer gradle task, so it works just as a complement of Protocol Buffer code generation process.
[codesyntax lang=”groovy” lines=”normal”]
protobuf { protoc { artifact = 'com.google.protobuf:protoc:3.7.0' } plugins { javalite { artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' } } generateProtoTasks { all().each { task -> task.builtins { remove java } task.plugins { javalite { } } // `getOutputDir()` can only be read during configuration, // so it’s out of action block def outputDir = task.getOutputDir(task.plugins[0]) task.doLast { exec { commandLine file('protobuf-optimizer.py') args outputDir } } } } }
[/codesyntax]
In the script, I have made the following modification on generated java code.
1. Change the modifier of Builder constructor from private to package
Simply running the sample addressbook.proto from Protocol Buffer tutorial side, you would get a class like the following snippet:
[codesyntax lang=”java” lines=”normal”]
public static final class Person { private Person() {} protected final Object dynamicMethod( com.google.protobuf.GeneratedMessageLite.MethodToInvoke method, Object arg0, Object arg1) { switch (method) { case NEW_BUILDER: { return new Builder(); } } } public static final class Builder { private Builder() {} } }
[/codesyntax]
When analyzing the APK file, we could would find some synthetic classes and methods:
This is because the outer class is accessing the private constructor of the inner class. The generated synthetic class would contribute an extra constructor to the Dex file. Therefore, by simply removing the private from Builder(), you can remove such classes and methods.
Read More →