YYKit series source code analysis article:

  • YYText source code analysis: CoreText and asynchronous drawing
  • YYModel source code analysis: focus on performance
  • YYCache source code analysis: highlights
  • YYImage source code analysis: image processing skills
  • YYAsyncLayer source code analysis: asynchronous drawing
  • YYWebImage source code analysis: thread processing and caching strategy

The introduction

In iOS development, the asynchronous Web image download framework can be a huge productivity liberator, often with simple code developers can asynchronously download web images and display them on the phone screen, with cache optimization.

The most famous asynchronous image download framework in the industry is SDWebImage, and then the predecessor of Ibireme opened source YYWebImage to optimize the performance. I have briefly browsed the source code of SDWebImage before. Compared with the source code of YYWebImage, THE author actually prefers YYWebImage, because its code style is very simple and its code structure is clearer.

Technically, the two have different processing methods for thread processing, and the cache strategy is also different in details. Although YYWebImage has superior performance in the author’s understanding, it is not verified by sufficient test cases. Unfortunately, YYWebImage seems to have not been maintained for a long time. The author said in this issue that NSURLConnection will be replaced by NSURLSession, so far there is no action 😂.

So the actual development in order to stability may still be the first choice SDWebImage, but this does not affect us to learn YYWebImage excellent source code, this paper is mainly to analyze the core ideas and highlights of YYWebImage.

Source version: 1.0.5

I. Overview of the framework

H (.m) // The request task preprocessing class _YYWebImagesetter.h (.m) // The request task management class YYWebImagemanager.h (.m) // Custom request class (inherits from NSOperation) yyWebImageOperation.h (.m) // Convenient classification of business call CALayer+ yyWebImage.h (.m) MKAnnotationView+YYWebImage.h (.m) UIButton+YYWebImage.h (.m) UIImage+YYWebImage.h (.m) UIImageView+YYWebImage.h (.m)Copy the code

The above classification for convenient business invocation, their implementation is almost the same, the most used is UIImageView+YYWebImage.h, can be used as the entrance to explore the principle of the framework.

As the author’s framework briefly explains:

Asynchronous image loading framework.

The core of this framework is the asynchronous downloading of network images.

  • Since asynchronous downloading involves efficient scheduling of threads, performance of thread processing is critical because the task of downloading images in a business scenario can be onerous.
  • After the image is downloaded successfully, in order to avoid decompression in the main thread when the image is displayed, the framework does asynchronous decompression, forGIF, APNG, WebPAnd so have support, this part of the function is based on the author of another framework YYImage, the author wrote source analysis before:YYImage source code analysis: image processing skills.
  • In order not to repeat the download and unpack, do the cache optimization framework, as to whether to cache the image after the decompression, can choose by the developer, of course, share memory buffer and disk cache, read and write speed and memory more than disk, this part of the function is based on the author’s YYCache another framework, the author was written before the source analysis: YYCache source code analysis: highlights.

Repeat download request processing

The processing is based on an attribute under _yyWebImagesetter.h:

@property (nonatomic, readonly) int32_t sentinel;
Copy the code

From a method of UIImageView+ yyWebImage.h:

- (void)yy_setImageWithURL:(NSURL *)imageURL placeholder:(UIImage *)placeholder options:(YYWebImageOptions)options manager:(YYWebImageManager *)manager progress:(YYWebImageProgressBlock)progress transform:(YYWebImageTransformBlock)transform completion:(YYWebImageCompletionBlock)completion { ... // Step 1: Bind a _YYWebImageSetter object to UIImageView _YYWebImageSetter *setter = objc_getAssociatedObject(self, &_YYWebImageSetterKey);if(! setter) { setter = [_YYWebImageSetter new]; objc_setAssociatedObject(self, &_YYWebImageSetterKey, setter, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } int32_t sentinel = [setter cancelWithNewURL:imageURL]; _yy_dispatch_sync_on_main_queue(^{ ... __weak typeof(self) _self = self; dispatch_async([_YYWebImageSetter setterQueue], ^{ ... // Step 2: Start downloading task newSentinel = [settersetOperationWithSentinel:sentinel url:imageURL options:options manager:manager progress:_progress transform:transform completion:_completion];
            weakSetter = setter;
        });
    });
}
Copy the code

I have omitted most of the code to ignore these threading operations and now focus only on handling repeated requests.

In the first step, UIImageView is bound to a _YYWebImageSetter object with the Runtime, and then a cancelWithNewURL: method is called: cancelWithNewURL:

- (int32_t)cancelWithNewURL:(NSURL *)imageURL {
    int32_t sentinel;
    dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
    if (_operation) {
        [_operation cancel];
        _operation = nil;
    }
    _imageURL = imageURL;
    sentinel = OSAtomicIncrement32(&_sentinel);
    dispatch_semaphore_signal(_lock);
    return sentinel;
}
Copy the code

You can see that the author cancels the _operation task, or the last requested task, for repeated requests on the same UIImageView.

Then there is a crucial line of code: sentinel = OSAtomicIncrement32(&_sentinel); Atomic increment is used to ensure thread safety and read performance of global variable _sentinel. That is, for the same UIImageView each time yy_setImageWithURL:… Method will cancel the last request and increment its _sentinel by one.

The point of this? Look down.

In the second step, call _YYWebImageSetter setOperationWithSentinel:… Methods:

- (int32_t)setOperationWithSentinel:(int32_t)sentinel url:(NSURL *)imageURL options:(YYWebImageOptions)options manager:(YYWebImageManager *)manager progress:(YYWebImageProgressBlock)progress Transform (YYWebImageTransformBlock) transform completion (YYWebImageCompletionBlock) completion {/ / 1, to determine whether the current request the latest requestif(sentinel ! = _sentinel) {if (completion) completion(nil, imageURL, YYWebImageFromNone, YYWebImageStageCancelled, nil);
        return_sentinel; } NSOperation *operation = ... Dispatch_semaphore_wait (_lock, DISPATCH_TIME_FOREVER);if (sentinel == _sentinel) {
        if (_operation) [_operation cancel];
        _operation = operation;
        sentinel = OSAtomicIncrement32(&_sentinel);
    } else {
        [operation cancel];
    }
    dispatch_semaphore_signal(_lock);
    return sentinel;
}
Copy the code

You can see that in both places there is logic to determine whether the current request is the latest. For the first place, because when the method is pushed, it’s possible that the next UIImageView yy_setImageWithURL:… Once again, the _sentinel may have been incremented by one, so there is no need to continue with the network request logic below (code omitted); For the second location, the same consideration is given. If the _sentinel has been incremented by one at the moment, the current NSOperation created is cancelled. If the _sentinel has not changed at the moment, the last _operation is cancelled, and the _sentinel is incremented.

It is worth noting that the semaphore usage here is for _operation read and write safety, not to protect _sentinel (since atomic augmentation is inherently thread-safe).

Roughly repeat request processing is such, if read a little confusing suggest to read the source code several times inside the complete code.

Third, the processing of threads

1. Pre-processing of download tasks

The same entry method in UIImageView+ yyWebImage.h:

- (void)yy_setImageWithURL:(NSURL *)imageURL placeholder:(UIImage *)placeholder options:(YYWebImageOptions)options manager:(YYWebImageManager *)manager progress:(YYWebImageProgressBlock)progress transform:(YYWebImageTransformBlock)transform completion:(YYWebImageCompletionBlock)completion { ... _yy_dispatch_sync_on_main_queue(^{ ... UIImage *imageFromMemory = nil; UIImage *imageFromMemory = nil; UIImage *imageFromMemory = nil;if(manager.cache && ! (options & YYWebImageOptionUseNSURLCache) && ! (options & YYWebImageOptionRefreshImageCache)) { imageFromMemory = [manager.cache getImageForKey:[manager cacheKeyForURL:imageURL] withType:YYImageCacheTypeMemory]; }if (imageFromMemory) {
            if(! (options & YYWebImageOptionAvoidSetImage)) { self.image = imageFromMemory; }if(completion) completion(imageFromMemory, imageURL, YYWebImageFromMemoryCacheFast, YYWebImageStageFinished, nil);
            return; }... __weak typeof(self) _self = self; Dispatch_async ([_YYWebImageSetter setterQueue], ^{... newSentinel = [settersetOperationWithSentinel:sentinel url:imageURL options:options manager:manager progress:_progress transform:transform completion:_completion];
            weakSetter = setter;
        });
    });
}
Copy the code

The first step

Read the cache from memory as quickly as possible (if any). This is an interesting optimization point. Readers who are familiar with YYCache framework should know that the author is using bidirectional linked list +hash to achieve memory cache, the cost of direct search is less than switching background thread search and then return to the main thread cost.

The second step

The download task is preprocessed in a [_YYWebImageSetter setterQueue] queue with the following code:

+ (dispatch_queue_t)setterQueue {
    static dispatch_queue_t queue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        queue = dispatch_queue_create("com.ibireme.webimage.setter", DISPATCH_QUEUE_SERIAL);
        dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
    });
    return queue;
}
Copy the code

You can see that this is a serial queue with a priority of DISPATCH_QUEUE_PRIORITY_DEFAULT, smaller than the main queue.

May have friends will ask, download tasks in the asynchronous queue? Isn’t there only one download task running at a time?

Ha ha, pay attention to the author’s description: download task preprocessing. This contains the logic of task creation, repeated request processing, and so on. There is no time-consuming operation, using an asynchronous thread to process is also to reduce the main thread pressure. The thread processing of the download task will be discussed later, not the serial queue here.

2. Processing of download tasks

The framework uses NSURLConnection to handle downloads, not to mention its usage, which is now obsolete. Its proxy thread is created like this:

/// Network thread entry point.
+ (void)_networkThreadMain:(id)object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"com.ibireme.webimage.request"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
/// Global image request network thread, used by NSURLConnection delegate.
+ (NSThread *)_networkThread {
    static NSThread *thread = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        thread = [[NSThread alloc] initWithTarget:self selector:@selector(_networkThreadMain:) object:nil];
        if ([thread respondsToSelector:@selector(setQualityOfService:)]) {
            thread.qualityOfService = NSQualityOfServiceBackground;
        }
        [thread start];
    });
    return thread;
}
Copy the code

This code appears in older versions of AFNetwork and SDWebImage. It creates a resident thread to handle the callback of the download task, and adds an NSMachPort to ensure that the thread’s runloop runs properly. Since manually created threads do not contain auto-release pools, the authors add one.

Here is such a bright spot of the method: thread. QualityOfService = NSQualityOfServiceBackground; .

The author very carefully sets the thread priority to NSQualityOfServiceBackground, this is a low priority, the author hope pictures download callback related processing won’t compete with other threads of CPU resources (such as the main thread of the UI operation, etc.).

3. Picture reading and decompression processing

Image from the disk read, write, decompression and other operations are in the following queue processing (image processing principle can see YYImage source code analysis: image processing skills:

+ (dispatch_queue_t)_imageQueue {
    #define MAX_QUEUE_COUNT 16
    static int queueCount;
    static dispatch_queue_t queues[MAX_QUEUE_COUNT];
    static dispatch_once_t onceToken;
    static int32_t counter = 0;
    dispatch_once(&onceToken, ^{
        queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
        queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;
        if([UIDevice currentDevice]. SystemVersion. FloatValue > = 8.0) {for (NSUInteger i = 0; i < queueCount; i++) {
                dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
                queues[i] = dispatch_queue_create("com.ibireme.image.decode", attr); }}else {
            for (NSUInteger i = 0; i < queueCount; i++) {
                queues[i] = dispatch_queue_create("com.ibireme.image.decode", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)); }}}); int32_t cur = OSAtomicIncrement32(&counter);if (cur < 0) cur = -cur;
    return queues[(cur) % queueCount];
    #undef MAX_QUEUE_COUNT
}
Copy the code

YYAsyncLayer source code analysis: asynchronous drawing discussion of threads, this kind of concurrent thread processing is the author of a general idea, not much to say.

4. Cache strategy

In this framework, the upper level business logic looks like this:

  1. Memory cache is searched first and returned if found
  2. If the memory cache is not found, the system asynchronously searches the memory cache from the disk. If the memory cache is found, the system returns the memory cache and writes the memory cache to facilitate the next search
  3. If the disk cache is still not found, initiate a network request
  4. The network request was successful and was written to the disk cache and memory cache

In fact, this logic is basically the same as SDWebImage. It is worth noting that there are custom methods for finding memory or disk caches, whether caches are needed, size limits for caches, and so on.

The core logic of the upper layer is such, on the memory cache and disk cache of the underlying implementation, you can view YYCache source code analysis: highlights.

Five, loading indicator processing

The load indicator is handled in YYWebImagemanager.m and the rest of the code is not posted

@interface _YYWebImageApplicationNetworkIndicatorInfo : NSObject
@property (nonatomic, assign) NSInteger count;
@property (nonatomic, strong) NSTimer *timer;
@end

+ (_YYWebImageApplicationNetworkIndicatorInfo *)_networkIndicatorInfo {
    returnobjc_getAssociatedObject(self, @selector(_networkIndicatorInfo)); } + (void)_setNetworkIndicatorInfo:(_YYWebImageApplicationNetworkIndicatorInfo *)info { objc_setAssociatedObject(self, @selector(_networkIndicatorInfo), info, OBJC_ASSOCIATION_RETAIN); }...Copy the code

Binding to a class variable YYWebImageManager _YYWebImageApplicationNetworkIndicatorInfo, that is, the timer variables and count are global.

. Dealing with the nature of indicators is easy, but the author’s thinking is interesting.

First, the author uses an NSTimer to turn the load indicator on or off 1/30 of a second later.

Second, the author controls whether the indicator is displayed by “counting”, that is, the count above. When a network task starts, the count is increased by one; when a network task ends or is cancelled abnormally, the count is decreased by one.

That’s a pretty neat idea.

Performance bottlenecks of the framework

– the connectionDidFinishLoading under the in YYWebImageOperation. M: the proxy method extract can see pictures of logic, it is performed for _imageQueue, unzipped the cached convenient display.

Although decompression is performed on an asynchronous thread, it usually does not affect the main thread, but when too many images are decompressed or the image resolution is too large, decompression and caching can take up a large amount of memory, resulting in a spike in memory.

So, developers need to do some optimization on performance, but the good news is that can download success through YYWebImageOptions YYWebImageOptionIgnoreImageDecoding value ban after decompression and caching logic, in order to reduce memory peaks.

7. Tips from the framework

1. Automatic release pool

As you can see, the framework uses a lot of autofree pools to avoid spikes in memory, and developers may wonder if using autofree pools so frequently would cause performance problems, but it doesn’t really matter. To understand the underlying principle of automatic release pool’s friends know, add an automatic release pool is to add a logo (sentry), however, need to manage object to join automatically release pool can be seen as stack operation, when the automatic release the end of the pool, on the top of the stack will automatically send pool object release news (pool here is to stack to the scope of “sentry”).

2, the use of locks

Yywebimageoperation. m uses recursive lock NSRecursiveLock to avoid deadlock caused by multiple lock acquisition. Of course, THE author believes that the recursive implementation of pthread_mutex_t mutex should have better processing performance.

The use of dispatch_semaphoRE_t semaphore is thread-safe and has performance advantages when operating on small amounts of code that takes less time.

Using the OSAtomicIncrement32() atomic method is certainly a good choice when securing int32_T type variables.

3. Avoid circular references

The framework avoids circular references by forwarding messages through an intermediate class:

@interface _YYWebImageWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation _YYWebImageWeakProxy
- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}
+ (instancetype)proxyWithTarget:(id)target {
    return [[_YYWebImageWeakProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)selector {
    return _target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    void *null = NULL;
    [invocation setReturnValue:&null];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
Copy the code

About the specific analysis can see the author of the article YYImage source analysis: picture processing skills have the corresponding analysis.

conclusion

I have to say, there’s a pattern. In reading the code of YYKit series, I also understand the author’s formula, so the author reads the source code of YYWebImage very fast, almost no jam, maybe this is a small embodiment of “thick accumulation”.

Considering the length and code word too tired, the author of the analysis of the article is peeling cocoon silk, if the reader friends read obstacles, please sink to heart, more combined with the source, more thinking 😁.