[騰訊內部干貨分享] 分析 Dalvik 字節碼進行減包優化


作者:騰訊移動客戶端開發工程師 彭旭康

無論是開發還是發行,不可避免的會遇到包體過大需要壓縮的情況。
對于發行商來說,盡管現在wifi遍地,但就算移動運營有一天開放4G免費,包體越小的你依然具備優勢。
對于玩家來說則更為簡單明了,用戶喜歡連續點擊幾個應用同時下載,包體小的很快就跑完讀條更容易被玩家接受。
但真正壓縮的時候遇到的麻煩事可真的不少:對游戲整體的壓縮卻不影響場景,對圖片的壓縮卻不影響品質。最慘的對代碼進行壓縮,簡直是讓程序們熬白了頭發只為包體再小幾K。
今天,直面減包優化這件事,分享一下騰訊人是怎么進行減包的!

Android結合版最近幾個版本在包大小配額上超標了,先后采用了包括圖片壓縮,功能H5,無用代碼移除等手段減包,還是有著很大的減包壓力。組內希望我能從代碼的角度減少一些包大小,感覺有點壓力山大。經過一段時間對手q安裝包反編譯后的Dalvik字節碼的分析,發現通過調整Java代碼可以減少編譯后的Dalvik字節碼,從而減少包大小。在這方面我做了許多的嘗試,有成功有失敗,拿出來給大家分享分享,多拍磚多交流。

優化思路
通過dexdump反編譯apk中的dex,得到對應Dalvik字節碼,找到尋找冗余的字節碼,嘗試去除或替換冗余的字節碼

目前主要是替換或去除原有的java代碼,減少對應的Dalvik指令,從而減少安裝包大小。

現在主要是從Dalvik字節碼分析來調整Java代碼,之后希望能夠通過ASM等框架直接調整字節碼減少現在的包大小。

優化效果
去除初始化賦值方案 ————減少整個手q的發布包大小80k左右。

插樁函數優化———減少整個手q的發布包大小2k左右。

其它嘗試方案,包括字符串拼接、移除interface很多空方法等,因為效果比較小、難以統一修改等問題,只是列舉下分析
![Uploading 2_171528.jpg . . .]結果,大家如果項目中出現的量比較多也是可以嘗試去優化的。

優化方案如下:
1、去除初始化賦值冗余
1.1、問題分析:
靜態變量為類的所有對象共享,在類加載的準備階段就會初始設置為系統零值(如下圖),比如String被設置初始值為null,而在類中存在

public static String A=null;

這樣的賦值行為會在之后的<cinit>()類構造器方法中執行,重復設置String A為null,增加了對應的<cinit>()方法的Dalvik指令,沒有必要,可以干掉。


2.jpg

成員變量在對象創建內存分配完成后,對應的內存空間會被初始設置為系統零值(和靜態變量一樣),比如int類型被設置為0,而在類中存在

public int B=0;

這樣的賦值行為會在之后的<init>()對象構造方法中執行,重復設置int B為0,增加了對應的<init>方法中的Dalvik指令,沒有必要,可以干掉。

對于初始化賦值為系統分配默認零值的靜態變量和成員變量,去掉初始化賦值,直接使用系統賦的系統零值,可以減少<cinit>和<init>中的Dalvik指令,從而減少包大小,而且可以提高類加載和對象創建的效率。

public static String A=null; 改成 public static String A;public int B=0; 
改成 public int B;

1.2、優化要點
注意對于static final的變量必須賦初值;

interface的變量都是static final類型的;

注意只有賦值為系統賦予的零值的靜態變量和成員變量才能按照這種方式優化,其它比如局部變量的改動會導致編譯不通過等問題。

1.3、冗余示例:
優化前:

public class FrostTest {
 public int report_posi=0;
 public FrostTest(){
 }
}

對應字節碼:

0795b4: |[0795b4] com.example.frosttest.FrostTest.<init>:()V
0795c4: 7010 5925 0100 |0000: invoke-direct {v1}, Ljava/lang/Object;.<init>:()V // method@2559
0795ca: 1200 |0003: const/4 v0, #int 0 // #0
0795cc: 5910 c909 |0004: iput v0, v1, Lcom/example/frosttest/FrostTest;.report_posi:I // field@09c9
0795d6: 0e00 |0009: return-void

對應字節碼:

0795b4: |[0795b4] com.example.frosttest.FrostTest.<init>:()V
0795c4: 7010 5925 0000 |0000: invoke-direct {v0}, Ljava/lang/Object;.<init>:()V // method@2559
0795ca: 0e00 |0003: return-void

減少了兩行Dalvik指令的執行,最后分析結果平均優化一處可以減少安裝包8個字節左右。
1.4、優化結果:
目前在手Q6.3.0分支上利用自行寫的過濾腳本(可以私下找我要對應的優化腳本用于對應的工程)可以看到優化的效果,如果對整個手q執行這個方案,預計能夠優化80k左右,修改了4677個文件,修改了17164處冗余。

2、調整插樁對應的代碼
Qzone補丁包引入了插樁這一步,需要在所有qzone類的構造函數中加入對mqq.app.MobileQQ類的引用。
優化的方案是將插樁插入到對象構造函數中的語句由

CtConstructor localCtConstructor = arrayOfCtConstructor[0];
localCtConstructor.insertBeforeBody("if (com.qzone.dalvikhack.NotDoVerifyClasses.DO_VERIFY_CLASSES) System.out.print(mqq.app.MobileQQ.class);");
localCtClass.writeFile(str);

改為

CtConstructor localCtConstructor = arrayOfCtConstructor[0];
localCtConstructor.insertBeforeBody(“if (com.qzone.dalvikhack.NotDoVerifyClasses.DO_VERIFY_CLASSES) mqq.app.MobileQQ.class.getName();”);
localCtClass.writeFile(str);

以Qzone某個類的<init>為例,由原本的字節碼

0e640c: |[0e640c] ADV_REPORT.E_REPORT_POSITION.<init>:()V
0e641c: 7010 0b84 0200 |0000: invoke-direct {v2}, Ljava/lang/Object;.<init>:()V // method@840b
0e6422: 6300 1f26 |0003: sget-boolean v0, Lcom/qzone/dalvikhack/Not
DoVerifyClasses;.DO_VERIFY_CLASSES:Z // field@261f
0e6426: 3800 0900 |0005: if-eqz v0, 000e // +0009
0e642a: 6200 4463 |0007: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@63440
e642e: 1c01 2a14 |0009: const-class v1, Lmqq/app/MobileQQ; // type@142a
0e6432: 6e20 7883 1000 |000b: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.print:(Ljava/lang/Object;)V // method@83780
e6438: 0e00 |000e: return-void

變成了

0e63a4: |[0e63a4] ADV_REPORT.E_REPORT_POSITION.<init>:()V0
e63b4: 7010 8183 0100 |0000: invoke-direct {v1}, Ljava/lang/Object;.<init>:()V // method@83810
e63ba: 6300 0326 |0003: sget-boolean v0, Lcom/qzone/dalvikhack/NotDoVerifyClasses;.DO_VERIFY_CLASSES:Z // field@26030
e63be: 3800 0700 |0005: if-eqz v0, 000c // +00070
e63c2: 1c00 6714 |0007: const-class v0, Lmqq/app/MobileQQ; // type@14670
e63c6: 6e10 2883 0000 |0009: invoke-virtual {v0}, Ljava/lang/Class;.getName:()Ljava/lang/String; // method@83280
e63cc: 0e00 |000c: return-void

這里替換一處代碼,將System.out.print改成getName,可以減少對象構造函數的一行Dalvik指令,替換了1314處初始化函數中插入的代碼,最終將對應的qzone_plugin.apk減少了2459字節,整個手q減少2457字節左右。<font color=#FF0000>一行代碼,2k收益</font>,其實還是很劃算的。3、字符串拼接下面是我針對String拼接的特殊情況“變量+”””和“””+變量”的不同形式舉例分析Dalvik字節碼

public abstract class FrostTest implements FrostInterface{
public String a="f";
public int b=1;
@Override
public void doSth1() { 
Log.i("frostpeng", a);
}

@Override
public void doSth2() {
 // TODO Auto-generated method stub
 Log.i("frostpeng", a+"");
}

@Override
public void doSth3() {
 // TODO Auto-generated method stub
 Log.i("frostpeng", ""+a);
}

@Override
public void doSth4() {
 // TODO Auto-generated method stub
 Log.i("frostpeng", String.valueOf(a));
}

@Override
public void doSth5() {
 // TODO Auto-generated method stub 
Log.i("frostpeng", String.valueOf(b));
}

public void doSth6() { 
// TODO Auto-generated method stub
 Log.i("frostpeng", b+"");
}

public void doSth7() { 
// TODO Auto-generated method stub
 Log.i("frostpeng", ""+b);}
}

字節碼

098ee4: |[098ee4] com.example.frosttest.FrostTest.doSth1:()V
098ef4: 1a00 b715 |0000: const-string v0, "frostpeng" // string@15b7
098ef8: 5421 c809 |0002: iget-object v1, v2, Lcom/example/frosttest/FrostTest;.a:Ljava/lang/String; // field@09c8
098efc: 7120 a321 1000 |0004: invoke-static {v0, v1}, Landroid/util/Log;.i:(Ljava/lang/String;Ljava/lang/String;)I // method@21a3
098f02: 0e00 |0007: return-void098f04: |[098f04] com.example.frosttest.FrostTest.doSth2:()V
098f14: 1a00 b715 |0000: const-string v0, "frostpeng" // string@15b7
098f18: 2201 2f05 |0002: new-instance v1, Ljava/lang/StringBuilder; // type@052
f098f1c: 5432 c809 |0004: iget-object v2, v3, Lcom/example/frosttest/FrostTest;.a:Ljava/lang/String; // field@09c8
098f20: 7110 a225 0200 |0006: invoke-static {v2}, Ljava/lang/String;.valueOf:(Ljava/lang/Object;)Ljava/lang/String; // method@25a2
098f26: 0c02 |0009: move-result-object v2
098f28: 7020 a525 2100 |000a: invoke-direct {v1, v2}, Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V // method@25a5
098f2e: 6e10 b125 0100 |000d: invoke-virtual {v1}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@25b1
098f34: 0c01 |0010: move-result-object v1
098f36: 7120 a321 1000 |0011: invoke-static {v0, v1}, Landroid/util/Log;.i:(Ljava/lang/String;Ljava/lang/String;)I // method@21a3
098f3c: 0e00 |0014: return-void
098f40: |[098f40] com.example.frosttest.FrostTest.doSth3:()V
098f50: 1a00 b715 |0000: const-string v0, "frostpeng" // string@15b7
098f54: 2201 2f05 |0002: new-instance v1, Ljava/lang/StringBuilder; // type@052f
098f58: 7010 a325 0100 |0004: invoke-direct {v1}, Ljava/lang/StringBuilder;.<init>:()V // method@25a3
098f5e: 5432 c809 |0007: iget-object v2, v3, Lcom/example/frosttest/FrostTest;.a:Ljava/lang/String; // field@09c8
098f62: 6e20 ac25 2100 |0009: invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; // method@25ac
098f68: 0c01 |000c: move-result-object v1
098f6a: 6e10 b125 0100 |000d: invoke-virtual {v1}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@25b1
098f70: 0c01 |0010: move-result-object v1
098f72: 7120 a321 1000 |0011: invoke-static {v0, v1}, Landroid/util/Log;.i:(Ljava/lang/String;Ljava/lang/String;)I // method@21a3
098f78: 0e00 |0014: return-void
098f7c: |[098f7c] com.example.frosttest.FrostTest.doSth4:()V
098f8c: 1a00 b715 |0000: const-string v0, "frostpeng" // string@15b7
098f90: 5421 c809 |0002: iget-object v1, v2, Lcom/example/frosttest/FrostTest;.a:Ljava/lang/String; // field@09c8
098f94: 7110 a225 0100 |0004: invoke-static {v1}, Ljava/lang/String;.valueOf:(Ljava/lang/Object;)Ljava/lang/String; // method@25a2
098f9a: 0c01 |0007: move-result-object v1
098f9c: 7120 a321 1000 |0008: invoke-static {v0, v1}, Landroid/util/Log;.i:(Ljava/lang/String;Ljava/lang/String;)I // method@21a3
098fa2: 0e00 |000b: return-void098fa4: |[098fa4] com.example.frosttest.FrostTest.doSth5:()V
098fb4: 1a00 b715 |0000: const-string v0, "frostpeng" // string@15b7
098fb8: 5221 c909 |0002: iget v1, v2, Lcom/example/frosttest/FrostTest;.b:I // field@09c9
098fbc: 7110 a125 0100 |0004: invoke-static {v1}, Ljava/lang/String;.valueOf:(I)Ljava/lang/String; // method@25a1
098fc2: 0c01 |0007: move-result-object v1
098fc4: 7120 a321 1000 |0008: invoke-static {v0, v1}, Landroid/util/Log;.i:(Ljava/lang/String;Ljava/lang/String;)I // method@21a3
098fca: 0e00 |000b: return-void098fcc: |[098fcc] com.example.frosttest.FrostTest.doSth6:()V
098fdc: 1a00 b715 |0000: const-string v0, "frostpeng" // string@15b7
098fe0: 2201 2f05 |0002: new-instance v1, Ljava/lang/StringBuilder; // type@052f
098fe4: 5232 c909 |0004: iget v2, v3, Lcom/example/frosttest/FrostTest;.b:I // field@09c9
098fe8: 7110 a125 0200 |0006: invoke-static {v2}, Ljava/lang/String;.valueOf:(I)Ljava/lang/String; // method@25a1
098fee: 0c02 |0009: move-result-object v2
098ff0: 7020 a525 2100 |000a: invoke-direct {v1, v2}, Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V // method@25a5
098ff6: 6e10 b125 0100 |000d: invoke-virtual {v1}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@25b1
098ffc: 0c01 |0010: move-result-object v1
098ffe: 7120 a321 1000 |0011: invoke-static {v0, v1}, Landroid/util/Log;.i:(Ljava/lang/String;Ljava/lang/String;)I // method@21a3
099004: 0e00 |0014: return-void
099008: |[099008] com.example.frosttest.FrostTest.doSth7:()V
099018: 1a00 b715 |0000: const-string v0, "frostpeng" // string@15b709901c: 2201 2f05 |0002: new-instance v1, Ljava/lang/StringBuilder; // type@052f
099020: 7010 a325 0100 |0004: invoke-direct {v1}, Ljava/lang/StringBuilder;.<init>:()V // method@25a3
099026: 5232 c909 |0007: iget v2, v3, Lcom/example/frosttest/FrostTest;.b:I // field@09c9
09902a: 6e20 a825 2100 |0009: invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;.append:(I)Ljava/lang/StringBuilder; // method@25a8
099030: 0c01 |000c: move-result-object v1099032: 6e10 b125 0100 |000d: invoke-virtual {v1}, Ljava/lang/StringBuilder;.toString:()Ljava/lang/String; // method@25b1
099038: 0c01 |0010: move-result-object v1
09903a: 7120 a321 1000 |0011: invoke-static {v0, v1}, Landroid/util/Log;.i:(Ljava/lang/String;Ljava/lang/String;)I // method@21a3
099040: 0e00 |0014: return-void

從示例中可以看出各類字符串拼接方式的優劣,如果用String.valueOf()絕對是最優方案。只是通過對“變量+”””和“””+變量”的形式在手q整個項目調整以后大概能夠優化6k左右,如果只是優化qzone部分,效果比較微小,腳本方面不太好過濾對應情況,暫時沒有加入,只是做了下試驗。
PS:其實“String +”一般來說比StringBuffer的拼接更費字節碼,這個部分可以自行驗證,前提是a+b+…的形式中首位a這個為變量,而不是常量,如果a是常量,則實際上和StringBuffer等同,這也是個優化點,具體可以參考文章 從字節碼視角看java字符串的拼接 。

4、調整interface到class,減少實現接口造成的空方法
很多代碼中實現接口時有很多的空方法,并沒有作用但還是會占用字節碼,希望能夠通過調整對應的interface為class,去除冗余的空方法,減少字節碼,從而減少包大小。
示例如下:

public interface FrostInterface {
public abstract void doSth1();
public abstract void doSth2();
public abstract void doSth3();
}

public class FrostTest1 implements FrostInterface{

 @Override 
public void doSth1() { 
// TODO Auto-generated method stub

 }
@Override
 public void doSth2() {
 // TODO Auto-generated method stub 
} 
@Override public void doSth3() {
 // TODO Auto-generated method stub
 }
} 

改成

public abstract class FrostTest implements FrostInterface{
@Override
public void doSth1() {
 // TODO Auto-generated method stub
}
@Override
public void doSth2() {
 // TODO Auto-generated method stub
}
@Override
public void doSth3() {
 // TODO Auto-generated method stub
}
}
public class FrostTest1 extends FrostTest{
}

該方案的缺點在于修改必須手動,難度大,qzone中場景不足以引起量變,而且因為Qzone中<init>中還加入了插樁函數的負擔,所以整體優化效果不佳,優化完qzone才2k不到的大小縮減,優化難度高收益小,棄坑。這些減包思路希望能夠給一起在減包路上踩坑的朋友們一些幫助吧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容