iOS Swift 在后台下载大量小文件

Posted

技术标签:

【中文标题】iOS Swift 在后台下载大量小文件【英文标题】:iOS Swift download lots of small files in background 【发布时间】:2016-11-03 14:21:59 【问题描述】:

在我的应用中,我需要下载具有以下要求的文件:

下载大量(例如 3000 个)小 PNG 文件(例如 5KB) 一个一个 如果应用在后台继续下载 如果图像下载失败(通常是因为 Internet 连接已丢失),请等待 X 秒并重试。如果失败 Y 次,则认为下载失败。 能够在每次下载之间设置延迟以减少服务器负载

ios 能做到这一点吗?我正在尝试使用 NSURLSession 和 NSURLSessionDownloadTask,但没有成功(我想避免同时启动 3000 个下载任务)。

编辑:MwcsMac 要求的一些代码:

视图控制器:

class ViewController: UIViewController, URLSessionDelegate, URLSessionDownloadDelegate 

    // --------------------------------------------------------------------------------
    // MARK: Attributes

    lazy var downloadsSession: URLSession = 

        let configuration = URLSessionConfiguration.background(withIdentifier:"bgSessionConfigurationTest");
        let session = URLSession(configuration: configuration, delegate: self, delegateQueue:self.queue);

        return session;
    ()

    lazy var queue:OperationQueue = 

        let queue = OperationQueue();
        queue.name = "download";
        queue.maxConcurrentOperationCount = 1;

        return queue;
    ()

    var activeDownloads = [String: Download]();

    var downloadedFilesCount:Int64 = 0;
    var failedFilesCount:Int64 = 0;
    var totalFilesCount:Int64 = 0;

    // --------------------------------------------------------------------------------



    // --------------------------------------------------------------------------------
    // MARK: Lifecycle

    override func viewDidLoad() 

        super.viewDidLoad()

        startButton.addTarget(self, action:#selector(onStartButtonClick), for:UIControlEvents.touchUpInside);

        _ = self.downloadsSession
        _ = self.queue
    

    // --------------------------------------------------------------------------------



    // --------------------------------------------------------------------------------
    // MARK: User interaction

    @objc
    private func onStartButtonClick() 

        startDownload();
    

    // --------------------------------------------------------------------------------



    // --------------------------------------------------------------------------------
    // MARK: Utils

    func startDownload() 

        downloadedFilesCount = 0;
        totalFilesCount = 0;

        for i in 0 ..< 3000 

            let urlString:String = "http://server.url/\(i).png";
            let url:URL = URL(string: urlString)!;

            let download = Download(url:urlString);
            download.downloadTask = downloadsSession.downloadTask(with: url);
            download.downloadTask!.resume();
            download.isDownloading = true;
            activeDownloads[download.url] = download;

            totalFilesCount += 1;
        
    

    // --------------------------------------------------------------------------------



    // --------------------------------------------------------------------------------
    // MARK: URLSessionDownloadDelegate

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) 

        if(error != nil)  print("didCompleteWithError \(error)"); 

        failedFilesCount += 1;
    

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) 

        if let url = downloadTask.originalRequest?.url?.absoluteString 

            activeDownloads[url] = nil
        

        downloadedFilesCount += 1;

        [eventually do something with the file]

        DispatchQueue.main.async 

            [update UI]
        

        if(failedFilesCount + downloadedFilesCount == totalFilesCount) 

            [all files have been downloaded]
        
    

    // --------------------------------------------------------------------------------



    // --------------------------------------------------------------------------------
    // MARK: URLSessionDelegate

    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) 

        if let appDelegate = UIApplication.shared.delegate as? AppDelegate 

            if let completionHandler = appDelegate.backgroundSessionCompletionHandler 

                appDelegate.backgroundSessionCompletionHandler = nil

                DispatchQueue.main.async  completionHandler() 
            
        
    

    // --------------------------------------------------------------------------------

AppDelegate:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate 

    var window: UIWindow?
    var backgroundSessionCompletionHandler: (() -> Void)?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool 
        // Override point for customization after application launch.
        return true
    

    func applicationWillResignActive(_ application: UIApplication) 
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    

    func applicationDidEnterBackground(_ application: UIApplication) 
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    

    func applicationWillEnterForeground(_ application: UIApplication) 
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    

    func applicationDidBecomeActive(_ application: UIApplication) 
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    

    func applicationWillTerminate(_ application: UIApplication) 
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    

    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) 

        backgroundSessionCompletionHandler = completionHandler
    

下载:

class Download: NSObject 

    var url: String
    var isDownloading = false
    var progress: Float = 0.0

    var downloadTask: URLSessionDownloadTask?
    var resumeData: Data?

    init(url: String) 
        self.url = url
    

这段代码有什么问题:

我不确定背景部分是否正常工作。我遵循了本教程:https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started。它说如果我按主页然后双击主页以显示应用切换器,则应更新应用屏幕截图。似乎不能可靠地工作。当我重新打开应用程序时它会更新。从昨天开始有 iPhone,不知道这是不是正常现象? 3000 次下载在 startDownload 方法中启动。队列的 maxConcurrentOperationCount 似乎没有得到尊重:下载正在同时运行 downloadsSession.downloadTask(with: url);通话需要 30 毫秒。乘以 3000,需要 1mn30,这是一个大问题:/。等待几秒钟 (2-3) 即可。 我无法在两次下载之间设置延迟(这不是什么大问题。虽然会很好,但如果我不能这样做也可以)

理想情况下,我会异步运行 startDownload 方法,并在 for 循环中同步下载文件。但我想我不能在 iOS 后台执行此操作?

【问题讨论】:

显示不工作的代码。 @MwcsMac,查看我的编辑 【参考方案1】:

这就是我最终所做的:

在线程中开始下载,允许在后台运行几分钟(使用 UIApplication.shared.beginBackgroundTask) 使用允许设置超时的自定义下载方法在循环中逐一下载文件 在下载每个文件之前,检查 UIApplication.shared.backgroundTimeRemaining 是否大于 15 如果是,下载文件超时时间为 min(60, UIApplication.shared.backgroundTimeRemaining - 5) 如果否,则停止下载并将下载进度保存在用户默认值中,以便在用户导航回应用程序时能够继续下载 当用户导航回应用程序时,检查状态是否已保存,如果已保存,则继续下载。

这样,当用户离开应用程序时,下载会持续几分钟(iOS 10 上为 3 分钟),并在这 3 分钟过去之前暂停。如果用户在后台离开应用超过 3 分钟,他必须回来完成下载。

【讨论】:

以上是关于iOS Swift 在后台下载大量小文件的主要内容,如果未能解决你的问题,请参考以下文章

在后台下载多个文件(仅限 iOS 7)

针对大量小文件优化 S3 下载

颤振 | Dio Package ...在后台下载大文件

服务器写入大量小文件,nc结合bash并发拷贝大量的小文件

如何使用 AFNetworking 3.0 在后台下载大文件并在会话完成所有任务时显示本地通知

.NET MVC中使用WebClient在后台下载文件,前台显示进度。