Sdwebimage source code analysis (1)

Time:2021-1-27

1 Overview

SDWebImageIt’s basically standard for IOS projects. With a flexible and simple API, it provides a series of functions such as loading, parsing, processing, caching, cleaning and so on. Let’s focus on the business. But it doesn’t mean that you can use it. Through source code analysis and learning, let us know how to use it well. Learning and analyzing excellent source code can also imperceptibly provide us with a lot of ideas to solve our daily needs. Here is a picture to summarizeSDWebImageAll classes of:

Sdwebimage source code analysis (1)

Through the analysis of this picture, we canSDWebImageThere are three kinds of source code

  • Various categories:

    • UIButton(WebCache)byUIButtonClass to load the image. For example, under normal circumstances, click the case, the image properties and background images.

    • MKAnnotationView(WebCache)byMKAnnotationViewClass to add various methods to load pictures.

    • UIImageView(WebCache)byUIImageViewClass to add a method to load a picture.

    • UIImageView(HighlightedWebCache)byUIImageViewClass to add a method to load a picture in the highlighted state.

    • FLAnimatedImageView(WebCache)byFLAnimatedImageViewClass to add dynamic loading methods, this classification needs to be introducedFLAnimatedImageFramework.SDWebImageIt is recommended to use this framework to handle dynamic picture (GIF) loading.

    • Uiimageview, uibutton and flanimatedimageviewsd_setImageWithURLWait for API to do image loading request. That’s the only thing we need to do.

    • The above several uiview subclasses will callUIView(WebCache)classifiedsd_internalSetImageWithURLMethod to do the image loading request. Specifically, throughSDWebImageManagerCall. At the same time, operation cancellation and activity indicator addition and cancellation are realized.

  • Various tools:

    • NSData+ImageContentType: get the image type according to the image data, such as GIF, PNG, etc.

    • SDWebImageCompat: enlarge or reduce the size of the picture according to the resolution of the screen.

    • SDImageCacheConfig: image cache policy record. For example, whether to decompress, whether to allow icloud, whether to allow memory cache, cache time, etc. The default cache time is one week.

    • UIImage+MultiFormat: getting the data corresponding to uiimage object or generating uiimage of specified format according to data is actually the conversion between uiimage and nsdata.

    • UIImage+GIF: judge whether a picture is GIF or not. The uiimage object of a GIF can be returned according to nsdata, and only the GIF generated by the first picture of the GIF can be returned. If you want to display multiple gifs, useFLAnimatedImageView

    • SDWebImageDecoder: according to the situation of the picture, do the decompression of the picture. And decide how to deal with decompression according to the situation of the picture.

  • Core class:

    • SDImageCache: responsible for the whole cache of sdwebimage, is aSingle column object. Cache path processing, cache name processing, managing the creation and deletion of memory cache and disk cache, getting pictures according to the specified key, storing picture type processing, deleting cache according to the creation and modification date of cache.

    • SDWebImageManager: have aSDWebImageCacheandSDWebImageDownloaderAttributes are used for image caching and loading respectively. It provides a unified interface for uiview and its subclasses to load pictures. Manages the collection of loading operations. This class isA single column. There is also the processing of various loading options.

    • SDWebImageDownloader: realize the specific processing of image loading, if the image exists in the cache, then from the cache. If the cache does not exist, create one directly.SDWebImageDownloaderOperationObject to download the image. Manage the encapsulation, cache and cookie settings of nsurlrequest object request header. Loading options processing and other functions. Manage dependencies between operations. This class isA single column.

    • SDWebImageDownloaderOperation: a custom parallel operation subclass. This class mainly implements the specific operation of image download, as well as the image decompression and operation lifecycle management after image download.

    • UIView+WebCache: all uibutton and uiimageview call back this classification method to complete the image loading process. At the same timeUIView+WebCacheOperationClassification to manage the cancellation and recording of requests. All classifications of uiview and its subclasses use this classsd_intemalSetImageWithURL:To achieve the loading of images.

    • FLAnimatedImageView: the data of dynamic picture is encapsulated by alanimatedimage object.FLAnimatedImageViewyesUIImageViewSubclass of. Through it, dynamic pictures can be loaded, displayed and managed. And more thanUIImageViewProcess optimization has been done.

2 implementation process

SDWebImageFor us to achieve the image loading, data processing, image caching and other column work. Through the figure below, we can analyze his process:

Sdwebimage source code analysis (1)

From this graph, we find thatSDWebImageThe process of loading is to load data from the cache first. Moreover, the cache loading is priority from the memory cache, and then the disk loading. Finally, if there is no cache, it will be loaded from the network. At the same time, after the network successfully loads the image, it is stored in the local cache.

3 UIView+WebCacheanalysis

Uiimageview, uibutton and flanimatedimageview will callUIView(WebCache)classifiedsd_internalSetImageWithURLMethod to do the image loading request. Specifically, throughSDWebImageManagerCall. At the same time, operation cancellation and activity indicator addition and cancellation are realized. Let’s look at it firstsd_internalSetImageWithURLThe implementation of the method is as follows

/**
 All uiview and its subclasses use this method to load images

 @Param URL loaded URL
 @Param placeholder
 @Param options loading options
 @param operationKey key
 @param setImageBlock Block
 @Param progressblock
 @Param completedblock callback block
 */
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock {
    
    //Cancels all download operation objects corresponding to the current class
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    /*
     Associate the uiimageview's image loading operation with its own associated object to facilitate later operations such as canceling. The associated key is the class name corresponding to uiimageview
     */
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //If the station map is set, the station map will be displayed first
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
        // check if activityView is enabled or not
        //If the uiimageview object has a setting to add rotation chrysanthemum data, the rotation chrysanthemum data will be added when loading
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        
        __weak __typeof(self)wself = self;
        /*
         *Operation is a 'sdwebimagecombinedoperation' object. Get the image through this object
         */
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            //Stop Chrysanthemum
            [sself sd_removeActivityIndicator];
            if (!sself) {
                return;
            }
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                //If it is set not to display pictures automatically, call completedblock directly to let the caller handle the display of pictures
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    completedBlock(image, error, cacheType, url);
                    return;
                } else if (image) {
                    //Auto display picture
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];
                } else {
                    //If the delay display occupation bitmap is set, the occupation bitmap will be displayed if the picture loading fails
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                //Complete callback
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        //Associate the operationkey with the operation object. It is convenient to cancel the operation according to the key.
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        //Loading failure
        dispatch_main_async_safe(^{
            //Remove Chrysanthemum
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

Adding rotation chrysanthemum to uiview and its subclasses is realized by associating objects. It is realized by the following methods:

#Pragma mark realizes the addition of Chrysanthemum by associating objects
- (UIActivityIndicatorView *)activityIndicator {
    return (UIActivityIndicatorView *)objc_getAssociatedObject(self, &TAG_ACTIVITY_INDICATOR);
}

- (void)setActivityIndicator:(UIActivityIndicatorView *)activityIndicator {
    objc_setAssociatedObject(self, &TAG_ACTIVITY_INDICATOR, activityIndicator, OBJC_ASSOCIATION_RETAIN);
}
#Does pragma mark show rotation
- (void)sd_setShowActivityIndicatorView:(BOOL)show {
    objc_setAssociatedObject(self, &TAG_ACTIVITY_SHOW, @(show), OBJC_ASSOCIATION_RETAIN);
}

- (BOOL)sd_showActivityIndicatorView {
    return [objc_getAssociatedObject(self, &TAG_ACTIVITY_SHOW) boolValue];
}
#The style of rotating Chrysanthemum
- (void)sd_setIndicatorStyle:(UIActivityIndicatorViewStyle)style{
    objc_setAssociatedObject(self, &TAG_ACTIVITY_STYLE, [NSNumber numberWithInt:style], OBJC_ASSOCIATION_RETAIN);
}

- (int)sd_getIndicatorStyle{
    return [objc_getAssociatedObject(self, &TAG_ACTIVITY_STYLE) intValue];
}

And then there is throughUIView+WebCacheOperationClass to realize the image download of uiviewOperationAssociation and cancellation of. Specific key values can be obtained fromsd_internalSetImageWithURLFind the specific access method in this methodOperationAssociation and cancellation of.

/**
 Associate operation object with key object

 @Param operation object
 @param key key
 */
- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key {
    if (key) {
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self operationDictionary];
            operationDictionary[key] = operation;
        }
    }
}
/**
 Cancel all the operation objects corresponding to the current key that implement the sdwebimageoperation protocol

 @Param key operation
 */
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // Cancel in progress downloader from queue
    //Get all the keys corresponding to the current view
    SDOperationsDictionary *operationDictionary = [self operationDictionary];
    //Get the corresponding image to load operation
    id operations = operationDictionary[key];
    //Cancel all operations corresponding to the current view
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}

4. Analysis of flanimatedimageview

SDWebImageuseFLAnimatedImageFrame to handle dynamic images, which containFLAnimatedImageandFLAnimatedImageViewTwo thunder. The data of dynamic picture is transmitted throughALAnimatedImageObject.FLAnimatedImageViewyesUIImageViewSubclass of. Through it, dynamic pictures can be loaded, displayed and managed. And more thanUIImageViewProcess optimization has been done. Let’s take a lookFLAnimatedImageView.hInterface defined in it:

/**
 `Flanimatedimageview is a subclass of uiimageview. The 'start / stop / isanimating' method of 'uiimageview' is implemented. So we can directly use 'flanimatedimageview' instead of 'uiimageview'.
 The 'cadisplaylink' object is used to process the display of the current picture frame and the next picture.
 */
@interface FLAnimatedImageView : UIImageView
/**
 Encapsulation object of dynamic picture. First, by setting`[ UIImageView.image ]`Clear the existing dynamic image for nil. Setting the 'animatedimage' property will automatically set the new dynamic picture and start to display. In addition, the uiimage currently displayed will be saved in the 'currentframe'.
 */
@property (nonatomic, strong) FLAnimatedImage *animatedImage;

@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);
/**
 Uiimage object corresponding to the current animation frame
 */
@property (nonatomic, strong, readonly) UIImage *currentFrame;
/**
 The index of the current picture
 */
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;
/**
 Specifies the mode of the runloop in which the dynamic picture is executed. NSRunLoopCommonMode
 */
@property (nonatomic, copy) NSString *runLoopMode;
@end

We passedFLAnimatedImageView+WebCacheThis is classifiedsd_setImageWithURLTo load dynamic pictures:

/**
 Flanimatedimage + webcache classification uses this method to load dynamic images

 @Param URL the URL of the image
 @Param placeholder
 @Param options loading options
 @Param progressblock
 @Param completedblock
 */
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock {
    __weak typeof(self)weakSelf = self;
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
                       setImageBlock:^(UIImage *image, NSData *imageData) {
                           //Get the type of the picture according to the type of nsdata
                           SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                           //If it is GIF, it will be processed
                           if (imageFormat == SDImageFormatGIF) {
                               //Set the dynamic picture to the animatedimage property of flanimatedimageview. This setter method is overridden
                               weakSelf.animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData];
                               weakSelf.image = nil;
                           } else {
                               //If it is not a dynamic picture, it will be displayed normally
                               weakSelf.image = image;
                               weakSelf.animatedImage = nil;
                           }
                       }
                            progress:progressBlock
                           completed:completedBlock];
}

As can be seen from the above, after obtaining the image data. First of allSDImageFormatGet the type of picture. If it is GIF type, the image data is encapsulated into aFLAnimatedImageObject. Then set it to the animatedimage property. The setter method for this property is as follows:

/**
 The setter method of animatedimage. Use this property setter method to set the data of flanimatedimageview. And start dynamic display

 @Param animatedimage animatedimage property
 */
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
{
    if (![_animatedImage isEqual:animatedImage]) {
        if (animatedImage) {
            //Clear image data before uiimageview
            super.image = nil;
            super.highlighted = NO;
            //Let's start with the intrinsiccontentsize, which is the built-in size of the control. For example, uilabel, uibutton and other controls have their own built-in sizes. The built-in size of a control is often determined by the content of the control itself. For example, if the text of a uilabel is very long, the built-in size of the uilabel will be very long. The built-in size of the control can be obtained by using the intrinsic contentsize property of uiview, or the intrinsic contentsize can be recalculated in the next UI planning event by using the invalideintensize method. If you create an original uiview object directly, its built-in size is obviously 0.
            [self invalidateIntrinsicContentSize];
        } else {
            //Stop dynamic display of dynamic pictures
            [self stopAnimating];
        }
        //Assignment
        _animatedImage = animatedImage;
        //Current dynamic picture data frame
        self.currentFrame = animatedImage.posterImage;
        //Current data frame index
        self.currentFrameIndex = 0;
        if (animatedImage.loopCount > 0) {
            self.loopCountdown = animatedImage.loopCount;
        } else {
            self.loopCountdown = NSUIntegerMax;
        }
        self.accumulator = 0.0;
        
        //Update the state of the object. This updates the value of the attribute shouldanimated.
        [self updateShouldAnimate];
        if (self.shouldAnimate) {
            //Start dynamic display
            [self startAnimating];
        }
        
        [self.layer setNeedsDisplay];
    }
}
/**
 Determine whether the current flanimatedimageview needs to display animation
 */
- (void)updateShouldAnimate
{
    BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
    self.shouldAnimate = self.animatedImage && isVisible;
}

5 CADisplayLink

What’s interesting is thatFLAnimatedImageViewRefresh the display of dynamic picture frame through cadisplaylink. Cadisplaylink is a timer that allows us to draw content to the screen at the same rate as the screen refresh rate. We create a new cadisplaylink object in the application, add it to a runloop, and provide it with a target and selector to call when the screen is refreshed.

Once cadisplaylink is registered with runloop in a specific mode, runloop will call the selector on the target bound to cadisplaylink whenever the screen needs to be refreshed. At this time, the target can read the timestamp of each call of cadisplaylink to prepare the data needed for the next frame display. For example, a video application uses a timestamp to calculate the video data to be displayed in the next frame. In the process of UI animation, we need to calculate the size of the UI object to be updated in the next frame of the animation through the timestamp. When adding runloop, we should choose a higher priority to ensure the smoothness of the animation. We can imagine that in the process of animation, a high priority task is added to the runloop. Then, the next call will be suspended, and the high priority task will be executed first, and then the call of cadisplaylink will be executed. As a result, the animation process will get stuck and the animation will not flow smoothly. The duration property provides the time between each frame, that is, the time between each refresh of the screen. We can use this time to calculate the value of the UI to be displayed in the next frame. But duration is only a rough time. If the CPU is busy with other calculations, it can’t guarantee to perform the screen drawing operation at the same frequency, which will skip the chance to call the callback method several times. The frameinterval attribute is a readable and writable nsinteger type value, which identifies how many frames to call the selector method once. The default value is 1, that is, every frame is called once. If every frame is called once, for IOS devices, the refresh rate is 60Hz, that is, 60 times per second. If the frameinterval is set to 2, two frames will be called once, that is, 30 times per second. We control the operation of cadisplaylink through the pause property. When we want to end a cadisplaylink, we should call – (void) invalidate to delete the previously bound target and selector from the runloop. In addition, cadisplaylink cannot be inherited.

//Call back every 1 / 60 seconds and use the displaydidrefresh method to do UI processing
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
//Add the DisplayLink to the commommode of the main thread        
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];

lastOriginal address.html),Demo address