渠道發(fā)行的Android多渠道打包實(shí)踐
01 前言
多渠道打包對(duì)于每一個(gè)Android開發(fā)來(lái)說(shuō)應(yīng)該都不陌生,從最早的Eclipse上純手動(dòng)打包到Ant腳本打包,再到現(xiàn)在Android Studio的自帶的渠道配置,以及gradle腳本實(shí)現(xiàn)批量打包。多渠道打包的方案在不斷的優(yōu)化,打包速度也從原來(lái)的幾十個(gè)渠道包打一天到現(xiàn)在只需要幾小時(shí)。
但是上述方案只是替換了配置文件中的渠道信息,如果沒有源碼,只有一個(gè)apk文件,并且根據(jù)不同的渠道每個(gè)包里的模塊和代碼都要定制化,有沒有解決方案呢?
02 游戲渠道發(fā)行的打包
目前國(guó)內(nèi)安卓市場(chǎng)的渠道非常之多,其中有華為、小米、vivo、oppo等自帶操作系統(tǒng)或硬件設(shè)備的硬核廠商,基于自己移動(dòng)設(shè)備建立了非常大的用戶群體,還有應(yīng)用寶、九游等雖然沒有自己的移動(dòng)設(shè)備,但是憑借其app廣大的受眾,也積累了許多的用戶。對(duì)于要在這些渠道上發(fā)行游戲,就要接入這些渠道不同的sdk來(lái)實(shí)現(xiàn)渠道的登錄、支付等能力,并不像常規(guī)app那樣,只是改個(gè)channelId就好了。游戲?qū)τ趩蝹€(gè)渠道的接入可能就需要花上一周甚至更多時(shí)間,如果要同時(shí)接入幾十個(gè)渠道,對(duì)于游戲研發(fā)來(lái)說(shuō)需要投入非常大的時(shí)間成本,另外上文所說(shuō)的打包方案又該如何解決,一個(gè)游戲包打包往往需要幾小時(shí),如果每個(gè)渠道單獨(dú)打包,打完幾十個(gè)渠道包需要花上數(shù)十上百個(gè)小時(shí),再加上后期對(duì)于每個(gè)渠道sdk的迭代維護(hù),其中的成本可想而知。為此,我們需要提供給游戲研發(fā)一套能低成本的打包方案。
有別于傳統(tǒng)的app,自家研發(fā)的產(chǎn)品,在編譯過(guò)程中可以配置各種腳本實(shí)現(xiàn)多渠道打包。
在游戲渠道發(fā)行中,發(fā)行方并不是游戲的開發(fā)者(以下簡(jiǎn)稱CP),因此我們只能拿到CP提供的apk(以下簡(jiǎn)稱母包),我們需要基于母包來(lái)進(jìn)行各個(gè)渠道的定制整合,其中包括集成每個(gè)渠道不同的sdk以及他們的鑒權(quán)、登錄、支付等能力,最終打出各個(gè)渠道不同的渠道子包。
03 目標(biāo)規(guī)劃
針對(duì)上述的痛點(diǎn)我們不妨先定個(gè)小目標(biāo):游戲只接一個(gè)sdk,游戲只打一次包。
那么要完成這個(gè)“小目標(biāo)”我們就需要解決兩個(gè)問(wèn)題:1、整合渠道,2、整合打包
3.1 整合渠道
這里的整合渠道并不是說(shuō)把所有的渠道都接完放到一個(gè)大的sdk里,然后根據(jù)channelId來(lái)調(diào)用不同渠道的方法,這么做既不優(yōu)化也難以維護(hù)。所謂整合其實(shí)是通過(guò)一個(gè)統(tǒng)一的出口來(lái)對(duì)渠道進(jìn)行封裝,在我們常規(guī)的app開發(fā)中也有通過(guò)不同的flavor或者buildType動(dòng)態(tài)加載dependency的場(chǎng)景,那我們只要把每個(gè)渠道當(dāng)成一個(gè)單獨(dú)的module,不過(guò)由于我們是合作方,只能拿到游戲打完的apk包,我們不能把渠道當(dāng)成module集成在游戲的代碼里,只能變成單獨(dú)的apk去集成,在每個(gè)獨(dú)立的渠道apk里集成sdk的能力,再通過(guò)統(tǒng)一封裝的代理層來(lái)實(shí)現(xiàn)這些接口的對(duì)外暴露就行了,具體的架構(gòu)如下所示:
通過(guò)上圖可以看出整體的業(yè)務(wù)流程是:游戲調(diào)用proxy的代理接口→代理接口調(diào)用具體集成的渠道api,這樣無(wú)論底層的渠道如何變換,只要代理層的接口設(shè)計(jì)能覆蓋渠道所有的能力,那么對(duì)于上層游戲來(lái)說(shuō)渠道的變化就是無(wú)感知的,這樣做到了游戲和渠道的徹底解耦,也做到了渠道的整合。
3.2 整合打包
游戲打一次包往往需要幾個(gè)小時(shí),如果每個(gè)渠道打一次包,耗時(shí)巨大,但是如果按照上文架構(gòu)設(shè)計(jì),游戲只要接入一次代理sdk,然后我們只要在打渠道包的時(shí)候替換渠道模塊的代碼以及資源就行了。
04 技術(shù)實(shí)現(xiàn)
既然目標(biāo)已經(jīng)確定,那么我們就需要具體的打包方案來(lái)實(shí)現(xiàn)。
傳統(tǒng)的渠道打包方式無(wú)法滿足,游戲發(fā)行需要有一套獨(dú)特的多渠道打包方式。
整理一下需求:我們有一個(gè)apk,還有一份渠道sdk的代碼,我們需要把這些代碼合并到apk中生成一個(gè)新的apk,這個(gè)流程聽著是不是很熟悉?這不就是反編譯和回編譯的過(guò)程嗎。
谷歌官方的apktool提供了反編譯,回編譯等能力,基于它我們可以設(shè)計(jì)出大致流程:
整個(gè)流程中大部分的工作調(diào)用apktool的api就能夠?qū)崿F(xiàn),但是如何去替換注入渠道sdk的代碼呢?
熟悉逆向的同學(xué)一定知道,apktool反編譯之后生成的是smali文件,大概長(zhǎng)成下面這樣:
別說(shuō)修改了,這種類似匯編的代碼的可讀性都很差。
換一種思路,如果我們編輯的是java文件,那是不是就方便很多了。
順著這種思路,如果我們有一個(gè)集成了渠道sdk的demo.apk,再提供給CP一個(gè)代理sdk,通過(guò)apktool反編譯demo.apk之后生成的smali文件替換母包反編譯后對(duì)應(yīng)的代理類,這樣就可以實(shí)現(xiàn)渠道代碼的注入了。
同樣,我們只要針對(duì)每個(gè)渠道單獨(dú)開發(fā)一個(gè)接入的demo.apk就可以復(fù)用在所有的游戲上。這樣既做到了渠道的獨(dú)立,又可以橫向擴(kuò)展。
因此,我們?cè)O(shè)計(jì)了一套代理層,對(duì)上暴露登錄,支付,鑒權(quán)等基礎(chǔ)能力的api,內(nèi)部是渠道api的調(diào)用,基于這套代碼打包出來(lái)的就是demo.apk了。
其次,渠道之間還有很多差異化的內(nèi)容要處理,最簡(jiǎn)單就是同一個(gè)游戲不同的渠道包名是不一樣的。
這里就要用到反編譯之后的yml文件了,這個(gè)文件記錄了反編譯的配置信息,用于回編譯的時(shí)候讀取,用修改包名舉例,只要增加第一行的配置就可以改變回編譯之后的包名了。
同時(shí),yml文件還可以自定義很多配置,這里就不展開了,感興趣的同學(xué)可以自行了解一下。
最后,解決了代碼合并,渠道差異化的配置之后,整個(gè)打包過(guò)程大致為以下幾步:
1. 準(zhǔn)備游戲母包和對(duì)應(yīng)的渠道demo.apk
2. 通過(guò)apktool d xxx命令分別反編譯這兩個(gè)apk,得到如下文件結(jié)構(gòu)
3. 合并AndroidManifest.xml,合并assets中的文件,合并lib,合并并替換res中的相應(yīng)資源和配置文件,替換smali中的相關(guān)文件
4. 通過(guò)apktool b xxx命令回編譯apk
5. 通過(guò)簽名工具對(duì)回編譯的apk進(jìn)行簽名
4.1 腳本打包
雖然打包步驟就簡(jiǎn)單的五步,但是其中步驟3的資源整合和替換是非常繁瑣的。
AndroidManifest合并:
1. 使用xml解析器,獲取所有的節(jié)點(diǎn)
2. 合并相同節(jié)點(diǎn)
3. 添加渠道特殊邏輯
4. 添加游戲特殊邏輯
5. 替換包名相關(guān)的節(jié)點(diǎn)(provider,permission等)
6. 創(chuàng)建新的manifest
assets合并:
1. 合并assets文件夾
2. 添加渠道特殊邏輯
3. Splash資源替換
4. 生成新的assets文件夾
lib合并:
1. 獲取母包libs
2. 獲取渠道libs,并且和母包libs進(jìn)行對(duì)比
3. 合并相同的libs
4. 根據(jù)游戲支持的cpu復(fù)制對(duì)應(yīng)的libs
5. 生成新的libs文件夾
res合并:
res比較特殊,它的合并需要拆成兩個(gè)部分:一個(gè)是anim,color,drawable,layout等文件夾的合并,另外是values的合并;
除values外其他文件的合并類似assets合并,替換相同的文件,合并其他文件,生成新的文件夾;
values文件夾的合并就要逐條解析文件內(nèi)容進(jìn)行合并;
最后還是要加上渠道的特殊邏輯,生成新的res文件夾。
smali合并:
首先找到對(duì)應(yīng)的代理層文件夾,把demo.apk的文件替換到母包中對(duì)應(yīng)位置;
需要注意的是很多渠道的sdk比較大,方法數(shù)可能會(huì)超65535的限制,合并的時(shí)候我們通過(guò)腳本統(tǒng)計(jì)每個(gè)smali文件夾里類的方法數(shù),當(dāng)方法數(shù)累加超過(guò)閾值之后會(huì)新建smali_classes2文件夾,把后續(xù)類遷移到后面的文件夾中。
這些操作如果單純靠人工手動(dòng)處理不僅非常耗時(shí),而且還容易出錯(cuò)。我們整理完合并替換規(guī)則之后,實(shí)現(xiàn)了一個(gè)打包工具來(lái)幫我們處理這些繁瑣的工作。
以下是資源替換的工具類:
至此,我們就可以把繁瑣的人工打包過(guò)程轉(zhuǎn)換成簡(jiǎn)單的腳本命令來(lái)實(shí)現(xiàn),節(jié)省時(shí)間的同時(shí)還能保證準(zhǔn)確率。
4.2 工具打包
雖然腳本打包已經(jīng)非常便捷了,但其實(shí)由于每位同學(xué)的電腦環(huán)境不同,同一份腳本在不同的電腦上運(yùn)行的結(jié)果也會(huì)有差異,環(huán)境差異的報(bào)錯(cuò)對(duì)打包也會(huì)有一定程度的影響,因此,我們需要一個(gè)相對(duì)統(tǒng)一穩(wěn)定的環(huán)境來(lái)執(zhí)行打包任務(wù),這就可以使用傳統(tǒng)的持續(xù)集成工具:jenkins
于是我們基于打包腳本和jenkins,部署了一套高可用的游戲渠道發(fā)行打包工具,降低了打包的門檻和費(fèi)力度,讓打包效率有了進(jìn)一步的提升。
4.3 平臺(tái)打包
腳本也好,jenkins也好,其實(shí)都是比較偏向于開發(fā)的工具,然而打包不僅僅是開發(fā)用,更重要的是打完包之后交付給測(cè)試以及業(yè)務(wù)方,那么如果有一個(gè)非開發(fā)也能使用的,更直觀、更低門檻、更產(chǎn)品化的方案,是否能在工作流提效上有更好的幫助,為此我們還設(shè)計(jì)、研發(fā)UO打包平臺(tái),致力于讓業(yè)務(wù)同學(xué)也能夠輕松的打出渠道包。
05 避坑建議
其實(shí)整個(gè)研發(fā)工程并不像上文所說(shuō)的一帆風(fēng)順,其中也遇到了許多奇怪問(wèn)題,以下挑幾個(gè)典型的與各位分享:
5.1 合并游戲母包和渠道demo
是遇到單dex方法數(shù)超 出64K問(wèn)題
部分渠道的sdk自帶了很多的方法,此時(shí)合并成一個(gè)dex文件時(shí),可能出現(xiàn)方法數(shù)超出65536的問(wèn)題。其實(shí)方法數(shù)超的問(wèn)題相信很多安卓研發(fā)都遇到過(guò),現(xiàn)在只需要配置multiDexEnable true就可以在編譯的時(shí)候自動(dòng)分dex打包,但是對(duì)于游戲來(lái)說(shuō),我們拿到的是已經(jīng)編譯后的apk,因此沒有編譯工具會(huì)替我們進(jìn)行dex1、dex2分包,我們需要通過(guò)配置來(lái)模擬編譯工具的分包邏輯,實(shí)行手動(dòng)分包。
5.2 加固對(duì)出包流程的影響
部分游戲接入了加固平臺(tái),這會(huì)導(dǎo)致合并好母包和渠道demo.apk之后,生成的游戲渠道包啟動(dòng)閃退。遇到這種情況,我們需要改變一下出包流程。
流程由原來(lái)的:
加固后母包 -> 生成未簽名的渠道包 -> 簽名 -> 得到渠道包,但是啟動(dòng)會(huì)閃退
將加固動(dòng)作往后移動(dòng),改為:
未加固母包 -> 生成未簽名的渠道包 -> 加固 -> 簽名 -> 得到可用的渠道包
5.3 資源文件
由于二次打包aapt會(huì)重新生成R.smali文件,會(huì)產(chǎn)生兩個(gè)問(wèn)題:
1. R文件的路徑產(chǎn)生變化:
由于每個(gè)渠道的游戲包名都不同,最終渠道包的R文件路徑也不同,如果游戲中直接通過(guò)R.id.xxx的方式調(diào)用,這里的R引用的是游戲原有的包名,R文件本質(zhì)是一個(gè)類,如果路徑發(fā)生了變化,那么我們代碼中對(duì)R文件的引用就會(huì)找不到類,解決這個(gè)問(wèn)題可以有兩個(gè)思路,修改所有R文件引用,改成新的包名,或者在老的包名路徑下復(fù)制一份R文件。
大部分游戲的native代碼并不多,其中極少部分游戲會(huì)通過(guò)R.id.xxx的方式獲取資源,對(duì)于這種游戲我們?cè)诤喜mali文件的時(shí)候,會(huì)根據(jù)配置來(lái)判斷是否要在游戲原來(lái)的包名路徑下保留R文件。
同時(shí),為了防止游戲的R文件id發(fā)生變化,我們會(huì)在新包名的目錄下復(fù)制一份游戲的R文件,確保游戲的資源id不發(fā)生變化。
對(duì)于我們sdk自己的id,為了防止sdk的資源id和游戲沖突,我們sdk的資源id就交給aapt重新生成,因此,我們sdk內(nèi)部不能通過(guò)R.id.xxx的方式來(lái)獲取資源id,我們調(diào)用Context.getResources().getIdentifier()這個(gè)方法,通過(guò)包名+資源名的方式來(lái)獲取到資源id,避免了二次打包之后id改變?cè)斐傻腸rash。
同樣,雖然大部分渠道sdk里面也調(diào)用了getIdentifier來(lái)獲取資源id,但是也有個(gè)別渠道直接使用了R.id.xxx的方式來(lái)獲取,對(duì)于這種情況,在二次打包后由于上述id改變的原因,會(huì)導(dǎo)致crash。
對(duì)于這種問(wèn)題其實(shí)和游戲的R文件一樣,只要保證原始包名下有對(duì)應(yīng)的R文件來(lái)避免引用錯(cuò)誤就可以解決了。
2. 資源id變化:
由于新增了資源,資源id會(huì)變化,同時(shí)母包和demo包里都會(huì)有一些公用的基礎(chǔ)組件,同一個(gè)資源的id在兩個(gè)包中可能是不一樣的。
大家都知道,打完包之后會(huì)生成resource.arsc文件,這個(gè)文件是資源索引文件,解包之后apktool會(huì)根據(jù)resource.arsc文件生成public.xml,這個(gè)文件里面保存了資源id的值,那么我們要統(tǒng)一id就需要用public.xml中的值覆蓋demo里面原有的值,大致流程如下:
我們通過(guò)解析game和sdk的public.xml文件,然后用sdk里的資源和game進(jìn)行對(duì)比:
1. 如果sdk內(nèi)的資源game里沒有,把資源保存到新增資源集合A中
2. 如果sdk中有g(shù)ame里一樣的資源,我們會(huì)保留game里的id,并且記錄sdk的新舊id映射保存到集合B中
3. 合并的時(shí)候會(huì)先去讀集合A中的id,判斷是否有沖突,如果和現(xiàn)有id沖突了,會(huì)通過(guò)一定的規(guī)則重新生成id,并且也和步驟2一樣,把新舊id的映射保存到集合B中
4. 讀取集合B,全量搜索舊id的值,替換成新id
5. 生成新的public.xml
大致方法如上所示,一些細(xì)節(jié)的實(shí)現(xiàn)就不占用篇幅贅述了,至此,資源文件相關(guān)的處理就完成了。
5.4 provider、permission
作為android四大組件的ContentProvider大家一定不會(huì)陌生,在使用的時(shí)候需要在manifest中聲明,如下:
其中authorities是唯一標(biāo)識(shí),渠道sdk中ContentProvider的authorities都會(huì)使用包名+類名的方式來(lái)聲明
這樣對(duì)于集成了渠道sdk的demo.apk來(lái)說(shuō),所有ContentProvider的authorities都是相同的,這就會(huì)導(dǎo)致如果單渠道多個(gè)游戲,當(dāng)?shù)诙€(gè)游戲安裝的時(shí)候就會(huì)由于authorities沖突導(dǎo)致失敗,因此我們首先需要用特殊的占位符替換包名字段,然后再manifest合并的時(shí)候識(shí)別占位符,用渠道子包的包名來(lái)替換。
當(dāng)然,如果這么簡(jiǎn)單就能處理完,就不會(huì)出現(xiàn)在”避坑建議“里了,其實(shí)很多渠道的ContentProvider聲明是放在自己sdk的manifest里的,對(duì)于這樣的渠道,我們需要在集成渠道sdk工程里的manifest進(jìn)行authorities替換,需要使用replace,這樣在渠道demo打包的時(shí)候,manifest合并過(guò)程中就會(huì)把渠道sdk內(nèi)部的authorities改成我們自定義的authorities。
最后,同樣的問(wèn)題還會(huì)出現(xiàn)在permission中,有一些渠道sdk中聲明了自定義的permission來(lái)限制對(duì)自己服務(wù)或者是組件的調(diào)用,處理這部分問(wèn)題的方法和provider類似,這里就不贅述了。
06 優(yōu)化對(duì)比
6.1 流程優(yōu)化
整個(gè)打包平臺(tái)的方案,解決了之前出包流程上耗時(shí)的點(diǎn):
1. 包體多次傳輸
打包平臺(tái)將原來(lái)的上傳→下載→上傳的三次傳輸過(guò)程簡(jiǎn)化成了單次上傳。
2. 人工響應(yīng)時(shí)間
工作時(shí)間響應(yīng)時(shí)間是比較穩(wěn)定的,但是當(dāng)出現(xiàn)突發(fā)情況,很有可能會(huì)有非工作時(shí)間(午夜、假日)的打包需求,這時(shí)候可能由于各種原因技術(shù)無(wú)法及時(shí)出包,通過(guò)平臺(tái)出包可以避免此類的響應(yīng)問(wèn)題。
3. 加固流程
現(xiàn)在加固基本是每個(gè)app必備的流程,有一部分游戲加固是在簽名前對(duì)每一個(gè)渠道包進(jìn)行一次加固,然而目前我們加固是采用第三方的解決方案,基于這種情況就會(huì)中斷我們的打包流程,在簽名前還需要給第三方進(jìn)行一次加固,然后再簽名,并且這樣也會(huì)造成多次的游戲包上傳下載的操作,嚴(yán)重影響出包效率,也增加了很多人工操作的工作量。
為了進(jìn)一步提升打包體驗(yàn)和效率,我們整合了部分加固方案,讓加固變成了打包流程的一個(gè)部分,流程對(duì)比如下:
從上圖可以看到通過(guò)平臺(tái)集成了加固流程后,每個(gè)包額外又減少了6次傳輸,以及一系列的人工操作,對(duì)于打包的時(shí)間和體驗(yàn)有非常大的提升。
6.2 耗時(shí)對(duì)比
游戲包不同于別的app,單個(gè)游戲都在1-2G不等,單次傳輸?shù)暮臅r(shí)也在3-5分鐘,如果是外網(wǎng)環(huán)境速度會(huì)更慢,優(yōu)化了兩次傳輸流程后,整體出包流程提升6-10分鐘/個(gè)包,平均每個(gè)游戲接入的渠道有8-10家,相當(dāng)于每次出包縮短了一個(gè)小時(shí)的耗時(shí)。
對(duì)于加固包而言,平時(shí)整個(gè)流程可能需要1-2個(gè)小時(shí),中間由于上傳、加固等多方響應(yīng)時(shí)間,可能會(huì)更長(zhǎng),但是通過(guò)系統(tǒng)只需要十幾分鐘就能完成所有渠道的出包。
我們用重生細(xì)胞做了一次測(cè)試,8個(gè)渠道整個(gè)打包、加固、簽名一共只用了20分鐘都不到,其中耗時(shí)的還是加固流程,可以看到下面無(wú)需加固的bangGream 7個(gè)渠道只用了3分鐘。
07 總結(jié)
本文看似簡(jiǎn)單的過(guò)程,其實(shí)由于各個(gè)渠道邏輯差異、底層依賴庫(kù)的沖突,對(duì)于ProxySDK的高內(nèi)聚和低耦合的設(shè)計(jì)要求還是比較高的,開發(fā)過(guò)程中也經(jīng)過(guò)了幾次改版和踩坑,最終才交了一份階段性的答卷。
其次apktool的本身的問(wèn)題也給我們?cè)斐闪瞬簧俾闊?,?jīng)過(guò)不同版本的嘗試,以及各種配置修改、試錯(cuò)之后才確定了一個(gè)符合我們需求的穩(wěn)定版本。
未來(lái)我們還會(huì)往平臺(tái)化的方向探索,追求推出一款高可用的游戲多渠道打包平臺(tái)。