As an iOS developer, you must have used several image download libraries, including SDWebImage, Kingfisher from Swift.

The purpose of this article is to drag you into the pit to construct a new wheel that AWebImage will use to download images in iOS apps.

For this new pit, he is

  • NSURLCache and NSCache are used for caching.
  • Download using NSURLSession;

He has no:

  • Not implementing their own caching system;
  • There’s no image manipulation, so he can’t create thumbnails, let alone GIFs;

I don’t want to say much about NSURLSession, because most network request libraries now use NSURLSession instead of NSURLConnection. But I do want to talk about NSURLCache.

Article: before NSURLCache

Now most of the images are downloaded libraries are using self-built cache system, of course we know that the principle is very simple, each corresponding to the URL in the local save a file, repeat the request URL will no longer need to access the network, but read the file from the local, in order to more efficient access to images, we also will save the image in memory. Thus, when we first request an image, we first try to read the file from memory, if not from disk, if not, then we actually request the address on the network.

The process of saving files is so simple, but when it comes to managing those cached files, it’s the real problem;

  • How do we know when a cached image expires (and maybe it never expires);
  • Our disks are not infinitely large, so how do we delete those long-ago image files?
  • Our memory is not infinite, how can I manage some of these cache files in memory? What do I do when I run out of memory?

So we have to use more mechanisms to manage these cache files, we can have a file to record the address and expiration time of these files, we have to monitor the disk and memory usage in the App in real time, and remove unnecessary image references when appropriate. Once you get started on this part of the job, you’ll find it so complicated that you’ll probably just give up and leave them alone. Our initial goal was to reduce the number of images downloaded and display them more efficiently in the App. Anyway, this goal has been achieved.

However, once you use NSURLCache, all you need to do is set the disk size and memory size. The advantages of using NSURLCache are:

  • The system automatically manages the cache content, so you don’t have to know when to delete the cache content from the disk or memory during App development. You don’t have to worry about how to effectively clean them up when the system runs out of memory. If the disk or memory becomes stressed, the system will clean them up automatically. Of course, you don’t have to worry about the cache time.
  • I feel that the biggest advantage is cache-control, because we just set the content of cache-Control on the server according to the Http protocol to tell NSURLCache when the Cache should expire. There is no need to write a line of code on the client (this is the default caching policy for NSURLRequest);

Use NSURLCache cache

The most convenient thing about using NSURLCache is that it only takes a few lines of code to create a stable cache system;

The first step is to create an NSURLCache, which specifies the size of the disk and memory used for caching.

let cache = NSURLCache(memoryCapacity: 10 * 1024 * 1024,diskCapacity: 30 * 1024 * 1024,diskPath: "adow.adimageloader.urlcache")Copy the code

Construct a SessionConfiguration and use the URLCache

sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
sessionConfiguration.URLCache = cacheCopy the code

Use this sessionConfiguration to construct an NSURLSession

let session = NSURLSession(configuration: sessionConfiguration, delegate: nil, delegateQueue: self.sessionQueue)Copy the code

For simplicity, you can set a global NSURLCache so that all requests will use this URLCache if not specified.

NSCache.setSharedURLCache(cache)Copy the code

And then, just like without caching, you can use session and NSURLRequest to make requests without changing anything, like using dataTaskWithRequest,

self.task = session.dataTaskWithRequest(request) { (data, response, error) in
        if let error = error {
            NSLog("error:%@", error.domain)
        }
        ...
    }Copy the code

When the NSURLRequest hits the cache, the dataTaskWithRequest will not make an actual network request, but will fetch the content from the cache, and we don’t care whether the cache comes from disk or memory.

CachePolicy

The main advantage of using NSURLCache is that you can manage the local Cache policy through server Control cache-control, and you can also specify several other policies.

  • NSURLRequestReloadIgnoringLocalCacheData: ignore the cache, it must be from the remote address download;
  • NSURLRequestReturnCacheDataElseLoad: as long as a local cache using local cache (regardless of the expiration time), only if the local cache using remote address download;
  • NSURLRequestReturnCacheDataDontLoad: only content was obtained from the local cache, if doesn’t, also won’t go to a remote address download (that is, offline mode);
  • NSURLRequestUseProtocolCachePolicy: the default caching strategy;

There are two places to set a CachePolicy:

  • A single request can be set for each NSURLRequestcachePolicy;
  • But can be by NSURLSessionConfiguration SettingscachePolicyTo implement theNSURLSessionAll requests are cached using the same cache policy.

Cache-Control

For the most commonly used NSURLRequestUseProtocolCachePolicy, we need to have the attention of the two places, even in the presence of a request in the local cache, If the request needs to be revalidated, a HEAD request will be sent to the server to determine whether the content has been modified, and if it has been modified, it will be redownloaded. If the cache content does not require validation, the system only needs to determine whether the cache time has expired.

In development, we only need to set the cache-Control response header on the server for each image resource, such as the address below

7 vihfk.com1.z0.glb.clouddn.com/photo-14573…

Cache-control :public, max-age=31536000 Response Header: cache-control :public, max-age=31536000 But in fact, during App development, we don’t need to worry about this, the only thing we need to do is tell the server developer to make sure that the correct Header is printed in the static resource.

In addition to cache-control, there are two fields that can be used to Control the Cache:

  • Last-ModifiedThe value of this header indicates when the requested resource was last modified;
  • EtagThis is short for “Entity Tag,” which is an identifier that represents the content of the requested resource.

For more information on the use of NSURLCache (such as custom caches), see nshipster.cn/nsurlcache/

However, there are a few potholes to be aware of when using NSURLCache

Post: implement AWebImage

Having introduced NSURLCache, we can start digging holes to implement our own image caching system, AWebImage, which consists of two parts;

  • AWebImage: containsAWImageLoaderObject to actually download the image.
  • UIImageView+AWebImage: extension of UIImageView. After all, most of the time we display downloaded images directly on UIImageView.

In addition, I also wrote a Demo to show this function. This App calls the interface of 500px.com to edit and select the list of pictures. We will display them in a UICollectionView.

Build AWebImage

We actually use an AWImageLoader that calls the func downloadImage(URL :NSURL, callback: AWImageLoaderCallback) to retrieve the web image content (or from the local cache).

Network access depends on NSURLSession. Using a block-based interface can quickly write out a network request operation, but before we do that we need to think about what NSURLSession needs to use;

  • A queue for network requests (NSOperationQueue);
  • A cache shared by all image requests (NSURLCache)
  • Determine the cache policy, in this case use the default cache policy;

All you need for all of this is a shared NSURLSession where all requests are made; So we can construct a single unique instance of NSURLSession (a global variable), and then all AWImageLoader uses the same session;

But we need something else that we can all share

  • List of callback functions;
  • Fast cache;
  • Other asynchronous operations share the queue;

The callback function

Since downloading images from the Internet is an asynchronous process, we need a callback function, in this case of type AWImageLoaderCallback, which is actually (UIImage,NSURL) -> (), so after getting the image content, We’re going to get UIImage and NSURL;

Sometimes there are two UIImageViews in the same interface that display the same image address. Instead of requesting the image twice, we just call their callbacks to display the image after retrieving the image content. So we might have multiple callbacks for each Url request. This is very common in UITableView and UICollectionView, because during scrolling, a lot of content is reloaded because of the Cell Reuse;

So we establish a fetchList: [String: AWImageLoaderCallbackList], that is, for each url, will there may be a list of corresponding callback function; When a request is completed, the corresponding callback function list is obtained and called successively, and then the callback list corresponding to the URL is cleared.

However, since all requests are done in asynchronous threads, this callback queue may operate in multiple threads, so we have to do some locking, locking when adding and removing callback functions, using dispatch_barrier_sync

// add a callback function to the URL. If the url is already in the task, just add the callback function. Func addFetch(key:String, callback:AWImageLoaderCallback) -> Bool { var skip = false let f_list = fetchList[key] if f_list ! = nil { skip = true } dispatch_barrier_sync(fetchListOperationQueue) { if var f_list = f_list { f_list.append(callback) Self. fetchList[key] = f_list} else {self.fetchList[key] = [callback,]}} return skip removeFetch(key:String) { dispatch_barrier_sync(fetchListOperationQueue) { self.fetchList.removeValueForKey(key) } } /// Func clearFetch() {dispatch_barrier_async(fetchListOperationQueue) {self.fetchlist.removeall ()}}Copy the code

Fast cache

Since NSURLCache already uses both memory and disk caching, why do we need another fast cache?

Because all caches of requests in NSURLCache fetch NSData, So every time you get something, you still have to construct it as a UIImage, and in the system + (UIImage * _Nullable)imageWithData:(NSData * _Nonnull)data doesn’t cache images, So it will cause you to create UIImage repeatedly. Another reason is that retrieving content from NSURLCache is also an asynchronous process. If we store our images in a separate in-memory cache, we only need to fetch the contents from this cache once each time we download the image (and we don’t have to do this asynchronously, which is very important for displaying images in the UICollectionViewCell). If you do not continue to download from NSURLCache or source address;

As a fast cache, NSCache is best suited. It will also automatically clean up the contents when the system memory is stressed, similar to NSMutableDictionary, but it is more efficient and fast, does not copy objects, and is safe to operate in any thread. Using NSCache is as simple as specifying the size of memory to use:

fastCache = NSCache()
fastCache.totalCostLimit = 30 * 1024 * 1024Copy the code

AWImageLoader and AWImageLoaderManager

We manage all shared things through AWImageLoaderManager, operation queue, NSURLCache, NSCache, callback queue, shared Session, etc., as a singleton;

Each download task is initiated by an AWImageLoader, which retrieves all shared content (and a shared session) from the AWImageLoaderManager. We can merge AWImageLoader and AWImageLoaderManager to form a singleton. He wants to hold the request task in the AWImageLoader object for later administration (cancellation). Since the AWImageLoader is created for each task, the repeated content is separated out and shared for all tasks through a singleton AWImageLoaderManager that doesn’t need to be known externally. Just call the AWImageLoader method;

Here is the code to actually get the image from AWImageLoader (first from fastCache (NSCache), then from URLCache or the real network. We don’t need to get the image from URLCache ourselves. This is done by NSURLSession.

func downloadImage(url:NSURL, callback : AWImageLoaderCallback){// Get and construct the image from fastCache(NSCache), If let cached_image = self.imageFromFastCache(url) {callback(cached_image, Fetch_key = url.absoluteString let f_callback = {(image:UIImage) -> () in if let f_list = AWImageLoaderManager.sharedManager.readFetch(fetch_key) { AWImageLoaderManager.sharedManager.removeFetch(fetch_key) dispatch_async(dispatch_get_main_queue(), {f_list.forEach({(f) in f(image,url)})})}} /// origin /// / addFetch returns whether the request is already in the list. Only need to add a callback function for him it is ok to let the skip. = AWImageLoaderManager sharedManager. AddFetch (fetch_key, callback: callback) if skip { // NSLog("skip") return } /// request let session = AWImageLoaderManager.sharedManager.defaultSession let request = NSURLRequest(URL: url) self.task = session.dataTaskWithRequest(request) { (data, response, error) in if let error = error { NSLog("error:%@", error.domain) } /// no data guard let _data = data else { NSLog("no image:%@", url.absoluteString) f_callback(emptyImage) return } dispatch_async(AWImageLoaderManager.sharedManager.imageDecodeQueue, { // NSLog("origin:%@", url.absoluteString) let image = UIImage(data: _data) ?? emptyImage AWImageLoaderManager.sharedManager.fastCache.setObject(image, forKey: Return (image)} self.task?.resume()}Copy the code

UIImageView + AWebImage

As mentioned earlier, most of the time we are displaying downloaded images in UIImageView, so creating an extension for UIImageView to download and display web images is the most natural way. Here’s a UIImageView + AWebImage that actually looks like this when it’s called:

imageView.aw_downloadImageURL(photo.imageURL, showLoading: true, completionBlock: { (image, url) in

       })Copy the code

AWImageLoader func downloadImage(URL :NSURL, callback: AWImageLoaderCallback) is a simple call.

But given that the images that UIImageView displays might not be fixed, like in the UITableViewCell, the UICollectionViewCell, because when you reuse it it’s going to display images from different places on the same UIImageView, In many cases, retrieving images is done asynchronously in an unpredictable order, so it’s possible that the image that ends up on the screen is not the one you want. There are two solutions:

  • When you start getting images, it will cancel thisUIImageViewPrevious image tasks;
  • For eachUIImageViewStore the download address of the current task. After obtaining the image, determine the address of the callback image and save it inUIImageViewIs the address in, if different, it means that the actual display of the picture has been changed, then there is no need to display this picture.

The second solution is used here, adding an attribute aw_image_URL to UIImageView to store the address of the current task. Since the storage attribute cannot be added in extension, we can only rely on the Associated Object to implement it:

private var imageUrlKey : Void? Imageurl var aw_image_url: NSURL? { get{ return objc_getAssociatedObject(self, &imageUrlKey) as? NSURL } set { objc_setAssociatedObject(self, &imageUrlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } }Copy the code

Uiimageview. aw_downloadImageURL (uiImageView. aw_downloadImageURL); uiImageView. aw_downloadImageURL;

func aw_downloadImageURL(url:NSURL, showLoading:Bool, CompletionBlock: AWImageLoaderCallback) {/ / / set to download picture address the self. The first aw_image_url = url if showLoading {self. Aw_showLoading ()}  let loader = AWImageLoader() loader.downloadImage(url) { [weak self](image, url) in if showLoading { self? .aw_hideLoading() } guard let _self = self, Let _aw_image_url = _self.aw_image_url else {NSLog("no imageView") return _aw_image_url.absoluteString ! = url.absoluteString { NSLog("url not match:%@,%@", _aw_image_url,url) } else{ self? .aw_setImage(image) completionBlock(image,url) } } }Copy the code

Start downloading images in NSDefaultRunLoopMode;

If you use AWebImage in UICollectionView, it will start processing in any RunLoop mode, but sometimes this is a waste of time because a lot of the download is actually replaced during the scroll, and it’s best to wait until the scroll is over before the actual image is downloaded.

If you start the download process in NSDefaultRunLoopMode, it doesn’t really start until the scroll ends, and the downloaded code is committed late. However, because the cell reuse reuses the previous image, no image is actually displayed when scrolling. This is where fastCache comes in. If you can retrieve content from fastCache, it will be displayed directly in the cell (instead of waiting until the scroll ends). If there is no image in fastCache, the download will start later.

But this leads to another problem, in fast scrolling, the task that might start to download is delayed while scrolling, But then the cell is reused and then he gets the image from fastCache in another location and displays it, and then the delayed task gets triggered later, and then he comes back, This will replace the image obtained from fastCache, and you will see that the image displayed in the Cell has changed and is not displayed in the desired location. Note that in this case there is no way to ignore the image display through the saved AW_image_URL match because the code is executed later (delayed submission), so the address has been updated. The solution is to introduce the aw_image_set as a judgment. If the AW_image_set is set before the task actually starts, the task doesn’t need to start because the UIImageView already displays the correct image.

Func aw_downloadImageURL_delay(url:NSURL, showloading:Bool, completionBlock: AWImageLoaderCallback) {self.aw_image_set = false if there is already an existing image, Don't load in DefaultRunLoopMode let loader = AWImageLoader () if the let cached_image = loader. ImageFromFastCache (url) { self.aw_hideLoading() self.aw_setImage(cached_image) self.aw_image_url = url completionBlock(cached_image, Let par = _AWImageLoaderPar(url: url, showLoading: showLoading, completionBlock: completionBlock) self.performSelector(#selector(UIImageView.aw_downloadImageURL_p(_:)), withObject: par, afterDelay: 0.0, inModes: [NSDefaultRunLoopMode,])} // Reuse reuses an image from a reuse object. He will replace the first one again, so check to see if any images are set before starting; @objc private func aw_downloadImageURL_p(par:_AWImageLoaderPar) { if self.aw_image_set { NSLog("image existed") return }  self.aw_downloadImageURL(par.url, showLoading: par.showLoading, completionBlock: par.completionBlock) } @objc private func aw_setImage(image:UIImage){ self.image = image self.aw_image_set = true /// Update the aw_image_set after setting the image, in case later tasks replace the current image}Copy the code

Complete AWebImage and Demo App code

Github.com/adow/AWebIm…

Because 500px.com is outside the wall, the image in this example will sometimes fail to download, and then the image will be displayed blank in UIImageView, and try again when the interface is updated after the next scroll.