这篇文章,简单总结了公司安卓组已有项目做插件化改造的必要性、目标和步骤,供公司内安卓组小伙伴参考之用。
首先要对本篇标题做个澄清,虽然叫做 “插件化改造” ,可实际上使用并不是大家所熟知的 Android 插件化技术:既不是滴滴公司的 VirtualAPK,也不是 DroidPlugin 等。
那是什么呢?
就是将所谓的 “插件APP” 的桌面图标隐藏,通过 “宿主APP” 对其进行安装、卸载、版本更新、统一登录等管理。
贻笑大方了。
至于为什么要叫做 “插件化改造”,一是我这里没有合适的称呼,二是在公司内业务层面上已经叫习惯了,这样叫他们更容易理解。(实际上私下我曾叫过 ghost 和影子应用,结果让同事们一脸懵,向领导层汇报起来也比较 lowB。)
那为什么放着牛X闪闪的 VirtualAPK 和 DroidPlugin 的方案不用,却选用内行看起来这么蠢逼的方案呢?感兴趣的小伙伴,可以看看我的这篇文章:Android 项目插件化改造记录——关于 DroidPlugin 和 VirtualAPK——还没写
技术服务业务,需求引导开发。
我们公司目前为集团服务的应用有 6 款,这 6 款应用各司其职,但它们的用户严重重合。
解释一下:
当这么多的 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 的改造成本。
所以,这一部分的内容,是强业务相关的,非公司内的小伙伴,不用拘泥于细节。
Appliaction module 的 build.gradle 中新增渠道配置如下所示。
这样做的目的是便于分别打独立包和插件包,同时也便于在代码中根据当前运行的是独立 APP 或者插件 APP 执行不同的代码逻辑。
android {
……
/*多渠道:独立包或者插件包*/
productFlavors {
// 独立包
independent {
manifestPlaceholders = [FLAVOR: "independent"]
}
// 插件包
ghost {
manifestPlaceholders = [FLAVOR: "ghost"]
}
}
flavorDimensions 'flavor'
}
在
标签中新增
如下所示。
这之后,就可以在代码中获取 name 是 FLAVOR
的
的 value,根据 value 判断当前运行的 APP 是独立 APP 或者 插件 APP,以便执行不同的代码逻辑。
<meta-data
android:name="FLAVOR"
android:value="${FLAVOR}" />
在启动页 LauncherActivity
的
中新增 如下所示。
这样做,就限定了 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。这样也不需要对主页面的初始化代码进行任何调整。
centralizationToken
,再使用 centralizationToken
作为参数访问接口 authLogin
拿到 LoginResultBean
的数据,此接口访问成功可跳转页面,访问失败可提示用户后回到宿主 APP;独立 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 的登录页。
插件 APP 中,账户相关的 UI 和 功能不开放,包括修改密码、退出登录、指纹验证设置等。
目前主要是指在主页面对键盘返回键点击事件的处理。
独立 APP 逻辑
一般会支持 “双击返回键退出应用” ,还会有诸如 “再点一次退出应用” 的 toast 提示。
插件 APP 逻辑
去除 “再点一次退出应用” 的 toast 提示,用户点一次返回键直接 finish 主页面,之后就回到了 宿主 APP。
注意,这里尽量不要遍历 finish 掉 Activity 栈中的所有 Activity,因为宿主 APP 的 Activity 与 插件 APP 的 Activity 大概率在同一栈中,可能会连同宿主 APP 的所有 Activity 一并 finish 掉,就回到手机桌面了。
在插件 APP 中要禁掉设置中 “多语言切换” 的功能,通过宿主 APP 提供 “多语言切换” 的功能,插件 APP 采用的语言随着宿主 APP 语言的改变而改变。
由于我们在 build.gradle 中配置了 flavor ,程序运行时可以判断是独立包或者插件包了,上述各步骤中涉及到的代码,在我们打独立包和插件包间不需要做什么调整。
目前无法实现动态化的只有上述步骤第 3 条涉及到的 标签中的
host
属性和 scheme
属性,打独立包要将 标签注释掉,打插件包再放开。
如大家所见,这篇文章所探究的内容,在技术上没什么深度,我认为值得关注的在于业务上的思考和实践。
如果大家有遇到跟我们类似的业务需求,且在使用 VirtualAPK、DroidPlugin 这样主流的解决方案遇到了技术上难以克服的困难的话,我们尝试的这种非主流、略显蠢逼的解决方案,不失为需求迫切时的救命稻草了。