學習版游戲”這個話題,無論何時何地,總能在玩家間掀起“腥風血雨”,輕則幾百樓的口水仗,重則上綱上線發起人身攻擊,甚至是現實層面的沖突。
對于“學習版游戲”的存在,支持者和反對者,態度也各不相同。而數量如此眾多的玩家群體,其發言自然也是良莠不齊,有的一針見血、頗有見地,也有的粗鄙不滿、胡攪蠻纏——至于孰是孰非,其實已經成了一筆很難算清的“糊涂賬”。
然而近日,CODEX小組官宣解散,這一強力破解團隊突然“金盆洗手”。或許會讓這場已經幾乎銷聲匿跡的爭論,再次引起新一輪“戰爭熱潮”。
CODEX這個名字,對于絕大部分玩家而言,應該都算不上陌生。畢竟,無論你究竟有沒有接觸過CODEX的學習版資源,但它們時常在游戲發行之后不久,就出現在各大論壇的資源板塊,讓你無可避免地和它們混個臉熟
CODEX之所以能有如此大名,根本原因只有一個——實力。在Denuvo加密保護,也就是玩家口中的“D加密”被廣泛應用之后,相當多的破解團隊就此被攔在門外。具備足夠實力對“D加密”繼續進行破解的,僅僅只有為數不多的幾個團隊,而CODEX小組便是其中之一。從2014年開橫空出世后,CODEX小組的存在,讓很多玩家有機會接觸到更多的單機游戲。也正是因為這幾年他們取得的“累累戰果”,在官宣解散的時候,才能在玩家群體中引發如此大的轟動。
伴隨一代“王者“的落幕,Sloclap開發的《師父》,成為了ODEX破解的最后一款游戲。在2月24日正式解散當天,CODEX小組為玩家送上了最后一份“禮物”——《模擬人生4》的最新DLC“婚旅奇緣”。自此之后,便是“江湖路遠,各自珍重”了。
在最后的告別信中,CODEX提到,成立之初的主要目標,便是為了和另外一家PC游戲破解團隊“Reloaded”進行競爭。整個過程稱得上互有輸贏。這段時間在CODEX口中,是一段“有趣但有時見不得光”的日子。可惜不久之后“Reloaded”團隊就煙消云散,在CODEX小組看來,趣味在此之后就開始逐漸消失。
此外,很多新的破解團隊在冒頭之后,都喜歡打著老團隊的旗號活動,被冒用的名頭中也包括了“Reloaded”。CODEX小組對此也十分不滿,認為這支新團隊曾多次搞砸各種事情,讓他們繼續頂著這個名頭行事,是對于昔日經典的一種抹黑。
要知道,CODEX小組攻破的加密技術,涵蓋了眾多平臺和不同類型:XBOX live、Denuvo、Uplay、EPIC、Steam(Stub+API+CEG)……而在CODEX最為活躍的2016-2020年這幾年里,CODEX小組對外發布的學習版游戲數量達到 驚人的3700款以上。即使是《刺客信條:起源》和《孤島驚魂5》這樣使用不同加密保護的游戲,仍難逃被攻破的命運;甚至2019年發推出《生化危機2重制版》,僅僅發售七天就被CODEX小組攻破。
自信源于實力,正是因為技術層面的強大,面對雨后春筍般的新生代破解團隊, CODEX仍舊是“面不改色心不跳”。在他們看來,這些新團隊尚顯稚嫩,根本不具備與自己同臺競技的能力,至于“分庭抗禮”更是癡人說夢。從2014年到2022年,縱觀CODEX小組這八年“見不得光“卻稱得上波瀾壯闊的生涯,多項加密技術的首破、7500款以上的游戲破解,足以作為他們的“墓志銘”。
在CODEX看來,最初的目標,很多年前就已經實現。在此之后,“事情就算不上有趣了”,如今已經是時候說聲再見了。CODEX的故事,在小組成員各奔東西之后,就正式畫上句號,但對于整個游戲產業來講,故事顯然還在繼續……
當然,就像前文提到的那樣, CODEX解散這一消息,必然會使得學習版游戲從陰暗的角落,來到舞臺正中的聚光燈下,接受不同玩家的審視。一千個人心中有一千個哈姆雷特,學習版資源或許是一些玩家口誅筆伐的“吸血鬼”,但同樣也會是一些玩家眼里的“綠林大盜”。
事實上,盜版游戲對于很多玩家而言,都是如同“啟蒙老師”一樣的存在。在那個物質生活相對匱乏的年代,大多數普通家庭,很難花費大量的資金,去滿足 看似虛無縹緲的精神需求。正版游戲這種動輒成百上千元的“大家伙”,必然不會成為絕大部分家庭的消費目標。而它們的身影,往往也只會出現在大城市的書店角落,抑或是軟件商店當中。
購買力和購買渠道的雙重限制,使得早期的正版玩家少之又少。破解光盤,紅白機和那些盜版卡帶,才是大多數人能夠接觸到的游戲資源。
對我來說,小時候最興奮的事情,就是將幾天的零花錢存下來,然后變成一張黃色的游戲卡帶。至于盜版和正版,在那個年代也很難形成這樣的概念。心中唯一的念頭,可能就是早點玩到新買的卡帶。
可以說,盜版資源在那個年代的出現,對于包括我在內的不少游戲玩家,都像是火種一樣的存在,是自己接觸游戲的啟蒙課程。正是因為當初播下了那些種子,才能在日后“生根發芽”,造就一個又一個忠實的游戲玩家。當他們 具備物質條件和購買渠道之后,為自己喜歡的游戲“剁手” ,就成了水到渠成的事。
此外,即便到了現在,學習版的游戲資源,對于近況拮據的玩家而言,讓自己有機會接觸到新游戲。同時也能讓那些搖擺不定的玩家,沒有顧慮地去嘗個鮮。
但對于廠商來講,游戲發行之后,學習版資源必然會導致利益最大化的目標受到影響。
畢竟,一款游戲的死忠粉在整個玩家群體當中,占比始終十分有限的。廠商想要獲取更大的利益,主要目標便是那些搖擺不定,糾結“買和不買”的路人玩家。但能夠零代價游玩的學習版資源,在吸引路人玩家這方面,顯然具備著正版資源所沒有的成本優勢。游戲制作者的創作熱情,也會由于這種情況遭受一定打擊。
玩家和廠商之間會出現立場的差異,其實再正常不過。玩家作為消費者,希望以最低的代價獲得游戲,換取精神層面的滿足。廠商作為銷售者,想要爭取到每一個潛在的正版玩家,通過售賣游戲實現利益最大化。雙方的訴求在某種意義上,本就是“對立”的。
即便在玩家群體中,對待“學習版游戲”的態度同樣迥然不同。在樂于為優秀游戲買單的玩家看來,認為自己購買的每一個游戲,都是對制作者的肯定,讓其擁有足夠高昂的熱情,投入到下一款游戲的制作當中。但對于“學習版游戲”愛好者而言,零成本的“學習版游戲”擺在面前,何苦去花費那筆“冤枉錢”?二者的驅動力本就不同,對待“學習版游戲”的態度南轅北轍也是意料之中的。
CODEX小組的解散,意味著這個團隊就此落幕,但卻并非“學習版游戲”的完結。CPY、STEAMPUNKS等小組,還會在這條路上繼續走下去,更何況一位CODEX的前成員,也會以“EMPRESS”的名頭繼續從事這方面的工作。但即便如此,在CODEX這個強力破解團隊官宣解散之后,各大游戲廠商想必也會為此長出一口氣。
對于廣大玩家來講,這個消息終究只是個樂子而已。
為CODEX以往的行為表示“不齒”?還是因為玩過那些學習版游戲而表示懷念?誰知道呢……
情回顧:抖音BoostMultiDex:Android低版本上首次啟動時間減少80%(一)
抖音自研的 BoostMultiDex 方案,可以大幅改善 Android 低版本(4.4 及其以下)手機更新或安裝后首次冷啟動時間。并且,不同于目前業界所有優化方案,我們是從 Android Dalvik 虛擬機底層機制入手,從根本上解決了安裝后首次執行 MultiDex 耗時過長問題。
我們上一篇文章中已經介紹了 BoostMultiDex 的核心優化思路,即如何避免 ODEX,直接加載原始 DEX 完成啟動。然而用這個方法加載 DEX 文件,相比于 ODEX 優化后的方式,其 Java 代碼執行性能上還是有所損失的。我們也可以從前面方法的注釋里面看出,虛擬機對于直接加載原始 DEX 的情況只是做了些基本優化:
The system will only perform "essential" optimizations on the given file.
所以,雖然第一次啟動我們是加載了原始 DEX 來執行的,但從長遠的角度考慮,后續的啟動,還是應該盡量采用 ODEX 的方式來執行。因此,我們還需要在第一次啟動完成后,在后臺適當的時候做好 ODEX 優化。
一開始我們是做法也比較簡單,在順利加載 DEX 字節數組,完成啟動之后,在后臺開辟單獨的線程執行DexFile.loadDex就可以了。這樣當后臺做完 ODEX 后,APP 第二次啟動時,就可以直接加載之前做好的 ODEX,得到較好的執行性能。這種做法在線下測試的時候也很正常,然而在上線之后,我們遇到了這樣一個問題……
線上報上來一個 Native Crash,它的堆棧如下所示:
Signal 16(SIGSTKFLT), Code -6(SI_TKILL)
#00 pc 00016db4 /system/lib/libc.so (write+12) [armeabi-v7a]
#01 pc 000884a5 /system/lib/libdvm.so (sysWriteFully(int, void const*, unsigned int, char const*)+28) [armeabi-v7a]
#02 pc 00088587 /system/lib/libdvm.so (sysCopyFileToFile(int, int, unsigned int)+114) [armeabi-v7a]
#03 pc 00050d41 /system/lib/libdvm.so (dvmRawDexFileOpen(char const*, char const*, RawDexFile**, bool)+392) [armeabi-v7a]
#04 pc 00064a41 /system/lib/libdvm.so [armeabi-v7a]
#05 pc 000276e0 /system/lib/libdvm.so [armeabi-v7a]
#06 pc 0002b5c4 /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+184) [armeabi-v7a]
#07 pc 0005fc79 /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+272) [armeabi-v7a]
#08 pc 0005fca3 /system/lib/libdvm.so (dvmCallMethod(Thread*, Method const*, Object*, JValue*, ...)+20) [armeabi-v7a]
#09 pc 0005481f /system/lib/libdvm.so [armeabi-v7a]
#10 pc 0000e3e8 /system/lib/libc.so (__thread_entry+72) [armeabi-v7a]
#11 pc 0000dad4 /system/lib/libc.so (pthread_create+160) [armeabi-v7a]
APP 收到 SIGSTKFLT 信號崩潰了,同時還輸出了這樣的日志:
06-25 15:10:53.821 7449 7450 E dalvikvm: threadid=2: stuck on threadid=135, giving up
06-25 15:10:53.821 7449 7450 D dalvikvm: threadid=2: sending two SIGSTKFLTs to threadid=135 (tid=8021) to cause debuggerd dump
SIGSTKFLT 是 Dalvik 虛擬機特有的一個信號。當虛擬機發生了 ANR 或者需要做 GC 的時候,就需要掛起所有 RUNNING 狀態的線程,如果此時 Dalvik 虛擬機等待了足夠長時間,線程仍舊無法被掛起,就會調用dvmNukeThread函數發送 SIGSTKFLT 信號給相應線程,從而殺死 APP。
具體代碼如下:
static void waitForThreadSuspend(Thread* self, Thread* thread)
{
const int kMaxRetries=10;
... ...
while (thread->status==THREAD_RUNNING) {
... ...
if (retryCount++==kMaxRetries) {
ALOGE("Fatal spin-on-suspend, dumping threads");
dvmDumpAllThreads(false);
/* log this after -- long traces will scroll off log */=> ALOGE("threadid=%d: stuck on threadid=%d, giving up",
self->threadId, thread->threadId);
/* try to get a debuggerd dump from the spinning thread */=> dvmNukeThread(thread);
/* abort the VM */
dvmAbort();
... ...
}
而從堆棧我們看出,殺死進程的時候,我們正調用DexFile.loadDex,這個方法最后會調用到dvmRawDexFileOpen里面,執行 write 操作。而這個 write 涉及 I/O 操作,是比較耗時的。所以,當線程在做 dexopt,長時間無法響應虛擬機的掛起請求時,就會觸發這個問題。
一般來說,虛擬機在執行 Java 代碼的時候,都會是 RUNNING 狀態。而只要調用了 JNI 方法,在執行到 C/C++代碼的時候,就會切換為 NATIVE 狀態。而虛擬機只會在 RUNNING 狀態下會掛起線程,如果是在 NATIVE 狀態下,虛擬機是不會要求線程必須掛起的。
不過,這里有一個特殊之處。雖然DexFile.loadDex方法最終也走到了 JNI 里面調用dvmRawDexFileOpen函數,但由于DexFile類是虛擬機的內部類,Dalvik 虛擬機不會在內部類執行 JNI 方法的時候將線程切換為 NATIVE 狀態,仍然會保持原來的 RUNNING 狀態。于是,在 RUNNING 狀態下,做 OPT 的線程就會被要求掛起。而此時由于正在執行耗時的 write 操作,無法響應掛起請求,便出現了如上的崩潰。
當然,可能有人會想到在 Native 代碼中,用CallStaticObjectMethod來觸發DexFile.loadDex,不過這種方式是不可行的。因為CallStaticObjectMethod調用 Java 方法DexFile.loadDex時,會使得狀態再次切換為 RUNNING。
具體來看下 CallStatciXXXMethod 方法的定義處:
static _ctype CallStatic##_jname##Method(JNIEnv* env, jclass jclazz, \
jmethodID methodID, ...) \
{ \
UNUSED_PARAMETER(jclazz); \
ScopedJniThreadState ts(env); \
JValue result; \
va_list args; \
va_start(args, methodID); \
dvmCallMethodV(ts.self(), (Method*)methodID, NULL, true, &result, args);\
va_end(args); \
if (_isref && !dvmCheckException(ts.self())) \
result.l=(Object*)addLocalReference(ts.self(), result.l); \
return _retok; \
}
關鍵在于 ScopedJniThreadState:
explicit ScopedJniThreadState(JNIEnv* env) {
mSelf=((JNIEnvExt*) env)->self;
... ...
CHECK_STACK_SUM(mSelf);
dvmChangeStatus(mSelf, THREAD_RUNNING);
}
~ScopedJniThreadState() {
dvmChangeStatus(mSelf, THREAD_NATIVE);
COMPUTE_STACK_SUM(mSelf);
}
在使用dvmCallMethodV調用 Java 方法前,會先切換狀態為THREAD_RUNNING,執行完畢后,ScopedJniThreadState析構,再切換回THREAD_NATIVE。這樣,JNI 執行DexFile.loadDex就和直接執行 Java 代碼一樣,狀態會有問題。不只是CallStaticXXXMethod,所有使用CallXXXMethod函數在 Native 下調用 Java 方法的情況都是如此。
好在,我們想到了另一個辦法:既然 Dalvik 不會對內部類的 JNI 調用做切換,我們就自己寫一個 JNI 調用,使其走到 Native 代碼中,這樣線程就會變為 Native 狀態,然后直接調用虛擬機內部函數做 dexopt 即可。這樣在做 dexopt 的時候,始終會處于 NATIVE 的狀態,不會切為 RUNNING,也不會被要求掛起,也就能避免這個問題。
這個虛擬機內部函數就是dvmRawDexFileOpen,我們先來看下它的代碼說明:
/*
* Open a raw ".dex" file, optimize it, and load it.
*
* On success, returns 0 and sets "*ppDexFile" to a newly-allocated DexFile.
* On failure, returns a meaningful error code [currently just -1].
*/
int dvmRawDexFileOpen(const char* fileName, const char* odexOutputName,
RawDexFile** ppDexFile, bool isBootstrap);
這個函數可以用來打開原始 DEX 文件,并且對它做優化和加載。對應到 libdvm.so 中的符號是_Z17dvmRawDexFileOpenPKcS0_PP10RawDexFileb,我們只需要用 dlsym 在 libdvm.so 里面找到它,就可以直接調用了,完整代碼如下:
using func=int (*)(const char* fileName, const char* odexOutputName, void* ppRawDexFile, bool isBootstrap);
void* handler=dlopen("libdvm.so", RTLD_NOW);
dvmRawDexFileOpen=(func) dlsym(handler, "_Z17dvmRawDexFileOpenPKcS0_PP10RawDexFileb");
dvmRawDexFileOpen(file_path, opt_file_path, &arg, false);
這樣,我們自己寫一個 JNI 調用,在 Native 狀態下執行上述代碼,就能達到完成 ODEX 的目的,從而根本上杜絕這個異常了。
另外,我們把 dexopt 操作放到了單獨進程執行,由此可以避免 ODEX 操作對主進程造成其他性能影響。此外,由于設備情況多種多樣,運行環境十分復雜,還可能會有一些廠商魔改,導致的 dlsym 找不到_Z17dvmRawDexFileOpenPKcS0_PP10RawDexFileb符號,雖然這種情況極為罕見,但理論上仍有可能發生。單獨進程里面由于環境比較純粹,基本很少發生 ANR 和 GC 事件,掛起的情況就很少,也能最大程度規避這個問題。
我們發現,相比于官方 MultiDex 加載 ZIP 形態的 DEX 文件,非 ZIP 方式的 DEX(也就是直接對 DEX 文件做 ODEX,而不用先把 DEX 壓縮進 ZIP 里面)對于整體時間也有一定程度的優化,因為這種非 ZIP 方式避免了原先的兩個耗時:
非 ZIP 的方式相比于 ZIP 方式,整體耗時會減少 40%左右,但是 DEX 文件磁盤占用空間比原先 ZIP 文件的方式增加一倍多。因此我們可以只在磁盤空間充裕的時候,優先使用非 ZIP 方式加載。
而我們openDexFile_bytearray加載 DEX 的方式,需要的只是原始 DEX 文件的字節數組(byte[])。這個字節數組我們在首次冷啟動的時候是直接從 APK 里面解壓提取得到的。我們可以在這次啟動提取完成后,先把這些字節數組落地為 DEX 文件。這樣如果再次啟動 APP 的時候,ODEX 沒做完,就可以直接使用前面保存的 DEX 文件來得到字節數組了,從而避免了從 APK 解壓的時間。
總體來看,我們整套方案中一共存在四種形態的 DEX:
生成各個產物的時序圖如下所示:
我們依次說明每一步:
正常情況下,我們會依次按 A -> B -> C 的時序依次產生各個文件,如果中間有中斷的情況,我們下次啟動后會繼續按照當前已有產物做對應操作。我們僅在磁盤空間不夠,且所在系統不支持直接加載字節數組的情況下才會走 ZIP&ODEX 方式的 D 路徑。這里不支持的情況主要是一些特殊機型,比如 4.4 卻采用了 ART 虛擬機的機型、阿里 Yun OS 機型等。
接下來我們繼續看下加載流程圖:
這么一來,APP 就可以根據當前情況,選擇最合適的方式執行加載 DEX 了。從而保證了任意時刻的最優性能。
前面提到,OPT 優化是在單獨的進程里面執行的。單獨進程除了可以減少前面的 SIGSTKFLT 問題,還能在做完 OPT 后及時終止后臺進程,避免過多的資源占用。
然而,在單獨進程處理 OPT 和其他進程執行 install 的時候,都涉及到 DEX 和 ODEX 文件的訪問和生成,因此在這些進程之間涉及到文件訪問和 OPT 時,都是加文件鎖互斥執行的。這樣可以避免加載的同時,另一個進程在操作 DEX 和 ODEX 文件導致的文件損壞。在官方的 MultiDex 中也是采用這種文件鎖的方式來進行互斥訪問的。
但這帶來了另一個問題,如果 OPT 進程在長時間做 dexopt,而此時主進程(或者其他后臺進程)需要再次啟動,便會因為 OPT 進程持有互斥文件鎖,而導致這些進程被阻塞住無法繼續啟動。可以看流程圖來理解這一過程:
正如圖中描繪的場景,用戶第一次打開了 APP,然后運行一會之后因為一些情況殺死了 APP,這時,后臺進程已經啟動并正在做 OPT。如果此時用戶想要再次打開,就會由于 OPT 進程互斥鎖導致阻塞而黑屏。這顯然是不可接受的。
因此,我們就需要采取更好的策略,使得在主進程能夠正常地繼續往下執行,而不至于被阻塞住。
這個問題的關鍵在于,主進程需要依賴 OPT 進程的產物,才能繼續往下執行,而 OPT 進程此時正在操作 DEX 文件,這個過程中的產物必定無法被主進程直接使用。
所以,如果想要主進程不再因 OPT 操作阻塞,我們很容易想到可以無視 OPT 進程,不使用 DEX 文件,只從 APK 里面獲取內存形式的 DEX 字節碼就可以了。不過這種方式的主要問題在于,如果 OPT 時間非常長,在這段時間內就不得不一直使用內存方式的 DEX 啟動 APP,這樣性能就會處于比較差的水平。
因此我們采用的是另一種方案。在主進程退出而再次啟動的時候,先中止 OPT 進程,直接取得現有 DEX 產物進行加載,然后再喚起 OPT 進程。
如下圖所示:
這里關鍵點在于如何中止進程。當然,我們可以直接在主進程發信號殺死 OPT 進程,不過這種方式過于粗暴,很可能導致 DEX 文件損壞。而且 kill 信號的方式沒有回調,我們無法得知是否進程確實地退出了。
因此,我們采取的方式是用兩個文件鎖來做同步,保證進程啟動和退出的信息可以在多個進程之間傳達。
第一個文件鎖就是單純用來作為互斥鎖,保證處理 DEX 和加載 DEX 的過程是互斥發生的。第二個文件鎖用來表示進程即將獲取互斥鎖,我們稱之為準備鎖,它可以用來通知 OPT 進程:此時有其他進程正需要加載 DEX 產物。
對于 OPT 進程而言,獲取文件鎖的步驟如下:
對于主進程(或其他非 OPT 進程)而言,獲取文件鎖的步驟如下:
具體情形見下圖:
首先,OPT 進程開始執行,會獲取到互斥鎖,然后做 DEX 處理。OPT 進程在處理完第一個 DEX 文件后,由于沒有其他進程持有準備鎖,因此 OPT 進程獲取準備鎖成功,然后釋放準備鎖,繼續做下一個 DEX 優化。
這時候,主進程(或其他非 OPT 進程)啟動,先成功地獲取準備鎖。然后繼續阻塞地獲取互斥鎖,此時由于 OPT 進程已經在前一步獲取到了互斥鎖,因此只能等待其釋放。
OPT 進程在處理完第二個 DEX 后,檢測到準備鎖已經被其他進程持有了,因此獲取失敗,從而停止繼續做 OPT,釋放互斥鎖并退出。
此時主進程就可以成功地獲取到互斥鎖,并且立即釋放準備鎖,以便其他進程可以獲取。接著,在完成 DEX 加載后,釋放互斥鎖,繼續執行后續業務流程。最后再喚起 OPT 進程接著做完原先的 DEX 處理。
總體看來,在這種模式下,OPT 進程可以主動發現有其他進程需要加載 DEX,從而中斷 DEX 處理,并釋放互斥鎖。主進程便不需要等待整個 DEX 處理完成,只需要等 OPT 進程完成最近一個 DEX 文件的處理就可以繼續執行了。
我們本地選取了幾臺 4.4 及以下的設備,對它們首次啟動的 DEX 加載時間進行了對比:
Android版本廠商機型原始MultiDex耗時(s)BoostMultiDex耗時(s)4.4.2LGLGMS32333.5455.0144.4.4MOTOG45.6916.7194.3SamsungGT-N710024.1863.6604.3.0SamsungSGH-T99930.3313.7914.2.2HUAWEIHol-T00崩潰3.7244.2.1HUAWEIG610-U0036.4654.9814.1.2SamsungI910030.9625.345
以上是在抖音上測得的實際數據,APK 中共有 6 個 Secondary DEX,顯而易見,BoostMultiDex 方案相比官方 MultiDex 方案,其耗時有著本質上的優化,基本都只到原先的 11%~17%之間。也就是說 BoostMultiDex 減少了原先過程 80%以上的耗時。 另外我們看到,其中有一個機型,在官方 MultiDex 下是直接崩潰,無法啟動的。使用 BoostMultiDex 也將使得這些機型可以煥發新生。 另外,我們在線上采取了對半分的方式,也就是 BoostMultiDex 和原始 MultiDex 隨機各自選取一半線上設備,對比二者的耗時。
我們先以設備維度來看,這里隨機選取了 15 分鐘的線上數據,圖中橫軸為每個 Android 版本 4.4 及以下的設備,縱軸為首次啟動加載 DEX 的耗時,按耗時升序排列,單位為納秒。
BoostMultiDex 下的設備耗時:
MultiDex 下的設備耗時:
兩張圖最大的區別在于縱軸的時間刻度。可以看到,絕大多數設備的 BoostMultiDex 耗時在 5s 左右,最多耗時也不會超過 35s。而反觀 MultiDex,大多數都需要耗時 30 多 s,最長的耗時甚至達到了將近 200s。
上面的圖可能差別不夠明顯,我們選取一段時間,每半小時取所有設備耗時的中位數,可以得到下面的對比曲線:
其中,下方橙色線為 BoostMultiDex,上方藍色線為原始 MultiDex,可以明顯看出,耗時下降的幅度非常巨大。
耗時的大幅減少會帶來怎樣的效果呢?我們統計了 4.4 及以下機型中,兩者進入到抖音播放頁的設備數占比,時間范圍為一周,其中右邊橙色為 BoostMultiDex,左邊藍色為原始 MultiDex。
由于我們所有設備對于兩種方案的選取是對半開的,所以理論上二者的設備數應該接近于 1 比 1,不過從圖中我們可以看到,BoostMultiDex 的設備數已經大幅超過 MultiDex 的設備數,兩者比例接近于 2 比 1。
從中可以看出,MultiDex 耗時的減少對于設備活躍數的提升,效果十分顯著!
最后,我們再梳理一下整個方案的實現要點:
對于國內偏遠地區,尤其對于海外許多發展中國家,Android 低版本機型仍然占比較高。目前 BoostMultiDex 方案在抖音和 TikTok 已經全量上線,這會使得這部分低版本 Android 用戶直接受益,極大優化升級和安裝啟動體驗。
我們后續將開源 BoostMultiDex 方案,以協助其他 APP 在低版本 Android 手機上改進性能體驗。
今后,各家對下沉市場有需要的 APP,都能直接使用 BoostMultiDex 方案,立即獲得飛一般的升級安裝體驗!這也是我們為改善 Android 生態貢獻的一小份力,后續很快就會發布開源地址,敬請期待!
最后的最后,仍然再提一句,抖音/TikTok Android 基礎技術團隊正在北上深杭四地尋求優秀 Android 開發人才,目前疫情期間我們也支持完全遠程無接觸面試。只要你的技術功力深厚或者潛力巨大,都可以通過 字節跳動招聘官網查詢抖音 Android 相關職位「鏈接」 或者聯系 xiaolin.gan@bytedance.com 來投遞簡歷,我們十分期待你的加入!
歡迎關注字節跳動技術團隊
絡抓包工具
地址:https://www.charlesproxy.com/
Charles 其實是一款代理服務器,通過將自己設置成系統(電腦或者瀏覽器)的網絡訪問代理服務器,然后截取請求和請求結果達到分析抓包的目的。該軟件是用Java寫的,能夠在Windows,Mac,Linux上使用。主要提供以下功能:
地址:https://www.telerik.com/fiddler
Fiddler是一個http協議調試代理工具,它能夠記錄并檢查所有你的電腦和互聯網之間的http通訊,設置斷點,查看所有的“進出”Fiddler的數據(指cookie,html,js,css等文件)。 Fiddler 要比其他的網絡調試器要更加簡單,因為它不僅僅暴露http通訊還提供了一個用戶友好的格式.。另外 Fiddler 是用C#寫出來的,它包含一個簡單卻功能強大的基于JScript .NET 事件腳本子系統,它的靈活性非常棒,可以支持眾多的http調試任務,并且能夠使用.net框架語言進行擴展
地址:https://www.wireshark.org/
Wireshark(前稱Ethereal)是一個網絡封包分析軟件。網絡封包分析軟件的功能是截取網絡封包,并盡可能顯示出最為詳細的網絡封包資料。Wireshark使用WinPCAP作為接口,直接與網卡進行數據報文交換
注意:上面三款抓包工具都可以用于移動端
反編譯
地址:https://github.com/Jermic/Android-Crack-Tool
mac下Android逆向神器,實用工具集
AndroidCrackTool集成了Android開發中常見的一些編譯/反編譯工具,方便用戶對Apk進行逆向分析,提供Apk信息查看功能.目前主要功能包括(詳細使用方法見使用說明):
地址:https://github.com/iBotPeaches/Apktool
一款功能強大且操作簡單的apk反編譯工具,能夠將反編譯的apk文件保存到同名目錄中,還能幫用戶將反編譯的dex、odex 重新編譯成dex文件
地址:https://zhuanlan.zhihu.com/p/390212630
將安卓可執行文件dex轉為jar包
地址:https://github.com/java-decompiler/jd-gui
可將安卓可執行文件dex轉為jar包,并提供可視化代碼閱讀能力
地址:https://github.com/skylot/jadx
非常方便的Java反編譯工具,一般用到的功能主要是搜索、反混淆、查找方法調用這幾樣,性能和反編譯出來的代碼效果都比使用dex2jar+jd-gui之類的方式好。
調試
啥用不用說了,只能說是最棒的一個靜態反編譯軟件
Hook
地址:https://github.com/rovo89/Xposed
Xposed 框架是一款可以在不修改 APK 的情況下影響程序運行(修改系統)的框架服務,基于它可以制作出許多功能強大的模塊,且在功能不沖突的情況下同時運作。 當前,Per APP Setting(為每個應用設置單獨的 dpi 或修改權限)、Cydia、XPrivacy(防止隱私泄露)、對原生 Launcher 替換圖標等應用或功能均基于此框架。
地址:https://github.com/frida/frida
?Frida 是一款輕量級HOOK框架,可用于多平臺上,例如android、windows、ios等。
Frida 分為兩部分,服務端運行在目標機上,通過注入進程的方式來實現劫持應用函數,另一部分運行在系統機器上。
?Frida上層接口支持js、python、c等。
地址:https://github.com/zhkl0228/unidbg
特別推薦該庫,這個庫可以讓你在PC端運行 Android 端的 so 庫,現在一般app都喜歡在 jni 層做數據加密的操作,該庫就可以直接幫你運行具體so庫中的具體方法,非常實用。