ios iOS下载并在应用程序内保存图像

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6238139/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 20:13:41  来源:igfitidea点击:

iOS download and save image inside app

iphonecocoa-touchiosipad

提问by Samuli Lehtonen

Is it possible for me to download an image from website and save it permanently inside my app? I really have no idea, but it would make a nice feature for my app.

我可以从网站下载图像并将其永久保存在我的应用程序中吗?我真的不知道,但它会为我的应用程序提供一个很好的功能。

回答by Mick MacCallum

Although it is true that the other answers here will work, they really aren't solutions that should ever be used in production code. (at least not without modification)

尽管这里的其他答案确实有效,但它们确实不是应该在生产代码中使用的解决方案。(至少不是没有修改)

Problems

问题

The problem with these answers is that if they are implemented as is and are not called from a background thread, they will block the main thread while downloading and saving the image. This is bad.

这些答案的问题在于,如果它们按原样实现并且不是从后台线程调用,它们将在下载和保存图像时阻塞主线程。这很糟糕

If the main thread is blocked, UI updates won't happen until the downloading/saving of the image is complete. As an example of what this means, say you add a UIActivityIndicatorView to your app to show the user that the download is still in progress (I will be using this as an example throughout this answer) with the following rough control flow:

如果主线程被阻塞,则在图像下载/保存完成之前不会发生 UI 更新。作为这意味着什么的示例,假设您向应用程序添加 UIActivityIndi​​catorView 以向用户显示下载仍在进行中(我将在整个答案中使用它作为示例),并使用以下粗略的控制流程:

  1. Object responsible for starting the download is loaded.
  2. Tell the activity indicator to start animating.
  3. Start the synchronous download process using +[NSData dataWithContentsOfURL:]
  4. Save the data (image) that was just downloaded.
  5. Tell the activity indicator to stop animating.
  1. 负责开始下载的对象被加载。
  2. 告诉活动指示器开始动画。
  3. 使用启动同步下载过程 +[NSData dataWithContentsOfURL:]
  4. 保存刚刚下载的数据(图像)。
  5. 告诉活动指示器停止动画。

Now, this might seem like reasonable control flow, but it is disguising a critical problem.

现在,这似乎是合理的控制流,但它掩盖了一个关键问题。

When you call the activity indicator's startAnimating method on the main (UI) thread, the UI updates for this event won't actually happen until the next time the main run loopupdates, and this is where the first major problem is.

当您在主 (UI) 线程上调用活动指示器的 startAnimating 方法时,此事件的 UI 更新实际上不会发生,直到下一次主运行循环更新时才会发生,这就是第一个主要问题所在。

Before this update has a chance to happen, the download is triggered, and since this is a synchronous operation, it blocks the main thread until it has finished download (saving has the same problem). This will actually prevent the activity indicator from starting its animation. After that you call the activity indicator's stopAnimating method and expect all to be good, but it isn't.

在这个更新有机会发生之前,下载被触发,由于这是一个同步操作,它会阻塞主线程,直到它完成下载(保存也有同样的问题)。这实际上会阻止活动指示器开始其动画。之后,您调用活动指示器的 stopAnimating 方法并期望一切正常,但事实并非如此。

At this point, you'll probably find yourself wondering the following.

此时,您可能会发现自己想知道以下内容。

Why doesn't my activity indicator ever show up?

为什么我的活动指示器从来没有出现过?

Well, think about it like this. You tell the indicator to start but it doesn't get a chance before the download starts. After the download completes, you tell the indicator to stop animating. Since the main thread was blocked through the whole operation, the behavior you actually see is more along the lines telling the indicator to start and then immediately telling it to stop, even though there was a (possibly) large download task in between.

嗯,这样想吧。您告诉指示器启动,但在下载开始之前它没有机会。下载完成后,您告诉指示器停止动画。由于主线程在整个操作过程中被阻塞,您实际看到的行为更像是告诉指标开始然后立即告诉它停止,即使中间有一个(可能)大型下载任务。

Now, in the best case scenario, all this does is cause a poor user experience (still really bad). Even if you think this isn't a big deal because you're only downloading a small image and the download happens almost instantaneously, that won't always be the case. Some of your users may have slow internet connections, or something may be wrong server side keeping the download from starting immediately/at all.

现在,在最好的情况下,所有这些都会导致糟糕的用户体验(仍然很糟糕)。即使您认为这没什么大不了,因为您只是下载一个小图像并且下载几乎是瞬间发生的,但情况并非总是如此。您的某些用户的 Internet 连接速度可能较慢,或者服务器端可能出现错误,使下载无法立即/根本无法启动。

In both of these cases, the app won't be able to process UI updates, or even touch events while your download task sits around twiddling its thumbs waiting for the download to complete or for the server to respond to its request.

在这两种情况下,应用程序将无法处理 UI 更新,甚至无法在您的下载任务等待下载完成或服务器响应其请求时摆弄拇指时处理触摸事件。

What this means is that synchronously downloading from the main thread prevents you from possibly implementing anything to indicate to the user that a download is currently in progress. And since touch events are processed on the main thread as well, this throws out the possibility of adding any kind of cancel button as well.

这意味着从主线程同步下载可以防止您实现任何向用户表明当前正在进行下载的内容。而且由于触摸事件也在主线程上处理,这也排除了添加任何类型的取消按钮的可能性。

Then in the worst case scenario, you'll start receiving crash reports stating the following.

然后在最坏的情况下,您将开始收到说明以下内容的崩溃报告。

Exception Type: 00000020 Exception Codes: 0x8badf00d

异常类型:00000020 异常代码:0x8badf00d

These are easy to identify by the exception code 0x8badf00d, which can be read as "ate bad food". This exception is thrown by the watch dog timer, whose job is to watch for long running tasks that block the main thread, and to kill the offending app if this goes on for too long. Arguably, this is still a poor user experience issue, but if this starts to occur, the app has crossed the line between bad user experience, and terrible user experience.

这些很容易通过异常代码识别0x8badf00d,可以理解为“吃了不好的食物”。这个异常是由看门狗定时器抛出的,它的工作是监视阻塞主线程的长时间运行的任务,如果这种情况持续太久,就杀死有问题的应用程序。可以说,这仍然是一个糟糕的用户体验问题,但如果这种情况开始发生,该应用程序已经跨越了糟糕的用户体验和糟糕的用户体验之间的界限。

Here's some more info on what can cause this to happen from Apple's Technical Q&Aabout synchronous networking (shortened for brevity).

以下是Apple关于同步网络的技术问答(为简洁起见)中有关可能导致这种情况发生的更多信息。

The most common cause for watchdog timeout crashes in a network application is synchronous networking on the main thread. There are four contributing factors here:

  1. synchronous networking — This is where you make a network request and block waiting for the response.
  2. main thread — Synchronous networking is less than ideal in general, but it causes specific problems if you do it on the main thread. Remember that the main thread is responsible for running the user interface. If you block the main thread for any significant amount of time, the user interface becomes unacceptably unresponsive.
  3. long timeouts — If the network just goes away (for example, the user is on a train which goes into a tunnel), any pending network request won't fail until some timeout has expired....

网络应用程序中看门狗超时崩溃的最常见原因是主线程上的同步网络。这里有四个促成因素:

  1. 同步网络——这是您发出网络请求并阻止等待响应的地方。
  2. 主线程 — 同步网络通常不太理想,但如果在主线程上进行,则会导致特定问题。请记住,主线程负责运行用户界面。如果你阻塞主线程很长一段时间,用户界面就会变得不可接受地无响应。
  3. long timeouts — 如果网络刚刚消失(例如,用户在进入隧道的火车上),任何挂起的网络请求都不会失败,直到某个超时到期......

...

...

  1. watchdog — In order to keep the user interface responsive, iOS includes a watchdog mechanism. If your application fails to respond to certain user interface events (launch, suspend, resume, terminate) in time, the watchdog will kill your application and generate a watchdog timeout crash report. The amount of time the watchdog gives you is not formally documented, but it's always less than a network timeout.

One tricky aspect of this problem is that it's highly dependent on the network environment. If you always test your application in your office, where network connectivity is good, you'll never see this type of crash. However, once you start deploying your application to end users—who will run it in all sorts of network environments—crashes like this will become common.

  1. 看门狗——为了保持用户界面的响应性,iOS 包含了一个看门狗机制。如果您的应用程序未能及时响应某些用户界面事件(启动、暂停、恢复、终止),看门狗将杀死您的应用程序并生成看门狗超时崩溃报告。看门狗给你的时间没有正式记录,但它总是小于网络超时。

这个问题的一个棘手方面是它高度依赖于网络环境。如果您总是在网络连接良好的办公室测试您的应用程序,您将永远不会看到这种类型的崩溃。但是,一旦您开始将应用程序部署到最终用户(他们将在各种网络环境中运行它),像这样的崩溃将变得很常见。

Now at this point, I'll stop rambling about why the provided answers might be problematic and will start offering up some alternative solutions. Keep in mind that I've used the URL of a small image in these examples and you'll notice a larger difference when using a higher resolution image.

现在,我将停止谈论为什么提供的答案可能有问题,并将开始提供一些替代解决方案。请记住,我在这些示例中使用了小图像的 URL,当使用更高分辨率的图像时,您会注意到更大的差异。



Solutions

解决方案

I'll start by showing a safe version of the other answers, with the addition of how to handle UI updates. This will be the first of several examples, all of which will assume that the class in which they are implemented has valid properties for a UIImageView, a UIActivityIndicatorView, as well as the documentsDirectoryURLmethod to access the documents directory. In production code, you may want to implement your own method to access the documents directory as a category on NSURL for better code reusability, but for these examples, this will be fine.

我将首先展示其他答案的安全版本,并添加如何处理 UI 更新。这将是几个示例中的第一个,所有这些示例都假设实现它们的类具有 UIImageView、UIActivityIndi​​catorView 的有效属性,以及documentsDirectoryURL访问文档目录的方法。在生产代码中,您可能希望实现自己的方法来访问文档目录作为 NSURL 上的类别以获得更好的代码可重用性,但对于这些示例,这会很好。

- (NSURL *)documentsDirectoryURL
{
    NSError *error = nil;
    NSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory
                                                        inDomain:NSUserDomainMask
                                               appropriateForURL:nil
                                                          create:NO
                                                           error:&error];
    if (error) {
        // Figure out what went wrong and handle the error.
    }

    return url;
}

These examples will also assume that the thread that they start off on is the main thread. This will likely be the default behavior unless you start your download task from somewhere like the callback block of some other asynchronous task. If you start your download in a typical place, like a lifecycle method of a view controller (i.e. viewDidLoad, viewWillAppear:, etc.) this will produce the expected behavior.

这些示例还将假设它们开始的线程是主线程。这可能是默认行为,除非您从某个其他异步任务的回调块之类的某个地方启动下载任务。如果你在一个典型的地方开始下载,比如视图控制器的生命周期方法(即 viewDidLoad、viewWillAppear: 等),这将产生预期的行为。

This first example will use the +[NSData dataWithContentsOfURL:]method, but with some key differences. For one, you'll notice that in this example, the very first call we make is to tell the activity indicator to start animating, then there is an immediate difference between this and the synchronous examples. Immediately, we use dispatch_async(), passing in the global concurrent queue to move execution to the background thread.

第一个示例将使用该+[NSData dataWithContentsOfURL:]方法,但有一些关键差异。首先,您会注意到,在此示例中,我们进行的第一个调用是告诉活动指示器开始动画,然后这与同步示例之间存在直接差异。立即,我们使用 dispatch_async(),传入全局并发队列以将执行移至后台线程。

At this point, you've already greatly improved your download task. Since everything within the dispatch_async() block will now happen off the main thread, your interface will no longer lock up, and your app will be free to respond to touch events.

此时,您已经大大改进了下载任务。由于 dispatch_async() 块中的所有内容现在都将在主线程之外发生,您的界面将不再锁定,您的应用程序将可以自由响应触摸事件。

What is important to notice here is that all of the code within this block will execute on the background thread, up until the point where the downloading/saving of the image was successful, at which point you might want to tell the activity indicator to stopAnimating, or apply the newly saved image to a UIImageView. Either way, these are updates to the UI, meaning you must dispatch back the the main thread using dispatch_get_main_queue() to perform them. Failing to do so results in undefined behavior, which may cause the UI to update after an unexpected period of time, or may even cause a crash. Always make sure you move back to the main thread before performing UI updates.

这里需要注意的重要一点是,此块中的所有代码都将在后台线程上执行,直到下载/保存图像成功为止,此时您可能想要告诉活动指示器停止动画,或将新保存的图像应用到 UIImageView。无论哪种方式,这些都是对 UI 的更新,这意味着您必须使用 dispatch_get_main_queue() 分派主线程来执行它们。不这样做会导致未定义的行为,这可能会导致 UI 在意外的一段时间后更新,甚至可能导致崩溃。始终确保在执行 UI 更新之前移回主线程。

// Start the activity indicator before moving off the main thread
[self.activityIndicator startAnimating];
// Move off the main thread to start our blocking tasks.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // Create the image URL from a known string.
    NSURL *imageURL = [NSURL URLWithString:@"http://www.google.com/images/srpr/logo3w.png"];

    NSError *downloadError = nil;
    // Create an NSData object from the contents of the given URL.
    NSData *imageData = [NSData dataWithContentsOfURL:imageURL
                                              options:kNilOptions
                                                error:&downloadError];
    // ALWAYS utilize the error parameter!
    if (downloadError) {
        // Something went wrong downloading the image. Figure out what went wrong and handle the error.
        // Don't forget to return to the main thread if you plan on doing UI updates here as well.
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.activityIndicator stopAnimating];
            NSLog(@"%@",[downloadError localizedDescription]);
        });
    } else {
        // Get the path of the application's documents directory.
        NSURL *documentsDirectoryURL = [self documentsDirectoryURL];
        // Append the desired file name to the documents directory path.
        NSURL *saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:@"GCD.png"];

        NSError *saveError = nil;
        BOOL writeWasSuccessful = [imageData writeToURL:saveLocation
                                                options:kNilOptions
                                                  error:&saveError];
        // Successful or not we need to stop the activity indicator, so switch back the the main thread.
        dispatch_async(dispatch_get_main_queue(), ^{
            // Now that we're back on the main thread, you can make changes to the UI.
            // This is where you might display the saved image in some image view, or
            // stop the activity indicator.

            // Check if saving the file was successful, once again, utilizing the error parameter.
            if (writeWasSuccessful) {
                // Get the saved image data from the file.
                NSData *imageData = [NSData dataWithContentsOfURL:saveLocation];
                // Set the imageView's image to the image we just saved.
                self.imageView.image = [UIImage imageWithData:imageData];
            } else {
                NSLog(@"%@",[saveError localizedDescription]);
                // Something went wrong saving the file. Figure out what went wrong and handle the error.
            }

            [self.activityIndicator stopAnimating];
        });
    }
});

Now keep in mind, that the method shown above is still not an ideal solutionconsidering it can't be cancelled prematurely, it gives you no indication of the progress of the download, it can't handle any kind of authentication challenge, it can't be given a specific timeout interval, etc. (lots and lots of reasons). I'll cover a few of the better options below.

现在请记住,上面显示方法仍然不是一个理想的解决方案,因为它不能过早取消,它不指示下载进度,它不能处理任何类型的身份验证挑战,它可以'没有给出特定的超时间隔等(很多很多原因)。我将在下面介绍一些更好的选择。

In these examples, I'll only be covering solutions for apps targeting iOS 7 and up considering (at time of writing) iOS 8 is the current major release, and Apple is suggesting only supporting versions N and N-1. If you need to support older iOS versions, I recommend looking into the NSURLConnectionclass, as well as the 1.0 version of AFNetworking.If you look at the revision history of this answer, you can find basic examples using NSURLConnection and ASIHTTPRequest, although it should be noted that ASIHTTPRequest is no longer being maintained, and should notbe used for new projects.

在这些示例中,考虑到(在撰写本文时)iOS 8 是当前的主要版本,而Apple 建议仅支持版本 N 和 N-1,我将仅涵盖针对 iOS 7 及更高版本的应用程序的解决方案。如果您需要支持较旧的 iOS 版本,我建议您查看 NSURLConnection类以及AFNetworking1.0 版本。如果您查看此答案的修订历史记录,您可以找到使用 NSURLConnection 和ASIHTTPRequest 的基本示例,但应该注意的是, ASIHTTPRequest 不再维护,应用于新项目。



NSURLSession

NSURLSession

Lets start with NSURLSession, which was introduced in iOS 7, and greatly improves the ease with which networking can be done in iOS. With NSURLSession, you can easily perform asynchronous HTTP requests with a callback block and handle authentication challenges with its delegate. But what makes this class really special is that it also allows for download tasks to continue running even if the application is sent to the background, gets terminated, or even crashes. Here's a basic example of its usage.

让我们从iOS 7 中引入的NSURLSession开始,它极大地提高了在 iOS 中完成网络的便利性。使用 NSURLSession,您可以轻松地使用回调块执行异步 HTTP 请求,并使用其委托处理身份验证挑战。但是这个类的真正特别之处在于,即使应用程序被发送到后台、被终止甚至崩溃,它也允许下载任务继续运行。这是其用法的基本示例。

// Start the activity indicator before starting the download task.
[self.activityIndicator startAnimating];

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// Use a session with a custom configuration
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
// Create the image URL from some known string.
NSURL *imageURL = [NSURL URLWithString:@"http://www.google.com/images/srpr/logo3w.png"];
// Create the download task passing in the URL of the image.
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:imageURL completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
    // Get information about the response if neccessary.
    if (error) {
        NSLog(@"%@",[error localizedDescription]);
        // Something went wrong downloading the image. Figure out what went wrong and handle the error.
        // Don't forget to return to the main thread if you plan on doing UI updates here as well.
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.activityIndicator stopAnimating];
        });
    } else {
        NSError *openDataError = nil;
        NSData *downloadedData = [NSData dataWithContentsOfURL:location
                                                       options:kNilOptions
                                                         error:&openDataError];
        if (openDataError) {
            // Something went wrong opening the downloaded data. Figure out what went wrong and handle the error.
            // Don't forget to return to the main thread if you plan on doing UI updates here as well.
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"%@",[openDataError localizedDescription]);
                [self.activityIndicator stopAnimating];
            });
        } else {
            // Get the path of the application's documents directory.
            NSURL *documentsDirectoryURL = [self documentsDirectoryURL];
            // Append the desired file name to the documents directory path.
            NSURL *saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:@"NSURLSession.png"];
            NSError *saveError = nil;

            BOOL writeWasSuccessful = [downloadedData writeToURL:saveLocation
                                                          options:kNilOptions
                                                            error:&saveError];
            // Successful or not we need to stop the activity indicator, so switch back the the main thread.
            dispatch_async(dispatch_get_main_queue(), ^{
                // Now that we're back on the main thread, you can make changes to the UI.
                // This is where you might display the saved image in some image view, or
                // stop the activity indicator.

                // Check if saving the file was successful, once again, utilizing the error parameter.
                if (writeWasSuccessful) {
                    // Get the saved image data from the file.
                    NSData *imageData = [NSData dataWithContentsOfURL:saveLocation];
                    // Set the imageView's image to the image we just saved.
                    self.imageView.image = [UIImage imageWithData:imageData];
                } else {
                    NSLog(@"%@",[saveError localizedDescription]);
                    // Something went wrong saving the file. Figure out what went wrong and handle the error.
                }

                [self.activityIndicator stopAnimating];
            });
        }
    }
}];

// Tell the download task to resume (start).
[task resume];

From this you'll notice that the downloadTaskWithURL: completionHandler:method returns an instance of NSURLSessionDownloadTask, on which an instance method -[NSURLSessionTask resume]is called. This is the method that actually tells the download task to start. This means that you can spin up your download task, and if desired, hold off on starting it (if needed). This also means that as long as you store a reference to the task, you can also utilize its canceland suspendmethods to cancel or pause the task if need be.

从这里您会注意到该downloadTaskWithURL: completionHandler:方法返回 NSURLSessionDownloadTask 的一个实例,在该实例-[NSURLSessionTask resume]上调用了一个实例方法。这是实际告诉下载任务开始的方法。这意味着您可以启动下载任务,如果需要,可以推迟启动它(如果需要)。这也意味着只要您存储对任务的引用,您还可以在需要时使用它的cancelsuspend方法来取消或暂停任务。

What's really cool about NSURLSessionTasks is that with a little bit of KVO, you can monitor the values of its countOfBytesExpectedToReceive and countOfBytesReceived properties, feed these values to an NSByteCountFormatter, and easily create a download progress indicator to your user with human readable units (e.g. 42 KB of 100 KB).

什么是真正酷的NSURLSessionTasks是,随着一点点的志愿,可以监视其countOfBytesExpectedToReceive和countOfBytesReceived属性的值,养活这些值的NSByteCountFormatter,轻松地创建一个下载进度指示器与人类可读的单位用户(如42 100 KB 的 KB)。

Before I move away from NSURLSession though, I'd like to point out that the ugliness of having to dispatch_async back to the main threads at several different points in the download's callback block can be avoided. If you chose to go this route, you can initialize the session with its initializer that allows you to specify the delegate, as well as the delegate queue. This will require you to use the delegate pattern instead of the callback blocks, but this may be beneficial because it is the only way to support background downloads.

不过,在我离开 NSURLSession 之前,我想指出,可以避免在下载的回调块中的几个不同点将 dispatch_async 返回到主线程的丑陋之处。如果您选择走这条路线,您可以使用它的初始化程序初始化会话,该初始化程序允许您指定委托以及委托队列。这将要求您使用委托模式而不是回调块,但这可能是有益的,因为它是支持后台下载的唯一方法。

NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
                                                      delegate:self
                                                 delegateQueue:[NSOperationQueue mainQueue]];


AFNetworking 2.0

AFNetworking 2.0

If you've never heard of AFNetworking, it is IMHO the end-all of networking libraries. It was created for Objective-C, but it works in Swift as well. In the words of its author:

如果您从未听说过AFNetworking,恕我直言,它是网络库的全部。它是为 Objective-C 创建的,但它也适用于 Swift。用它的作者的话来说:

AFNetworking is a delightful networking library for iOS and Mac OS X. It's built on top of the Foundation URL Loading System, extending the powerful high-level networking abstractions built into Cocoa. It has a modular architecture with well-designed, feature-rich APIs that are a joy to use.

AFNetworking 是一个适用于 iOS 和 Mac OS X 的令人愉快的网络库。它建立在 Foundation URL 加载系统之上,扩展了 Cocoa 中内置的强大的高级网络抽象。它具有模块化架构,带有精心设计、功能丰富的 API,使用起来很愉快。

AFNetworking 2.0 supports iOS 6 and up, but in this example, I will be using its AFHTTPSessionManager class, which requires iOS 7 and up due to its usage of all the new APIs around the NSURLSession class. This will become obvious when you read the example below, which shares a lot of code with the NSURLSession example above.

AFNetworking 2.0 支持 iOS 6 及更高版本,但在此示例中,我将使用其 AFHTTPSessionManager 类,由于它使用了围绕 NSURLSession 类的所有新 API,因此需要 iOS 7 及更高版本。当您阅读下面的示例时,这将变得显而易见,该示例与上面的 NSURLSession 示例共享大量代码。

There are a few differences that I'd like to point out though. To start off, instead of creating your own NSURLSession, you'll create an instance of AFURLSessionManager, which will internally manage a NSURLSession. Doing so allows you take advantage of some of its convenience methods like -[AFURLSessionManager downloadTaskWithRequest:progress:destination:completionHandler:]. What is interesting about this method is that it lets you fairly concisely create a download task with a given destination file path, a completion block, and an input for an NSProgresspointer, on which you can observe information about the progress of the download. Here's an example.

不过,我想指出一些不同之处。首先,您将创建一个 AFURLSessionManager 实例,而不是创建自己的 NSURLSession,它将在内部管理 NSURLSession。这样做可以让您利用它的一些便利方法,例如-[AFURLSessionManager downloadTaskWithRequest:progress:destination:completionHandler:]. 这种方法的有趣之处在于,它可以让您相当简洁地创建一个下载任务,该任务具有给定的目标文件路径、一个完成块和一个NSProgress指针的输入,您可以在其中观察有关下载进度的信息。这是一个例子。

// Use the default session configuration for the manager (background downloads must use the delegate APIs)
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// Use AFNetworking's NSURLSessionManager to manage a NSURLSession.
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

// Create the image URL from some known string.
NSURL *imageURL = [NSURL URLWithString:@"http://www.google.com/images/srpr/logo3w.png"];
// Create a request object for the given URL.
NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
// Create a pointer for a NSProgress object to be used to determining download progress.
NSProgress *progress = nil;

// Create the callback block responsible for determining the location to save the downloaded file to.
NSURL *(^destinationBlock)(NSURL *targetPath, NSURLResponse *response) = ^NSURL *(NSURL *targetPath, NSURLResponse *response) {
    // Get the path of the application's documents directory.
    NSURL *documentsDirectoryURL = [self documentsDirectoryURL];
    NSURL *saveLocation = nil;

    // Check if the response contains a suggested file name
    if (response.suggestedFilename) {
        // Append the suggested file name to the documents directory path.
        saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:response.suggestedFilename];
    } else {
        // Append the desired file name to the documents directory path.
        saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:@"AFNetworking.png"];
    }

    return saveLocation;
};

// Create the completion block that will be called when the image is done downloading/saving.
void (^completionBlock)(NSURLResponse *response, NSURL *filePath, NSError *error) = ^void (NSURLResponse *response, NSURL *filePath, NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        // There is no longer any reason to observe progress, the download has finished or cancelled.
        [progress removeObserver:self
                      forKeyPath:NSStringFromSelector(@selector(fractionCompleted))];

        if (error) {
            NSLog(@"%@",error.localizedDescription);
            // Something went wrong downloading or saving the file. Figure out what went wrong and handle the error.
        } else {
            // Get the data for the image we just saved.
            NSData *imageData = [NSData dataWithContentsOfURL:filePath];
            // Get a UIImage object from the image data.
            self.imageView.image = [UIImage imageWithData:imageData];
        }
    });
};

// Create the download task for the image.
NSURLSessionDownloadTask *task = [manager downloadTaskWithRequest:request
                                                         progress:&progress
                                                      destination:destinationBlock
                                                completionHandler:completionBlock];
// Start the download task.
[task resume];

// Begin observing changes to the download task's progress to display to the user.
[progress addObserver:self
           forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
              options:NSKeyValueObservingOptionNew
              context:NULL];

Of course since we've added the class containing this code as an observer to one of the NSProgress instance's properties, you'll have to implement the -[NSObject observeValueForKeyPath:ofObject:change:context:]method. In this case, I've included an example of how you might update a progress label to display the download's progress. It's really easy. NSProgress has an instance method localizedDescriptionwhich will display progress information in a localized, human readable format.

当然,由于我们已将包含此代码的类作为观察者添加到 NSProgress 实例的属性之一,因此您必须实现该-[NSObject observeValueForKeyPath:ofObject:change:context:]方法。在本例中,我提供了一个示例,说明如何更新进度标签以显示下载进度。这真的很容易。NSProgress 有一个实例方法localizedDescription,它将以本地化的、人类可读的格式显示进度信息。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    // We only care about updates to fractionCompleted
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(fractionCompleted))]) {
        NSProgress *progress = (NSProgress *)object;
        // localizedDescription gives a string appropriate for display to the user, i.e. "42% completed"
        self.progressLabel.text = progress.localizedDescription;
    } else {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

Don't forget, if you want to use AFNetworking in your project, you'll need to follow its installation instructionsand be sure to #import <AFNetworking/AFNetworking.h>.

不要忘记,如果你想在你的项目中使用 AFNetworking,你需要遵循它的安装说明并确保#import <AFNetworking/AFNetworking.h>.

Alamofire

阿拉莫菲尔

And finally, I'd like to give a final example using Alamofire. This is a the library that makes networking in Swift a cake-walk. I'm out of characters to go into great detail about the contents of this sample, but it does pretty much the same thing as the last examples, just in an arguably more beautiful way.

最后,我想给出一个使用Alamofire的最后一个例子。这是一个使 Swift 中的网络变得轻而易举的库。我没有足够的字符来详细介绍这个示例的内容,但它的作用与上一个示例几乎相同,只是以一种可以说更漂亮的方式。

// Create the destination closure to pass to the download request. I haven't done anything with them
// here but you can utilize the parameters to make adjustments to the file name if neccessary.
let destination = { (url: NSURL!, response: NSHTTPURLResponse!) -> NSURL in
    var error: NSError?
    // Get the documents directory
    let documentsDirectory = NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory,
        inDomain: .UserDomainMask,
        appropriateForURL: nil,
        create: false,
        error: &error
    )

    if let error = error {
        // This could be bad. Make sure you have a backup plan for where to save the image.
        println("\(error.localizedDescription)")
    }

    // Return a destination of .../Documents/Alamofire.png
    return documentsDirectory!.URLByAppendingPathComponent("Alamofire.png")
}

Alamofire.download(.GET, "http://www.google.com/images/srpr/logo3w.png", destination)
    .validate(statusCode: 200..<299) // Require the HTTP status code to be in the Successful range.
    .validate(contentType: ["image/png"]) // Require the content type to be image/png.
    .progress { (bytesRead, totalBytesRead, totalBytesExpectedToRead) in
        // Create an NSProgress object to represent the progress of the download for the user.
        let progress = NSProgress(totalUnitCount: totalBytesExpectedToRead)
        progress.completedUnitCount = totalBytesRead

        dispatch_async(dispatch_get_main_queue()) {
            // Move back to the main thread and update some progress label to show the user the download is in progress.
            self.progressLabel.text = progress.localizedDescription
        }
    }
    .response { (request, response, _, error) in
        if error != nil {
            // Something went wrong. Handle the error.
        } else {
            // Open the newly saved image data.
            if let imageData = NSData(contentsOfURL: destination(nil, nil)) {
                dispatch_async(dispatch_get_main_queue()) {
                    // Move back to the main thread and add the image to your image view.
                    self.imageView.image = UIImage(data: imageData)
                }
            }
        }
    }

回答by Jhaliya

You cannot save anything inside the app's bundle, but you can use +[NSData dataWithContentsOfURL:]to store the image in your app's documents directory, e.g.:

您无法在应用程序包中保存任何内容,但可以使用+[NSData dataWithContentsOfURL:]将图像存储在应用程序的文档目录中,例如:

NSData *imageData = [NSData dataWithContentsOfURL:myImageURL];
NSString *imagePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"/myImage.png"];
[imageData writeToFile:imagePath atomically:YES];

Not exactly permanent, but it stays there at least until the user deletes the app.

不完全是永久的,但至少在用户删除应用程序之前它会一直存在。

回答by cem

That's the main concept. Have fun ;)

这就是主要概念。玩得开心 ;)

NSURL *url = [NSURL URLWithString:@"http://example.com/yourImage.png"];
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
path = [path stringByAppendingString:@"/yourLocalImage.png"];
[data writeToFile:path atomically:YES];

回答by Alexander

Since we are on IO5 now, you no longer need to write images to disk neccessarily.
You are now able to set "allow external storage" on an coredata binary attribute. According to apples release notes it means the following:

由于我们现在在 IO5 上,您不再需要将图像写入磁盘。
您现在可以在 coredata 二进制属性上设置“允许外部存储”。根据苹果的发行说明,这意味着以下内容:

Small data values like image thumbnails may be efficiently stored in a database, but large photos or other media are best handled directly by the file system. You can now specify that the value of a managed object attribute may be stored as an external record - see setAllowsExternalBinaryDataStorage:When enabled, Core Data heuristically decides on a per-value basis if it should save the data directly in the database or store a URI to a separate file which it manages for you. You cannot query based on the contents of a binary data property if you use this option.

像图像缩略图这样的小数据值可以有效地存储在数据库中,但最好由文件系统直接处理大照片或其他媒体。您现在可以指定托管对象属性的值可以存储为外部记录 - 请参阅setAllowsExternalBinaryDataStorage:启用后,核心数据启发式地根据每个值决定是否应将数据直接保存在数据库中或存储 URI到它为您管理的单独文件。如果使用此选项,则无法基于二进制数据属性的内容进行查询。

回答by andreacipriani

As other people said, there are many cases in which you should download a picture in the background thread without blocking the user interface

正如其他人所说,在很多情况下,您应该在不阻塞用户界面的情况下在后台线程中下载图片

In this cases my favorite solution is to use a convenient method with blocks, like this one: (credit -> iOS: How To Download Images Asynchronously (And Make Your UITableView Scroll Fast))

在这种情况下,我最喜欢的解决方案是使用一种带有块的便捷方法,例如:(信用 -> iOS:如何异步下载图像(并使您的 UITableView 快速滚动)

- (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
{
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
                               if ( !error )
                               {
                                   UIImage *image = [[UIImage alloc] initWithData:data];
                                   completionBlock(YES,image);
                               } else{
                                   completionBlock(NO,nil);
                               }
                           }];
}

And call it like

并称之为

NSURL *imageUrl = //...

[[MyUtilManager sharedInstance] downloadImageWithURL:[NSURL URLWithString:imageURL] completionBlock:^(BOOL succeeded, UIImage *image) {
    //Here you can save the image permanently, update UI and do what you want...
}];

回答by Mohd. Asif

Here is code to download an image asynchronously from url and then save where you want in objective-c:->

这是从 url 异步下载图像的代码,然后将其保存在 Objective-c 中您想要的位置:->

    + (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
        {
            NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
            [NSURLConnection sendAsynchronousRequest:request
                                               queue:[NSOperationQueue mainQueue]
                                   completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
                                       if ( !error )
                                       {
                                           UIImage *image = [[UIImage alloc] initWithData:data];
                                           completionBlock(YES,image);
                                       } else{
                                           completionBlock(NO,nil);
                                       }
                                   }];
        }

回答by Bobby

Here's how I download an ad banner. It's best to do it in the background if you're downloading a large image or a bunch of images.

这是我下载广告横幅的方法。如果您要下载大图像或一堆图像,最好在后台执行此操作。

- (void)viewDidLoad {
    [super viewDidLoad];

    [self performSelectorInBackground:@selector(loadImageIntoMemory) withObject:nil];

}
- (void)loadImageIntoMemory {
    NSString *temp_Image_String = [[NSString alloc] initWithFormat:@"http://yourwebsite.com/MyImageName.jpg"];
    NSURL *url_For_Ad_Image = [[NSURL alloc] initWithString:temp_Image_String];
    NSData *data_For_Ad_Image = [[NSData alloc] initWithContentsOfURL:url_For_Ad_Image];
    UIImage *temp_Ad_Image = [[UIImage alloc] initWithData:data_For_Ad_Image];
    [self saveImage:temp_Ad_Image];
    UIImageView *imageViewForAdImages = [[UIImageView alloc] init];
    imageViewForAdImages.frame = CGRectMake(0, 0, 320, 50);
    imageViewForAdImages.image = [self loadImage];
    [self.view addSubview:imageViewForAdImages];
}
- (void)saveImage: (UIImage*)image {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString* path = [documentsDirectory stringByAppendingPathComponent: @"MyImageName.jpg" ];
    NSData* data = UIImagePNGRepresentation(image);
    [data writeToFile:path atomically:YES];
}
- (UIImage*)loadImage {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString* path = [documentsDirectory stringByAppendingPathComponent:@"MyImageName.jpg" ];
    UIImage* image = [UIImage imageWithContentsOfFile:path];
    return image;
}

回答by ASHISHT

If you are using AFNetworking library to download image and that images are using in UITableview then You can use below code in cellForRowAtIndexPath

如果您使用 AFNetworking 库下载图像并且该图像在 UITableview 中使用,那么您可以在 cellForRowAtIndexPath 中使用以下代码

 [self setImageWithURL:user.user_ProfilePicturePath toControl:cell.imgView]; 
 
-(void)setImageWithURL:(NSURL*)url toControl:(id)ctrl
{
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request imageProcessingBlock:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
        if (image) {
            if([ctrl isKindOfClass:[UIButton class]])
            {
                UIButton btn =(UIButton)ctrl;
                [btn setBackgroundImage:image forState:UIControlStateNormal];
            }
            else
            {
                UIImageView imgView = (UIImageView)ctrl;
                imgView.image = image;
            }

} } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { NSLog(@"No Image"); }]; [operation start];}

回答by Mohd. Asif

You can download image without blocking UI with using NSURLSessionDataTask.

您可以使用 NSURLSessionDataTask 在不阻塞 UI 的情况下下载图像。

+(void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
 {
 NSURLSessionDataTask*  _sessionTask = [[NSURLSession sharedSession] dataTaskWithRequest:[NSURLRequest requestWithURL:url]
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if (error != nil)
        {
          if ([error code] == NSURLErrorAppTransportSecurityRequiresSecureConnection)
                {
                    completionBlock(NO,nil);
                }
         }
    else
     {
      [[NSOperationQueue mainQueue] addOperationWithBlock: ^{
                        dispatch_async(dispatch_get_main_queue(), ^{
                        UIImage *image = [[UIImage alloc] initWithData:data];
                        completionBlock(YES,image);

                        });

      }];

     }

                                            }];
    [_sessionTask resume];
}