In front of the word

The introduction

Learn all kinds of knowledge points of iOS, such as Runloop, only understand the principle is not good, to understand its application scenarios. Study SDWebImage framework source code, only look at SDWebImage business logic, is not really understand. Effective learning is constantly asking questions, mining and summarizing SDWebImage knowledge points involved in these scenes, and then imitating it, combining those scenes to write this knowledge point is really understand. According to my observation, SD involves the following knowledge points: Mode switching in runloop, runtime associated objects, multi-threaded NSOperation starts and suspends, the difference between NSURLSession and NSURLConnection in network operations, NSCache for caching, NSFileManager operations, and so on.

directory
  1. Network Knowledge 1.1 Based on the scheme: NSURLConnection and NSURLSession 1.2 Breakpoint continuation (non-persistent, APP restart can not be resumed) 1.3 persistent breakpoint continuation (persistent, APP restart can be resumed) 1.4 Two forms of NSURLSession: Block and proxy 1.5 operate and change the location of files downloaded by NSURLSession

  2. Multithreading and NSOperation 2.1 SDWebImage based on NSURLConnection 2.2 SDWebImage based on NSURLSession

  3. Suspend mode switch of Runloop vs SUSPEND suspend of NSOperaiton 3.1 SDWebImage based on NSURLConnection 3.2 SDWebImage based on NSURLSession

  4. Cache and the key to get the memory cache file operations article 4.1-4.2 imageFromMemoryCacheForKey for disk cache key – diskImageForKey: 4.3 in addition to download cache examination before operation, real disk caching still have what use?

  5. Behind the words

  6. Thank you

The reason for writing this article

Somebody asked me, when I’m optimizing the UITableView, does SDWebImage pause the download when I swipe the screen? If so, how? If not, how? At first, I looked up the old version of SDWebImage and found that NSURLConnection would open several new child threads, but its callback would work on the main thread and specify that the current thread was working in defaultMode when it was created. If a child thread does have a runloop and switches mode, it does pause the child thread. But some people don’t know that this is a different thread (I’ve been misled). If so, some people say that the NSURLConnection based SDWebImage works on the main thread and specifies its mode, so the screen slide causes the main thread’s Runloop to switch mode. As a result, operations on SDWebImage on defaultMode will be paused. They are on the basis of original SDWebImage SDWebImageDownloaderOperation. In m to have such a words:

“Because it passes CFRunLoopInMode(…) This specifies the mode of the runloop on the main thread, so swiping the screen switches the mode of the Runloop and causes the SDWebImage to pause. There is a flaw in this sentence. It is not clear that these are two different threads. The main thread of the runloop switch with the child thread (in SDWebImageDownloaderOperation. M) in the runloop gan? The experimental results are shown in the figure below:

So, the question is, which part of SDWebImage works on the main thread? In fact, only the uppermost callback (in UIImageView+WebCache.m) works on the main thread:

As shown above, wself.image = image; Work is in the main thread, the main thread of the runloop mode switching effect is UIImageView + WebCache m of this, with SDWebImageDownloaderOperation. M file that part of the CFRunloop control has nothing to do.

However, it turns out that the new version of SDWebImage is based on NSURLSession, which differs from NSURLConnection in that it is not controlled by obtaining the runloop of the current thread. Instead, a new subthread is opened through NSOPeration, and the main thread runloop switch does not affect the subthread.

Regardless of whether the underlying work is in the child thread or not, the last thing that gets the image and sets it to the ImageView is in the top callback, and this part of the code is executed on the main thread, so it has this slipping-and-pausing effect. Later, I went to the iOS development group to discuss this, and got some inspiration and affirmation from some friends.

As you can see, as long as the setImage part is in the main thread, there is a slide pause effect. But what about not being in defualtMode? The solution could be this:

self.imageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];Copy the code

But what about setImage that’s not on the main thread? Send the selector to the main thread. The solution could be this:

[self performSelectorOnMainThread:@selector(setImage:)
                       withObject:downloadedImage
                    waitUntilDone:YES
                            modes:@[NSDefaultRunLoopMode]];Copy the code

This runs setImage and its parameters on the main thread and sets the mode.

The waitUntilDone parameters are as follows:

  • When YES, let the main thread run some of the selector operations first, and then do the operations in the current thread.
  • When it’s NO, do the operations in the current thread first, and then have the main thread run some of the selector operations.

1. Network knowledge

1.1 Based scheme: NSURLConnection and NSURLSession

  • The old version of SDWebImage is based on NSURLConnection. The new version of SDWebImage is based on NSURLSession.

  • Based on the NSURLConnection SDWebImage management, through the runloop although SDWebImage underlying the call stack SDWebImageDownloaderOperation (inherited from NSOperation) will open a new thread, NSURLConnection also creates two new child threads, but the final network request callback (that is, the final setImage part) works on the main thread.

  • The nsurLSession-based SDWebImage is managed through NSOperation, and although new subthreads are opened, the final network request callback also works on the main thread.

1.2 Resumable transfer (non-persistent, the APP can not be resumed after restart)

  • First comb the breakpoint steps: first download — — > to cancel the download > save have good data to memory – > moratorium — — > continue to download >…

NSURLSession

  • Undownload key apis
- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler; .Copy the code

This method can cancel and keep the downloaded data. It calls back a parameter, resumeData, which contains the requested path to the file downloaded, and the location of the file downloaded before the breakpoint.

  • Key APIS for breakpoint continuation:
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData; .Copy the code

You can use the resumeData that was downloaded before the last stop to start a new task to continue the download. Since we involve saving the resumeData we downloaded last time, we will save resumeData as a global variable so that we can use it. Such as:

self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];Copy the code
  • Summary: Add the following steps to the button that implements breakpoint download:
    1. Set a downloadTask, session, and resumeData global variable
    2. If you start downloading, create a new downloadTask and start the download
    3. If the download is suspended, call the function that cancelled the download and save the current resumeData in the block to the global resumeData.
    4. If the download resumes, add the last saved resumeData to the task and start the download.

1.3 Persistent breakpoint resume (persistent, APP can resume after restart)

ResumeData is stored as a property. It is stored in memory.

What if you want to implement persistent breakcontinued? Put it in the sandbox.

  • Combing the breakpoint steps here: download — — > to cancel the download for the first time > save have good data to the sandbox – > moratorium — — > continue to download >…

There are a lot of wonderful content behind, here temporarily do not paste the code, so only to provide ideas. However, this implementation requires a little more logic than non-persistent breakpoint continuation and needs to be done in conjunction with the NSURLSession proxy.

1.4 Two forms of NSURLSession: Block and proxy

(1) Block

// Create the download path NSURL *url = [NSURL URLWithString:@"http://bmob-cdn-8782.b0.upaiyun.com/2017/01/17/c6b6bb1640e9ae9e80b221c454c4e90d.jpg"]; NSURLRequest *request = [NSURLRequest requestWithURL: URL]; NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL: URL completionHandler:^(NSURL *location, NSURLResponse *response, NSError * error) {/ / file will be moved to the specified directory nsstrings * documentsPath = [NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, NSString *newFilePath = [documentsPath] StringByAppendingPathComponent: response. SuggestedFilename]; / / move files to the new path to the [[NSFileManager defaultManager] moveItemAtPath:location.path toPath:newFilePath error:nil]; }]; // start the downloadTask [downloadTask resume];Copy the code

(2) agent

  • The delegate cannot be specified by setter methods
/ / create the download path NSURL * url = [NSURL URLWithString: @ "http://dldir1.qq.com/qqfile/QQforMac/QQ_V5.4.0.dmg"); // Create the NSURLSession object and design the proxy method. The NSURLSessionConfiguration as the default configuration NSURLSession * session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]]; NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL: URL]; // start the task [downloadTask resume];Copy the code
  • An agent method that can be implemented by the agent. It is used when progress or data has been obtained, such as breakpoint continuation
Method # pragma mark < NSURLSessionDownloadDelegate > / file called when the download is complete * * * * / - (void) URLSession session: (NSURLSession *) DownloadTask: (NSURLSessionDownloadTask *) downloadTask didFinishDownloadingToURL: (NSURL *) location {/ / file will be moved to the specified directory NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; / / a new file path nsstrings * newFilePath = [documentsPath stringByAppendingPathComponent: @ "QQ_V5. 4.0. DMG"]. NSLog(@"File downloaded to: %@",newFilePath); / / move files to the new path to the [[NSFileManager defaultManager] moveItemAtPath: location. The path toPath: newFilePath error: nil]; } /** * This method is called each time data is written to a temporary file. Can be here for a download progress * * @ param bytesWritten the write file size * @ param totalBytesWritten has written to sandbox file size * @ param totalBytesExpectedToWrite Total file size */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten TotalBytesExpectedToWrite: int64_t totalBytesExpectedToWrite {/ / download progress self. ProgressView. Progress = 1.0 * totalBytesWritten / totalBytesExpectedToWrite; Self.progresslabel. text = [NSString stringWithFormat:@" Current download progress :%.2f%%",100.0 * totalBytesWritten / totalBytesExpectedToWrite]; } /** * Call */ - (void)URLSession (NSURLSession *)session downloadTask (NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes1 { }Copy the code

1.5 NSURLSession Changes the location of the downloaded file

You can use the downloadTaskWithURL callback location.path to get the file directory and copy or cut it to your desired directory.

The API:

  • copy
[[NSFileManager defaultManager] copyItemAtPath:location.path toPath:path error:NULL];Copy the code
  • shear
/ / move files to the new path to the [[NSFileManager defaultManager] moveItemAtPath: location. The path toPath: newFilePath error: nil];Copy the code

Such as:

// Create a downLoadTask task for the global session; / / the location parameters: [[[NSURLSession sharedSession] downloadTaskWithURL: URL completionHandler:^(NSURL *location, NSURLResponse *response, NSError * error) {/ / to specified path nsstrings * path = [NSSearchPathForDirectoriesInDomains (NSCachesDirectory NSUserDomainMask, YES) lastObject]; path = [path stringByAppendingPathComponent: FileName]; / / copy the files in the past [[NSFileManager defaultManager] copyItemAtPath: location. The path toPath: path error: NULL]; resume}]]; // Start the taskCopy the code

2. Multithreading and NSOperation

2.1 SDWebImage based on NSURLConnection

  • bysd_setImageWithURLMethod how to initiate network operations? Here is the call stack:

UIImageView+WebCache.m

- (void)sd_setImageWithURL:(NSURL *)url{Copy the code

And what it calls is this one,

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock{Copy the code

And it says,

// balabala
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:
// balabalaCopy the code

This statement returns an SDWebImageOperation with the purpose of caching the operation in the dictionary type operation cache property of UIView via the sd_setImageLoadOperation: method.

[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];Copy the code

But where does the system’s native UIView class get this dictionary property? Are objects that are classified by UIView+WebCacheOperation to be associated with UIView (in this case, UIImageView). This is where the knowledge of runloop’s associated objects comes in. Sd_setImageLoadOperation: implementation, not principle: