Skip to content

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 的架构十分简单,由RemoteConfigKeyRemoteConfigProvider所组成,其中RemoteConfigKey是一个空协议(Protocol),用于存放配置信息的唯一标识,其定义如下:

swift
protocol RemoteConfigKey { }

为了支持 Firebase 的 Remote Config 服务,我们定义一个遵循了RemoteConfigKey协议的枚举类型(Enum), 其具体的代码如下:

swift
enum FirebaseRemoteConfigKey: String, RemoteConfigKey {
    case isRoundedAvatar
}

因为 Firebase Remote Config 的标识都是字符串类型,所以我们把FirebaseRemoteConfigKeyrawValue也指定为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 模块的架构与实现。要添加远程开关的支持,我们只需要增加两个实现类型:RemoteToggleFirebaseRemoteTogglesDataStore结构体。我们先看一下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里的定义。如果都符合条件,那么就可以调用remoteConfigProvidergetBool(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的远程开关了。假如要使用其他的远程开关,只需要在RemoteToggleFirebaseRemoteConfigKey两个枚举类型里添加新的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

远程开关源码地址:https://github.com/lagoueduCol/iOS-linyongjian/blob/main/Moments/Moments/Foundations/Toggles/FirebaseRemoteTogglesDataStore.swift