When it comes to app development and distribution, it's crucial to keep the package size small for both distributors and users. Distributors benefit from smaller packages as they're easier to download and distribute, even with widespread Wi-Fi and 4G availability. Users prefer smaller packages as they can download multiple apps simultaneously, and apps with smaller sizes load faster, making them more appealing.
However, compressing an app without affecting its performance and quality can be challenging. Developers need to compress the game as a whole without impacting the scenes, reduce image sizes without losing quality, and compress code, which can be a labor-intensive process for minimal size reduction. Today, let's discuss how Tencent staff tackle the issue of package reduction and optimization to make their apps more efficient and user-friendly!
Recently, several versions of our Android combined edition have exceeded the package size quota. We've tried various methods to reduce the package size, such as image compression, using HTML5 for features, and removing unnecessary code. However, we still face significant pressure to minimize the package size further. My team hopes that I can achieve this reduction from a code perspective, which seems like a daunting task.
After analyzing the Dalvik bytecode of the decompiled HandQ installation package, I discovered that adjusting the Java code can reduce the compiled Dalvik bytecode, which in turn reduces the package size. I've made several attempts in this area, with varying degrees of success, and I'm sharing my findings for additional feedback and discussion.
By decompiling the dex file in the APK using dexdump, we can obtain the corresponding Dalvik bytecode, identify redundant bytecode, and attempt to remove or replace it.
Our primary approach involves replacing or removing the original Java code to reduce the corresponding Dalvik instructions and, consequently, the installation package size.
Currently, we adjust Java code based on Dalvik bytecode analysis. In the future, we hope to directly modify bytecode using frameworks like ASM to further reduce the package size.
- Removing the initialization assignment scheme reduces the entire HandQ release package size by about 80k.
- Instrumentation function optimization reduces the entire HandQ release package size by about 2k.
Other attempted solutions, such as string concatenation and removing numerous empty methods in interfaces, have smaller impacts and are challenging to modify uniformly. We only list the analysis results here, but if your project has a large number of these issues, you can also try optimizing them.
1.1. Problem analysis:
Static variables are shared by all objects of a class and are initially set to system default values during the class loading preparation stage (as shown below). For example, a String is initially set to null, and in the class, there is an assignment like:
String A = null;
This assignment will be executed later in the <cinit>() class constructor method, repeatedly setting String A to null, which increases the corresponding Dalvik instructions in the <cinit>() method. This is unnecessary and can be removed.
Member variables, after memory allocation is completed during object creation, will also be initialized to system default values (similar to static variables). For example, an int type is set to 0, and in the class, there is an assignment like:
public int B = 0;
This assignment will be executed later in the <init>() object constructor method, repeatedly setting int B to 0, which increases the corresponding Dalvik instructions in the <init> method. This is unnecessary and can be removed.
By eliminating the initialization assignments for static variables and member variables that are already initialized with default zero values by the system, we can reduce the Dalvik instructions in the <cinit> and <init> methods. This reduction not only decreases the package size but also improves the efficiency of class loading and object creation.
1.2. Optimization key points
Keep in mind that static final variables must be assigned initial values;
Variables in interfaces are always of the static final type;
Remember that only static variables and member variables assigned with system-assigned zero values can be optimized in this way. Other changes, such as those involving local variables, may cause compilation issues.
1.3. Redundancy example:
Before optimization:
Corresponding bytecode:
`
After optimization:
Corresponding bytecode:
This optimization eliminates the execution of two Dalvik instructions. The final analysis result shows that optimizing one instance can reduce the installation package size by about 8 bytes on average.
1.4. Optimization results
Currently, in the HandQ 6.3.0 branch, we have implemented this optimization using a custom-written filter script (you can contact me privately for the specific optimization script tailored to your project). The optimization effect is noticeable. If this solution is applied to the entire HandQ, it is estimated that it can reduce the package size by about 80k, modify 4,677 files, and eliminate 17,164 instances of redundancy.
The Qzone patch package introduces instrumentation, which requires adding a reference to the mqq.app.MobileQQ class in the constructor of all Qzone classes. The optimization plan is to change the statement inserted into the object constructor from:
to:
Taking a Qzone class's <init> as an example, the original bytecode:
becomes:
In this case, replacing one piece of code by changing System.out.print to getName can reduce one line of Dalvik instructions in the object constructor. This change was made in 1,314 initialization functions, ultimately reducing the corresponding qzone_plugin.apk by 2,459 bytes and the entire HandQ by about 2,457 bytes. A single line of code yields a 2k benefit, which is quite cost-effective.
Below are examples of different forms of string concatenation, specifically "variable + \"\"" and "\"\" + variable," analyzed in terms of Dalvik bytecode.
Bytecode:
From the examples, we can see the pros and cons of various string concatenation methods. Using String.valueOf() is definitely the best solution. However, after adjusting the forms of "variable + \"\"" and "\"\" + variable" throughout the entire HandQ project, the optimization effect is only about 6k. If only optimizing the Qzone part, the effect is minimal, and it is difficult to filter the corresponding situations with a script. This has not been implemented yet, but an experiment was conducted.
PS: In general, "String +" concatenation consumes more bytecode than StringBuffer concatenation. This can be verified on your own, provided that the first element "a" in the form of "a + b + ..." is a variable, not a constant. If "a" is a constant, it is actually equivalent to StringBuffer, which is also an optimization point. For more details, please refer to the article "Looking at Java String Concatenation from a Bytecode Perspective."
In many codes, implementing interfaces involves many empty methods that do not serve any purpose but still occupy bytecode. The hope is to adjust the corresponding interface to a class, remove redundant empty methods, reduce bytecode, and thus reduce the package size.
For example:
Change to:
The disadvantage of this solution is that the modifications must be done manually, which is difficult. In Qzone, the scenario is not significant enough to cause a substantial change. Moreover, the overall optimization effect is not good due to the additional burden of the instrumentation function in Qzone's <init>. After optimizing Qzone, the size reduction is less than 2k. The optimization is difficult with little return, so this approach was abandoned.
I hope these package reduction ideas can provide some help to those who are also facing challenges in reducing package sizes.