最近需要制作一个能够后台长期运行的移动应用。该应用需要调用摄像头周期性捕获数据,然后对数据处理过后,实时反馈结果。支持android和ios平台。
主要有下面几点:
1、摄像头实时捕获
2、能够适配多款不同机型的处理算法
3、能在后台以服务形式常驻运行,不影响用户使用其他应用
4、根据数据处理结果,给用户提醒,通常用户这时在使用其他应用
在安卓平台上,已经通过多款不同型号的手机,验证了方案与算法,包括用户易用性方面也进行了一些界面设计与调整。
那么接下来,理论上,ios采用一样的方案,就能够实现。
实际摸下来的结果,1和2很容易实现,3和4在ios上,apple有很大的限制,要遵守隐私和用户体验协议。
对于第四点,android采用系统级弹窗,可以在其他应用上方弹出悬浮窗,给出提醒。
但是在iOS平台上,由于Apple的限制,一个应用无法直接在其他应用上方弹出悬浮窗(也被称为悬浮窗或者覆盖层)。这种操作属于对用户体验的侵犯,Apple在设计iOS时明确规定应用之间不能有重叠的UI元素。
这里可以采用一个替代的方式,就是用声音提醒,录制几个小的音频文件就可以。
对于第三点,后台运行应用,我做了很多尝试,都不成功。下面记录下过程。
1、网上搜索的后台方案,很多误导
我一开始主要采用百度搜索的方式,结果发现产生了很大的误导。
无论是百度还是某些AI,都给出了可行的方案,但实践下来都行不通。
筛去不完整、有错误的,主要是两种方式,一在ViewController里修改,二在AppDelegate里修改
首先都要设置Background Modes,根据不同应用场景,选择不同模式,要用到视频和音频,所以设置的是Audio, AirPlay, and Picture in Picture。设置后,info会自动刷新,不用再修改info。
1)在ViewController直接注册通知消息,来启动后台接口。有一点效果的方法,是采用beginBackgroundTask和endBackgroundTask,可以启动有限实践的后台任务。达不到需求。
2)不少文章给出在AppDelegate里修改,实测下来发现applicationDidEnterBackground接口进不去,没有打印。后来发现原因是方案太旧了,早就过时了。
performFetchWithCompletionHandler 是旧的应用程序,ios13不再调用,现在都已经ios18了
在iOS 10 后 applicationDidEnterBackground和applicationWillEnterForeground已经被SceneDelegate给接管了。
所以关于前台后台等的处理操作,不应该写在AppDelegate里。
理论上把AppDelegate的接口,转到SceneDelegate,应该也能打通后台流程。后来找了下官方的示例,给出的也是AppDelegate相关的。
3)如果要实现长时间的后台任务。
有个说法是启动一个无线时长的无声音频,可以让程序持续保活,但是被打断就会失效。这个方法我没有去试验,因为要用到声音提醒,同时其他应用也会使用音频。
4)比较新的后台框架 BGAppRefreshTask和BGProcessingTask,通过后台schegual的方式,起到类似周期性刷新数据的效果,可以理解为这个模仿的限制性少一点,当然对应的Background Modes是Background Fetch。
这个框架主要参考了官方示例。见下文。
2、官方文档也很抽象
网上文章比较乱,那么找官方的支持总应该可以吧。实践下来也是很多坑。
从apple官方文档里,找到一个比较新的后台任务框架:
BGAppRefreshTask和BGProcessingTask
参考官方给的样例ColorFeed。
移除SceneDelegate
首先官方给的工程里是没有SceneDelegate的,而我使用xcode创建的工程默认有scene文件。比对了info文件的差异。我就想是不是可以移除,删除SceneDelegate文件,删除info里的权限Application Scene Manifest键值对,删除AppDelegate里UISceneSession Lifecycle下的接口。
参考下面文章:
(Objective-C) SceneDelegate使用及移除注意事项和分屏_ios 删除scenedelegate-CSDN博客
有一点注意,删除后运行可能报错无入口,需要在AppDelegate里,增加var window: UIWindow?,虽然代码里看不出哪里在使用。
裁剪方案
官方这个案例的后台任务框架部分很简单,但是还自定义了一些自定义的比较复杂的数据结构,比如Mocks、Operations、PersistentContainer,这些我的应用不需要,也用不着数据库,所以最好是剪裁一下。
为此我又找了两篇网上的文章作参照:
参考第一篇文章,去掉无用的对象,写了一套简化实现,如下:
Swift
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let view = UIView()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
setupAudioSession()
// MARK: Registering Launch Handlers for Tasks
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.background.faceDistance.refresh", using: nil) { task in // Downcast the parameter to an app refresh task as this identifier is used for a refresh request.
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true
}
func handleAppRefresh(task: BGAppRefreshTask) {
scheduleAppRefresh()
print("任务触发")
// setupAudioSession()
CameraManager.shared.stopCamera()
CameraManager.shared.setupCamera(forgroundFlag: false, view: view)
DispatchQueue.global(qos: .background).async {
}
task.expirationHandler = {
// 任务时间结束调用
CameraManager.shared.stopCamera()
task.setTaskCompleted(success: false)
}
CameraManager.shared.stopCamera()
task.setTaskCompleted(success: true)
}
func applicationDidEnterBackground(_ application: UIApplication) {
scheduleAppRefresh()
}
func scheduleAppRefresh() {
let taskRequest = BGAppRefreshTaskRequest(identifier:"com.background.faceDistance.refresh")
taskRequest.earliestBeginDate = Date(timeIntervalSinceNow: 30.0)
do{
try BGTaskScheduler.shared.submit(taskRequest)
print("任务提交成功")
}catch{
print("任务提交失败")
}
}
}
核心思想是初始注册后台任务标识forTaskWithIdentifier和接口,在应用进入后台时applicationDidEnterBackground,启动任务编排,提交一个后台任务到BGTaskScheduler里。待系统调度执行后台任务时,handleAppRefresh里先提交一个新的BGTask,然后执行后台任务。
taskRequest.earliestBeginDate设置了30s,实践ios能在什么实践触发不好说。
因为有时候需要等很久才会触发,用官方推荐的方式触发,设置断点,提交下面命令:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.test.BackgroundDemo"]
这样测试是能够成功的。说明代码逻辑和实现都没问题。
但是真机测试,一个晚上没有激活任何后台处理。
所以还是回到需求本身,实时摄像头捕捉要用到AVFoundation,而这套接口,目前看来在后台无法运行。
如果说类似ColorFeed那样,通过数据库或者网络来刷新应用,用BGAppRefreshTask应该是没问题的。
但是我的应用需要摄像头、实时视频,类似监控或者直播。所以从后台运行这个方向找方案不行。
受到小视频窗口启发,另一个方向,就是画中画。在下一篇博文,我再总结画中画的摸索经验。