常用通信方式总结
Universal Links、URL Scheme
-
Universal Links:需要在服务器根目录下配置apple-app-site-association文件,服务器必须支持 https. 例如:https://自己域名.com/.well-known/apple-app-site-association
-
URL Scheme:需要在 Xcode info.plist 配置好 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;
}
遇见问题及解决
遇见问题
- 服务端进入后台程序就暂停运行
当客户端给服务器发送数据的时候,服务器是不能够接受到数据的,只有当服气器从后台回到前台的时候才能够正常接收到,并且会一次性把之前没有接受到的信息接受回来
- 开启后台任务,只能够维持 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;
}
- 解决办法,开始后台播放模式
xcode 开启后台音频播放模式
在进入后台的时候,启动一个没有任何声音的音频播放
- (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所在的领域,是否满足后台播放权限,例如音乐类、说书类、视频类、等符合后台播放模式。这些都是满足。其他领域根据自己的实际情况申请。