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