iOS App文件共享

通过UIDocumentInteractionController或者是QLPreviewController来预览PDF等格式文件的时候,我们可以通过自带的UIActivityViewController把该文件共享出去或进行打印等处理。如图所示:

*行的AirDrop是iOS7之后给用户提供的一种在苹果设备之间共享文件的快捷方式,类似于安卓上的蓝牙无线传输文件。

第二行是通过文档类型关联技术识别的App的列表。

第三行是通过文档关联技术识别的Action的列表,表示对文件可进行的一些操作,如复制,打印,保存等。

我们知道在iOS系统下有一种安全体系–沙盒机制,每个iOS应用程序都是一个独立的文件系统,并且只能在自己的文件系统进行操作,所以iOS系统下并不能像安卓一样轻松取到其他应用程序下的文件。

既然我们能在自己的应用程序下预览文件时把文件共享到其他App中,那么反过来怎么能让其他App共享文件到我们App呢?

info.plist注册文件类型
我们需要在info.plist文件中,添加一个新的属性CFBundleDocumentTypes(实际上输入的是Document type),这是一个数组类型的属性,意思就是我们可以同时注册多个类型。而针对数组中的每一个元素,都有许多属性可以指定,详细的属性列表我们可以从官方文档上找到: Core Foundation Keys —- CFBundleDocumentTypes。这里列举我们在做iOS开发时常用的属性:

CFBundleTypeName

字符串类型,指定某种类型的别名,也就是用来指代我们规定的类型的别称,一般为了保持唯一性,我们使用UTI来标识。

CFBundleTypeIconFiles
数组类型,包含指定的png图标的文件名,指定代表某种类型的图标,而图标有具体的尺寸标识:

Device Sizes
iPad 64 x 64 pixels, 320 x 320 pixels
iPhone and iPod touch 22 x 29 pixels, 44 x 58 pixels (high resolution)
* LSItemContentTypes

数组类型,包含UTI字符串,指定我们的应用程序所有可以识别的文件类型集合
* LSHandlerRank
字符串类型,包含Owner,Default,Alternate,None四个可选值,指定对于某种类型的优先权级别,而Launcher Service会根据这个优先级别来排列显示的App的顺序。优先级别从高到低依次是Owner,Alternate,Default。None表示不接受这种类型。

我们选择Source code方式打开info.plist文件添加以下代码:

1 <key>CFBundleDocumentTypes</key>
2     <array>
3         <dict>
4             <key>CFBundleTypeName</key>
5             <string>PDF</string>
6             <key>LSHandlerRank</key>
7             <string>Owner</string>
8             <key>LSItemContentTypes</key>
9             <array>
10                 <string>com.adobe.pdf</string>
11             </array>
12         </dict>
13         <dict>
14             <key>CFBundleTypeName</key>
15             <string>Microsoft Word</string>
16             <key>LSHandlerRank</key>
17             <string>Alternate</string>
18             <key>LSItemContentTypes</key>
19             <array>
20                 <string>com.microsoft.word.doc</string>
21                 <string>com.microsoft.word.wordml</string>
22                 <string>org.openxmlformats.wordprocessingml.document</string>
23             </array>
24         </dict>
25         <dict>
26             <key>CFBundleTypeName</key>
27             <string>Microsoft Excel</string>
28             <key>LSHandlerRank</key>
29             <string>Alternate</string>
30             <key>LSItemContentTypes</key>
31             <array>
32                 <string>com.microsoft.excel.xls</string>
33                 <string>org.openxmlformats.spreadsheetml.sheet</string>
34             </array>
35         </dict>
36         <dict>
37             <key>CFBundleTypeIconFiles</key>
38             <array/>
39             <key>CFBundleTypeName</key>
40             <string>Microsoft PowerPoint</string>
41             <key>LSHandlerRank</key>
42             <string>Alternate</string>
43             <key>LSItemContentTypes</key>
44             <array>
45                 <string>com.microsoft.powerpoint.​ppt</string>
46                 <string>org.openxmlformats.presentationml.presentation</string>
47                 <string>public.presentation</string>
48             </array>
49         </dict>
50         <dict>
51             <key>CFBundleTypeName</key>
52             <string>Text</string>
53             <key>LSHandlerRank</key>
54             <string>Alternate</string>
55             <key>LSItemContentTypes</key>
56             <array>
57                 <string>public.text</string>
58                 <string>public.plain-text</string>
59                 <string>public.utf8-plain-text</string>
60                 <string>public.utf16-external-plain-​text</string>
61                 <string>public.utf16-plain-text</string>
62                 <string>com.apple.traditional-mac-​plain-text</string>
63                 <string>public.source-code</string>
64                 <string>public.c-source</string>
65                 <string>public.objective-c-source</string>
66                 <string>public.c-plus-plus-source</string>
67                 <string>public.objective-c-plus-​plus-source</string>
68                 <string>public.c-header</string>
69                 <string>public.c-plus-plus-header</string>
70                 <string>com.sun.java-source</string>
71                 <string>public.script</string>
72                 <string>public.shell-script</string>
73             </array>
74         </dict>
75         <dict>
76             <key>CFBundleTypeName</key>
77             <string>Rich Text</string>
78             <key>LSHandlerRank</key>
79             <string>Alternate</string>
80             <key>LSItemContentTypes</key>
81             <array>
82                 <string>public.rtf</string>
83                 <string>com.apple.rtfd</string>
84                 <string>com.apple.flat-rtfd</string>
85             </array>
86         </dict>
87         <dict>
88             <key>CFBundleTypeName</key>
89             <string>HTML</string>
90             <key>LSHandlerRank</key>
91             <string>Alternate</string>
92             <key>LSItemContentTypes</key>
93             <array>
94                 <string>public.html</string>
95                 <string>public.xhtml</string>
96             </array>
97         </dict>
98         <dict>
99             <key>CFBundleTypeName</key>
100             <string>Web Archive</string>
101             <key>LSHandlerRank</key>
102             <string>Alternate</string>
103             <key>LSItemContentTypes</key>
104             <array>
105                 <string>com.apple.webarchive</string>
106             </array>
107         </dict>
108         <dict>
109             <key>CFBundleTypeName</key>
110             <string>Image</string>
111             <key>LSHandlerRank</key>
112             <string>Alternate</string>
113             <key>LSItemContentTypes</key>
114             <array>
115                 <string>public.image</string>
116             </array>
117         </dict>
118         <dict>
119             <key>CFBundleTypeName</key>
120             <string>iWork Pages</string>
121             <key>LSHandlerRank</key>
122             <string>Alternate</string>
123             <key>LSItemContentTypes</key>
124             <array>
125                 <string>com.apple.page.pages</string>
126                 <string>com.apple.iwork.pages.pages</string>
127                 <string>com.apple.iwork.pages.template</string>
128             </array>
129         </dict>
130         <dict>
131             <key>CFBundleTypeName</key>
132             <string>iWork Numbers</string>
133             <key>LSHandlerRank</key>
134             <string>Alternate</string>
135             <key>LSItemContentTypes</key>
136             <array>
137                 <string>com.apple.numbers.numbers</string>
138                 <string>com.apple.iwork.numbers.numbers</string>
139                 <string>com.apple.iwork.numbers.template</string>
140             </array>
141         </dict>
142         <dict>
143             <key>CFBundleTypeName</key>
144             <string>iWork Keynote</string>
145             <key>LSHandlerRank</key>
146             <string>Alternate</string>
147             <key>LSItemContentTypes</key>
148             <array>
149                 <string>com.apple.keynote.key</string>
150                 <string>com.apple.iwork.keynote.key</string>
151                 <string>com.apple.iwork.keynote.kth</string>
152             </array>
153         </dict>
154         <dict>
155             <key>CFBundleTypeName</key>
156             <string>Audio</string>
157             <key>LSHandlerRank</key>
158             <string>Alternate</string>
159             <key>LSItemContentTypes</key>
160             <array>
161                 <string>public.audio</string>
162             </array>
163         </dict>
164         <dict>
165             <key>CFBundleTypeName</key>
166             <string>Movie</string>
167             <key>LSHandlerRank</key>
168             <string>Alternate</string>
169             <key>LSItemContentTypes</key>
170             <array>
171                 <string>public.movie</string>
172             </array>
173         </dict>
174         <dict>
175             <key>CFBundleTypeName</key>
176             <string>Archive</string>
177             <key>LSHandlerRank</key>
178             <string>Alternate</string>
179             <key>LSItemContentTypes</key>
180             <array>
181                 <string>public.archive</string>
182             </array>
183         </dict>
184     </array>

添加这些代码,你的App就可以支持大部分文件类型了,可以根据自己项目的需求,添加相关类型的代码,像我自己的项目只需要支持PDF和word格式的文件。添加完这些代码,我们选择Property list打开info.plist文件:

或者在info页面打开Document types列表

这个时候就代表我们已经成功的注册好了App支持的文件类型,这个时候我们在编译运行,然后再到其他App(我这边用的QQ)打开下载好的文件,这个时候出来的页面是这样的:

我们可以看到自己的App图标已经出现在第二栏的列表中,这个时候我们点击图标按钮即可把文件共享到自己App中。

如何处理共享文件
当点击图标按钮的时候,会跳转到我们自己的应用程序中,这个时候在AppDelegate.m会走- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options该回调方法。

但是在iOS9之前回调的是- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation,所以我们需要针对不同的设备版本做出改变。

我们可以在回调方法里进行文件处理操作,如将文件上传、文件预览、文件保存一些工作。在做文件预览的时候我们必定得跳转到对应的控制器中,这个时候我们首先得获取到当前的视图控制器

1 //获取当前屏幕显示的viewcontroller
2 – (UIViewController *)getCurrentVC
3 {
4     UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
5
6     UIViewController *currentVC = [self getCurrentVCFrom:rootViewController];
7
8     return currentVC;
9 }
10
11 – (UIViewController *)getCurrentVCFrom:(UIViewController *)rootVC
12 {
13     UIViewController *currentVC;
14
15     if ([rootVC presentedViewController]) {
16         // 视图是被presented出来的
17
18         rootVC = [rootVC presentedViewController];
19     }
20
21     if ([rootVC isKindOfClass:[UITabBarController class]]) {
22         // 根视图为UITabBarController
23
24         currentVC = [self getCurrentVCFrom:[(UITabBarController *)rootVC selectedViewController]];
25
26     } else if ([rootVC isKindOfClass:[UINavigationController class]]){
27         // 根视图为UINavigationController
28
29         currentVC = [self getCurrentVCFrom:[(UINavigationController *)rootVC visibleViewController]];
30
31     } else {
32         // 根视图为非导航类
33
34         currentVC = rootVC;
35     }
36
37     return currentVC;
38 }

拿到控制器我们可以回到回调方法里进行跳转工作,我这边还是用UIDocumentInteractionController做文件预览

1 #if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_9_0
2 – (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation{
3     // 判断传过来的url是否为文件类型
4     if ([url.scheme isEqualToString:@”file”]) {
5         _docVc = [UIDocumentInteractionController interactionControllerWithURL:url];
6         _docVc.delegate = self;
7         [_docVc presentPreviewAnimated:YES];
8
9     }
10
11 }
12
13 #else
14 – (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options{
15     // 判断传过来的url是否为文件类型
16     if ([url.scheme isEqualToString:@”file”]) {
17         _docVc = [UIDocumentInteractionController interactionControllerWithURL:url];
18         _docVc.delegate = self;
19         [_docVc presentPreviewAnimated:YES];
20     }
21     return YES;
22 }
23 #endif
24
25 #pragma mark — UIDocumentInteractionControllerDelegate
26 – (UIViewController *)documentInteractionControllerViewControllerForPreview:(UIDocumentInteractionController *)controller
27 {
28     // 返回当前控制器
29     return [self getCurrentVC];
30 }

 

iOS App后台保活

笔者查询了相关资料后发现,iOS App可以实现后台保活。

短时间保活的方式有beginBackgroundTaskWithName;

App长时间保活的方式有:播放无声音乐、后台持续定位、后台下载资源、BGTaskScheduler等;

唤醒App的方式有:推送、VoIP等;

导读

本文分为如下几部分:

App 运行状态、及状态变化

App 后台保活方式简介

短时间App后台保活

Background Modes AVAudio,AirPlay,and Picture in Picture

Background Modes Location updates

BGTaskScheduler (iOS13.0+)

1

App 运行状态、及状态变化

不低于iOS13.0的设备端App 运行状态

%title插图%num

不低于iOS13.0设备端App 运行状态

iOS13.0+的设备,支持多场景,共有上图中的Unattached、Foreground Inactive、Foreground Active、Forground Inactive、Background、Suspended 6种状态。

1、Unattached:多个场景的情况,如果创建的场景不是当前显示的场景,那么场景处于Unattached状态;

2、Foreground Inactive:应用启动后,显示启动图的过程中,处于Foreground Inactive状态;

3、Forground Active:应用启动后,显示出来我们设置的rootViewController之后,场景处于Forground Active;

4、Foreground Inactive:应用启动后,场景处于显示状态,数据加载完毕,且用户和App没有交互过程中,处于Forground Inactive状态;

5、Background:用户点击Home键、或者是切换App后、锁屏后,应用进入Background状态;

6、Suspended:进入Background后,应用的代码不执行后,应用进入Suspended状态;(代码是否在运行,可以在应用中写定时器,定时输出内容,从Xcode控制台,或Mac端控制台查看是否有输出内容来判断)

低于iOS13.0的设备端App 运行状态

%title插图%num

低于iOS13.0设备端App 运行状态

上图是低于iOS13.0的设备端App的运行状态,分别是Not Running、Foreground Inactive、Foreground Active、Forground Inactive、Background、Suspended 6种状态。

Not Running:指用户没有启动App,或用户Terminate App 后,App处于的状态;其他的五种状态和不低于iOS13.0的设备端App的运行状态意义相同。

App 进入后台状态变化

笔者写了个定时器,定时输出“普通定时器进行中”,可以看到,应用进入后台后,基本上立刻,就没有内容输出了。笔者认为可以认为此时App 已经进入Suspended的状态。

%title插图%num

App 进入后台

下边笔者介绍下,尝试的App后台保活方式。

2

iOS App 后台保活方式简介

短时间App后台保活

beginBackgroundTaskWithName 和 endBackgroundTask

笔者尝试过使用相关API,测试过2款手机。

对于系统版本低于iOS13(iOS 12.3)的设备(iPhone6 Plus)后台运行时间约3分钟(175秒);

对于系统版本不低于iOS13(

iOS 13.0)的设备(iPhone6 Plus)后台运行时间约31秒;

播放无声音乐

App 进入后台后,播放无声音乐,适用于音视频类App。

笔者对逆向不了解,从iOS项目技术还债之路《一》后台下载趟坑中得知,腾讯视频、爱奇艺采用了播放无声音乐保活的方式。

后台持续定位

对于定位类App,持续定位App,可以实现App后台保活。定位类App需要后台保活,像系统的地图应用,在导航的时候切换App的时候,就需要后台保活。

后台下载资源

对于需要下载资源的App,需要后台下载资源,如我们在某App下载资源的时候,我们希望在切换App时候,或者App退出后台后,资源仍然继续下载,这样当我们打开App的时候,资源已经下载好了。

BackgroundTasks

BackgroundTasks.framework 是iOS13新增的framework,笔者认为此framework中的API可以在信息流类的App中发挥作用。

3

短时间App后台保活

系统版本低于iOS13.0的设备

系统版本低于iOS13.0的设备,在应用进入后台的时候,开始后台任务([[UIApplication sharedApplication] beginBackgroundTaskWithName:)。在应用进入前台时或后台任务快过期的回调中,终止后台任务([[UIApplication sharedApplication] endBackgroundTask:)。

示例代码如下:

– (void)applicationDidEnterBackground:(UIApplication *)application {

self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:kBgTaskName expirationHandler:^{
if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}
}];
}
– (void)applicationWillEnterForeground:(UIApplication *)application {

[[UIApplication sharedApplication] endBackgroundTask: self.backgroundTaskIdentifier];
}
添加相关代码后,笔者在iOS12.4的6 Plus上测试结果如下,应用在进入后台后,大概还运行了175秒。

2019-12-29 19:06:55.647288+0800 QiAppRunInBackground[1481:409744] -[AppDelegate applicationDidEnterBackground:]:应用进入后台DidEnterBackground
2019-12-29 19:06:56.256877+0800 QiAppRunInBackground[1481:409744] 定时器运行中
….
2019-12-29 19:09:50.812460+0800 QiAppRunInBackground[1481:409744] 定时器运行中
系统版本不低于iOS13.0的设备

– (void)sceneDidEnterBackground:(UIScene *)scene API_AVAILABLE(ios(13.0)){

self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:kBgTaskName expirationHandler:^{
if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}
}];
}
– (void)sceneWillEnterForeground:(UIScene *)scene API_AVAILABLE(ios(13.0)){

[[UIApplication sharedApplication] endBackgroundTask: self.backgroundTaskIdentifier];
}
添加相关代码后,笔者在iOS13.0的6s上测试结果如下,应用在进入后台后,大概还运行了31秒。
%title插图%num

iOS13.0+ App 进入后台

Xs·H 提到过,如果持续后台播放无声音频或是使用后台持续定位的方式实现iOS App后台保活,会浪费电量,浪费CPU,所以一般情况下,使用这种短时间延长App 后台保活的方式,应该够开发者做需要的操作了。

4

Background Modes AVAudio,AirPlay,and Picture in Picture

对于音视频类App,如果需要后台保活App,在App 进入后台后,可以考虑先使用短时间保活App的方式,如果后台保活App方式快结束后,还没处理事情,那么可以考虑使用后台播放无声音乐。相关示例代码如下。

– (AVAudioPlayer *)player {

if (!_player) {
NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@”SomethingJustLikeThis” withExtension:@”mp3″];
AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil];
audioPlayer.numberOfLoops = NSUIntegerMax;
_player = audioPlayer;
}
return _player;
}
[self.player prepareToPlay];
系统版本低于iOS13.0的设备

– (void)applicationDidEnterBackground:(UIApplication *)application {

NSLog(@”%s:应用进入后台DidEnterBackground”, __FUNCTION__);
self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:kBgTaskName expirationHandler:^{

if ([QiAudioPlayer sharedInstance].needRunInBackground) {
[[QiAudioPlayer sharedInstance].player play];
}
if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}
}];
}
– (void)applicationWillEnterForeground:(UIApplication *)application {

NSLog(@”%s:应用将进入前台WillEnterForeground”, __FUNCTION__);
if ([QiAudioPlayer sharedInstance].needRunInBackground) {
[[QiAudioPlayer sharedInstance].player pause];
}
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundTaskIdentifier];
}
系统版本不低于iOS13.0的设备

– (void)sceneDidEnterBackground:(UIScene *)scene API_AVAILABLE(ios(13.0)){

NSLog(@”%s:应用已进入后台DidEnterBackground”, __FUNCTION__);

self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithName:kBgTaskName expirationHandler:^{
if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
if ([QiAudioPlayer sharedInstance].needRunInBackground) {
[[QiAudioPlayer sharedInstance].player play];
}
NSLog(@”终止后台任务”);
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}
}];
}
– (void)sceneWillEnterForeground:(UIScene *)scene API_AVAILABLE(ios(13.0)){

if ([QiAudioPlayer sharedInstance].needRunInBackground) {
[[QiAudioPlayer sharedInstance].player pause];
}
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundTaskIdentifier];
NSLog(@”%s:应用将进入前台WillEnterForeground”, __FUNCTION__);
}
5

Background Modes Location updates

开启后台定位持续更新配置,添加了位置隐私申请后,在应用使用持续定位的情况下,可以实现后台保活App。

%title插图%num

添加后台获取位置及音频使用能力

%title插图%num
添加获取位置隐私申请

对于定位类App,如果需要后台保活App,在用户使用了定位功能后,App 进入后台后,App自动具备后台保活能力,部分示例代码如下。

self.locationManager = [CLLocationManager new];
self.locationManager.delegate = self;
[self.locationManager requestAlwaysAuthorization];
@try {
self.locationManager.allowsBackgroundLocationUpdates = YES;
} @catch (NSException *exception) {
NSLog(@”异常:%@”, exception);
} @finally {

}
[self.locationManager startUpdatingLocation];
如果遇到如下异常信息:

2019-12-29 19:57:46.481218+0800 QiAppRunInBackground[1218:141397] 异常:Invalid parameter not satisfying: !stayUp || CLClientIsBackgroundable(internal->fClient) || _CFMZEnabled()
检查:

Signing&Capablities 的 backgounrd Modes 中 Location updates是否勾选;

后台下载资源

当需要实现下载资源类的App在进入后台后,持续下载资源的需求时。我们可能需要使用后台如下示例示例代码。

创建指定标识的后台NSURLSessionConfiguration,配置好

NSURL *url = [NSURL URLWithString:@”https://images.pexels.com/photos/3225517/pexels-photo-3225517.jpeg”];
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@”com.qishare.ios.wyw.backgroundDownloadTask”];
// 低于iOS13.0设备资源下载完后 可以得到通知 AppDelegate.m 文件中的 – (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
// iOS13.0+的设备资源下载完后 直接在下载结束的代理方法中会有回调
sessionConfig.sessionSendsLaunchEvents = YES;
// 当传输大数据量数据的时候,建议将此属性设置为YES,这样系统可以安排对设备而言*佳的传输时间。例如,系统可能会延迟传输大文件,直到设备连接充电器并通过Wi-Fi连接到网络为止。此属性的默认值为NO。
sessionConfig.discretionary = YES;
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:url];
[downloadTask resume];
6

BGTaskScheduler(iOS13.0+)

如果我们的App是信息流类App,那么我们可能会使用到BGTaskScheduler.framework中的API,实现后台保活App,帮助用户较早地获取到较新信息。

笔者尝试使用BGTaskScheduler 做了一个获取到App调度的时候。更新首页按钮颜色为随机色并且记录调度时间的Demo。

项目配置

为了App 支持 BGTaskScheduler,需要在项目中配置Background fetch,及Background Processing;

需要在Info.plist文件中添加 key 为Permitted background task scheduler identifiers,Value为数组的内容。

Value的数组填写,刷新的任务标识和清理的任务标识。

注册后台任务

在应用启动后,注册后台任务。

– (void)registerBgTask {

if (@available(iOS 13.0, *)) {
BOOL registerFlag = [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:kRefreshTaskId usingQueue:nil launchHandler:^(__kindof BGTask * _Nonnull task) {
[self handleAppRefresh:task];
}];
if (registerFlag) {
NSLog(@”注册成功”);
} else {
NSLog(@”注册失败”);
}
} else {
// Fallback on earlier versions
}

if (@available(iOS 13.0, *)) {
[[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:kCleanTaskId usingQueue:nil launchHandler:^(__kindof BGTask * _Nonnull task) {
[self handleAppRefresh:task];
}];
} else {
// Fallback on earlier versions
}
}
调度App 刷新

应用进入后台后,调度App 刷新。

– (void)sceneDidEnterBackground:(UIScene *)scene API_AVAILABLE(ios(13.0)){

[self scheduleAppRefresh];
}

– (void)scheduleAppRefresh {

if (@available(iOS 13.0, *)) {
BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:kRefreshTaskId];
// *早15分钟后启动后台任务请求
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15.0 * 60];
NSError *error = nil;
[[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error];
if (error) {
NSLog(@”错误信息:%@”, error);
}

} else {
// Fallback on earlier versions
}
}
得到后台任务调度的时候,调用App刷新的方法,笔者在这个方法中做了发送更新首页按钮颜色的通知,并且记录了当前更新时间的记录。

– (void)handleAppRefresh:(BGAppRefreshTask *)appRefreshTask API_AVAILABLE(ios(13.0)){

[self scheduleAppRefresh];

NSLog(@”App刷新====================================================================”);
NSOperationQueue *queue = [NSOperationQueue new];
queue.maxConcurrentOperationCount = 1;

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{

[[NSNotificationCenter defaultCenter] postNotificationName:AppViewControllerRefreshNotificationName object:nil];

NSLog(@”操作”);
NSDate *date = [NSDate date];
NSDateFormatter *dateFormatter = [NSDateFormatter new];
[dateFormatter setDateFormat:@”yyyy-MM-dd HH:mm”];
NSString *timeString = [dateFormatter stringFromDate:date];

NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@”QiLog.txt”];
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSData *data = [timeString dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:data attributes:nil];
} else {
NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
NSString *originalContent = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSString *content = [originalContent stringByAppendingString:[NSString stringWithFormat:@”\n时间:%@\n”, timeString]];
data = [content dataUsingEncoding:NSUTF8StringEncoding];
[data writeToFile:filePath atomically:YES];
}
}];

appRefreshTask.expirationHandler = ^{
[queue cancelAllOperations];
};
[queue addOperation:operation];

__weak NSBlockOperation *weakOperation = operation;
operation.completionBlock = ^{
[appRefreshTask setTaskCompletedWithSuccess:!weakOperation.isCancelled];
};
}
经过测试,发现App 在退到后台,没有手动Terminate App的情况下。苹果有调用过App调度任务的方法。现象上来看就是隔一段时间,我们再打开App 的时候可以发现,首页的按钮颜色改变了,相应的日志中追加了,调起相关方法的时间记录。

手动触发后台任务调度

Xcode运行我们的App

-> App 退到后台

-> 打开App 进入前台

-> 点击下图中蓝框中的Pause program execution,输入如下内容

后台模拟调起App

e -l objc — (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier: @”com.qishare.ios.wyw.background.refresh”]
-> 再次点击Continue program execution,就可以模拟后台启动任务,调用我们的App。

%title插图%num

Continue program execution

查看日志记录小提示

之前记得听沐灵洛提过怎么便于查看日志,正好我这里也用到了。便于我们可以直接在File App中查看写入到我们App的Documents中的文件,可以在Info.plist文件中添加key为LSSupportsOpeningDocumentsInPlace ,value为YES的键值对App 接入 iOS 11 的 Files App。

经过我们操作后,就可以打开File App -> 浏览 -> 我的iPhone -> 查看选择我们的App -> 查看我们的日志记录文件。