乐趣区

关于android:Android-App出海全解析

原文链接:https://juejin.cn/post/7195374985077063739

以后,国内各个公司 APP 出海创收曾经是互联网行业的常见操作,本文将对海内 APP 一些开发教训做一些分享。首次出海的时候,咱们总结了须要适配海内环境的方方面面,比方,客户端内的很多通用模块须要反对海内环境:

  • 确认一些三方服务对于海内环境的反对水平,例如云信、声网 SDK
  • 一些常见 APP 性能的海内版本封装,例如登录,文件上传,推送,分享
  • 底层库性能自查,反对上架政策和一些资源配置。

咱们的最终目标是,尽量放弃原有的技术框架去开发新的 APP,不要因为经营环境变了,技术架构也大改。同时,Android APP 的公布渠道和公布格局。海内 Android 利用以 Google Play 上架公布为主,这里咱们须要额定反对 aab(android app bundle) 格局进行公布。

一、海内利用设计

1.1 根底库海内实现

根底模块咱们遵循接口实现拆散的设计准则,以文件上传底层库为例,咱们会有 3 个最终打成 aar 的 module:

  • uploader_interface 提供文件上传相干的各种接口
  • uploader_module、uploader_interface module 各个接口的具体实现,例如文件通过中台的 CDN 接口上传。
  • uploader_module_oversea 是 uploader_interface module 外面各个接口的具体实现,实现逻辑从间接 CDN 接口上传改为先上传至亚马逊云,而后把亚马逊云的上传信息同步给 CDN。

得益于下面的设计准则,根底模块咱们只须要提供对应的海内实现即可。业务代码内调用的依然是接口 module 的 API,这样做一来一些依赖底层的业务代码能够间接复用,二来开发同学也不须要再去相熟另一套底层库 API,其架构图如下。

 

1.2 底层库合规查看

海内 APP 在 Google Play 作为次要散发渠道的状况下,隐衷政策可能和国内略有不同。而一些底层库可能包含了一些不合规的代码,这部分须要进行排查,一般来说,遵循上面 2 个准则就不容易呈现问题:

  • 底层库代码外面没有违规的 API 调用,例如和热修复这种动静代码下发的。Google Play 不容许相干性能。
  • 底层库的依赖里不要蕴含海内环境用不到的性能。例如一些之前全公司 APP 都通用的三方服务的 SDK 被集成在了某个底层库,尽管海内没有应用相干性能,然而这些 SDK 十分有可能因为包含了动静下发 so 而被查看进去。

Google Play 隐衷政策能够参考:[Google Play 隐衷政策]

1.3 底层库资源

另一方面,对于比较简单的底层逻辑,咱们个别状况也不会对其做接口与实现拆分,然而底层有可能会应用一些通用的资源,例如文案、图标等。如果咱们把这些值作为变量设置进去,一方面底层库的改变比拟大,另一方面初始化时候的设置也十分的繁琐。这里咱们能够利用 Android 本身的资源合并策略。

如上图,底层库外面定义的 key1 字符串,咱们在下层定义同名的字符串 key2, 最终在打包的时候,资源合并会保留 key2。所以也须要咱们在设计底层库的时候防止间接应用字符串硬编码,免得不能灵便反对海内利用。

二、aab 文件与 Play Store 散发

2.1 app bundle 格局

App Bundle 是 Android 提供的新的利用散发格局 , 用于取代之前传统的 APK 散发格局。Android App Bundle 文件不能间接用于下载 , Google Play 会从该 App Bundle 中提取必要文件 , 主动生成一个匹配用户的 APK 文件 ; 这些优化的 APK 文件 , 比传统的繁多 APK 文件体积小很多。咱们能够应用上面的命令来生成 App Bundle 文件:

./gradlew :app:bundleRelease

然而因为 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 的回归公布流程如下图所示。

2.2 Google Play 签名

Android 利用通过 Google Play 公布的时候,还须要开启 Google Play 利用签名性能,具体的操作和规定能够参考 Play 管理中心文档:Google Play 利用签名。依照官网图示,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。

2.3 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()}
        }
    }
}

三、多语言反对

3.1 多语言工作流

提到利用出海,还有一个绕不开的话题就是利用多语言问题。咱们通过设置 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 转为 s 的时候,是否有字符缺失或者减少了空字符导致 String.format 出错。
  • 文案外面存在 & 符号,须要批改为 &。
     

3.2 多语言解耦

在 app 的日常保护中,时常会有多语言文案须要替换。在上述工作流中,非客户端开发在须要替换文案的时候,须要频繁的发问客户端开发须要替换的具体 key。这样无疑减少了须要沟通老本。咱们还能够通过一些技术手段来缩小这部分的耦合。常见的文案的替换场景大略分为两类:

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

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

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

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

退出移动版