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.
2. Create helper method to do bit mask check
The big difference between proto2 and proto3 is that the later version has removed has<Field>()
method for String and primitive types. If your project is using proto2 and heavily checking the field presences, the following trick would help you on app size.
Proto2 uses bit field to mark the presence of declared fields, those has<Field>()
methods have the same pattern:
[codesyntax lang=”java” lines=”normal”]
public boolean hasId() { return ((bitField0_ & 0x00000002) == 0x00000002); } public boolean hasEmail() { return ((bitField0_ & 0x00000004) == 0x00000004); }
[/codesyntax]
But actually, the final executable bytecode would be more concise if you change the code as below:
[codesyntax lang=”java” lines=”normal”]
public boolean hasId() { return checkBitMask(0x00000002); } public boolean hasEmail() { return checkBitMask(0x00000002); } public final boolean checkBitMask(int bitMask) { return ((bitField0_ & bitMask) == bitMask); }
[/codesyntax]
On the method count perspective, this could enable the Code Shrink tool to inline has<Field>()
, because it is a one line method call, then you would only have checkBitMask()
in the final Dex file.
On JVM bytecode perspective, ((bitField0_ & 0x00000002) == 0x00000002)
requires 4 more instructions than checkBitMask(0x00000002)
. Therefore the more has<Field>()
methods you have, the more you would save on it. Even if it’s just a few methods, because they would be inlined, which will be talked about below, there would be no cost.
3. Change Enum to @IntDef
Each Enum type introduces at least 4 methods, that’s why Android Developer Docs recommend to use @IntDef
and @StringDef
to replace them. But Protocol Buffer follows Effective Java recommendation, and generates Enum classes for message types like enum and oneof.
The Protocol Buffer generated Enum class only has one int value, such Enum is called a CountEnum, which is straightforward to strip out the class and use the value directly. You can use the script to modify it to be @IntDef, but because you can also integrate a powerful bytecode optimizer: Redex, it can complete the work for us.
4. Use R8 Rule -alwaysinline
to force inline has<Field>()
methods
From Jake Wharton’s blog post, I found this special R8 rule to force inline some methods. Sometimes, method inline could increase the app size as it duplicates the content of the methods. But if we combine the trick 2 in the list, we can see the method reduce very small app size increasing.
[codesyntax lang=”java” lines=”normal”]
# ProtoBuf generated class - always inline hasXXX() methods -alwaysinline class com.iderzheng.proto.* { public boolean has*(); }
[/codesyntax]