Android APP 出海实践(app出海服务)

Android APP 出海实践(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。

Android APP 出海实践(app出海服务)

底层库合规检查

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

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

Google Play 隐私政策可以参考

support.google.com/googleplay/…

底层库资源

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

Android APP 出海实践(app出海服务)

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

aab 文件与 Play Store 分发

app bundle 格式

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

我们使用

./Gradlew :app:bundleRelease复制代码

构建我们的 app bundle 文件上传至 Google Play 后台进行发布。

但是由于 aab 文件并不能直接安装在设备上,所以在日常的测试、回归阶段,我们仍然是安装 apk 文件来进行,流程如下图:

Android APP 出海实践(app出海服务)

从理论上来说,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_apkscd splitsadb 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 实现这个功能

Android APP 出海实践(app出海服务)

代码如下:

val installer = InstallApp.application().packageManager.packageInstallerval 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_ACTIONval pendingIntent = PendingIntent.getActivity(InstallApp.application(), 0, intent, FLAG_MUTABLE)val statusReceiver = pendingIntent.intentSenderinstallSession.commit(statusReceiver)复制代码

安装结果我们可以通过 Intent 里面的 android.content.pm.extra.STATUS 获取。

这里我们就可以不适用脚本命令行,直接使用安装工具安装aab文件,app 的回归发布流程就比较完善了:

Android APP 出海实践(app出海服务)

Google Play 签名

Android 应用通过 Google Play 发布的时候,还需要开启 Google Play 应用签名功能,具体的操作和规则可以参考 Play 管理中心文档:

support.google.com/googleplay/…。

按照官方图示,Google Play 会把开发者上传的密钥重新签名为新的密钥进行发布。

Android APP 出海实践(app出海服务)

最终 Google Play 控制台里面会显示最终的密钥指纹和上传密钥指纹:

Android APP 出海实践(app出海服务)

Google Play 之所以设计这套看起来有点复杂的秘钥管理,是为了保障 APP 的签名安全。当我们的上传秘钥出现被盗取或者丢失的情况下,也只需要申请重新替换上传秘钥即可。 但是我们的 APP 在发布的时候,我们不仅需要在 Google Play 进行发布,还需要发布自己的 APK 渠道包。在后台升级密钥的时候,会有如下几个选项

Android APP 出海实践(app出海服务)

如果使用默认的 Google Play 生成新的密钥,我们只能导出一个后缀名为 der 的证书,这个证书里面只包括了公钥,所以即使同 keystore 工具导出 jks 文件,也不能正常打包。所以我们需要选择 “从Java密钥库上传新的应用签名密钥”

Android APP 出海实践(app出海服务)

这里还需要注意一点,选择新的密钥规则默认选择 Android T 及以上版本升级,且此选项默认收起。我们需要选择下面的 “所有Android版本的所有新安装”,否则无法达到最终目的。

Android APP 出海实践(app出海服务)

所有我们最终签名流程如下图所示:

Android APP 出海实践(app出海服务)

我们拥有 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。流程如下图:

Android APP 出海实践(app出海服务)

随着出海APP增多及运营国家支持语种增多,上述简单的多语言导入流程也逐渐的不够使用,包括:

  • 语言较多,并且定义在代码内,每次新增语言配置都需要各个使用的地方(例如注册选择语言,设置切换语言等)修改代码。配置化程度比较低。一旦漏改,就会存在bug。
  • 从多语言平台下载文案并放入res文件夹里面的时候,需要有一个 values 文件夹作为默认语言文案,在开发阶段,我们从交互稿上看到并且录入的基本为中文,但是发布后的默认文案应该为英文。如果全程手动操作非常繁琐。

我们使用 Gradle 插件来解决这2个问题。

  • 每个应用支持的多语言类型通过配置文件定义,Gradle 插件根据配置文件内容生成语言信息的常量代码。
  • 在编译期添加一个自动拉取多语言的 task,注册在 pre${variant}Build task 之后。当 variant 属于 debug 的时候,res/values 里面放的为中文的xml文件。当 variant 属于 release 的时候,res/values 里面放的为英文的xml文件。

整个 language plugin 的工作如下:

Android APP 出海实践(app出海服务)

其中,自动拉取插件在替换文案之前,还可以做一次预检查操作。防止因为翻译错误等原因导致编译报错。例如

  • 文案里面检查 1 转为 转为s 的时候,是否有字符缺失或者增加了空字符导致 String.format 出错
  • 文案里面存在 & 符号,需要修改为 &

多语言解耦

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

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

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

Android APP 出海实践(app出海服务)

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

运行时查询多语言key

当 app 业务方开发新区的时候,我们也可以把查询文案这件事尽可能的和技术剥离开。我们在 debug 运行时提供了一个悬浮窗工具,当工具开启的时候,可以选择当前页面的 TextView, 如果这个 TextView 得内容是通过 string id 加载的,那么就会把这个 key 显示在屏幕上。具体效果如下图:

Android APP 出海实践(app出海服务)

这样我们能节省开区过程中很大一部分查询多语言 key 的沟通,增加开区效率。

展望与总结

这里介绍了一些 Android APP 出海的实践,涵盖了技术框架设计,发布流程,多语言等内容。并且对于大部分海外地区来说,Android 机型分布比较混乱,低端机型较多,且网络环境较国内比较差。在启动速度,内存管理、网络优化等方面,我们出海的 APP 还有很多需要建设的地方,希望能和大家进行分享交流。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

(0)
上一篇 2023年4月24日 下午4:58
下一篇 2023年4月24日 下午5:08

相关推荐