需求说明:
一个web页面,需要嵌入到App进行文件上传,但是App在一段时间就会息屏,息屏类似于进入后台,会停止App的一些活动。上传大型文件时需要很长时间,经常息屏,所以看下App能不能实现在不改动web代码的前提下让web在后台上传文件。
问题解决的思路。
- 首先是让App进入后台不被挂起(杀死),可以在后台进行网络请求。
- 拦截需要上传的文件,再拦截web的文件上传的链接(网络请求拦截),App在本地进行文件上传,上传成功之后告诉网页上传成功了。
- 补充方法,让屏幕保持常亮
[[UIApplication sharedApplication] setIdleTimerDisabled: YES]
不过要记得在dealloc
中设置为 NO.
在iOS16下,即使App一直在后台存活,WKWebView会停止网络请求,所以必须进行拦截,然后App进行文件上传操作,然后及时返回给网页,这样就可以保证WKWebView的网络请求存活.
在iOS17beta3中,不进行拦截的情况下,只要保证App在后台存活,WKWebView可以进行网络请求。后续再iOS17正式版发布之后在进行测试是否真的支持。
拦截后文件上传有两种方案,
方案一 :
由于拦截到的文件formData丢失,但是可以拦截到上传的 range,也就是文件范围,App对文件进行分段,然后自己组装formData数据进行实时上传。
截取部分文件代码
ini
NSFileHandle * readFileHandle;
//offset偏移量 size长度
[readFileHandle seekToFileOffset:offset];
NSData * fileFragmentData = [fileModel.readFileHandle readDataOfLength:size];
方案二 :
App通过AFNetworking进行文件上传,并通知记录上传进度,在拦截到网页的接口时获取网页当前进度,如果App进度超过网页进度,那么直接返回网页,告诉他当前已经上传成功,形成一个"伪同步"。
例如一个文件10M,通过表单分成10段上传, App如果网速很快,1秒上传了8M, 那么在拦截的网页中,前八次请求都返回成功。 第九次上传,进入隧道,App信号不好了。上传了20秒,那么拦截到web第九次请求就一直等待,直到App请求成功,在进行web接口的响应。
| App | web |
|--------|-----|---------------|
| 5%-10% | 1% | 那么1-5直接告诉他成功, |
需要解决的技术点
问题一:如何让App在一直在后台存活。
解决:通过让App在后台播放无声音乐的方式可以做到让App理论上的无限存活。 问题二:怎么拦截要上传的文件。 解决:使用Runtime方法交换,Hook到上传的文件。 问题三:怎么拦截文件上传链接(网络请求)。 解决:使用WKURLSchemeHandler
下面详细介绍这些问题的解决过程。
一、如何让App在一直在后台存活。
iOS7以后提供的后台接口模式
1、Background Audio,这是后台的音频,类似于各种音乐播放器。
2、Location Services,这是后台的定位,类似于各种地图导航应用。
3、VoIP,后台语音服务,类似于各种聊天软件。
4、Newsstand,报刊杂志后台自动下载更新,其能够自动实时更新。
我选择的是第一种,可以做到无感知。简而言之就是在后台循环播放一段无声地音乐。
代码:
objectivec
/// 创建音乐播放器
- (**void**)creatAVAudioSessionObject{
//设置后台模式和锁屏模式下依然能够播放
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:**nil**];
[[AVAudioSession sharedInstance] setActive: **YES** error: **nil**];
//初始化音频播放器
NSError *playerError;
NSURL *urlSound = [[NSURL alloc]initWithString:[[NSBundle mainBundle]pathForResource:@"laojie" ofType:@"mp3"]];
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:urlSound error:&playerError];
_audioPlayer.numberOfLoops = -1;//无限播放
_audioPlayer.volume = 0;
}
/// 开始播放声音
- (**void**)startPlayAudioSession{
**BOOL** isPlay = [_audioPlayer play];
NSLog(@"isPlay===%id", isPlay);
}
/// 停止播放声音
- (**void**)stopPlayAudioSession{
[_audioPlayer stop];
}
使用方式。
首先,打开后台播放音乐开关。
1.在didFinishLaunchingWithOptions
中 调用 creatAVAudioSessionObject
2.在 applicationDidEnterBackground
中 调用 startPlayAudioSession
3.在 applicationWillEnterForeground
中 调用 stopPlayAudioSession
进入后台时,可以写一个倒计时,通过下面代码后台剩余活跃时间。
[UIApplication sharedApplication].backgroundTimeRemaining
怎么拦截(Hook)要上传的文件
添加分类方法,在内部通过Runtime更换掉系统的获取文件的方法。从而拦截从文件系统/相机相册获取的文件。
具体代码看文件:
拦截UIImagePickerController
拦截UIDocumentPickerViewController
拦截PHPickerViewController(iOS14之后从文件管理里面获取文件使用这个)。
使用方式:
1、在viewDidLoad
中 hookDelegate
例如 [UIImagePickerController hookDelegate] 2、在dealloc
中 unHookDelegate
由于文件过大,所以文件管理使用了 NSFileHandle
。
怎么拦截文件上传链接(网络请求)
iOS11之后系统提供了:WKURLSchemeHandler
来拦截WKWenView中的网络请求。
首先创建一个对象遵守WKURLSchemeHandler
代理,用于进行请求的拦截处理。
.h代码查看
.m代码查看
使用方式
ini
WKWebViewConfiguration * con = [[WKWebViewConfiguration alloc] init];
con.allowsInlineMediaPlayback = YES;
con.allowsPictureInPictureMediaPlayback = YES;
GSKHFURLSchemeHandler *Scheme = [GSKHFURLSchemeHandler new];
[con setURLSchemeHandler:Scheme forURLScheme:@"https"];
_webview = [[WKWebView alloc] initWithFrame:CGRectZero configuration:con];
这里拦截 https
会 crash,因为系统不支持,所以还需要做一下处理。
给WkWebView添加一个分类,在分类里面通过Runtime,把handlesURLScheme
替换成自己的方法,对拦截的https
进行单独的处理。
代码如下:
less
#import "WKWebView+SchemeHandle.h"
#import <objc/runtime.h>
@implementation WKWebView (SchemeHandle)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod1 = class_getClassMethod(self, @selector(handlesURLScheme:));
Method swizzledMethod1 = class_getClassMethod(self, @selector(yyhandlesURLScheme:));
method_exchangeImplementations(originalMethod1, swizzledMethod1);
});
}
+ (BOOL)yyhandlesURLScheme:(NSString *)urlScheme {
if ([urlScheme isEqualToString:@"http"] || [urlScheme isEqualToString:@"https"] || [urlScheme isEqualToString:@"file"]) {
return NO; //这里让返回NO,应该是默认不走系统断言或者其他判断啥的
} else {
return [self handlesURLScheme:urlScheme];
}
}
@end
到这里就基本结束了。