本文作者:烧麦
当时国内各个公司 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 隐私方针能够参考
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 转为 %1s 的时分,是否有字符缺失或许增加了空字符导致 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!