Appearance
29远程开关:如何远程遥控上线App的产品行为?
在前面《09 | 开关组件:如何使用功能开关,支持产品快速迭代》那一讲中,我介绍过如何实现编译时开关和本地开关。有了这两种开关,我们就可以很方便地让测试人员在 App 里面手动启动或者关闭一些功能。那有没有什么好的办法可以让产品经理远程遥控功能呢?远程开关就能完成这一任务。
通过远程开关,我们就可以在无须发布新版本的情况下开关 App 的某些功能,甚至可以为不同的用户群体提供不同的功能。 远程功能开关能帮助我们快速测试新功能,从而保证产品的快速迭代。
远程功能开关模块的架构与实现
下面我们通过 Moments App 来看看如何架构一个灵活的远程功能开关模块,并使用 Firebase 来实现一个远程功能开关。该模块主要由两部分所组成:Remote Config 模块 和Toggle 模块。远程功能开关模块的架构图如下所示:
1. Remote Config 模块的架构与实现
由于 Toggle 模块依赖于 Remote Config 模块,所以我们就先看一下 Remote Config 模块的架构与实现。
Remote Config 也叫作"远程配置",它可以帮助我们把 App 所需的配置信息存储在服务端,让所有的 App 在启动的时候读取相关的配置信息,并根据这些配置信息来调整 App 的行为。 Remote Config 应用广泛,可用于远程功能开关、 A/B 测试和强制更新等功能上。
Remote Config 的架构十分简单,由RemoteConfigKey
和RemoteConfigProvider
所组成,其中RemoteConfigKey
是一个空协议(Protocol),用于存放配置信息的唯一标识,其定义如下:
swift
protocol RemoteConfigKey { }
为了支持 Firebase 的 Remote Config 服务,我们定义一个遵循了RemoteConfigKey
协议的枚举类型(Enum), 其具体的代码如下:
swift
enum FirebaseRemoteConfigKey: String, RemoteConfigKey {
case isRoundedAvatar
}
因为 Firebase Remote Config 的标识都是字符串类型,所以我们把FirebaseRemoteConfigKey
的rawValue
也指定为String
类型,这样就能很方便地取出case
的值,例如,通过FirebaseRemoteConfigKey.isRoundedAvatar.rawValue
来得到"isRoundedAvatar"字符串。
有了配置信息的标识以后,我们再来看看如何在 App 里面访问 Remote Config 服务。首先,我们定义一个名叫RemoteConfigProvider
的协议,其定义如下:
swift
protocol RemoteConfigProvider {
func setup()
func fetch()
func getString(by key: RemoteConfigKey) -> String?
func getInt(by key: RemoteConfigKey) -> Int?
func getBool(by key: RemoteConfigKey) -> Bool
}
RemoteConfigProvider
协议定义了setup()
、fetch()
等五个方法。为了使用 Firebase 的Remote Config 服务,我们定义了一个结构体FirebaseRemoteConfigProvider
来遵循该协议,该结构体实现了协议里的五个方法。
我们先来看一下setup()
和fetch()
方法的具体代码实现:
swift
private let remoteConfig = RemoteConfig.remoteConfig()
func setup() {
remoteConfig.setDefaults(fromPlist: "FirebaseRemoteConfigDefaults")
}
func fetch() {
remoteConfig.fetchAndActivate()
}
在初始化的时候,我们调用 Firebase SDK 所提供的RemoteConfig.remoteConfig()
方法来生成一个RemoteConfig
的实例并赋值给remoteConfig
属性,然后在setup()
里调用remoteConfig.setDefaults(fromPlist:)
方法从 FirebaseRemoteConfigDefaults.plist 文件里读取配置的默认值。下图展示的就是该 plist 文件,在该文件里,我们把isRoundedAvatar
的默认值设置为 false,这样能保证 App 在无法联网的情况下也能正常运行。
在fetch()
里,我们调用了 Firebase SDK 里的fetchAndActivate()
方法来获取远程配置信息。
接着我们再来看看另外三个方法的具体实现:
swift
func getString(by key: RemoteConfigKey) -> String? {
guard let key = key as? FirebaseRemoteConfigKey else {
return nil
}
return remoteConfig[key.rawValue].stringValue
}
func getInt(by key: RemoteConfigKey) -> Int? {
guard let key = key as? FirebaseRemoteConfigKey else {
return nil
}
return Int(truncating: remoteConfig[key.rawValue].numberValue)
}
func getBool(by key: RemoteConfigKey) -> Bool {
guard let key = key as? FirebaseRemoteConfigKey else {
return false
}
return remoteConfig[key.rawValue].boolValue
}
这三个方法都使用了RemoteConfigKey
作为标识符从remoteConfig
对象里读取相关的配置信息,然后把获取到的信息分别转换成所需的类型,例如字符串、整型或者布尔类型。
至此,我们就实现了 Remote Config 模块,假如还需要支持其他的远程配置服务,只需为RemoteConfigProvider
协议实现另外一个子类型即可,例如需要支持 Optimizely 的远程配置服务时,可以实现一个名叫OptimizelyRemoteConfigProvider
的结构体来封装访问 Optimizely 后台服务的逻辑。
2. Toggle 模块的架构与实现
有了 Remote Config 模块,实现 Toggle 模块就变得十分简单了。在前面《09 | 开关组件:如何使用功能开关,支持产品快速迭代》里面,我们讲过 Toggle 模块的架构与实现。要添加远程开关的支持,我们只需要增加两个实现类型:RemoteToggle
和FirebaseRemoteTogglesDataStore
结构体。我们先看一下RemoteToggle
的实现:
swift
enum RemoteToggle: String, ToggleType {
case isRoundedAvatar
}
和编译时开关以及本地开关一样,RemoteToggle
也是一个遵循了ToggleType
协议的枚举类型。所有的远程开关功能的名称都罗列在case
里面,例如,isRoundedAvatar
表示是否把朋友圈页面里的头像显示为圆形。
有了功能开关的名称定义以后,我们就要为TogglesDataStoreType
提供一个远程开关的具体实现。因为我们使用了 Firebase 服务,所以就把它命名为FirebaseRemoteTogglesDataStore
,其具体实现如下:
swift
struct FirebaseRemoteTogglesDataStore: TogglesDataStoreType {
static let shared: FirebaseRemoteTogglesDataStore = .init()
private let remoteConfigProvider: RemoteConfigProvider
private init(remoteConfigProvider: RemoteConfigProvider = FirebaseRemoteConfigProvider.shared) {
self.remoteConfigProvider = remoteConfigProvider
self.remoteConfigProvider.setup()
self.remoteConfigProvider.fetch()
}
func isToggleOn(_ toggle: ToggleType) -> Bool {
guard let toggle = toggle as? RemoteToggle, let remoteConfiKey = FirebaseRemoteConfigKey(rawValue: toggle.rawValue) else {
return false
}
return remoteConfigProvider.getBool(by: remoteConfiKey)
}
func update(toggle: ToggleType, value: Bool) { }
}
FirebaseRemoteTogglesDataStore
依赖了 Remote Config 模块。在init()
方法里面,我们通过依赖注入的方式把FirebaseRemoteConfigProvider
的实例传递进来,并调用setup()
方法来初始化 Firebase 的 Remote Config 服务,然后调用fetch()
方法来读取所有的配置信息。
因为FirebaseRemoteTogglesDataStore
遵循了TogglesDataStoreType
协议,所以必须实现isToggleOn(_:)
和update(toggle:value:)
两个方法。
isToggleOn(_:)
方法用于判断某个开关是否打开,在方法实现里,我们先判断传递进来的toggle
是否为RemoteToggle
类型,然后再判断该 Toggle 的名称是否匹配FirebaseRemoteConfigKey
里的定义。如果都符合条件,那么就可以调用remoteConfigProvider
的getBool(by:)
方法来判断开关是否打开。
update(toggle: ToggleType, value: Bool)
方法的实现非常简单,因为 App 是无法更新远程开关信息的,所以它的实现为空。
至此,我们就为 Toggle 模块添加好了 Firebase 远程开关的支持。
远程开关的使用与配置
使用远程开关仅仅需要两步 ,下面我们就以MomentListItemView
为例子看看如何使用远程开关来控制头像的显示风格吧。
第一步是在init()
方法里面把TogglesDataStoreType
子类型的实例通过依赖注入的方式传递进去,具体代码如下:
java
private let remoteTogglesDataStore: TogglesDataStoreType
init(frame: CGRect = .zero, ..., remoteTogglesDataStore: TogglesDataStoreType = FirebaseRemoteTogglesDataStore.shared) {
self.remoteTogglesDataStore = remoteTogglesDataStore
super.init(frame: frame)
}
因为 Moments App 使用了 Firebase 作为远程开关服务,所以我们就把FirebaseRemoteTogglesDataStore
的实例赋值给remoteTogglesDataStore
属性。
第二步是调用isToggleOn(_:)
方法来判断远程开关是否开启,示例代码如下:
swift
if remoteTogglesDataStore.isToggleOn(RemoteToggle.isRoundedAvatar) {
avatarImageView.asAvatar(cornerRadius: 10)
}
我们把isRoundedAvatar
作为标识符来调用isToggleOn(_:)
方法,如果该方法返回true
,就把avatarImageView
的圆角设置为 10 pt。因为avatarImageView
的高度和宽度都为 20 pt,所以当圆角设置为 10 pt 时就会显示为圆形。
就这样,我们就能在 App 里使用名为isRoundedAvatar
的远程开关了。假如要使用其他的远程开关,只需要在RemoteToggle
和FirebaseRemoteConfigKey
两个枚举类型里添加新的case
,并在 FirebaseRemoteConfigDefaults.plist 文件设置默认值即可。
但是,产品经理怎样才能在 Firebase 服务端配置远程开关呢?下面我们一起看一下这个配置的步骤吧。
我们可以在 Firebase 网站上点击 Engage -> Remote Config 菜单来打开 Remote Config 配置页面,然后点击"Add parameter"来添加一个名叫"isRoundedAvatar"的配置,如下图所示:
当添加或修改完配置后,一定要记住点击下图的"Publish changes"按钮来发布更新。
现在我们就能很方便地在 Firebase 网站上修改"isRoundedAvatar"配置的值来控制头像的显示风格了。
除了简单地启动或者关闭远程开关以外,Firebase 还可以帮我们根据用户的特征进行条件配置,例如,我们可以让所有使用中文的用户启动圆形头像风格,而让其他语言的用户保留原有风格。
下面我们就来看看如何在 Firebase 网站上进行条件配置。
我们可以点击修改按钮的图标来打开修改弹框,然后点击"Add value for condition"按钮来添加条件。如下图所示,我们添加了一个名叫"Chinese users"的条件,该条件会判断用户是否使用中文作为他们设备的默认语言。
然后我们就可以为符合该条件的用户配置不同的值,例如在下图中,符合"Chinese users"条件的用户在读取"isRoundedAvatar"配置时都会得到true
。
下面是 Moments App 运行在不同语言设备上的效果图,你可以对比一下。
总结
在这一讲中,我们主要讲解了如何架构一个灵活的远程开关模块,该模块可以使用不同的后台服务来支持远程开关。接着我们以 Firebase 作为例子讲述了如何使用 Remote Config 来实现一个头像风格的远程开关,并且演示了如何根据用户的特征来为远程开关配置不同的值。
有了远程开关,产品经理就能很方便地遥控 App 的行为,并能快速地尝试新功能。但需要注意的是:不能滥用远程开关,并且最好能经常回顾上线的远程开关,把测试完毕的开关及时删除掉,否则会导致 App 里面的开关越来越多,使得程序的逻辑变得十分复杂且难以维护,再加上每个远程开关都需要从网络读取相关的配置信息,太多的开关还会影响到用户的使用体验。
思考题
在 FirebaseRemoteTogglesDataStore 里面,为什么没有直接使用 Firebase SDK 来读取 Remote Config 呢?另外,把读取 Remote Config 的逻辑封装在 FirebaseRemoteConfigProvider 里有什么好处呢?
可以把你的思考与答案写到留言区哦。下一讲我将介绍"如何使用 A/B 测试协助产品抉择"的相关内容,记得按时来听课哦。
源码地址
RemoteConfig 源码地址:https://github.com/lagoueduCol/iOS-linyongjian/tree/main/Moments/Moments/Foundations/RemoteConfig