iOS 跨App通知

3,058 阅读4分钟

App内通知

在iOS App内的通知我们都比较熟悉,通过NSNotificationCenter即可完成。那么跨App之间是否有方式实现通知呢?

跨App通知

实际上iOS上是可以通过系统API实现跨进程通知的,详细的内容可以参考官方文档:CFNotificationCenterGetDarwinNotifyCenter

以下是一段代码示例:

/// listener
static void NotificationCallback(CFNotificationCenterRef center,
                               void * observer,
                               CFStringRef name,
                               void const * object,
                               CFDictionaryRef userInfo) {
    NSString *identifier = (__bridge NSString *)name;
    NSLog(@"%@",identifier);
}

CFNotificationCenterRef const center = CFNotificationCenterGetDarwinNotifyCenter();
NSString *identifier = @"test";
CFStringRef str = (__bridge CFStringRef)identifier;
CFNotificationCenterAddObserver(center,
                                (__bridge const void *)(self),
                                NotificationCallback,
                                str,
                                NULL,
                                CFNotificationSuspensionBehaviorDeliverImmediately);

/// poster

CFNotificationCenterRef const center = CFNotificationCenterGetDarwinNotifyCenter();
NSString *identifier = @"test";
CFStringRef str = (__bridge CFStringRef)identifier;
CFNotificationCenterPostNotification(center, str, NULL, NULL, YES);

特别注意:

  1. 上面的代码需要分别在两个App实现
  2. 发送通知时,监听的App需要还存活,比如切后台的时候开启了后台任务,后台任务还没有执行结束。

细致的同学应该会发现,CoreFoundation里面的这一套API似乎和FoundationNSNotificationCenter参数基本一致。所以想着,是否就可以基于此API实现跨App通知,并且传递信息了?

然鹅,这是不可能的。

For this center, there are limitations in the API. There are no notification "objects", "userInfo" cannot be passed in the notification, ... CFNotificationCenterPostNotification(): the 'object', 'userInfo', and 'deliverImmediately' arguments are ignored.

官方文档里面特别说明了,userInfo等参数是会被忽略的。参考stackoverflow上面的解答,可以了解到,这一整套通知的API是基于Darwin Notify的。

也就是说,我们只能跨App发送通知,但是不能实现通知信息的传递。

跨App的通信

上面虽然提到,Darwin Notify的这一套API只能发送通知,没法传递信息,那我们有没有其他方式来传递信息呢?

显然是存在。但是有限制。

App Groups

苹果提供一套App Group机制,可以让同一个开发者证书下面的App通过共享沙盒来共享信息。

那这个有什么作用呢?简单点说就是一个App可以往沙盒里面写入内容,另一个App可以读取内容。

Darwin Notify + App Group

于是,升级+限制版的跨App通知就来了:

  1. 限制版是因为仅限于拥有相同App Group的App
  2. 升级版是因为可以发送通知内容了

具体的原理如下:

有了这个之后,基本上就实现了部分App之间的无障碍通知了。MMWormhole就是一个基于Darwin Notify和App Group实现跨App通知开源组件。有兴趣的同学可以深入阅读源码。

NSFileCoordinator

其实,除了上文提到的Darwin Notify + App Group的方式可以实现跨App的通知,还存在另一种方式实现通知。这就是NSFileCoordinator

实际上NSFileCoordinator并不是为了实现跨进程通信的,它是为了解决跨进程通过共享文件进行分享信息时多进程的读写问题。为了解决读写问题,又恰好实现了通信。

先上一个具体示例:


#import "BDExNotification.h"
#import "NSDictionary+BDExTracker.h"

static NSString * const kBDExNotificationName              = @"name";
static NSString * const kBDExNotificationInfo              = @"info";

@interface BDExNotification ()<NSFilePresenter>

@property (nonatomic, copy) NSURL *fileURL;
@property (nonatomic, strong) NSOperationQueue *queue;
@property (nonatomic, strong) NSFileCoordinator *coordinator;
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSMutableDictionary<NSString *, BDExNotificationBlock> *> *observers;

@end


@implementation BDExNotification

- (void)presentedItemDidChange {
    if (self.fileURL == nil) {
        return;
    }
    [self.coordinator coordinateReadingItemAtURL:self.fileURL
                                         options:NSFileCoordinatorReadingWithoutChanges
                                           error:nil
                                      byAccessor:^(NSURL *fileURL) {
        NSDictionary *data = [NSDictionary dictionaryWithContentsOfURL:fileURL];
        if (![data isKindOfClass:[NSDictionary class]]) {
            return;
        }
        NSString *name = [data bdex_stringValueForKey:kBDExNotificationName];
        if (name == nil) {
            return;
        }
        
        [self.queue addOperationWithBlock:^{
            NSMutableDictionary<NSString *, BDExNotificationBlock> *observers = [self.observers objectForKey:name];
            if (observers.count < 1) {
                return;
            }
            NSDictionary *info = [data objectForKey:kBDExNotificationInfo];
            [observers enumerateKeysAndObjectsUsingBlock:^(NSString *key, BDExNotificationBlock block, BOOL *stop) {
                block(info);
            }];
        }];
    }];
}

- (void)relinquishPresentedItemToWriter:(void (^)(dispatch_block_t reacquirer))writer {
    writer(nil);
}

- (void)relinquishPresentedItemToReader:(void (^)(dispatch_block_t reacquirer))reader {
    reader(nil);
}

- (NSOperationQueue *)presentedItemOperationQueue {
    return self.queue;
}

- (NSURL *)presentedItemURL {
    return self.fileURL;
}

- (void)dealloc {
    [self stop];
}

+ (instancetype)sharedInstance {
    static BDExNotification *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [self new];
    });

    return sharedInstance;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        self.queue = [NSOperationQueue new];
        self.queue.maxConcurrentOperationCount = 1;
        self.observers = [NSMutableDictionary dictionaryWithCapacity:10];
    }
    
    return self;
}

- (void)stop {
    if (self.fileURL == nil) {
        return;
    }
    [NSFileCoordinator removeFilePresenter:self];
    self.fileURL = nil;
    self.coordinator = nil;
}

- (void)startWithLockFilePath:(NSURL *)path {
    if ([self.fileURL.path isEqualToString:path.path]) {
        return;
    }
    [self stop];
    self.fileURL = path;
    self.coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self];
    [NSFileCoordinator addFilePresenter:self];
}

- (NSString *)addObserverForName:(NSString *)name
                       withBlock:(BDExNotificationBlock)block {
    if (name == nil || block == nil) {
        return nil;
    }
    
    NSString *identifier = [NSString stringWithFormat:@"%p",block];
    [self.queue addOperationWithBlock:^{
        NSMutableDictionary<NSString *, BDExNotificationBlock> *observers = [self.observers objectForKey:name];
        if (observers == nil) {
            observers = [NSMutableDictionary dictionaryWithCapacity:10];
            [self.observers setValue:observers forKey:name];
        }
        [observers setValue:block forKey:identifier];
    }];
    
    return identifier;
}

- (void)removeObserver:(NSString *)identifier forName:(NSString *)name {
    if (identifier == nil || name == nil) {
        return;
    }
    
    [self.queue addOperationWithBlock:^{
        NSMutableDictionary<NSString *, BDExNotificationBlock> *observers = [self.observers objectForKey:name];
        if (observers != nil) {
            [observers removeObjectForKey:identifier];
        }
    }];
}

- (void)postNotification:(NSString *)name
                userInfo:(NSDictionary *)userInfo
              completion:(dispatch_block_t)completion {
    if (name == nil || self.fileURL == nil) {
        return;
    }
    [self.coordinator coordinateWritingItemAtURL:self.fileURL
                                         options:NSFileCoordinatorWritingForReplacing
                                           error:nil
                                      byAccessor:^(NSURL *fileURL) {
        NSDictionary *data = @{
            kBDExNotificationName:name,
            kBDExNotificationInfo:[userInfo bdex_safeJsonObject] ?: @{},
        };
        if (@available(iOS 11, *)) {
            [data writeToURL:fileURL error:nil];
        } else {
            [data writeToFile:fileURL.path atomically:YES];
        }
        if (completion) {
            completion();
        }
    }];
}

@end

基本流程如下:

其中关键点是:

  1. 大家都实现NSFilePresenter,并共享同一个文件
  2. sender申请对共享文件的写入
  3. 其他监听者能收到通知
  4. sender完成写入
  5. 其他监听者读取信息,实现App内通信。

其实我们在MMWormhole中也发现对NSFileCoordinator的使用,只是MMWormhole是为了避免多进程读写文件异常,没有基于此实现通知。

总结

示例都是比较简化的实现版本。但是基于这些原理,在一定的条件下实现跨App的通知是完全没有问题的。