NSURLSessionDownloadTask 移动临时文件 [英] NSURLSessionDownloadTask move temporary file

查看:48
本文介绍了NSURLSessionDownloadTask 移动临时文件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试访问我之前通过 NSURLSession 下载的文件.似乎我无法读取文件的位置,即使我在委托方法结束之前这样做(因为文件是临时的).

I'm trying to reach the file which I was downloading earlier by NSURLSession. It seems I can't read the location of the file, even though I'm doing it before delegate method ends (as the file is temporary).

不过,当我尝试访问从 NSURLSession 委托 返回的位置下的数据和错误 257 时,我得到 nil.

Still, I'm getting nil when trying to access the data under location returned from NSURLSession delegate and error 257.

代码如下:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    NSError *movingError = nil;
    NSData *fileData = [NSData dataWithContentsOfFile:location.path options:0 error:&movingError]; // is nil
    NSLog(@"%@", movingError); // is error 257
}

这段代码有什么问题..?我看到了类似的问题 NSURLSessionDownloadTask - 下载但以错误结束iPhone - 将文件从资源复制到文档会出错 但这些完全不适用于我的情况.

What's wrong with this code..? I saw similar questions NSURLSessionDownloadTask - downloads but ends with error and iPhone - copying a file from resources to Documents gives an error but these completely doesn't apply to my case.

-- 编辑--

我创建了一个新项目并粘贴了相同的代码.它有效......所以:

I've created a new project and pasted the very same code. It works... So:

1) 在我的项目中,我收到错误 257,可能是项目的某些配置无效,或者我在应用程序的其他地方使用 backgroundTasks 的事实把事情搞砸了

1) In my project I'm receiving error 257, probably some configuration of the project is invalid or the fact I'm using backgroundTasks somewhere else in the app messes things up

2) 如果我将此下载的源文件放到 Carthage 链接的外部框架中,则会发生与 1 相同的情况

2) Same as in 1 happens if I put the source files of this download to the external framework linked in by Carthage

3) 在我创建的演示项目上(1 和 2 中使用的复制粘贴文件)一切正常.

3) On demo project I created (copy-pasted files used in 1 & 2) everything works corretly.

如果有人知道是什么原因导致它不起作用 - 那就太棒了.

If someone has an idea what can cause the fact it isn't working - would be awesome.

推荐答案

讨论晚了,但我已经看到相同/类似的问题并进行了调查.

Late to the discussion, but I've seen the same/similar issue and investigated.

根据我的调查,您看到的是意料之中的(我应该说观察到了"吗?).但由于我不是 100% 确定您如何使用 URL 会话,我在下面列出了我想要做的事情,然后是我的观察(仅重要的观察).此外,我创建了一个 示例项目 并将其放在 此处 作为 downLoader.zip.您可以播放并检查 URL 会话后台下载的工作原理.但是在尝试演奏之前最好通读以下内容,尽管这是一个相当长的注释.

With my investigation, what you are seeing is expected (should I say "it's observed"?). But since I'm not 100% sure how you are using URL session, I put down below what I'm trying to do, followed by my observations (important ones only). Also, I created a sample project and put it here as downLoader.zip. You can play and check how URL session background download works. But It's better to read thru below before trying to play, though this is fairly a long note.

A.我想做什么

我正在开发一种地图应用程序,我需要在其中一次下载 1000 多个小尺寸(0.5-150kB,大部分为 ~20kB)PNG 文件.它们的总下载大小约为 50MB,下载所有这些需要几分钟时间.我认为让用户一直使用我的应用程序只是等待下载是糟糕的设计,所以我让应用程序使用 URL Session 的后台下载.但是,我必须承认,Apple 的文档 说后台会话已针对传输少数大型资源可根据需要恢复."所以,我使用后台会话的方式是完全相反的.但是,无论如何...

I'm developing kind a map app, where I need to download 1000+ of small size (0.5-150kB, mostly ~20kB) PNG files at a time. Total download size of them is ~50MB, and it takes a couple of minutes to download all of them. I think that keeping users to my app just waiting for download is bad design, so I made the app to use URL Session’s background download. I have to admit, however, that Apple’s doc says that "Background sessions are optimized for transferring a small number of large resources that can be resumed as necessary." So, the way I’m using background session is totally opposite. But, anyway...

B.我观察到的

下面,我列出了我的观察结果和我对原因的猜测.我应该说猜测,因为它们没有记录.

Below, I’ve listed my observations and my guess of why. I should say guess since they are not documented.

(1) 观察:有时,应该带有 didFinishDownlaodingTo 的文件不存在.

(1) observation: sometimes, files that should come with didFinishDownlaodingTo don’t exist.

临时文件放在:/var/mobile/Containers/Data/Application/randomHexString/Library/Caches/com.apple.nsurlsessiond/Downloads/yourName.yourApp.randomHexString 随应用程序的运行而变化.当文件不存在时, didFinishDownloadingTo 附带的 randomHexString 是上一个"会话中的一个.现在,这里的上一个"是指应用程序上次运行的会话!,在当前运行的当前会话中显然不存在.不存在的文件还有另一种情况,即 randomHexString 没问题,但文件不存在.这似乎发生在请求取消会话之后 (invalidateAndCancel) 和会话变为无效之前 (didBecomeInvalidWithError).

Temporary files are put at: /var/mobile/Containers/Data/Application/randomHexString/Library/Caches/com.apple.nsurlsessiond/Downloads/yourName.yourApp. The randomHexString changes from app’s run to run. When the files don’t exist, the randomHexString that came with didFinishDownloadingTo is the one from the "previous" session. Now, "previous" here means the session at app’s previous run !, which clearly doesn't exist at the current session at current run. There's another scenario for non-existent file, which is that the randomHexString is ok, but the files don't exist. This seems to happen after session cancel is requested (invalidateAndCancel) and before session becomes invalid (didBecomeInvalidWithError).

猜测:这种情况尤其发生在开发阶段,因为我们在下载时终止了应用程序,手动或通过调试器或仅通过错误.似乎一旦下载请求被提交并被操作系统接受,即使在应用程序退出后,操作系统也会处理我们的请求.请注意,我们无法知道操作系统是否确实接受了请求.即使在从 URLSessionDownloadTask:resume() 返回之后,有时文件不会在下次启动时退出,有时会.

Guess: This happens especially on the development phase since we terminate the app while downloading, manually or by debugger or just by bug. It seems once the download requests are handed and accepted to the OS, the OS handles our requests even after the app quits. Note we can’t know if the OS has definitely accepted the requests or not. Even after the return from URLSessionDownloadTask:resume(), sometimes files don’t exit at the next launch and sometimes they do.

解决方法:如果文件不存在,则忽略它们.过一会儿,这次"的文件应该会来.

Workaround: If files don’t exist, just ignore them. In a while, "this time’s" files should come.

(2) 观察:有时,重复的(相同)文件带有 didFinishDownloadingTo.

(2) observation: sometimes, the duplicated (same) files come with didFinishDownloadingTo.

我的应用程序将下载的 PNG 文件转换为其他格式.在 didFinishDownlaodingTo 中,我将临时文件(== OS 指定)移动到另一个我的应用程序目录,然后生成线程 (GCD) 以转换格式并删除下载的临时文件.因此,要覆盖的另一个线程 (didFinishDownlaodingTo) 是一个问题.

My app converts downloaded PNG files to other format. W/in didFinishDownlaodingTo, I move temp files (== OS designated) to another my app’s directory, then spawn thread (GCD) to convert the format and delete downloaded temp files. So, the other thread (didFinishDownlaodingTo) to overwrite is an issue.

解决方法:我列出了 URLSessionTask:taskIdentifier 和 w/in didFinishDownlaodingTo,通过检索 taskID 列表来检查重复以忽略重复的文件.

Workaround: I make list of URLSessionTask:taskIdentifier and w/in didFinishDownlaodingTo, check the duplication by retrieving the taskID list to ignore duplicated files.

(3) 观察:即使在用户终止应用后,操作系统也会再次重新启动应用.

(3) observation: even after the user terminated the app, OS relaunches the app again.

在用户从任务切换器终止应用程序后,操作系统通常会使用 application:handleEventsForBackgroundURLSession:completionHandler 重新启动应用程序.请注意,重新启动的顺序是 didFinishLaunchingWithOptions 作为常规启动首先出现,然后是 handleEventsForBackgroundURLSession.从用户的 POV 来看,当他/她终止应用程序时,就是这样,完成了!即使在他们终止应用程序后,它看起来也很奇怪,它会自行重新启动并向他们通知某些内容.就像僵尸一样.

After the user terminated the app from task switcher, quite often, the OS relaunches the app w/ application:handleEventsForBackgroundURLSession:completionHandler. Note the sequence of relaunch is that didFinishLaunchingWithOptions comes first as regular launch, then handleEventsForBackgroundURLSession comes next. From user’s POV, when he/she terminated the app, that’s it, done! It looks strange even after they terminated the app, it relaunches in itself and notifies something to them. It's like a zombie.

猜测:Apple 的文档 说如果用户终止您的应用程序,系统会取消任何待处理的任务".待处理任务"的定义没有明确说明,但我理解这是来自 iOS POV 而不是用户或程序开发人员的.正如(1)中猜测的那样,iOS 似乎已经接受了下载请求,它们不再是待处理的任务"了.

Guess: The Apple’s document says "If the user terminates your app, the system cancels any pending tasks". The definition of "pending tasks" is not clearly stated but I understand this is from iOS POV and not user’s or program developer’s. As guessed in (1), iOS seemed to have accepted the download requests, and they are not "pending tasks" anymore.

解决方法:在所有下载请求都通过 URLSessionDownloadTask:resume() 传递给 iOS 后,我创建了一个flagFile",其中的文件表示正在下载".当用户终止应用程序时,在 UIApplicationDelegate:applicationWillTerminate 处,我删除标志文件.或者,如果所有下载请求都没有交给 iOS,则没有标志文件.然后在 UIApplicationDelegate:handleEventsForBackgroundURLSession 的应用程序重新启动时,我检查我们是否有标志文件.如果丢失,那么我可以假设用户终止了.这里有两个选择.选择 1:我不会重新创建 URL 会话.接下来发生的是 iOS 将在大约 20 秒内终止我的应用程序.我不知道这(== 不创建 URL 会话)是否是合法操作,但它有效.用户可以在这 20 秒内启动,所以我添加了更多代码来处理这种情况.选择 2:我创建 URL 会话.接下来发生的是 iOS 调用委托方法 didFinishDownlaodingTo/didCompleteWithError,然后是 urlSessionDidFinishEvents.如果我在这里不做任何事情,进程(应用程序)将无限期地保持活动状态,而不会向用户发出任何通知:任务切换器中没有任何内容.这无非是浪费内存.这里的选项是触发本地通知并让用户知道我的应用程序,以便他们可以返回到我的应用程序并可以终止(再次!),尽管我的应用程序显然显示为僵尸.两种选择都有一个问题:在某些情况下可能不会调用 applicationWillTerminate(尽管我尚未确认).在这种情况下,标志文件被保留为常规操作并向用户显示僵尸.所以,标志文件方法只是缓解了这个问题,但我认为它大部分时间都适用于我的应用程序.

Workaround: I create a "flagFile" after all of download requests are handed to iOS w/ URLSessionDownloadTask:resume(), of which file means that "download is ongoing". When users terminate the app, at UIApplicationDelegate:applicationWillTerminate, I delete the flag file. Or, if all of download requests not handed to iOS, there's no flag file. Then at app’s relaunch at UIApplicationDelegate:handleEventsForBackgroundURLSession, I check if we have the flag file. If missing, then I can assume that users terminated. Two choices here. Choice-1: I will not recreate URL session. What happens next is that iOS will terminate my app in about 20 seconds. I have no idea if this (== not creating the URL session) is a legal operation but it works. Users can launch w/in this 20 sec, so I put some more code to handle that scenario. Choice-2: I create URL session. What happens next is that iOS calls delegate methods didFinishDownlaodingTo/didCompleteWithError, followed by urlSessionDidFinishEvents. If I don't do anything here, the process (app) keeps alive indefinitely w/o any notification to users: nothing in task switcher. This is nothing more than waste of memory. The option here is to fire local notification and let users know of my app, so that they can go back to my app and can terminate (Again!), though my app clearly appears as a zombie. One issue for both choices: applicationWillTerminate may not be called in certain situation (though I've yet confirmed). At this case, flag file is left as regular ops and show zombie to users. So, the flag file method is just mitigation to the issue, but I think it works most of the time for my app.

请注意,当应用被 xcode 调试器杀死或被带有错误的操作系统 (SEGFALUT) 杀死时,有时会重新启动该应用.

Note the app is relaunched sometimes when it's killed by xcode debugger or killed by OS w/ bug (SEGFALUT).

(4) 观察:在应用程序被终止(由用户等)然后由操作系统重新启动后,应用程序偶尔处于活动状态(UIApplication.shared.applicationState 为 .active).

(4) observation: after the app is terminated (by user, etc.) then relaunched by OS, the app is occasionally in active state (UIApplication.shared.applicationState is .active).

我想通过本地通知在下载完成时通知用户,但由于它处于活动状态,本地通知不会触发.所以,我需要使用 UIAlertController 代替.因此,我无法提供一致的用户体验,而且对于用户来说应该看起来很奇怪:大部分时间是本地通知,偶尔会出现 UIAlert.请注意,当应用以活动状态启动时,它会出现在任务切换器中.

I want to notify the user on the download completion by local notification, but since it's active, local notification doesn't fire. So, I need to use UIAlertController instead. Therefore, I can't provide consistent user experience, and should look strange for users: most of the time local notification and very occasionally UIAlert. Note when app started in active state, it appears in task switcher.

猜测:完全不知道这是怎么发生的.一件好事(?)是这种情况只是偶尔发生.

Guess: totally no idea how this can happen. One good(?) thing is this happens only occasionally.

解决方法:似乎没有.

(5) 观察:handleEventsForBackgroundURLSession/urlSessionDidFinishEvents 只被调用一次.

(5) observation: handleEventsForBackgroundURLSession/urlSessionDidFinishEvents is called just once.

我在启动后台任务 (application.beginBackgroundTask) 后开始下载.然后在 beginBackgroundTask 的过期处理程序中,我调用 endBackgroundTask.我不知道确切的原因,但是在 endBackgroundTask 之后,我的进程仍然有很多处理时间,所以我可以继续请求下载.这可能是因为使用 didFinishDownlaodingTo 不断下载文件.作为一个好公民,我暂停请求进一步下载,并向用户发出本地通知以将应用程序置于前台.现在,一旦我暂停请求,在 4-5 秒内,操作系统确定 URL 会话结束并调用 handleEventsForBackgroundURLSession,然后调用 urlSessionDidFinishEvents.这是一次性事件.当用户将应用程序置于前台以恢复下载,然后再次将其置于后台时,将不再出现 handleEventsForBackgroundURLSession/urlSessionDidFinishEvents.我不清楚的是会话开始和结束的定义.似乎会话首先从 URLSessionTask.resume() 开始,然后以超时结束,这似乎由 URLSessionConfiguration.timeoutIntervalForRequest.但是,这里设置大的数字(1000 秒)不会有任何影响,总是 4-5 秒.

I start the download after started background task (application.beginBackgroundTask). Then in expiration handler of beginBackgroundTask, I call endBackgroundTask. I don't know exactly why, but after endBackgroundTask, my process is still given lots of process time, so I can keep requesting download. This might be because download files keep coming w/ didFinishDownlaodingTo. To be a good citizen, I suspend to request further download, and fire local notification to user to put the app to foreground. Now, once I suspend the request, in 4-5 seconds, OS determines the URL session is over and handleEventsForBackgroundURLSession then urlSessionDidFinishEvents are called. This is one-off event. When the user put the app in foreground to resume download, then put it again in background, no handleEventsForBackgroundURLSession/urlSessionDidFinishEvents will come anymore. What's unclear to me is the definition of session's start and end. It seems the session starts at first URLSessionTask.resume(), then ends w/ timeout, which seems to be determined by URLSessionConfiguration.timeoutIntervalForRequest. However, setting large number here (1000 sec) doesn't affect anything, it's always 4-5 seconds.

猜测:不知道

解决方法:不要在应用程序处于活动状态时中继 urlSessionDidFinishEvents.仅在应用由操作系统重新启动时以及在初始 handleEventsForBackgroundURLSession/urlSessionDidFinishEvents 时进行中继.

Workaround: don't relay on urlSessionDidFinishEvents while the app is alive. Relay only when app relaunched by OS and at initial handleEventsForBackgroundURLSession/urlSessionDidFinishEvents.

================

===============

下面,我列出了示例项目 (downLoader.zip).您可以通过示例验证以上所有内容.

Below, I listed about the sample project (downLoader.zip). You can verify all of above with the sample.

  1. 该应用有一个下载文件列表.文件数为1921个,总共56MB.它们是 256x256 PNG 地图图块文件,位于由 日本地理空间信息管理局管理的服务器中 (GSI).下载后,它们被移动到库/缓存/下载.如果您的设备已越狱,您可以使用 Filza 查看它们.
  2. 自行崩溃以测试重新启动
  3. 模拟后台任务到期
  4. 记录到文件,因为在操作系统重新启动后调试器不起作用.该文件在文档中,可以移动到 PC.
  5. 一定要玩真机.模拟器不会重新启动应用.
  6. 使用 Xcode 8.3.3 构建项目并使用 iphone6+/9.3.3 和 iphone7+/10.3.1 进行测试
  7. 要查看应用是否使用 xcode 重新启动,请转到调试/附加到进程"菜单并查看是否列出了 downLoader.
  1. The app has a list of download files. The number of files is 1921, and 56MB in total. They are 256x256 PNG map tile files that are located in a server managed by Geo Spatial Information Authority of Japan (GSI). After downloaded, they are moved to Library/Cache/download. If your device is jail-broken, you can view them w/ Filza.
  2. crash itself to test relaunch
  3. emulate background task expiration
  4. logging to file since debugger doesn't work after relaunch by OS. The file is in Documents, can be moved to PC.
  5. Do play with the real device. Simulator doesn't relaunch the app.
  6. Project built w/ Xcode 8.3.3 and tested w/ iphone6+/9.3.3 and iphone7+/10.3.1
  7. To see if app relaunched w/ xcode, go to Debug/Attach to Process menu and see if downLoader is listed.

================

===============

我认为 URL 会话后台下载的行为很棘手,尤其是在重新启动时.我们至少需要考虑我上面列出的观察结果,否则应用程序用户会感到困惑.

I think URL session background download behaves trickily, especially at relaunch. We need to consider at least the observations I listed above, or the app users will get confused.

这篇关于NSURLSessionDownloadTask 移动临时文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆