iOS 新思路两个不同 App之间的通信

1,724 阅读5分钟

常用通信方式总结

Universal Links、URL Scheme

Keychain 钥匙串

  • 只有 2 个 App 在同一个 Group (同一个开发者账号),这样才能够共享钥匙串

UIPasteboard 粘贴板

  • 主要痛点是,需要用户授权粘贴板,才能

 XPC 进程之间

  • 主要是针对 MacOs , iOS 和 iOS 之间还不可以,并且主要用于扩展通信 方式

同一台设备,根据设备维度唯一标识识别

根据设备维度生成:例如设备更新时间、以及系统文件夹第一次创建日期/名字、系统更新时间、设备重启时间等。

服务器:通过把生成的唯一标识上传到服务器上面、另一个 App 根据生成的标识去服务器匹配,能够匹配上就可以拉取相关的配置

本地 Socket 通信

简单陈述原理:socket 通信原理(TCP 或者 UDP),一个作为服务端、一个作为客户端,服务端监听客户端链接状态,两者链接成功后,就可以互相发送消息。(如果想要详细了解 socket 原理,可以自行 deepseek 和 豆包 支持国产)

废话不多说直接上代码

采用 CocoaAsyncSocket 第三方库实现 socket 通信 

服务端 Server代码

@interface ViewController ()<GCDAsyncSocketDelegate>

@property (nonatomic, strong) GCDAsyncSocket *serverSocket;
@property (nonatomic, strong) NSMutableArray *clientSocket;
@property (nonatomic, strong) NSTimer *heartbeatTimer;
@property (nonatomic, strong) AVAudioPlayer *audioPlayer;
@property (nonatomic, assign) BOOL isBackground;
@property (nonatomic, strong) UITextView *msgTextView;
@property (nonatomic, strong) NSMutableString *muString;

@end


-(void)initSocket{
    //初始化
    self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
    self.serverSocket.autoDisconnectOnClosedReadStream = YES;
    
    NSError *error = nil;
    if([self.serverSocket acceptOnPort:1111 error:&error]){
        // 启动心跳包定时器
        self.heartbeatTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(sendHeartbeat) userInfo:nil repeats:YES];
    }
    // 前后台通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
}

#pragma mark GCDAsyncSocketDelegate

- (void)socket:(GCDAsyncSocket *)sock didConnectToUrl:(NSURL *)url{
    NSLog(@"socket = %s",__FUNCTION__);
    NSData *data = [@"connect sucess" dataUsingEncoding:NSUTF8StringEncoding];
    [sock writeData:data withTimeout:-1 tag:0];
}

-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
    NSLog(@"socket = %s",__FUNCTION__);
    
    NSData *data = [@"connect sucess___" dataUsingEncoding:NSUTF8StringEncoding];
    [sock readDataWithTimeout:-1 tag:0];
}


-(void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
    NSLog(@"socket = %s",__FUNCTION__);
    // 注意注意,这里要保存当前链接的客户端 socket ,链接成功之后立刻回断开
    [self.clientSocket addObject:newSocket];
    newSocket.delegate = self;
    //newSocket为客户端的Socket。这里读取数据

    // 这里timeout -1 可能表示无超时限制
    [newSocket readDataWithTimeout:-1 tag:100];

    ///刚刚链接的时候,服务器已经进入到后台了,开始后台任务,保证 app 存活
    if (_isBackground) {
        [self startBackgroundTask];
    }
}

// 读取数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    NSLog(@"socket = %s",__FUNCTION__);
    //接收到数据
    NSString *receiverStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    
    receiverStr = [receiverStr stringByReplacingOccurrencesOfString:@"\r" withString:@""];
    receiverStr = [receiverStr stringByReplacingOccurrencesOfString:@"\n" withString:@""];
    
    [_muString appendString:receiverStr];
    [_muString appendString:@"\n"];
    
    self.msgTextView.text = _muString;
    [sock readDataWithTimeout:-1 tag:0];
    NSLog(@"socket_reivece = %@",receiverStr);
}


#pragma mark 服务器写数据给客户端
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    
    NSLog(@"socket = %s",__FUNCTION__);
    
    [sock readDataWithTimeout:-1 tag:0];
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    NSLog(@"socket = %s",__FUNCTION__);
}

客户端代码

@interface ViewController ()<GCDAsyncSocketDelegate>

@property (nonatomic, strong) GCDAsyncSocket *listenSocket;
@property (nonatomic, strong) NSMutableArray *clientSocket;
@property (nonatomic, strong) UITextView *msgTextView;
@property (nonatomic, strong) NSMutableString *muString;
@end

-(void)initSocket{
    // 客户端初始化
    self.listenSocket =  [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}

/// 开始链接127.0.0.1 端口 1111 服务器
-(void)start{ 
    /// 使用本地 127.0.0.1 作为 host , 端口 1111,根据自己情况修改
    NSError *error = nil;
    [self.listenSocket connectToHost:@"127.0.0.1" onPort:1111 error:&error];
    NSLog(@"error = %@",error);
    [self.listenSocket readDataWithTimeout:-1 tag:0];
}

- (void)sendHeartbeatResponse:(GCDAsyncSocket *)socket {
    NSString *responseMessage = @"HEARTBEAT_ACK";
    NSData *data = [responseMessage dataUsingEncoding:NSUTF8StringEncoding];
    [socket writeData:data withTimeout:-1 tag:0];
    NSLog(@"发送心跳回应");
}

#pragma mark - GCDAsyncSocketDelegate

- (void)socket:(GCDAsyncSocket *)sock didConnectToUrl:(NSURL *)url{
    NSLog(@"socket = %s",__FUNCTION__);
    NSData *data = [@"connect sucess" dataUsingEncoding:NSUTF8StringEncoding];
    [sock writeData:data withTimeout:-1 tag:0];
}

-(void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
    NSLog(@"socket = %s",__FUNCTION__);
    
    NSData *data = [@"connect sucess___" dataUsingEncoding:NSUTF8StringEncoding];
    [sock readDataWithTimeout:-1 tag:0];
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
    NSLog(@"socket = %s",__FUNCTION__);
    
    //接收到数据
    NSString *receiverStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    if ([receiverStr containsString:@"HEARTBEAT"]) {
        // 收到心跳包,进行回应
        [self sendHeartbeatResponse:sock];
    }
    
    receiverStr = [receiverStr stringByReplacingOccurrencesOfString:@"\r" withString:@""];
    receiverStr = [receiverStr stringByReplacingOccurrencesOfString:@"\n" withString:@""];
    
    [_muString appendString:receiverStr];
    [_muString appendString:@"\n"];
    
    self.msgTextView.text = _muString;
    
    [sock readDataWithTimeout:-1 tag:0];

    NSLog(@"socket——receive = %@ -- %ld",receiverStr, tag);
}

-(void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag{
    NSLog(@"socket = %s",__FUNCTION__);
}

-(void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
    NSLog(@"socket = %s",__FUNCTION__);
}

- (NSMutableArray *)clientSocket{
    if (!_clientSocket) {
        _clientSocket = [NSMutableArray new];
    }
    return _clientSocket;
}

- (UITextView *)msgTextView{
    if (_msgTextView == nil) {
        _msgTextView = [[UITextView alloc] init];
        _msgTextView.frame = CGRectMake(10, [UIScreen mainScreen].bounds.size.height - 200, [UIScreen mainScreen].bounds.size.width - 20, 200);
    }
    return _msgTextView;
}

image.png

遇见问题及解决

遇见问题

  1. 服务端进入后台程序就暂停运行

当客户端给服务器发送数据的时候,服务器是不能够接受到数据的,只有当服气器从后台回到前台的时候才能够正常接收到,并且会一次性把之前没有接受到的信息接受回来

  1. 开启后台任务,只能够维持 30s 的活跃状态(测试机 iPhone XS)
- (void)applicationDidEnterBackground:(UIApplication *)application {  
    [self startBackgroundTask];
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    [self stopBackgroundTask];
}

/// 在 iPhone xs 测试在 3
- (void)startBackgroundTask {
    if (self.backgroundTask == UIBackgroundTaskInvalid) {
        NSLog(@">>>>>>>>>> Start background upload task =============");
        _startTime = CACurrentMediaTime();
        self.backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
          // 如果在系统规定时间内任务还没有完成,在时间到之前会调用到这个方法,一般是10分钟
            NSLog(@"taskExpiration = %f",CACurrentMediaTime() - self->_startTime);
          [self stopBackgroundTask];
        }];
    }
}

- (void)stopBackgroundTask {
    if (self.backgroundTask != UIBackgroundTaskInvalid) {
        NSLog(@">>>>>>>>>> Stop background upload task =============");
        [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
    }
    self.backgroundTask = UIBackgroundTaskInvalid;
}
  1. 解决办法,开始后台播放模式

xcode 开启后台音频播放模式

image.png

在进入后台的时候,启动一个没有任何声音的音频播放

- (void)setupAudioSession {
    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    NSError *error;
    if ([audioSession setCategory:AVAudioSessionCategoryPlayback  withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error]) {
        if ([audioSession setActive:YES error:&error]) {
            NSLog(@"音频会话设置成功");
        } else {
            NSLog(@"激活音频会话出错: %@", error.localizedDescription);
        }
    } else {
        NSLog(@"设置音频会话类别出错: %@", error.localizedDescription);
    }
}

- (void)playAudio {
    NSURL *audioURL = [[NSBundle mainBundle] URLForResource:@"test" withExtension:@"mp3"];
    NSLog(@"audioURL = %@",audioURL);
    if (!audioURL) {
        NSLog(@"未找到音频文件");
        return;
    }
    NSError *error;
    self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:audioURL error:&error];
    if (error) {
        NSLog(@"音频播放出错: %@", error.localizedDescription);
        return;
    }
    self.audioPlayer.numberOfLoops = -1; // 设置为 -1 表示无限循环播放
    [self.audioPlayer play];
    NSLog(@"播放音频------");
}

注意 Apple 审核机制限制

有用开启了后台相关任务,例如后台播放声音,审核会根据你当前 App所在的领域,是否满足后台播放权限,例如音乐类、说书类、视频类、等符合后台播放模式。这些都是满足。其他领域根据自己的实际情况申请。