本文作者:烧麦

当时国内各个公司 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 在 Google Play 作为首要分发途径的状况下,隐私方针可能和国内略有不同。而一些底层库可能包含了一些不合规的代码,这部分需求进行排查,一般来说,遵从下面 2 个准则就不容易呈现问题:

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

Google Play 隐私方针能够参考

support.google.com/googleplay/…

底层库资源

另一方面,关于比较简单的底层逻辑,咱们一般状况也不会对其做接口与完成拆分,可是底层有可能会运用一些通用的资源,例如案牍、图标等。如果咱们把这些值作为变量设置进去,一方面底层库的改动比较大,另一方面初始化时分的设置也非常的繁琐。这儿咱们能够利用 Android 本身的资源兼并战略。

Android APP 出海实践

如上图,底层库里边界说的 key1 字符串,咱们在上层界说同名的字符串 key2, 终究在打包的时分,资源兼并会保留 key2。所以也需求咱们在规划底层库的时分避免直接运用字符串硬编码,避免不能灵活支撑海外运用。

aab 文件与 Play Store 分发

app bundle 格局

运用 app bundle 格局当下在 Google Play 进行分发是唯一挑选。

咱们运用

./gradlew :app:bundleRelease

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

可是因为 aab 文件并不能直接装置在设备上,所以在日常的测验、回归阶段,咱们仍然是装置 apk 文件来进行,流程如下图:

Android 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_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 完成这个功用

Android APP 出海实践

代码如下:

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 的回归发布流程就比较完善了:

Android APP 出海实践

Google Play 签名

Android 运用经过 Google Play 发布的时分,还需求敞开 Google Play 运用签名功用,详细的操作和规矩能够参考 Play 办理中心文档:

support.google.com/googleplay/…。

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

Android APP 出海实践

终究 Google Play 控制台里边会显现终究的密钥指纹和上传密钥指纹:

Android APP 出海实践

Google Play 之所以规划这套看起来有点复杂的秘钥办理,是为了保障 APP 的签名安全。当咱们的上传秘钥呈现被盗取或许丢掉的状况下,也只需求申请重新替换上传秘钥即可。 可是咱们的 APP 在发布的时分,咱们不仅需求在 Google Play 进行发布,还需求发布自己的 APK 途径包。在后台晋级密钥的时分,会有如下几个选项

Android APP 出海实践

如果运用默许的 Google Play 生成新的密钥,咱们只能导出一个后缀名为 der 的证书,这个证书里边只包含了公钥,所以即使同 keystore 东西导出 jks 文件,也不能正常打包。所以咱们需求挑选 “从Java密钥库上传新的运用签名密钥”

Android APP 出海实践

这儿还需求注意一点,挑选新的密钥规矩默许挑选 Android T 及以上版别晋级,且此选项默许收起。咱们需求挑选下面的 “一切Android版别的一切新装置”,不然无法达到终究意图。

Android APP 出海实践

一切咱们终究签名流程如下图所示:

Android 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增多及运营国家支撑语种增多,上述简单的多言语导入流程也逐步的不够运用,包含:

  • 言语较多,而且界说在代码内,每次新增言语装备都需求各个运用的当地(例如注册挑选言语,设置切换言语等)修正代码。装备化程度比较低。一旦漏改,就会存在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 出海实践

其中,主动拉取插件在替换案牍之前,还能够做一次预查看操作。避免因为翻译错误等原因导致编译报错。例如

  • 案牍里边查看 1转为1 转为 %1s 的时分,是否有字符缺失或许增加了空字符导致 String.format 犯错
  • 案牍里边存在 & 符号,需求修正为 &

多言语解耦

在 app 的日常保护中,时常会有多言语案牍需求替换。在上述作业流中,非客户端开发在需求替换案牍的时分,需求频频的提问客户端开发需求替换的详细 key。这样无疑增加了需求沟通本钱。咱们还能够经过一些技能手段来减少这部分的耦合。 常见的案牍的替换场景大约分为两类

  • 测验、走查阶段发现某些语种存在翻译缺失
  • 开新区增加新翻译的时分,某些语种的案牍长度不合理需求精简 这两种场景,非开发人物不经过沟通并不知道详细的多言语 key 是什么。 针对上述两种状况,咱们的多言语插件规划了两部分功用。

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

Android APP 出海实践

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

运行时查询多言语key

当 app 业务方开发新区的时分,咱们也能够把查询案牍这件事尽可能的和技能剥脱离。咱们在 debug 运行时供给了一个悬浮窗东西,当东西敞开的时分,能够挑选当时页面的 TextView, 如果这个 TextView 得内容是经过 string id 加载的,那么就会把这个 key 显现在屏幕上。详细作用如下图:

Android APP 出海实践

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

展望与总结

这儿介绍了一些 Android APP 出海的实践,涵盖了技能结构规划,发布流程,多言语等内容。而且关于大部分海外地区来说,Android 机型分布比较紊乱,低端机型较多,且网络环境较国内比较差。在发动速度,内存办理、网络优化等方面,咱们出海的 APP 还有许多需求建设的当地,希望能和大家进行共享沟通。

本文发布自网易云音乐技能团队,文章未经授权制止任何形式的转载。咱们常年接收各类技能岗位,如果你预备换作业,又刚好喜欢云音乐,那就参加咱们 grp.music-fe(at)corp.netease.com!