这篇文章,简单总结了公司安卓组已有项目做插件化改造的必要性、目标和步骤,供公司内安卓组小伙伴参考之用。
标题党的目录
前言
首先要对本篇标题做个澄清,虽然叫做 “插件化改造” ,可实际上使用并不是大家所熟知的 Android 插件化技术:既不是滴滴公司的 VirtualAPK,也不是 DroidPlugin 等。
那是什么呢?
就是将所谓的 “插件APP” 的桌面图标隐藏,通过 “宿主APP” 对其进行安装、卸载、版本更新、统一登录等管理。
贻笑大方了。
至于为什么要叫做 “插件化改造”,一是我这里没有合适的称呼,二是在公司内业务层面上已经叫习惯了,这样叫他们更容易理解。(实际上私下我曾叫过 ghost 和影子应用,结果让同事们一脸懵,向领导层汇报起来也比较 lowB。)
😜
背景
技术服务业务,需求引导开发。
我们公司目前为集团服务的应用有 6 款,这 6 款应用各司其职,但它们的用户严重重合。
解释一下:
- 围绕着集团的核心业务,这 6 款应用的核心功能各不相同;
- 各应用的用户,是相对稳定的集团体系内的用户,根据其层级和权限不同,其需要使用到的应用,少则一两款,多则达到 6 款;
- 加之服务区域不同以及其业务上的差异,同一款应用可能会分离出拥有着不同 applicationId 的多个 APP,这样 6 款应用甚至会分离出二十多个 APP。
(组件化改造,我们几个月前就已经在做了。完全完成组件化改造的应用,可以很好的改善 “同一应用,由于服务区域不同及业务差异,需要拆分为多个 APP” 的情况)
当这么多的 APP 安装到用户的手机上,用户需要来回切换才能完成日常工作、上报数据、查看报表时,用户会很痛苦。
所以,我们需要一个管理软件(下文称宿主 APP),对这么多APP(下文称插件 APP)进行统一管理,像加载宿主 APP 的一个模块一样加载插件 APP,但又要保持插件 APP 的相对独立,不能对其业务和代码有太强的侵入性。
最终,我们选择了本文所述的解决方案,让我们来看看做插件化改造的具体目标是什么。
目标
-
由 Server 中台打通各应用的账户系统,宿主 APP 一次登录,加载插件 APP 处理各自独立的业务;
-
将各插件 APP 共有业务抽离出来,作为宿主 APP 的一个模块,直接对接 Server 中台;
-
宿主 APP 对插件 APP 的安装、卸载、更新等进行管理;
-
插件 APP 的 Project,要既能打包为独立 APP,又能打包为插件 APP,要尽量支持动态化;
宿主 APP 模式的完善和推广,一定是一步步进行的,将在很长一段时间内,既要支持一部分用户使用多个独立 APP,又要支持另一部分用户使用宿主 APP。
-
各 APP 原有的账户相关操作,如账号登录、指纹登录、退出登录、修改密码等,在作为插件 APP 存在时要全部交给宿主APP处理,不可以有相关功能和 UI 的开放;
-
插件 APP 在桌面没有启动图标(当然在手机设置的“应用管理”中是可见的);
-
宿主 APP 与插件 APP 间支持进程间通信等。
步骤
怎样对原有 APP 的 Project 进行 插件化改造 ,是这部分的重点。
同时,为各插件 APP 提供了 SDK。SDK 将在与 Server 中台进行业务交互、与宿主 APP 进行通信、抽离共有功能模块及 UI 等方面为各插件 APP 提供支持,尽可能地降低插件 APP Project 的改造成本。
所以,这一部分的内容,是强业务相关的,非公司内的小伙伴,不用拘泥于细节。
1、build.gradle 改造
Appliaction module 的 build.gradle 中新增渠道配置如下所示。
这样做的目的是便于分别打独立包和插件包,同时也便于在代码中根据当前运行的是独立 APP 或者插件 APP 执行不同的代码逻辑。
android {
……
/*多渠道:独立包或者插件包*/
productFlavors {
// 独立包
independent {
manifestPlaceholders = [FLAVOR: "independent"]
}
// 插件包
ghost {
manifestPlaceholders = [FLAVOR: "ghost"]
}
}
flavorDimensions 'flavor'
}
2、Manifest.xml 改造
在 <application>
标签中新增 <meta-data>
如下所示。
这之后,就可以在代码中获取 name 是 FLAVOR
的 <meta-data>
的 value,根据 value 判断当前运行的 APP 是独立 APP 或者 插件 APP,以便执行不同的代码逻辑。
<meta-data
android:name="FLAVOR"
android:value="${FLAVOR}" />
3、启动页改造
在启动页 LauncherActivity
的 <intent-filter>
中新增 <data>
如下所示。
这样做,就限定了 LauncherActivity
启动方式,将不允许通过桌面图标启动(实际桌面上就没有图标了),宿主 APP 可通过 host + scheme
匹配的方式启动插件 APP。
<intent-filter>
……
<data
android:host="LauncherActivity"
android:scheme="com.company.usercenter.ui"
tools:ignore="AppLinkUrlError" />
</intent-filter>
注意:不可以为插件包设置独立的启动页,如LauncherGhostActivity
,否则当用户手机上安装的是独立APP时,宿主APP无法找到其启动页。
这里有一点要说明:
插件 APP 为什么还要用启动页 LauncherActivity
,而不是直接到插件 APP 的主页面 MainActivity
?
原因有两方面:
-
插件 APP 在系统看来,依旧是完整的应用,当其启动时,会出现大家所熟知的 “黑白屏问题” 。
“黑白屏问题” 的解决办法大家也熟知,就是给第一个启动的 Activity 设置
theme
:
<!-- 启动页theme -->
<style name="StartAppTheme" parent="AppTheme">
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@drawable/bg_window</item>
</style>
上述代码中的资源文件 bg_window
,就是黑白屏的替换图片。要解决黑白屏问题, bg_window
不可缺少,而其样式一般与启动页的样式一致或接近。
-
业务决定。
当各 APP 以独立 APP 启动时,访问它自己的
login
接口,会拿到一个LoginResultBean
对象,其中包含着账户相关的信息,是后续业务所必须的基础数据。当各 APP 以插件 APP 启动时,需要从宿主 APP 拿到其中台
centralizationToken
,再使用centralizationToken
作为参数访问另一个接口authLogin
拿到同样的LoginResultBean
对象,这是必需的。所以插件 APP 中访问
authLogin
的代码,要么放在启动页,要么放在主页面,其成功访问是其他所有接口的前置条件。而
bg_window
又是不可缺少的。我们就干脆单独拎出来了启动页,在其中访问接口
authLogin
,成功则跳转插件 APP 主页面,失败则回到宿主 APP。这样也不需要对主页面的初始化代码进行任何调整。
4、启动页 UI 及跳出逻辑改造
- 独立 APP 逻辑
- 应用的一些初始化;
- 会有倒计时和 “立即启动” 按钮的 UI,在倒计时结束或者用户点击 “立即启动” 后进行页面跳转;
- 根据登录状态、指纹登录配置情况,跳往登录页、指纹验证登录页或者主页面。
- 插件 APP 逻辑
- 应用的一些初始化;
- 倒计时和 “立即启动” 的 UI 和功能不开放;
- 如上文第 3 步所述,从宿主 APP 拿到其中台
centralizationToken
,再使用centralizationToken
作为参数访问接口authLogin
拿到LoginResultBean
的数据,此接口访问成功可跳转页面,访问失败可提示用户后回到宿主 APP; - 页面跳转,只能跳往主页面,不可跳往登录页或指纹验证登录页,且其 UI 和功能不开放。
5、token失效处理方式改造
-
独立 APP 逻辑
(1) 清除账户配置、缓存等(集中在SharePreference中);
(2) 跳转登录页重新登录。 -
插件 APP 和 宿主 APP 逻辑
这里分为两种情况:
-
插件 APP 自己的 token 失效
1.1 清除账户配置、缓存等(集中在SharePreference中);
1.2 提示用户后,杀掉插件 APP 进程,回到宿主 APP。
那么,为什么这里是杀掉插件 APP 进程回到宿主 APP,而不是跳往 启动页重新访问接口
authLogin
?原因是,登录统一后,插件 APP token 是与宿主 APP token 强相关的,插件 APP token 失效,难以确定是自己的业务逻辑造成的还是宿主 token 失效造成的,所以干脆回到宿主 APP。如果是插件 APP 自己的业务逻辑造成自己的 token 失效,用户重新启动一次插件 APP 即可;如果是宿主 APP token 失效造成,宿主 APP 自然会跳往宿主 APP 的登录页。
-
宿主 APP token 失效
跳往宿主 APP 的登录页。
6、个人中心页/设置页改造
插件 APP 中,账户相关的 UI 和 功能不开放,包括修改密码、退出登录、指纹验证设置等。
7、主页面改造
目前主要是指在主页面对键盘返回键点击事件的处理。
-
独立 APP 逻辑
一般会支持 “双击返回键退出应用” ,还会有诸如 “再点一次退出应用” 的 toast 提示。
-
插件 APP 逻辑
去除 “再点一次退出应用” 的 toast 提示,用户点一次返回键直接 finish 主页面,之后就回到了 宿主 APP。
注意,这里尽量不要遍历 finish 掉 Activity 栈中的所有 Activity,因为宿主 APP 的 Activity 与 插件 APP 的 Activity 大概率在同一栈中,可能会连同宿主 APP 的所有 Activity 一并 finish 掉,就回到手机桌面了。
8、多语言切换改造
在插件 APP 中要禁掉设置中 “多语言切换” 的功能,通过宿主 APP 提供 “多语言切换” 的功能,插件 APP 采用的语言随着宿主 APP 语言的改变而改变。
9、打包
由于我们在 build.gradle 中配置了 flavor ,程序运行时可以判断是独立包或者插件包了,上述各步骤中涉及到的代码,在我们打独立包和插件包间不需要做什么调整。
目前无法实现动态化的只有上述步骤第 3 条涉及到的 <data>
标签中的 host
属性和 scheme
属性,打独立包要将 <data>
标签注释掉,打插件包再放开。
总结
如大家所见,这篇文章所探究的内容,在技术上没什么深度,我认为值得关注的在于业务上的思考和实践。
如果大家有遇到跟我们类似的业务需求,且在使用 VirtualAPK、DroidPlugin 这样主流的解决方案遇到了技术上难以克服的困难的话,我们尝试的这种非主流、略显蠢逼的解决方案,不失为需求迫切时的救命稻草了。