Android APP 出海實踐
本文作者:燒麥
當前國內各個公司 APP 出海創收已經是互聯網行業的常見操作。筆者最近約 2 年的時間裏,都在進行雲音樂旗下首個出海應用 Android 客户端的開發。本文對海外 APP 一些開發經驗做一些分享。
初次出海的時候,我們總結了需要適配海外環境的方方面面,包括
-
客户端內的很多通用模塊需要支持海外環境。這裏包括
- 確認一些三方服務對於海外環境的支持程度,例如雲信、聲網 SDK
- 一些常見 APP 功能的海外版本封裝,例如登錄,文件上傳,推送,分享
- 底層庫功能自查,支持上架政策和一些資源配置。
我們的目的是,儘量保持原有的技術框架去開發新的 APP,不要因為運營環境變了,技術架構也大改。
-
Android APP 的發佈渠道和發佈格式。海外 Android 應用以 Google Play 上架發佈為主,這裏我們需要額外支持 aab(android app bundle) 格式進行發佈。
海外應用設計
基礎庫海外實現層
基礎模塊我們遵循接口實現分離的設計原則,以文件上傳底層庫為例,我們會有3個最終打成 aar 的 module:
- uploader_interface 提供文件上傳相關的各種接口
- uploader_module uploader_interface module各個接口的具體實現,例如文件通過中台的 CDN 接口上傳。
- uploader_module_oversea 同樣是 uploader_interface module裏面各個接口的具體實現,實現邏輯從直接 CDN 接口上傳改為先上傳至亞馬遜雲,然後把亞馬遜雲的上傳信息同步給 CDN。
得益於上面的設計原則,基礎模塊我們只需要提供對應的海外實現即可。業務代碼內調用的仍然是接口 module 的 API,這樣做一來一些依賴底層的業務代碼可以直接複用,二來開發同學也不需要再去熟悉另一套底層庫 API。
底層庫合規檢查
海外 APP 在 Google Play 作為主要分發渠道的情況下,隱私政策可能和國內略有不同。而一些底層庫可能包括了一些不合規的代碼,這部分需要進行排查,一般來説,遵循下面 2 個原則就不容易出現問題:
- 底層庫代碼裏面沒有違規的 API 調用,例如和熱修復這種動態代碼下發的。Google Play 不允許相關功能
- 底層庫的依賴裏不要包含海外環境用不到的功能。例如一些之前全公司 APP 都通用的三方服務的SDK被集成在了某個底層庫,雖然海外沒有使用相關功能,但是這些 SDK 非常有可能因為包括了動態下發 so 而被檢查出來。
Google Play 隱私政策可以參考
http://support.google.com/googleplay/android-developer/answer/9888170?hl=zh-Hans&ref_topic=9877467
底層庫資源
另一方面,對於比較簡單的底層邏輯,我們一般情況也不會對其做接口與實現拆分,但是底層有可能會使用一些通用的資源,例如文案、圖標等。如果我們把這些值作為變量設置進去,一方面底層庫的改動比較大,另一方面初始化時候的設置也非常的繁瑣。這裏我們可以利用 Android 自身的資源合併策略。
如上圖,底層庫裏面定義的 key1 字符串,我們在上層定義同名的字符串 key2, 最終在打包的時候,資源合併會保留 key2。所以也需要我們在設計底層庫的時候避免直接使用字符串硬編碼,以免不能靈活支持海外應用。
aab 文件與 Play Store 分發
app bundle 格式
使用 app bundle 格式當下在 Google Play 進行分發是唯一選擇。
我們使用
kotlin
./gradlew :app:bundleRelease
構建我們的 app bundle 文件上傳至 Google Play 後台進行發佈。
但是由於 aab 文件並不能直接安裝在設備上,所以在日常的測試、迴歸階段,我們仍然是安裝 apk 文件來進行,流程如下圖:
從理論上來説,apk測試迴歸沒有什麼問題,aab 也就沒什麼問題。但是在日常實踐,我們可能會有一些 Gradle Plugin 的 task 在 hook 一些編譯任務的時候,忽略了 aab 的情況,從而導致一些運行時的錯誤。針對這種情況,在正式的 aab 文件發佈前,我們還是有必要對其做一個快速的走查。
Google 官方也提供了方法讓我們安裝 aab 文件到設備上,使用 bundletool 工具根據 aab 文件生成 apks 文件,然後使用 adb install-multiple
命令安裝:
kotlin
java -jar bundletool.jar build-apks --bundle=${FILE_NAME} --output=${target_apks}
unzip target_apks
cd splits
adb install-multiple bae-master.apk xx.apk
這樣測試迴歸流程則可以加上 aab,但是讓 qa 同學每次使用腳本安裝總也是個麻煩的事情,所以能否更徹底點呢?答案當然是可以的,既然可以通過 install-multiple
安裝 apks 文件,那麼 CI 流程上每次 aab 構建的時候,輸出 aab 和 apks 2個產物,然後通過一個安裝 apks 文件的 APP 進行安裝。
我們可以通過 android.content.pm.PackageInstaller
這個 Android API 實現這個功能
代碼如下:
```kotlin val installer = InstallApp.application().packageManager.packageInstaller val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) val sessionId = installer.createSession(params)
val installSession = installer.openSession(sessionId) apks.forEach { installSession.openWrite(it.hashCode().toString(), 0, -1) .use { out-> FileInputStream(it).use {fin-> val buffer = ByteArray(16384) var len: Int while (fin.read(buffer).also { len = it } != -1) { out.write(buffer, 0, len) } } installSession.fsync(out) installSession.close() } }
val intent = Intent(InstallApp.application(), RetActivity::class.java) intent.action = PACKAGE_INSTALLED_ACTION val pendingIntent = PendingIntent.getActivity(InstallApp.application(), 0, intent, FLAG_MUTABLE) val statusReceiver = pendingIntent.intentSender installSession.commit(statusReceiver) ```
安裝結果我們可以通過 Intent 裏面的 android.content.pm.extra.STATUS
獲取。
這裏我們就可以不適用腳本命令行,直接使用安裝工具安裝aab文件,app 的迴歸發佈流程就比較完善了:
Google Play 簽名
Android 應用通過 Google Play 發佈的時候,還需要開啟 Google Play 應用簽名功能,具體的操作和規則可以參考 Play 管理中心文檔:
http://support.google.com/googleplay/android-developer/answer/9842756。
按照官方圖示,Google Play 會把開發者上傳的密鑰重新簽名為新的密鑰進行發佈。
最終 Google Play 控制枱裏面會顯示最終的密鑰指紋和上傳密鑰指紋:
Google Play 之所以設計這套看起來有點複雜的祕鑰管理,是為了保障 APP 的簽名安全。當我們的上傳祕鑰出現被盜取或者丟失的情況下,也只需要申請重新替換上傳祕鑰即可。 但是我們的 APP 在發佈的時候,我們不僅需要在 Google Play 進行發佈,還需要發佈自己的 APK 渠道包。在後台升級密鑰的時候,會有如下幾個選項
如果使用默認的 Google Play 生成新的密鑰,我們只能導出一個後綴名為 der
的證書,這個證書裏面只包括了公鑰,所以即使同 keystore 工具導出 jks 文件,也不能正常打包。所以我們需要選擇 “從Java密鑰庫上傳新的應用簽名密鑰”
這裏還需要注意一點,選擇新的密鑰規則默認選擇 Android T 及以上版本升級,且此選項默認收起。我們需要選擇下面的 “所有Android版本的所有新安裝”,否則無法達到最終目的。
所有我們最終簽名流程如下圖所示:
我們擁有 2 個打包簽名文件,分別為 release.jks 和 store.jks,通過 Google 的 pepk.jar 工具把 Google Play 的簽名換位 store.jks。最終在發佈的時候:
- aab 文件使用 release.jks 構建,上傳後會重籤為 store.jks 發佈
- release 渠道包的apk文件使用 store.jks 構建,這樣 apk 和商店下載的 aab 文件簽名才一致,才能算是同一個 APP
Google Play 發佈問題
在使用 Google Play 發佈的時候,如果我們使用了 uses-feature
聲明功能的時候,最終在發佈的時候,可能會導致最終發佈後顯示支持設備類型數為 0,這樣用户將無法下載甚至無法在 Google Play上看到該版本。
我們需要在聲明的地方添加上 android:required="false"
即可。為了避免底層庫和上層的定義有矛盾導致 AndroidManifest 合併出錯,我們可以通過 Gradle 腳本修改合併後的 AndroidManifest 文件,把 reuqired 的值全部改為 true:
```kotlin android.applicationVariants.all { variant -> variant.outputs.each { output -> def processManifest = output.getProcessManifestProvider().get() processManifest.doLast { task -> def outputDir = task.multiApkManifestOutputDirectory File outputDirectory if (outputDir instanceof File) { outputDirectory = outputDir } else { outputDirectory = outputDir.get().asFile } File manifestOutFile = file("$outputDirectory/AndroidManifest.xml")
if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) {
def manifestPath = manifestOutFile
def xml = new XmlParser().parse(manifestPath)
def androidSpace = new Namespace('http://schemas.android.com/apk/res/android', 'android')
xml."uses-feature".each {it->
println it.attributes().get(androidSpace.name)
if (it.attributes()[androidSpace.name] == "android.hardware.camera.front" ||
it.attributes()[androidSpace.name] == 'android.hardware.camera.front.autofocus') {
it.attributes()[androidSpace.required] = false
}
}
PrintWriter pw = new PrintWriter(manifestPath)
def content = XmlUtil.serialize(xml)
println content
pw.write(content)
pw.close()
}
}
}
} ```
應用多語言
多語言工作流
提到應用出海,還有一個繞不開的話題就是應用多語言問題。 我們通過設置 Locale 來設置語言。並且在語言切換的時候重建 Activity:
kotlin
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
config.locale = target
res.updateConfiguration(config, res.displayMetrics)
config.setLocale(target)
context.createConfigurationContext(config)
} else {
config.locale = target
res.updateConfiguration(config, res.displayMetrics)
}
具體多語言我們會從內部的多語言平台拉取打包後的xml文件,放到對應的文件夾下。應用在 Locale 修改後會自動選擇對應語言的文件。例如英文目錄為 /res/values-en
,印尼語為 /res/values-in
。流程如下圖:
隨着出海APP增多及運營國家支持語種增多,上述簡單的多語言導入流程也逐漸的不夠使用,包括:
- 語言較多,並且定義在代碼內,每次新增語言配置都需要各個使用的地方(例如註冊選擇語言,設置切換語言等)修改代碼。配置化程度比較低。一旦漏改,就會存在bug。
- 從多語言平台下載文案並放入res文件夾裏面的時候,需要有一個 values 文件夾作為默認語言文案,在開發階段,我們從交互稿上看到並且錄入的基本為中文,但是發佈後的默認文案應該為英文。如果全程手動操作非常繁瑣。
我們使用 Gradle 插件來解決這2個問題。
- 每個應用支持的多語言類型通過配置文件定義,Gradle 插件根據配置文件內容生成語言信息的常量代碼。
- 在編譯期添加一個自動拉取多語言的 task,註冊在
pre${variant}Build
task 之後。當 variant 屬於 debug 的時候,res/values 裏面放的為中文的xml文件。當 variant 屬於 release 的時候,res/values 裏面放的為英文的xml文件。
整個 language plugin 的工作如下:
其中,自動拉取插件在替換文案之前,還可以做一次預檢查操作。防止因為翻譯錯誤等原因導致編譯報錯。例如
- 文案裏面檢查 $1 轉為 %1$s 的時候,是否有字符缺失或者增加了空字符導致 String.format 出錯
- 文案裏面存在 & 符號,需要修改為 &
多語言解耦
在 app 的日常維護中,時常會有多語言文案需要替換。在上述工作流中,非客户端開發在需要替換文案的時候,需要頻繁的提問客户端開發需要替換的具體 key。這樣無疑增加了需要溝通成本。我們還可以通過一些技術手段來減少這部分的耦合。 常見的文案的替換場景大概分為兩類 * 測試、走查階段發現某些語種存在翻譯缺失 * 開新區增加新翻譯的時候,某些語種的文案長度不合理需要精簡 這兩種場景,非開發角色不經過溝通並不知道具體的多語言 key 是什麼。 針對上述兩種情況,我們的多語言插件設計了兩部分功能。
缺失文案檢查及 mock 文案生成 多語言插件在文案拉取的時候,對平台生成的多語言 xml 文件進行分別檢查。當某語種中某個文案不存在的時候,會生成一個模擬的多語言文案寫入到xml文件。模擬文案則會帶上這條文案的 key。
例如 key 為 common_hello 的文案在印尼語有缺失,那麼運行時切換到印尼語時使用的文案就是 mock 的文案 "客户端mock common_hello(id)",這樣 qa 或者策劃看到就知道這裏缺失了一條文案翻譯。
運行時查詢多語言key
當 app 業務方開發新區的時候,我們也可以把查詢文案這件事儘可能的和技術剝離開。我們在 debug 運行時提供了一個懸浮窗工具,當工具開啟的時候,可以選擇當前頁面的 TextView, 如果這個 TextView 得內容是通過 string id 加載的,那麼就會把這個 key 顯示在屏幕上。具體效果如下圖:
這樣我們能節省開區過程中很大一部分查詢多語言 key 的溝通,增加開區效率。
展望與總結
這裏介紹了一些 Android APP 出海的實踐,涵蓋了技術框架設計,發佈流程,多語言等內容。並且對於大部分海外地區來説,Android 機型分佈比較混亂,低端機型較多,且網絡環境較國內比較差。在啟動速度,內存管理、網絡優化等方面,我們出海的 APP 還有很多需要建設的地方,希望能和大家進行分享交流。
本文發佈自網易雲音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡雲音樂,那就加入我們 grp.music-fe(at)corp.netease.com!
- 不一樣的Android堆棧抓取方案
- 雲音樂 Swift 混編 Module 化實踐
- iOS雲音樂APM性能監控實踐
- 雲音樂iOS端代碼靜態檢測實踐
- 網易雲音樂全面開源一款雲原生應用部署平台:Horizon
- dex 優化編年史
- 如何實現 iOS 16 帶來的 Depth Effect 圖片效果
- 雲音樂 iOS 跨端緩存庫 - NEMichelinCache
- 雲音樂 Android 內存監控探索篇
- Android APP 出海實踐
- Android 調試實戰與原理詳解
- 社交場景下iOS消息流交互層實踐
- 你構建的代碼為什麼這麼大
- 扒一扒 Jetpack Compose 實現原理
- Recoil 狀態管理方案的淺入淺出
- 基於自建 VTree 的全鏈路埋點方案
- 雲音樂 iOS 啟動性能優化「開荒篇」
- 雲音樂播放頁直播推薦實戰
- 雲音樂iOS端網絡圖片下載優化實踐
- 雲音樂 iOS 啟動性能優化「開荒篇」