一个不好的设计

101 阅读2分钟

最近在调iOS13的推送通知扩展的时候,遇到个问题,连续2个通知一起下来,第一个的内容会被第二个的覆盖。经过了一番调查,发现是这样一个设计导致的BUG:

问题模型

  1. 一个通用的扩展接口
protocol Supporter {
    func support(completionHandler: @escaping () -> Void)
}

接口的主要方法就是 support 方法,方法执行完毕之后触发异步回调。

  1. 一个配置类
class Config {
    var supporters: [Supporter] = []
    public internal(set) var content: UNMutableNotificationContent
    public init(content: UNNotificationContent) {
        self.content = content.mutableCopy() as! UNMutableNotificationContent
    }
    public func append(supporters: [Supporter]) {
        self.supporters.append(contentsOf: supporters)
    }
}

这个配置类相当于一个容器,可以往里面添加 Supporter。 每个 Supporter 会对内容做一些处理,比如执行埋点,下载头像等等。 Config 类初始化时,传入 UNNotificationContent 对象,供 Supporter 修改。

  1. 一个触发执行的入口
public Push {
    static var config: Config!
    static func register(with config: Config,
                      completionHandler: @escaping (UNNotificationContent) -> Void) {
        self.config = config
        self.execute(completionHandler)
    }
    static func execute(_ completionHandler: @escaping (UNNotificationContent) -> Void {        
        let group = DispatchGroup()       
        config.supporters.forEach { supporter in
            group.enter()
            supporter.support {
                group.leave()
            }
        }
        
        group.notify(queue: .main) {
            completionHandler(config.content)
        }
    
}

Push 类提供的是一个静态的register方法,传入一个 Config 对象,触发 Config 里的每一个 Supporter 执行,全部执行完毕之后触发异步回调,异步回调时传的参数是 config.content(已经被各个Supporter 处理后的 content )。

注意 register 是个静态方法,并且,此方法把传入的 config 对象保存到一个类属性,然后执行 execute 触发 Supporter 的执行,之所以要保存 config 到一个类属性,是为了让 Supporter 可以访问 config.content。

那么问题来了,如果有2个推送消息同时来到,那么,就会出现第一个通知内容正在执行 Supporter 的异步处理的过程中,第二个通知内容又触发执行 register 方法,于是 Push 对象的静态属性 config

设计改正

现在我们来讲一下,应该如何改正这个设计的缺陷。 首先需要明确一点就是,静态方法不应该保存参数给异步操作使用。

  1. Push 类去掉类型属性 config ;
  2. Push 类的execute方法,改为 Config 类的实例方法;
  3. Push 类的register方法,改为执行 config.execute 方法;
  4. Config 类的实例方法 execute 执行时,传递self,这样也能让 Supporter 可以访问和修改content。

补充说明

通知扩展的触发机制 在iOS13版本有个特殊的地方,就是连续2个通知同时到达手机,系统是会创2个UNNotificationServiceExtension 实例,分别调用其didReceiveNotificationRequest:withContentHandler:方法。 而在iOS14上,却不会,系统会依次创建 UNNotificationServiceExtension 实例。