dex分包變形記
一、背景
就在項目灰度測試前不久,爆出了在 Android 3.0以下手機上安裝時出現(xiàn) INSTALL _ FAILED_DEXOPT,導(dǎo)致安裝失敗。這一問題意味著項目將不能在 Android 3.0以下的手機上安裝使用,對項目的發(fā)布有比較大的影響,所以必須盡快解決。
INSTALL _ FAILED_DEXOPT導(dǎo)致無法安裝的問題,從根本上來說,可能是兩個原因造成的:
(1) 單個 dex 文件方法總數(shù)65K 的限制。
(2) Dexopt 的 LinearAlloc 限制。
當(dāng) Android 系統(tǒng)安裝一個應(yīng)用的時候,有一步是對 Dex 進行優(yōu)化,這個過程有一個專門的工具來處理,叫 DexOpt。DexOpt 是在***次加載 Dex 文件的時候執(zhí)行的。這個過程會生成一個 ODEX 文件,即 Optimised Dex。執(zhí)行 ODEX 的效率會比直接執(zhí)行 Dex 文件的效率要高很多。
但是在早期的 Android 系統(tǒng)中,DexOpt 有兩個問題。(一):DexOpt 會把每一個類的方法 id 檢索起來,存在一個鏈表結(jié)構(gòu)里面,但是這個鏈表的長度是用一個 short 類型來保存的,導(dǎo)致了方法 id 的數(shù)目不能夠超過65536個。當(dāng)一個項目足夠大的時候,顯然這個方法數(shù)的上限是不夠的。(二):Dexopt 使用 LinearAlloc 來存儲應(yīng)用的方法信息。Dalvik LinearAlloc 是一個固定大小的緩沖區(qū)。在Android 版本的歷史上,LinearAlloc 分別經(jīng)歷了4M/5M/8M/16M限制。Android 2.2和2.3的緩沖區(qū)只有5MB,Android 4.x提高到了8MB 或16MB。當(dāng)方法數(shù)量過多導(dǎo)致超出緩沖區(qū)大小時,也會造成dexopt崩潰。
盡管在新版本的 Android 系統(tǒng)中,DexOpt 修復(fù)了方法數(shù)65K的限制問題,并且擴大了 LinearAlloc 限制,但是我們?nèi)匀恍枰獙Φ桶姹镜?Android 系統(tǒng)做兼容。
回頭說項目。由于項目新版本新增功能點和代碼較多,在方法數(shù)減無可減的時候,仍然不能解決INSTALL _ FAILED _ DEXOPT的問題。所以,最終我們采用了 dex 分包的方案,來避開了 Android 3.0以下平臺的方法數(shù)和 LinearAlloc 限制。
簡單的說,分包就是在打包時將應(yīng)用的代碼分成多個 dex,使得主 dex 的方法數(shù)和所需的 LinearAlloc 不超過系統(tǒng)限制。在應(yīng)用啟動或運行過程中,首先是主 dex 啟動運行后,再加載從 dex,這樣就繞開了這兩個限制。
這樣,我們的分包方案就要解決兩個問題:一是如何對 dex 進行拆分,二是如何加載從 dex。
二、Google 官方方案
1.Dex 拆分
首先,我們需要解決如何對dex進行拆分?
通過學(xué)習(xí)資料,我們知道,對于方法數(shù)超過65K 的問題,Google 官方從 Android Build tools 21.1就開始著手解決了。
先看官方網(wǎng)站提供的配置。Google MultiDex 官方文檔是針對 Gradle 進行配置的,如下:
- android {
- compileSdkVersion 21
- buildToolsVersion "21.1.0"
- defaultConfig {
- ...
- minSdkVersion 14
- targetSdkVersion 21
- ...
- // Enabling multidex support.
- multiDexEnabled true
- }
- ...
- }
- dependencies {
- compile 'com.android.support:multidex:1.0.0'
- }
那么,是不是按 Google 官方文檔配置一下就 OK 了呢?不管怎樣,這是官方提供的方案,而且是最直接的做法,所以我們應(yīng)該先試一試。
因為我們項目的 RDM 構(gòu)建環(huán)境采用的是 ant 腳本編譯,所以首先要想辦法把 Google 官方編譯配置改造成 ant 腳本。
官 方文檔上只提供了如何使用 MultiDex,沒有說明構(gòu)建時如何打包出多個 dex。其實是因為如果用了這種 Gradle來構(gòu)建,當(dāng)應(yīng)用構(gòu)建時,構(gòu)建工具會自動分析哪些類必須放在***個 DEX 文件(主 dex),哪些類可以放在附加的 DEX 文件(從 dex)中,并將分析結(jié)果輸出到 dx 進行后續(xù)打包。當(dāng)它創(chuàng)建了主 dex 文件(classes.dex)后,如果有必要會繼續(xù)創(chuàng)建從 DEX 文件,如 classes2.dex, classes3.dex。這種方法優(yōu)點是配置比較簡單,但是***的缺點是不能指定哪些類必須包含在主 dex 中,容易導(dǎo)致應(yīng)用啟動時某些類找不到,出現(xiàn) Class Not Found Exception。
我們把上述 Gradle 的配置改成 ant 腳本時,就不能簡單套用了。通過查看 dx 工具的用法:
參數(shù)說明:
--multi-dex:多 dex 打包的開關(guān)。
--main-dex-list=:參數(shù)是一個類列表的文件,在該文件中的類會被打包在***個 dex 中。
--minimal-main-dex:只有在--main-dex-list 文件中指定的類被打包在***個 dex,其余的都在第二個 dex 文件中。
因為后兩個參數(shù)是 optional 參數(shù),所以理論上只需給 dx 加上“--multi-dex”參數(shù)即可生成出 classes.dex、classes2.dex、classes3.dex、…。
在 Gradle 中可以做如下的配置:
- afterEvaluate {
- tasks.matching {
- it.name.startsWith('dex')
- }.each { dx ->
- if (dx.additionalParameters == null) {
- dx.additionalParameters = ['--multi-dex']
- } else {
- dx.additionalParameters += '--multi-dex'
- }
- }
- }
好了,這樣我們就可以改造我們的 ant 腳本了。
改造的方法是在項目打包的 ant 腳本中引入 Android build Tools 21.1.2,并把用 dx 生成 dex 的部分改造成下面的樣子:
編譯、打包,并沒有像預(yù)期那樣生成多個 dex,而是只生成了一個 classes.dex:
生成的 apk 包跟 dex 分包前一樣。為什么會這樣?
再看 dx 的參數(shù),main-dex-list 和 minimal-main-dex 只會影響到主 dex 中包含的文件,不會影響到從 dex 是否生成,所以應(yīng)該是其他原因造成的。
查不到資料,分析源代碼就是解決問題的不二法門。于是我把 dx.jar 反編譯了一下,通過分析,找到了下面的幾行關(guān)鍵代碼:
顯然,dx 進行多 dex 打包時,默認(rèn)每個 dex 中方法數(shù)***為65536。而查看當(dāng)前應(yīng)用 dex 的方法數(shù),一共只有51392(方法數(shù)沒超標(biāo),主要是 LinearAlloc 超標(biāo)),沒有達到65536,所以打包的時候只有一個 dex。
再繼續(xù)分析代碼,發(fā)現(xiàn)下面一段關(guān)鍵代碼:
這說明 dx 有一個隱藏的參數(shù):--set-max-idx-number,這個參數(shù)可以用于設(shè)置 dx 打包時每個 dex ***的方法數(shù),但是此參數(shù)并未在 dx 的 Usage 中列出(坑爹?。。?。
我們在 ant 腳本中把這個參數(shù)設(shè)置上,暫時設(shè)置每個 dex 的方法數(shù)***為48000:
重新打包,結(jié)果如下:
果然,第二個 dex 出現(xiàn)了!
可是,觀察一下 res 目錄,這里出現(xiàn)了一個新的問題,drawable 密度后綴的資源目錄都多了一個 v4:
為 什么這幾個目錄會帶 v4后綴呢?原來這是 R6以上的 Android SDK Tools 自動打包工具新加的一個處理,即為這些在 Android 1.0 時不存在的密度后綴命名的資源路徑名稱后面自動添加一個適合的版本后綴,以確保老版本不使用這些資源(只有 API level 4以及更高版本支持后綴),v4 就表示使用在 Android 1.6 或更高版本。
上述的 Dex 拆分過程采用的就是 Google 官方的方案。Dex 拆分已經(jīng)完成,如何加載呢?
2.Dex加載
因為 Android 系統(tǒng)在啟動應(yīng)用時只加載了主 dex(Classes.dex),其他的 dex 需要我們在應(yīng)用啟動后進行動態(tài)加載安裝。
Google 官方方案是如何加載的呢?
Google 官方支持 Multidex 的 jar 包是 android-support-multidex.jar,該 jar 包從 build tools 21.1 開始支持。這個 jar 加載 apk 中的從 dex 流程如下:
此處主要的工作就是從 apk 中提取出所有的從 dex(classes2.dex,classes3.dex,…),然后通過反射依次安裝加載從 dex 并合并 DexPathList 的 Element 數(shù)組。
如果引用這個 jar 包,MultiDexApplication 的 Java Doc 提供了三種方式來加載從 dex:
1)在 AndroidManifest.xml 中,把 application 定義為 android.support.multidex.MultiDexApplication。
2)用自定義的 Application 類繼承 android.support.multidex.MultiDexApplication,再配置 application 為自定義的類。
3) 如果之前自定義的 Application 類已經(jīng)繼承了其他 Application 類,而且不想改變,那么可以重寫自定義 Application 類的 attachBaseContext() 或者 onCreate() 方法,并添加語句 MultiDex.install(this)。
為了使改動最小,我們采用上述3)中的調(diào)用方式:
到此為止,用 Google 官方方案進行 dex 拆分和加載就已經(jīng)完成了。安裝運行一下試試!
3.安裝運行
我們把分包后的 apk 在 Android 4.3的手機上進行安裝。沒有問題,順利安裝上了!
沒想到的是,啟動時沒出現(xiàn)任何頁面,直接 crash。Crash 的 log 如下:
從 log 上看,項目在啟動閃屏頁面時無法實例化 com.example.AppService.AstApp,因為找不到 com.example.AppService.AstApp 這個類。既然 Application 類都找不到,那么我們在 Application 中加載從 dex 更加沒有執(zhí)行到了。
反編譯一下 classes.dex 和 classes2.dex,果然 com.example.AppService.AstApp 是在classes2.dex,所以剛啟動時在主 dex(classes.dex) 中找不到 com.example.AppService.AstApp(Application 類)。
理 論上,啟動必需的代碼應(yīng)該放在主 dex 中,這些代碼包括 Application、BaseActivity 等代碼以及繼承自它們的代碼的一個依賴集。但是我們看到,單純依賴于構(gòu)建工具自動進行 dex 拆分時,我們無法決定或干預(yù)哪些類應(yīng)該放在主 dex,哪些類應(yīng)該放在從 dex,這就可能導(dǎo)致啟動時往往會有類庫找不到。
接下來,我們就得想辦法來自主定制主、從 dex 包含的文件,使它們完全可控。
4.Google 官方方案的小結(jié)
采用 Google 官方的拆包方案走到現(xiàn)在,我們需要再梳理一下思路了。
到現(xiàn)在為止,已經(jīng)解決的問題是:
1)能正常打出多個 dex;
2)可以指定每個 dex 的大小;
3)可以加載多個 dex。
尚未解決的問題是:如何指定哪些類應(yīng)該放到主 dex,哪些類應(yīng)該放到從 dex?
關(guān) 于這個問題,從前面 dx 工具的用法中可得知,我們可以在 dx 的參數(shù)中加入--main-dex-list,指定哪些類應(yīng)該放在主 dex 中(也可同時配合使用參數(shù)--minimal-main-dex,指定主 dex 中只包含在--main-dex-list 文件中指定的類)。
可是問題又來了,怎么得到 main-dex-list 文件?在大的工程開發(fā)中,手動添加文件列表顯然不現(xiàn)實。
同時,在前面研究和驗證 Google 官方方案的過程中,也有幾個不得不提的問題:
1)需要高版本的 build Tools、SDK Tools 編譯打包;
2)編譯打包 apk 后生成的 drawable 密度后綴目錄被添加了 v4 后綴;
3)Google 的 MultiDex 方案在運行中需要比較大的 LinearAlloc,但是由于 Android 4.0 (API level 14) 以下的機器上 Dalvik LinearAlloc 的一個缺陷 (Issue 22586) 和限制 (Issue 78035),可能導(dǎo)致運行時無法滿足 LinearAlloc 的需求而造成 DexOpt 失敗或者 Dalvik 虛擬機崩潰;
4)從 dex 不能太大,否則在運行時安裝加載從 dex 的過程比較復(fù)雜和耗時,可能會導(dǎo)致應(yīng)用程序無響應(yīng) (ANR) 的錯誤。
由 于項目是***做分包,安裝包改動已經(jīng)比較大了,如果再將一直使用且沒有問題的 build Tools、SDK Tools 冒然升級以及 drawable 密度后綴目錄改變,那么無論怎樣,它們所帶來的風(fēng)險和挑戰(zhàn)都是比較大的,也會帶來后期測試和維護的工作量。所以,我們的方案一定要做到盡量減少這些改變。 而對于后面兩點,我們就應(yīng)該考慮對 dex 的拆分進行干預(yù),使每個 dex 的大小在一定的合理范圍內(nèi),消除或減少觸發(fā) Dalvik LinearAlloc 缺陷和限制的概率以及分包引起的 ANR。
綜合以上幾點,我們就需要在對官方方案透徹研究的基礎(chǔ)上,自己實現(xiàn)工具腳本來進行 dex 的自主拆分、加載,便于靈活的適應(yīng)低版本 Android SDK tools 以及 Android 平臺。
三、DEX 自動拆包和動態(tài)加載方案
1.Dex 拆分
根據(jù)前面對官方方案的研究總結(jié),我們可以很快梳理出下面幾個dex拆分步驟:
1)自動掃描整個工程代碼得到 main-dex-list;
2)根據(jù) main-dex-list 對整個工程編譯后的所有 class 進行拆分,將主、從 dex 的 class 文件分開;
3)用 dx 工具對主、從 dex 的 class 文件分別打包成 .dex 文件,并放在 apk 的合適目錄。
怎么自動生成 main-dex-list?
Android SDK 從 build tools 21 開始提供了 mainDexClasses 腳本來生成主 dex 的文件列表。查看這個腳本的源碼,可以看到它主要做了下面兩件事情:
1)調(diào)用 proguard 的 shrink 操作來生成一個臨時 jar 包;
2)將生成的臨時 jar 包和輸入的文件集合作為參數(shù),然后調(diào)用com.android.multidex.MainDexListBuilder 來生成主 dex 文件列表。
Proguard的官網(wǎng)執(zhí)行步驟如下:
在 shrink 這一步,proguard 會根據(jù) keep 規(guī)則保留需要的類和類成員,并丟棄不需要的類和類成員。也就是說,上面 shrink 步驟生成的臨時 jar 包里面保留了符合 keep 規(guī)則的類,這些類是需要放在主 dex 中的入口類。
但 是僅有這些入口類放在主 dex 還不夠,還要找出入口類引用的其他類,不然仍然會在啟動時出現(xiàn) NoClassDefFoundError。而找出這些引用類,就是調(diào)用的 com.android.multidex.MainDexListBuilder,它的部分核心代碼如下:
在調(diào)用 com.android.multidex.MainDexListBuilder 之后,符合 keep 規(guī)則的主 dex 文件列表就生成了。
既 然 Android SDK 已經(jīng)提供了這樣一種比較方便的工具,我們就不再重復(fù)發(fā)明輪子了。所以我們首先把 mainDexClasses 腳本進行了一些適當(dāng)?shù)母脑?,然后移植?RDM 構(gòu)建環(huán)境下,然后根據(jù)項目代碼的實際情況將主要的基礎(chǔ)類、common 類、wakeup 類做為補充規(guī)則加入掃描規(guī)則中,再加上基本規(guī)則 Application、Activity、Service、Provider、Receiver 等類,就組成了項目的主 dex 掃描規(guī)則。
這時,新的問題是,由于項目編譯打包時有代碼混淆的步驟,那我們掃描主 dex 文件列表時到底是在代碼混淆之前還是之后?理論上,混淆前后都可以掃描,但是混淆之后掃描時主要的問題是:在制定 keep 規(guī)則時,最合理的方式是采用包路徑來制定規(guī)則,而混淆后的代碼中大部分包路徑被混淆了,我們無法根據(jù)混淆后的包路徑來制定 keep 規(guī)則,也就無法完全指定哪些文件應(yīng)該放在主 dex 中。所以,結(jié)論就是,我們必須在代碼混淆之前掃描生成主 dex 文件列表。
再往下做 時,問題又出現(xiàn)了,我們是在掃描生成主 dex 文件列表后就立刻將主、從 dex 的 class 文件拆分到不同目錄,然后各自進行代碼混淆呢還是統(tǒng)一混淆后再進行 class 文件的拆分呢?答案是,我們需要統(tǒng)一混淆后再做拆分。因為如果拆分后各自混淆,則必然會造成混淆后主、從 dex 引用類名的不一致,從而導(dǎo)致應(yīng)用無法正常運行。
但是,這樣又有了新的問題,我們是在代碼混淆之前掃描生成的主 dex 文件列表,當(dāng)代碼混淆之后,大部分類名稱和路徑都改變了,我們又如何根據(jù)主 dex 文件列表做拆分呢?答案是,因為 proguard 做代碼混淆時生成了一個混淆前后代碼之間的 mapping 關(guān)系文件,我們只需要根據(jù)這個 mapping 文件進行映射,即可得到混淆后的主 dex 文件列表。
到此為止,思路已經(jīng)梳理得比較清楚了。
按照這個思路,很快就實現(xiàn)了工具腳本,完成了對主、從 dex 的拆分。這樣就實現(xiàn)了主、從 dex 的靈活的生成和定制,不僅解決了前面 Google 官方方案存在的問題,而且也為將來從 dex 的異步加載、按需加載提供了比較好的基礎(chǔ)。
***,項目的從 dex 是打成 jar 包放在 assets 目錄,如下圖所示:
2.Dex加載
Google 官方提供的 android-support-multidex.jar 可以用來加載官方方案打包的 dex,也完全可以用于加載我們自己的方案打包的 dex,但是這種方式有下面幾個不利的地方:
1)靈活性不夠,需要所有的從 dex 跟主 dex 在同一級目錄,即都在 apk 的根目錄,而且從 dex 的命名要符合 classes2.dex、classes3.dex、…、classes(N).dex。
2)該 jar 包提供的是同步加載方式,而且是啟動時一次性加載所有的從 dex,但是從項目分包的需求以及其他產(chǎn)品的經(jīng)驗來看,加載接口提供異步加載和按需加載的能力是很有必要的。
因 此,我們的加載方案需要有比較好的靈活性以及提供同步加載、異步加載、按需加載的能力。根據(jù)這些要求,我們研究了網(wǎng)上一些開源的代碼(也包括 Google 官方 android-support-multidex.jar 的代碼),然后經(jīng)過改造和驗證,實現(xiàn)了一種比較靈活的加載方案。
跟 Google 官方加載方案一樣,這個方案采用的也是運行時動態(tài)加載的方式,利用了 Dalvik 虛擬機的類加載器。
我 們知道,在 Java 虛擬機里動態(tài)加載用的是 ClassLoader。但是在 Dalvik 虛擬機里,卻不是 ClassLoader,Android 為我們從 ClassLoader 派生出了兩個類:DexClassLoader 和 PathClassLoader。這兩者的區(qū)別就是 PathClassLoader 不能主動從 zip 包中釋放出 dex,因此只支持直接操作 dex 格式文件,或者已經(jīng)安裝的 apk(因為已經(jīng)安裝的 apk 在 cache 中存在緩存的 dex 文件);而 DexClassLoader 可以支持 .apk、.jar 和 .dex文件,并且會在指定的 outpath 路徑釋放出 dex 文件。
由于前面說了,在安裝包里有多個 dex 時,應(yīng)用安裝時不會主動釋放從 dex,所以我們需要用 DexClassLoader 來釋放加載從 dex。當(dāng)需要加載從 dex 時,加載邏輯會先從 apk 相應(yīng)的目錄釋放出所需加載的從 dex,然后執(zhí)行加載。
加載過程的部分核心代碼如下:
上 述代碼是通過反射獲取 PathClassLoader 中的 DexPathList 中的 Element 數(shù)組(加載主 dex 后的 Element 數(shù)組)和 DexClassLoader 中的 DexPathList 中的 Element 數(shù)組(加載從 dex 后的 Element 數(shù)組),然后將兩個 Element 數(shù)組合并之后,再將其賦值給 PathClassLoader 的 Element 數(shù)組。這樣就將主、從 dex 中類的訪問方式進行了統(tǒng)一,所以也稱為 dex 的注入。
那么什么時候加載從 dex 呢?這個問題也就是從 dex 的加載時機。
如 果是啟動時同步加載,一般可以在 Application 的 onCreate 或 attachBaseContext 中執(zhí)行加載,兩者區(qū)別不大。不過,由于 Application 的 onCreate 調(diào)用是在 ContentProvider 的 OnCreate 調(diào)用之后,而 attachBaseContext 的調(diào)用是在 ContentProvider 的 OnCreate 調(diào)用之前,所以當(dāng) app 有注冊 ContentProvider 的時候,就必須在 attachBaseContext 中加載從 dex。
如果是按需加載,則在代碼充分解耦后,只要在從 dex 中的代碼調(diào)用之前執(zhí)行加載,都是可以的。
3.安裝運行
Dex 拆分腳本和加載代碼都完成了,打一個包,然后在 Android 2.3 系統(tǒng)的手機上安裝運行試試吧。一切順利,終于出現(xiàn)了久違的閃屏頁!
4.小結(jié)
上 面就是項目 dex 分包方案的研究經(jīng)過,主要是把 Google 的方案研究清楚以后,又參考了網(wǎng)上的一些開源代碼,從而實現(xiàn)了自己的 DEX 自動拆包和動態(tài)加載方案。在我們的方案中,可以通過腳本工具來完全定制拆分過程和主、從 dex 文件內(nèi)容,在運行時也能比較自由、靈活的動態(tài)加載從 dex。
四、性能影響
Dex 分包后,如果是啟動時同步加載,對應(yīng)用的啟動速度會有一定的影響,但是主要影響的是安裝后***啟動。這是因為安裝后***啟動時,Android 系統(tǒng)會對加載的從 dex 做 Dexopt 并生成 ODEX,而 Dexopt 是比較耗時的操作,所以對安裝后***啟動速度影響較大。在非安裝后***啟動時,應(yīng)用只需加載 ODEX,這個過程速度很快,對啟動速度影響不大。同時,從 dex 的大小也直接影響啟動速度,即從dex 越小則啟動越快。
目前項目的從 dex 的原始大小在 1M 左右。經(jīng)過測試,安裝后***啟動時,在 GT-I8160(Android 2.3) 上加載耗時大約 1200ms,在 N i9250(Android 4.3) 上加載耗時大約 1000ms;非安裝后***啟動時,在這兩臺測試手機上的加載速度分別為約 10ms 和 4ms。
五、后續(xù)
分包方案落地后,我們又解決了覆蓋安裝和 MD5 校驗的問題。不過后續(xù)還有不少可優(yōu)化的點如下:
(1) 應(yīng)用啟動性能的優(yōu)化。如添加啟動頁、提前做 DexOpt 等;
(2) 編譯腳本性能優(yōu)化。由于分包是一個比較復(fù)雜和耗時的過程,開始時分包腳本的性能并不理想,后來經(jīng)過我們兩次優(yōu)化,將打***程中的分包時間從7分多鐘優(yōu)化到10秒以內(nèi);
(3) 研究未來可能的按需加載或異步加載從 dex 的問題。
騰訊Bugly簡介
Bugly 是騰訊內(nèi)部產(chǎn)品質(zhì)量監(jiān)控平臺的外發(fā)版本,支持iOS和Android兩大主流平臺,其主要功能是App發(fā)布以后,對用戶側(cè)發(fā)生的crash以及卡頓現(xiàn)象進 行監(jiān)控并上報,讓開發(fā)同學(xué)可以***時間了解到app的質(zhì)量情況,及時修改。目前騰訊內(nèi)部所有的產(chǎn)品,均在使用其進行線上產(chǎn)品的崩潰監(jiān)控。
騰 訊內(nèi)部團隊4年打磨,目前騰訊內(nèi)部所有的產(chǎn)品都在使用,基本覆蓋了中國市場的移動設(shè)備以及網(wǎng)絡(luò)環(huán)境,可靠性有保證。使用Bugly,你就使用了和手機 QQ、QQ空間、手機管家相同的質(zhì)量保障手段,Bugly會持續(xù)對產(chǎn)品進行優(yōu)化打磨,在服務(wù)好內(nèi)部團隊的同時,幫助更多的開發(fā)者。