亚洲精品中文免费|亚洲日韩中文字幕制服|久久精品亚洲免费|一本之道久久免费

      
      

            <dl id="hur0q"><div id="hur0q"></div></dl>

                抖音 Android 包體積優(yōu)化探索:基于 ReDex 的 DEX 優(yōu)化落地實踐

                抖音 Android 包體積優(yōu)化探索:基于 ReDex 的 DEX 優(yōu)化落地實踐

                本文作者:馮瑞;廖斌斌;劉豐愷

                前言

                應用安裝包的體積會顯著影響應用的下載速度和安裝速度,按照 Google 的經(jīng)驗數(shù)據(jù),包體積每增加 1M 會造成 0.17%的新增折損。抖音的一些實驗也證明了包體積會顯著影響下載激活的轉(zhuǎn)化率。

                Android 的安裝包是 APK 格式的,在抖音的安裝包中 DEX 的體積占比達到了 40%以上,所以針對 DEX 的體積優(yōu)化是一種行之有效的包體積優(yōu)化手段。

                DEX 本質(zhì)上是由 Java/Kotlin 代碼編譯而成的字節(jié)碼,因此,針對字節(jié)碼進行業(yè)務無感的通用優(yōu)化成為我們的一個探索方向。

                優(yōu)化結(jié)果

                終端基礎技術團隊和抖音基礎技術團隊在過去的一年里,利用 ReDex 在抖音包體積優(yōu)化方面取得了一些明顯的收益,這些優(yōu)化也被同步到了其他各大 App 上。

                在抖音、頭條和其他應用上,我們的優(yōu)化對 APK 體積的縮減普遍達到了 4%以上,對 DEX 體積的縮減則可以達到 8% ~ 10%

                優(yōu)化思路

                在 android 應用的構建過程中,Java/Kotlin 代碼會先被編譯成 Class 字節(jié)碼,在這個階段 gradle 提供了 Transformer 可以進行字節(jié)碼的自定義處理,很多插件都是在這個階段處理字節(jié)碼的。然后,Class 文件經(jīng)過 dexBuilder/mergeDex 等任務的處理會生成 DEX 文件,并最終被打進安裝包中。整個過程如下所示:

                所以,針對字節(jié)碼的優(yōu)化是有 2 個時機可以進行的:

              1. 在 transformer 階段對 Class 字節(jié)碼進行優(yōu)化
              2. 在 DEX 階段對 DEX 文件進行優(yōu)化
              3. 顯然,對 DEX 進行優(yōu)化是更理想的一種方式,因為在 DEX 文件中,除了字節(jié)碼指令外,還存在跨 DEX 引用、字符串池這樣的結(jié)構,針對這些 DEX 格式的優(yōu)化是無法在 transformer 階段進行的。

                在確定了針對 DEX 文件進行優(yōu)化的思路后,我們選擇了 facebook 的開源框架 ReDex 作為優(yōu)化工具,并對其進行了定制開發(fā)。

                選擇 ReDex 的原因是它提供了豐富的基礎能力,ReDex 的基礎能力包括:

              4. 讀寫及解析 DEX 的能力,同時可以在一定程度上讀取并解析 xml 和 so 文件
              5. 解析簡單的 proguard keep 規(guī)則并匹配類/方法/成員變量的能力
              6. 對字節(jié)碼進行數(shù)據(jù)流分析的能力,提供了常用的數(shù)據(jù)流分析算法
              7. 對字節(jié)碼進行合法性校驗的能力,包括寄存器檢查、類型檢查等
              8. 一系列的字節(jié)碼優(yōu)化項,每項優(yōu)化稱為一個 pass,多個 pass 組成 pipeline 對 DEX 進行優(yōu)化
              9. 我們基于這些能力進行了定制和擴展,并期望最終建立完善的優(yōu)化體系。

                優(yōu)化項

                在抖音落地的優(yōu)化項,包括 facebook 開源的優(yōu)化和我們自研的優(yōu)化,從其出發(fā)點來看,可以大致分為下面幾種:

                • 通用字節(jié)碼優(yōu)化:通常意義下的編譯優(yōu)化,如常量傳播、內(nèi)聯(lián)等,一般也可在 Transformer 階段實現(xiàn)
                • DEX 格式優(yōu)化:DEX 中除了字節(jié)碼指令外,還包括字符串池、類/方法引用、debug 信息等等,針對這些方面的優(yōu)化歸類為 DEX 格式優(yōu)化
                • 針對編程語言的優(yōu)化:Java/Kotlin 的一些語法糖會生成大量字節(jié)碼,可以對這些字節(jié)碼進行針對性的分析和優(yōu)化
                • 提升壓縮率的優(yōu)化:將 DEX 打包成 APK 實質(zhì)上是個壓縮的過程,對 DEX 內(nèi)容進行針對性的優(yōu)化可以提升壓縮率,從而產(chǎn)生體積更小的 APK

                這幾種優(yōu)化沒有明確的標準和界線,有時一個 Pass 會涉及到多種,下面詳細介紹一下各項優(yōu)化。

                通用字節(jié)碼優(yōu)化

                ConstantPropagationPass

                該 Pass 實際上包含了常量折疊和常量傳播。

                常量折疊是在編譯期簡化常量的過程,比如

                1 y = 7 – 14 / 22 —>3 y = 0

                常量傳播是在編譯期替代指令中已知常量的過程,比如

                1 int x = 14;2 int y = 7 – x / 2;3 return y * (28 / x + 2);4 —>5 int x = 14;6 int y = 7 – 14 / 2;7 return (7 – 14 / 2) * (28 / 14 + 2);

                上面的例子經(jīng)過 常量折疊 + 常量傳播優(yōu)化后就會簡化為

                1 int x = 14;2 int y = 0;3 return 0;

                再經(jīng)過死代碼刪除就可以最終變?yōu)閞eturn 0。

                具體的優(yōu)化過程是:

              10. 對方法進行數(shù)據(jù)流分析,主要針對 const/move 等指令,得出一個寄存器在某個位置可能的取值
              11. 根據(jù)分析的結(jié)果,進行指令替換或指令刪除,包括:
                • 如果值肯定是非空的,可以將對應的判空去掉,比如 kotlin 生成的 null check 調(diào)用
                • 如果值肯定為空,可以將指令替換為拋空異常
                • 如果值肯定讓某 if 分支走不到,可以刪除對應的分支
                • 如果值是固定的,可以用 const 指令替換對應的賦值或計算指令

                一個方法經(jīng)過 ConstantPropagationPass 優(yōu)化后,可能會產(chǎn)生一些死代碼,比如例子中的int y = 0,這也為后續(xù)的死代碼刪除創(chuàng)造了條件。

                AnnoKillPass

                該 Pass 是用來移除無用注解的。注解主要分為三種類型:

                • SOURCE:java 源碼編譯為 class 字節(jié)碼就不可見,此類注解一般不用過于關注
                • CLASS:字節(jié)碼通過 dx 工具轉(zhuǎn)成 DEX 就不可見,代碼運行時不需要獲取信息,所以一般來說也不需要關注,實測發(fā)現(xiàn)部分注解仍然存在于 DEX 中,這部分注解可以進行優(yōu)化
                • RUNTIME:DEX 中仍然可見,代碼運行中可以通過 getAnnotations 等接口獲取注解信息,但是隨著業(yè)務的迭代,可能獲取注解信息的代碼已經(jīng)去掉,注解卻沒有下掉,這部分注解會被 ReDex 安全的移除

                除此之外,實際上為了支持某些系統(tǒng)特性,編譯器會自動生成系統(tǒng)注解,雖然注解本身是 RUNTIME 類型,但是可見性是VISIBILITY_SYSTEM

                • AnnotationDefault : 默認注解,不能刪除
                • EnclosingClass : 當前內(nèi)部類申明時所在的類
                • EnclosingMethod : 當前內(nèi)部類申明時所在的方法
                • InnerClass : 當前內(nèi)部類名稱
                • MemberClasses : 當前類的所有內(nèi)部類列表
                • MethodParameters : 方法參數(shù)
                • Signature : 泛型相關
                • Throws : 異常相關

                舉例說明

                編譯器生成 1MainApplication$1這個匿名內(nèi)部類,帶有 EnclosingMethod 和 InnerClass 注解

                系統(tǒng)提供以下接口獲取類相關的信息,就是通過分析相關的系統(tǒng)注解來實現(xiàn)的

                • Class.getEnclosingMethod
                • Class.getSimpleName
                • Class.isAnonymousClass
                • ….

                如果代碼中不存在使用這些接口獲取類信息的邏輯,就可以安全的移除這部分注解,從而達到縮減包大小的目的。

                RenameClassesPass

                該 Pass 通過縮減類名的字符串長度來減小包體積

                比如把類名從La/b/c/d/e;改為LX/a;,可以類名字符串的長度,從而達到包大小縮減的目的。實際上 Proguard 本身已經(jīng)提供類似的功能: -repackageclasses ‘X’,效果如下:

                但是-repackageclasses ‘X’的處理會影響 ReDex 的 InterDexPass 的算法邏輯(InterDexPass 可以參考下文),導致收益縮減

                • 收益測試
                  • Proguard -repackageclasses ‘X’ 收益: 600K+
                  • Redex InterDexPass 收益: 400K+
                  • 同時應用 Proguard -repackageclasses ‘X’ 和 Redex InterDexPass 收益: 40K+

                本質(zhì)原因在于 Proguard 重命名后,影響了 InterDexPass 函數(shù)引用權重分配,導致 InterDex 收益被回收

                • 解決方案
                  • InterDexPass 深入分析原理,優(yōu)化權重算法
                  • 先執(zhí)行 InterDexPass,后執(zhí)行類似 Proguard 的-repackageclasses ‘X’

                權重算法優(yōu)化相對來說比較復雜,同時存在眾多不可確定性,比如潛在的跟其他優(yōu)化的沖突,所以我們采取了第二種解決方案。

                這里需要解決的一個關鍵點在于如何確定一個類名是否可以被安全的重命名,我們采取了一個比較取巧的方式,ReDex 會分析 Proguard 傳遞上來 mapping.txt 文件,只要我們保持跟 Proguard 類重命名優(yōu)化一樣的處理策略,就不會引發(fā)反射/native 調(diào)用/序列化等一系列問題。

                但是執(zhí)行起來還是碰到各種千奇百怪的問題,比如 Signature 系統(tǒng)注解失效問題。Signature 注解的內(nèi)容是非標準的類名格式,所以類重命名后簡單回寫字符串或者更新 Type 類型會導致 Signature 注解失效,最后通過深入解析 Signature 格式規(guī)避了這個問題。

                StringBuilderOutlinerPass

                該 Pass 是針對 StringBuilder 的 CallSites 進行分析縮略的優(yōu)化,與死代碼刪除搭配使用可以有不錯的優(yōu)化效果。

                為何要優(yōu)化 StringBuilder 呢?在 Java 的代碼開發(fā)過程中,字符串操作幾乎是我們最經(jīng)常做的一件事情,無論是實際處理字符串拼接還是各種不同數(shù)據(jù)類型之間的拼接操作。而這些拼接操作都會被 Java 的 de-sugar 優(yōu)化為 StringBuilder 操作。比如:var log = “A” + 1 + “B” + 1.0f + other_var; 會被優(yōu)化為:

                1 StringBuilder builder = new StringBuilder();2 builder.append(“A”); builder.append(1);3 builder.append(“B”); builder.append(1.0f);4 builder.append(other_var);5 builder.toString();

                因此我們對 StringBuilder 的所有 Callsites 進行分析,在最好情況下多個方法調(diào)用可以被優(yōu)化為一個調(diào)用,這個方法是一個 outline (外聯(lián))方法,具體的參數(shù)拼接和 toString 被隱藏在函數(shù)內(nèi)部:

                1 invoke-static {v1, v2, v3} Outline;.bind:([Ljava/lang/Object)Ljava/lang/String;

                優(yōu)化步驟可以被簡單的分為如下幾個步驟:

              12. 生成一個泛型的外聯(lián)方法、以及數(shù)個特定參數(shù)的方法:我們可以認為生成的方法大概是這樣的
              13. 1 @Keep2 public static String bind(Object… args) {3 StringBuilder builder = new StringBuilder();4 for (int i = 0; i < args.length ; i++) {5 builder.append(args[i]);6 }7 return builder.toString();8 }

              14. 收集 StringBuilder 的 CallSites :通過抽象解釋和不動點分析,分析所有的 StringBuilder 操作,對 append、new-instance、和 init 方法分類。判斷每次 append 的參數(shù)是不是 immutable 操作,如果增加的 insn 少于減少的 insn 即會減少代碼,就對這里進行處理。
              15. 生成外聯(lián)方法調(diào)用:由于我們使用了泛型方法來接受參數(shù),因此我們要對基礎類型生成 ValueOf 的轉(zhuǎn)換操作、并且刪除 append 方法前為了防止被錯誤優(yōu)化我們還需要插入 move 指令來 copy 原有參數(shù)(這些 move 指令會被后續(xù)優(yōu)化正確刪除)、如果參數(shù)個數(shù)還在我們生成的特定 outline 方法范圍內(nèi)我們就可以使用特定方法來生成外聯(lián)函數(shù),其余的將使用泛化的外聯(lián)來接受。
              16. DEX 格式優(yōu)化

                InterDexPass

                該 Pass 是針對跨 DEX 引用的優(yōu)化。

                跨 DEX 引用是指當一個 DEX 需要“使用”到另一個 DEX 中的類/方法/變量時,需要在本 DEX 中保存一份對應的類/方法/變量的 id,如果 2 個 DEX 用到了相同的字符串,那么這個字符串在 2 個 DEX 都需要進行定義。所以,改變類/方法/變量和字符串在 DEX 中的分布,可以減小引用的數(shù)量,從而減小 DEX 的體積。從原理中也可以看出,該優(yōu)化對單 DEX 的應用是無效的。

                從上圖可以看到,進行類重排后,DEX0 的類引用和方法引用數(shù)量都減少了,DEX 的體積也會因此減小。

                具體的優(yōu)化過程是:

              17. 收集每個類涉及的所有引用,按照引用數(shù)量和類型計算出類的權重
              18. 根據(jù)權重計算出每個類的優(yōu)先級
              19. 根據(jù)優(yōu)先級選取一個類放入 DEX 中,然后調(diào)整剩余類的優(yōu)先級,重復此步驟直到所有類都被處理
              20. ReBindRefsPass

                該 Pass 是針對方法引用的優(yōu)化,其原理同 InterDexPass。

                在字節(jié)碼中,invoke-virtual/interface指令需要一個方法引用,在很多情況下,這個引用指向的是子類或者實現(xiàn)類的引用,把這個引用替換成父類和接口的方法引用不會影響運行時邏輯,同時會減少 DEX 中方法引用的數(shù)量。在生成 DEX 的時候,方法引用的 65536 限制通常是最先遇到的瓶頸,該優(yōu)化也可以緩解這種情況。

                如上圖所示,優(yōu)化前 caller 方法的 invoke 指令使用的是子類引用,其偽指令如下所示,需要用到 2 個引用

                1 new-instance v0, Sub12 invoke-virtual v0, Sub1.a()3 new-instance v1, Sub24 invoke-virtual v1, Sub2.a()

                優(yōu)化后,invoke 指令都指向其父類應用,2 個引用可以合并為 1 個,減少了 DEX 中的引用數(shù)量

                1 new-instance v0, Sub12 invoke-virtual v0, Base.a()3 new-instance v1, Sub24 invoke-virtual v1, Base.a()

                針對編程語言的優(yōu)化

                KotlinDataClassPass

                該 Pass 是對 Kotlin data class 的優(yōu)化,基本思路是對 data class 的生成代碼進行精簡。

                • 解構聲明優(yōu)化

                Kotlin 中存在解構聲明這種語法,可以更方便的創(chuàng)建多個變量,基本用法如下

                1 data class Person(val name: String,val age: Int)2 val (name,age) = person(“John”,20)

                kotlinc 會為Person類生成 get 方法和 componentN 方法,如下是偽代碼表示

                1 Person { 2 String name; 3 Int age; 4 5 getName(): String { return name; } 6 getAge(): Int { return age; } 7 component1(): String { return name; } 8 component2(): Int { return age; } 9 }10 // 解構聲明編譯為11 val name = person.component12 1()13 val age = person.component2()

                可以看到,get 和 component 的邏輯是一樣的,所以在編譯期,可以進行全局的匹配,用 get 替換掉 component,然后再刪除 component。

                • toString 等生成方法優(yōu)化

                kotlin compiler 為 data class 生成的 toString 具有相似的代碼結(jié)構,因此可以生成一個輔助方法,然后在所有 data class 的 toString 方法中調(diào)用這個輔助方法,即外聯(lián),從而減少指令數(shù)量。

                equals 和 hashCode 也可以進行類似優(yōu)化,但是風險相對較高,因此單獨為這些優(yōu)化配置了開關,業(yè)務方可以視情況開啟。

                提升壓縮率的優(yōu)化

                RegAllocPass

                DEX 及其他文件經(jīng)過壓縮打成 APK,如果能通過改變 DEX 的內(nèi)容來提升壓縮率,那么也會減小最終的包體積。RegAllocPass 就是通過重新分配寄存器來提升壓縮率的。

                dx 生成 DEX 時使用的是線性寄存器分配算法,其基本步驟是進行存活變量分析,然后計算出每個變量的活躍區(qū)間,再根據(jù)活躍區(qū)間依次為變量分配寄存器,超出活躍區(qū)間的寄存器可以進行再分配,其優(yōu)點是運行速度快,但結(jié)果往往不是最優(yōu)的。

                比如下面的代碼,dx 分配了 6 個寄存器,v0 ~ v5

                1 public static double calculateLuminance(@ColorInt int color) {2 final double[] result = getTempDouble3Array();3 colorToXYZ(color,result);4 return result[1] / 100;5 }

                相對的,ReDex 使用了圖著色算法進行寄存器分配,基本步驟是進行存活變量分析,并構建沖突圖,沖突圖的每個節(jié)點是一個變量,如果 2 個變量可以同時存活,就在兩個節(jié)點之間建立邊,最后為沖突圖著色,每個顏色代表一個寄存器,著色完成即寄存器分配完成。著色法相對更慢,結(jié)果一般更優(yōu)。對上面同樣的代碼,著色法使用了 4 個寄存器,v0 ~ v3。

                DEX 中的方法使用的寄存器越少,其內(nèi)容重復率就越高,壓縮率也會更大,從而減小了包體積。

                抖音落地

                抖音是字節(jié)跳動規(guī)模最大、運行環(huán)境復雜度最高的應用之一。在 ReDex 落地初期,由于對復雜度估計不足,在獨立灰度和全量灰度期間引起了一些問題,在解決問題的過程中,我們也逐步形成了一套迭代流程以保證優(yōu)化的穩(wěn)定性。下面介紹一下我們遇到過的典型問題及當前的迭代流程。

                遇到的問題

                兼容性問題

                一般來說,只要按照字節(jié)碼規(guī)范進行優(yōu)化,就不會有兼容性問題,因為 dalvik/art 也是按照規(guī)范去校驗和運行字節(jié)碼的,即使進行了錯誤的優(yōu)化,引起的問題也應該是共性問題。但很多事都有例外,ReDex 就在某品牌手機的部分 Android 5.x 的機型上遇到了問題。

                從 log 和一些 hook 來看,某品牌手機對 5.x 的 art 做了大量的魔改,可以推斷其魔改存在一些問題,導致對正確的字節(jié)碼的校驗和運行也可能出現(xiàn)問題。一個可能的原因是:在 ReDex 進行優(yōu)化時,會對一些方法體的指令順序進行重排,這種重排是不影響方法的邏輯的,但是可能會改變一部分指令,魔改后的 art 在校驗這樣的方法時可能會報 verify error,引起 crash。

                最終通過黑名單配置跳過了這些方法的優(yōu)化規(guī)避了問題,在后續(xù)的優(yōu)化過程中,沒有再遇到類似的問題。

                復雜場景優(yōu)化問題

                抖音業(yè)務復雜,代碼寫法多樣,給靜態(tài)分析和優(yōu)化增加了一些難度,也更容易遇到問題。下面是 2 個典型問題:

              21. 空方法優(yōu)化問題 代碼中可能存在一些空方法,排除掉反射和 natvie 調(diào)用等場景后,剩下的空方法應該是可以刪除的。但是在做優(yōu)化時,卻遇到了 crash,如以下代碼
              22. 1 object XXXSDKHelper { 2 init { 3 initXXXSDK() 4 } 5 fun fakeInit() { 6 } 7 } 8 9 // 初始化任務10 public class XXInitTask implements Runnable {11 @Override12 public void run() {13 XXXSDKHelper.INSTANCE.fakeInit();14 }15 }

                在初始化代碼中調(diào)用fakeInit,它是一個空方法,調(diào)用它的目的是觸發(fā)XXSDKHelper類加載從而執(zhí)行init語句塊,如果刪除了這個空方法,就會導致初始化未執(zhí)行,在后續(xù)的流程中拋空指針。

              23. 復雜反射問題
              24. 對于 Class.forname(…)等簡單的反射用法,靜態(tài)分析是可以分析出來的,但是對一些經(jīng)過字符串拼接或者嵌套之后的反射,靜態(tài)分析很難分析到。因此,對可能會被反射的代碼進行優(yōu)化需要非常小心,通常來說,匿名內(nèi)部類是不會通過反射調(diào)用的,基于此前提,我們進行了匿名內(nèi)部類的重命名優(yōu)化,但是在灰度后,發(fā)現(xiàn)某些第三方 SDK 會通過復雜的運行時邏輯對匿名內(nèi)部類進行了反射調(diào)用,最終導致了 ClassNotFoundError。

                復雜場景的優(yōu)化問題有些是業(yè)務代碼不規(guī)范造成的,但更多的是優(yōu)化前提(空方法可以刪除/匿名內(nèi)部類不會被反射)不成立所導致,所以在進行優(yōu)化時首先需要對假設進行謹慎的驗證。

                迭代流程

                為了減少穩(wěn)定性問題,我們總結(jié)了 ReDex Pass 的迭代流程。

                在對一項 Pass 有了初步構思后,組內(nèi)會進行可行性討論,如果理論上可行就進入開發(fā)和驗證階段,之后同步進行至少 2 輪的獨立灰度驗證和業(yè)務方 Pass 評審,最后進行全量灰度驗證。其中任意一個環(huán)節(jié)發(fā)現(xiàn)問題,都會重新進行整個流程。

                通過這個流程,我們大大減少了穩(wěn)定性問題遺留到灰度階段的可能,在不斷完善迭代流程的同時我們也在探索通過加強單元測試、自動化測試等方式來提升質(zhì)量。

                后續(xù)規(guī)劃

                ReDex 仍然在持續(xù)迭代中,未來我們會在以下幾個方向繼續(xù)進行深入探索:

              25. 更多包體積優(yōu)化的探索和迭代,同時探索字節(jié)碼優(yōu)化在性能提升方面的可能性
              26. 提升字節(jié)碼質(zhì)量
                • 更加嚴格的合法性校驗;ReDex 之前已經(jīng)檢測出若干自定義插件和 proguard 的問題,將問題攔截在了編譯期,后續(xù)會繼續(xù)提升該能力
                • 建立更加完善的質(zhì)量驗證體系;ReDex 作為編譯期的全局字節(jié)碼優(yōu)化方案,如果保證優(yōu)化后的字節(jié)碼質(zhì)量一直是個痛點,我們會繼續(xù)在單元測試、自動化測試等方向探索質(zhì)量提升的手段
              27. 增加編譯期監(jiān)控,更加快速便捷的解決編譯期字節(jié)碼問題,提升接入體驗
              28. 其他應用方向探索;如方法插樁、某些條件下的死代碼掃描等。
              29. 加入我們

                字節(jié)跳動終端技術團隊(Client Infrastructure) 是大前端基礎技術的全球化研發(fā)團隊(分別在北京、上海、杭州、深圳、廣州、新加坡和美國山景城設有研發(fā)團隊),負責整個字節(jié)跳動的大前端基礎設施建設,提升公司全產(chǎn)品線的性能、穩(wěn)定性和工程效率;支持的產(chǎn)品包括但不限于抖音、今日頭條、西瓜視頻、飛書、瓜瓜龍等,在移動端、Web、Desktop 等各終端都有深入研究。

                就是現(xiàn)在!客戶端/前端/服務端/端智能算法/測試開發(fā) 面向全球范圍招聘!一起來用技術改變世界,感興趣請聯(lián)系 [email protected],郵件主題 簡歷-姓名-求職意向-期望城市-電話。

                抖音 Android 基礎技術團隊是一個深度追求極致的團隊,我們專注于性能、架構、包大小、穩(wěn)定性、基礎庫、編譯構建等方向的深耕,保障超大規(guī)模團隊的研發(fā)效率和數(shù)億用戶的使用體驗。目前北京、上海、杭州、深圳都有大量人才需要,歡迎有志之士與我們共同建設億級用戶 APP!

                可以進入字節(jié)跳動招聘官網(wǎng)查詢「抖音基礎技術 Android」相關職位,或者聯(lián)系郵件:[email protected] ,直接發(fā)送簡歷內(nèi)推或者咨詢相關信息!

                鄭重聲明:本文內(nèi)容及圖片均整理自互聯(lián)網(wǎng),不代表本站立場,版權歸原作者所有,如有侵權請聯(lián)系管理員(admin#wlmqw.com)刪除。
                用戶投稿
                上一篇 2022年7月3日 12:25
                下一篇 2022年7月3日 12:25

                相關推薦

                • 分享4條發(fā)微商朋友圈的方法(微商朋友圈應該怎么發(fā))

                  對于微商朋友來說,朋友圈的重要性不言而喻了。 那么微商的朋友圈到底該怎么發(fā)呢? 為什么同樣是經(jīng)營一個朋友圈,有的微商看起來逼格滿滿,實際效果也不錯;而有的卻動都不動就被屏蔽甚至拉黑…

                  2022年11月27日
                • 《寶可夢朱紫》夢特性怎么獲得?隱藏特性獲取方法推薦

                  寶可夢朱紫里有很多寶可夢都是擁有夢特性會變強的寶可夢,很多玩家不知道夢特性怎么獲得,下面就給大家?guī)韺毧蓧糁熳想[藏特性獲取方法推薦,感興趣的小伙伴一起來看看吧,希望能幫助到大家。 …

                  2022年11月25日
                • 《寶可夢朱紫》奇魯莉安怎么進化?奇魯莉安進化方法分享

                  寶可夢朱紫中的奇魯莉安要怎么進化呢?很多玩家都不知道,下面就給大家?guī)韺毧蓧糁熳掀骠斃虬策M化方法分享,感興趣的小伙伴一起來看看吧,希望能幫助到大家。 奇魯莉安進化方法分享 奇魯莉安…

                  2022年11月25日
                • 非匿名指令新手十連怎么選?非匿名指令新手無限十連選擇推薦

                  非匿名指令新手十連怎么選?進入游戲之后大家能得到一個新手無限十連,可以幫大家抽到滿意的角色,新手十連的選擇小編在下面也會有分享,不知道如何選擇的可以看看小編提供的攻略,了解新手十連…

                  2022年11月25日
                • 《寶可夢朱紫》暴飛龍怎么抓?暴飛龍獲得方法

                  寶可夢朱紫暴飛龍位置在哪?在游戲中,很多玩家還不清楚暴飛龍具體要怎么樣獲得,其實獲得方法很簡單,暴飛龍直接是沒得抓的,需要玩家從寶貝龍進化得到,下面一起來看一下寶可夢朱紫暴飛龍獲得…

                  2022年11月23日
                • 《寶可夢朱紫》布土撥怎么進化?布土撥進化方法介紹

                  寶可夢朱紫中,不同的寶可夢有不同的進化方法,其中布土撥的進化方法是比較特殊的。很多玩家不知道寶可夢朱紫布土撥怎么進化,下面就帶來寶可夢朱紫布土撥進化方法介紹,一起來看看吧,希望能幫…

                  2022年11月23日
                • 《寶可夢朱紫》薄荷怎么獲得?薄荷獲得方法

                  寶可夢朱紫中薄荷有改變寶可夢的屬性或性格等效果,很多玩家想知道寶可夢朱紫薄荷怎么獲得,下面就帶來寶可夢朱紫薄荷獲得方法,感興趣的小伙伴一起來看看吧,希望能幫助到大家。 薄荷獲得方法…

                  2022年11月23日
                • 《寶可夢朱紫》怎么交換精靈?交換精靈方法一覽

                  寶可夢朱紫中玩家可以和好友或者npc進行交換寶可夢獲得自己沒有的寶可夢,很多玩家想知道寶可夢朱紫怎么交換精靈,下面就帶來寶可夢朱紫交換精靈方法一覽,感興趣的小伙伴不要錯過,希望能幫…

                  2022年11月23日
                • 《寶可夢朱紫》龍爪技能怎么獲得?龍爪技能獲取方法

                  寶可夢朱紫龍爪技能怎么獲得?在游戲中,很多玩家還不清楚龍爪技能應該怎么獲取,其實獲取方法有很多,下面一起來看一下寶可夢朱紫龍爪技能獲取方法,希望可以幫助各位玩家順利的進行游戲內(nèi)容?!?/p>

                  2022年11月23日
                • 《寶可夢朱紫》怎么刷努力值?刷努力值方法推薦

                  寶可夢朱紫中努力值是一項隱藏的數(shù)值,累積努力值可以提升寶可夢的屬性。很多玩家想知道寶可夢朱紫怎么刷努力值,下面就帶來寶可夢朱紫刷努力值方法推薦,感興趣的小伙伴不要錯過,希望能幫助到…

                  2022年11月23日

                聯(lián)系我們

                聯(lián)系郵箱:admin#wlmqw.com
                工作時間:周一至周五,10:30-18:30,節(jié)假日休息