iOS中宿主APP与录屏扩展进程数据传递方式

发布时间:2023年12月20日

背景

在iOS生态系统中,应用程序的功能不再局限于单一的宿主应用,而是可以通过扩展进程实现更丰富的用户体验和功能。其中一种引人注目的扩展是录屏功能,它使用户能够捕捉设备屏幕上的活动,无论是游戏过程、教育演示还是其他应用场景。

然而,实现这一功能并非只涉及宿主应用的单一努力,更需要与扩展进程之间的高效数据传递。这种数据传递是确保录屏功能顺利运作的关键,也是开发者需要深入了解和灵活应用的技术之一。

在本篇博客中,我们将深入探讨iOS宿主APP与扩展进程之间的数据传递方式,了解它们如何协同工作,以实现无缝的录屏体验。

实现方案

方案一:通知

关于Fondation框架中的NSNotificationCenter应该都不陌生,但这里要使用的是Core Fondation框架中的CFNotificationCenterRef,是NSNotificationCenter更底层的实现。使用CFNotificationCenterRef进行数据传递的前提是,主应用和扩展应用需要设置相同的APP Group.

另外需要注意的是,通知只适合传递少量数据,比如一些重连,断开,或者其它少量信息的情况,大量数据的传递不适用,和我们使用NSNotificationCenter一样,应该也没有人使用它来连续不断地传递数据。

以主应用监听扩展应用为例,主应用中需要有下面三个需要实现方法:

  • 声明回调block
static NSString * const notificationName = @"notificationName";
void notificationCallback(CFNotificationCenterRef center,
                                   void * observer,
                                   CFStringRef name,
                                   void const * object,
                                   CFDictionaryRef userInfo) {
    NSString *identifier = (__bridge NSString *)name;
    NSObject *sender = (__bridge NSObject *)observer;
    NSDictionary *notiUserInfo = @{@"identifier":identifier};
    //为了简化处理过程,使用常规通知承接一下。
    [[NSNotificationCenter defaultCenter] postNotificationName:notificationName
                                                        object:sender
                                                      userInfo:notiUserInfo];
}
  • 注册通知
//参数1:通知中心
//参数2:接收监听的对象。
//参数3:监听的回调
//参数4:通知给观察者的名称
//参数5:对于非Darwin通知中心,要观察的对象。
//参数6:决定应用程序在后台时如何处理通知。

- (void)registerNotificationsWithIdentifier:(nullable NSString *)identifier {
    [self unregisterNotificationsWithIdentifier:identifier];

    CFNotificationCenterRef const center = CFNotificationCenterGetDarwinNotifyCenter();
    CFStringRef str = (__bridge CFStringRef)identifier;
    CFNotificationCenterAddObserver(center,
                                    (__bridge const void *)(self),
                                    notificationCallback,
                                    str,
                                    NULL,
                                    CFNotificationSuspensionBehaviorDeliverImmediately);
}
  • 注销通知
- (void)unregisterForNotificationsWithIdentifier:(nullable NSString *)identifier {
    CFNotificationCenterRef const center = CFNotificationCenterGetDarwinNotifyCenter();
    CFStringRef str = (__bridge CFStringRef)identifier;
    CFNotificationCenterRemoveObserver(center,
                                       (__bridge const void *)(self),
                                       str,
                                       NULL);
}

而在使用过程中和NSNotificationCenter十分相似:

  • 开始注册监听
//MARK: 注册监听插件消息通知
- (void)addUploaderEventMonitor {
    [self registerForNotificationsWithIdentifier:@"setupSocket"];
    //这里同时注册了纷发消息的通知,在宿主App中使用
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(broadcastInfo:) name:notificationName object:nil];
}
  • 实现监听到通知的回调方法
- (void)broadcastInfo:(NSNotification *)noti {
    NSDictionary *userInfo = noti.userInfo;
    NSString *identifier = userInfo[@"identifier"];
    if ([identifier isEqualToString:@"setupSocket"]) {
        
    }
}
  • dealloc方法中移除监听
- (void)removeUploaderEventMonitor {
    [self unregisterForNotificationsWithIdentifier:@"setupSocket"];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:ScreenHoleNotificationName object:nil];
}

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

而扩展进程只需要负责在合适的时候发送通知:

//MARK: 发送通知
- (void)sendNotificationForMessageWithIdentifier:(nullable NSString *)identifier userInfo:(NSDictionary *)info {
    CFNotificationCenterRef const center = CFNotificationCenterGetDarwinNotifyCenter();
    CFDictionaryRef userInfo = (__bridge CFDictionaryRef)info;
    BOOL const deliverImmediately = YES;
    CFStringRef identifierRef = (__bridge CFStringRef)identifier;
    CFNotificationCenterPostNotification(center, identifierRef, NULL, userInfo, deliverImmediately);
}


- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
    // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
   [self sendNotificationForMessageWithIdentifier:@"setupSocket" userInfo:@{}];
}

我们可以把数据放到userInfo中传递给主应用。

方案二:KVO和KVC

iOS 7引入了应用群组功能允许创建一个共享沙盒,应用和应用扩展都可以访问它。还可以支持多个应用之间共享数据,但前提是应用必须使用相同的证书进行签名。

该方案同样也离不开应用群组。同时需要借助NSUserDefauls及KVO和KVC。

仍然是以扩展进程向主应用传递数据为例,主应用需要实现以下几个步骤:

  • 创建NSUserDefaults对象,并进行监听(注意该对象需要显式的强引用)。
self.userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.m10v.hperf"];
[self.userDefaulst addObserver:self forKeyPath:@"didReceiveVideo" options:NSKeyValueObservingOptionNew context:nil];
  • 实现监听的回调
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { 
    if ([keyPath isEqualToString:@"didReceiveVideo"]){
         NSData *data = [userDefaulst objectForKey:@"didReceiveVideo”];
     } 
}

而对于扩展进程同样也只需要负责发送数据:

  • 创建NSUserDefaults对象,并使用KVC给didReceiveVideo进行赋值。
NSUserDefaults * userDefaulst = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.m10v.hperf"]; 
NSData *videoData = [NSData new]; 
[userDefaulst setObject:videoData forKey:@"didReceiveVideo"];

方案三:使用NSFileManager

该方案也需要使用到应用群组,核心思想还是通过应用群组创建共享容器来帮我祝我们在两个进程间共享数据。

以扩展进程向主APP传递数据为例,与上面方案的不同之处在于,此方案的侧重点在扩展进程。

扩展进程:

  • 创建NSFileManager将数据写入文件
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"your.app.group.identifier"];
NSURL *dataURL = [containerURL URLByAppendingPathComponent:@"recordedData.bin"];

// 将 CMSampleBuffer 数据写入文件
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
NSData *data = [NSData dataWithContentsOfURL:dataURL];
[data writeToURL:dataURL atomically:YES];

主应用:

  • 读取共享文件中的内容
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"your.app.group.identifier"];
NSURL *dataURL = [containerURL URLByAppendingPathComponent:@"recordedData.bin"];

// 从文件读取 CMSampleBuffer 数据
NSData *data = [NSData dataWithContentsOfURL:dataURL];

方案四:使用Socket

使用这种方式进行数据传递比较灵活,但实现起来也比较繁琐,首先需要两个进程之间建立好Socket链接,之后才能进行数据传递的操作。以GCDAysncSocket为例。

以扩展进程发送数据到主应用为例

主应用:

  • 属性声明
#import "GCDAsyncSocket.h"
@interface ViewController ()<GCDAsyncSocketDelegate>
@property (nonatomic, strong) GCDAsyncSocket * socket;
@property (nonatomic, strong) NSMutableArray * socketArrayM;
//消息接口队列(串行)
@property (nonatomic, strong) dispatch_queue_t queue;
@end
  • 数据初始化,创建队列,初始化socket
- (void)viewDidLoad {
    [super viewDidLoad];
    [self initData];
    [self setupSocket];
}

- (void)initData{
    self.socketArrayM = [NSMutableArray array];
    self.queue = dispatch_queue_create("com.joyme.panghu.socket", DISPATCH_QUEUE_SERIAL);
}

//MARK: 初始化socket
- (void)setupSocket{
    self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.queue];
    self.socket.IPv6Enabled = NO;
    NSError * error;
    //设置端口号
    [self.socket acceptOnPort:8999 error:&error];
    //读取数据
    [self.socket readDataWithTimeout:-1 tag:0];
    
}
  • 实现Socket的代理方法
//MARK: GCDAsyncSocketDelegate

//链接断开
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    [self.socketArrayM removeObject:sock];
}

//读取流通道关闭
- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock{
    [self.socketArrayM removeObject:sock];
}

//接收到新的链接
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
    [self.socketArrayM addObject:newSocket];
    [newSocket readDataWithTimeout:-1 tag:0];
}

//读取到数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    NSLog(@"收到:%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}

扩展进程:

扩展进程同样也需要进行类似的步骤,来创建Socket以进行连接。

  • 属性声明
#import "SampleHandler.h"
#import "GCDAsyncSocket.h"

@interface SampleHandler ()<GCDAsyncSocketDelegate>

@property (nonatomic, strong) GCDAsyncSocket * socket;
///消息接收队列
@property (nonatomic, strong) dispatch_queue_t queue;

@end
  • 初始化数据
(void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
    [self initData];
    [self setupSocket];
}

- (void)initData{
    self.queue = dispatch_queue_create("com.joyme.panghu.socket_2", DISPATCH_QUEUE_SERIAL);
}

- (void)setupSocket{
    self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.queue];
    NSError * error;
    [self.socket connectToHost:@"127.0.0.1" onPort:8999 error:&error];
    [self.socket readDataWithTimeout:-1 tag:0];
}
  • Socket代理方法
//MARK: GCDAsyncSocketDelegate
//链接成功
- (void)socket:(GCDAsyncSocket *)sock didConnectToUrl:(NSURL *)url{
    
}

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
    [self.socket readDataWithTimeout:-1 tag:0];
    [self sendReadData];
}

//写入数据成功
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    
}

//读取到数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    
}
  • 发送数据
//MARK: 发送数据
- (void)sendReadData{
    NSString * ready = @"ready";
    NSData * data = [ready dataUsingEncoding:NSUTF8StringEncoding];
    [self.socket writeData:data withTimeout:-1 tag:0];
}

建立连接成功之后,我们会在APP接收到扩展进程传递来的数据

2023-02-10 11:49:50.143508+0800 RecordDemo[7575:1395544] 收到:ready

使用Socket与上面的方法不同之处,首先不需要创建应用群组;数据传递是双向的,建立好链接后,主APP可以向扩展进程发送数据,扩展进程也可以向宿主APP发送数据。

但上面的只是一个基础的数据传递方案,想要相对完整的建立链接流程,我们还需要考虑到端口冲突的问题。实现大量数据的传递还需要其它工具来实现比如(NTESSocketPacket,数据打包,NTESTPCircularBuffer数据解包)。

结语

当涉及主APP与扩展进程之间的数据传递方案时,我们面临着多种选择,并没有一种绝对优越的方案,只有最适合特定场景的方案。在实践中,我们可能需要结合多个方案以满足不同需求。这种灵活性使得我们能够根据具体情境选择最合适的方式,从而更好地满足项目的要求。在选择方案时,务必考虑到实际需求和性能优化,以确保系统的稳定性和效率。综上所述,灵活运用不同的数据传递方案,是构建强大应用的重要一环。

文章来源:https://blog.csdn.net/weixin_39339407/article/details/135076172
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。