Android APP海外実務

この記事の著者: Siu Mai

現在、インターネット業界では、国内のさまざまな企業がAPPを通じて海外に出て収入を得ることが一般的です。この2年間、筆者はCloud Music初の海外向けアプリのAndroidクライアントを開発してきました。この記事では、海外での APP 開発の経験を紹介します。

初めて海に行ったとき、海外の環境に適応するためのすべての側面をまとめました。

  • クライアントの多くの共通モジュールは、海外環境をサポートする必要があります。ここに含める

    • YunxinやShengwang SDKなど海外環境向けの一部サードパーティサービスの対応度を確認
    • ログイン、ファイルのアップロード、プッシュ、共有など、いくつかの一般的なAPP機能の海外版パッケージ
    • 基盤となるライブラリ関数の自己検査、シェルフ ポリシーのサポート、および一部のリソース構成。

    私たちの目標は、可能な限り元の技術フレームワークを維持して新しいアプリを開発することであり、動作環境の変化によって技術フレームワークが変更されることはありません。

  • Android アプリのチャネルとフォーマットの公開。海外の Android アプリは主に Google Play でリリースされていますが、ここでは公開用に aab (android app bundle) 形式を追加でサポートする必要があります。

海外出願の設計

基本ライブラリ海外実装層

基本モジュールについては、インターフェイスの実装を分離するという設計原則に従います.ファイル アップロードの基盤となるライブラリを例にとると、最終的に aar とラベル付けされる 3 つのモジュールがあります。

  • uploader_interface は、ファイルのアップロードに関連するさまざまなインターフェースを提供します
  • uploader_module uploader_interface モジュールの各インターフェースの特定の実装。たとえば、ファイルは中間プラットフォームの CDN インターフェースを介してアップロードされます。
  • uploader_module_oversea は、uploader_interface モジュール内の各インターフェイスの特定の実装でもあります.実装ロジックは、CDN インターフェイスに直接アップロードすることから、最初に Amazon Cloud にアップロードし、次に Amazon Cloud のアップロード情報を CDN に同期するように変更されています。

上記の設計原則のおかげで、基本モジュールの対応する海外実装を提供するだけで済みます。インターフェイス モジュールの API は引き続きビジネス コードで呼び出されます. このようにして、下層のレイヤーに依存する一部のビジネス コードを直接再利用できます. API。

基盤となるライブラリ コンプライアンス チェック

海外 APP 在 Google Play 作为主要分发渠道的情况下,隐私政策可能和国内略有不同。而一些底层库可能包括了一些不合规的代码,这部分需要进行排查,一般来说,遵循下面 2 个原则就不容易出现问题:

  • 底层库代码里面没有违规的 API 调用,例如和热修复这种动态代码下发的。Google Play 不允许相关功能
  • 底层库的依赖里不要包含海外环境用不到的功能。例如一些之前全公司 APP 都通用的三方服务的SDK被集成在了某个底层库,虽然海外没有使用相关功能,但是这些 SDK 非常有可能因为包括了动态下发 so 而被检查出来。

Google Play 隐私政策可以参考

support.google.com/googleplay/…

底层库资源

另一方面,对于比较简单的底层逻辑,我们一般情况也不会对其做接口与实现拆分,但是底层有可能会使用一些通用的资源,例如文案、图标等。如果我们把这些值作为变量设置进去,一方面底层库的改动比较大,另一方面初始化时候的设置也非常的繁琐。这里我们可以利用 Android 自身的资源合并策略。

如上图,底层库里面定义的 key1 字符串,我们在上层定义同名的字符串 key2, 最终在打包的时候,资源合并会保留 key2。所以也需要我们在设计底层库的时候避免直接使用字符串硬编码,以免不能灵活支持海外应用。

aab 文件与 Play Store 分发

app bundle 格式

使用 app bundle 格式当下在 Google Play 进行分发是唯一选择。

我们使用

./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 命令安装:

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 实现这个功能

代码如下:

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 管理中心文档:

support.google.com/googleplay/…

按照官方图示,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:

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:

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 ~ %1 s 的时候,是否有字符缺失或者增加了空字符导致 String.format 出错
  • 文案里面存在 & 符号,需要修改为 &

多语言解耦

在 app 的日常维护中,时常会有多语言文案需要替换。在上述工作流中,非客户端开发在需要替换文案的时候,需要频繁的提问客户端开发需要替换的具体 key。这样无疑增加了需要沟通成本。我们还可以通过一些技术手段来减少这部分的耦合。 常见的文案的替换场景大概分为两类

  • 测试、走查阶段发现某些语种存在翻译缺失
  • 开新区增加新翻译的时候,某些语种的文案长度不合理需要精简 这两种场景,非开发角色不经过沟通并不知道具体的多语言 key 是什么。 针对上述两种情况,我们的多语言插件设计了两部分功能。

缺失文案检查及 mock 文案生成 多语言插件在文案拉取的时候,对平台生成的多语言 xml 文件进行分别检查。当某语种中某个文案不存在的时候,会生成一个模拟的多语言文案写入到xml文件。模拟文案则会带上这条文案的 key。

例如 key 为 common_hello 的文案在印尼语有缺失,那么运行时切换到印尼语时使用的文案就是 mock 的文案 "客户端mock common_hello(id)",这样 qa 或者策划看到就知道这里缺失了一条文案翻译。

运行时查询多语言key

アプリ ビジネス側が新しい領域を開発する場合、クエリのコピーライティングとテクノロジーを可能な限り分離することもできます。デバッグの実行時にフローティング ウィンドウ ツールが提供されます。ツールが開かれると、現在のページの TextView を選択できます。TextView のコンテンツが文字列 ID を介して読み込まれる場合、このキーが画面に表示されます。 . 具体的な効果は以下の通りです。

このようにして、ゾーンを開くプロセスで多言語キーを照会する通信の大部分を節約し、ゾーンを開く効率を高めることができます。

展望とまとめ

ここでは、技術的なフレームワークの設計、リリース プロセス、多言語化など、Android APP の海外での実践例をいくつか紹介します。また、海外のほとんどの地域では、Android モデルの分布は混乱しており、ローエンド モデルが多く、ネットワーク環境は中国よりも劣っています。起動速度、メモリ管理、ネットワークの最適化など、海外のアプリにはまだまだ改善すべき点がたくさんあります。

この記事は NetEase Cloud Music Technology Team によって発行されたものであり、許可なく転載することは禁じられています。技術系の職種は年中無休で募集しておりますので、クラウドミュージックが好きな方で転職をお考えの方はgrp.music-fe(at)corp.netease.comまでご連絡ください!

おすすめ

転載: juejin.im/post/7195374985077063739