美文网首页bibibi ios
bibibi SDWebImage 4.0源码

bibibi SDWebImage 4.0源码

作者: bibibime | 来源:发表于2017-08-18 17:57 被阅读0次

    SDWebImage是ios上常用的一套网络图片加载的方案。

    oc和swift语言都支持

    探索了一下源码,受益良多,如下:

    代码组织结构及调用关系

    调用关系

    SDWebImage时序.png

    从Cache中取到图片后,如果没有必要,就不会再去下载

    代码组织结构

    SDWebImage类图.png

    用到了哪些设计模式?(后续扩展)

    涉及知识点

    对象的含义

    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    

    子类A的对象a调用父类B的方法时,此时在父类中调用[self class]方法,获取到的类是A。
    这是因为当调用该方法时,a已经生成好了,地址也确定了,此时self就是a,取类名取出来的一定是A。
    这块知识点的衍生有meta class的概念

    resource.jpg

    通过写代码打日志,可以看出来此图完全正确。

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        // Override point for customization after application launch.
        
        Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
        class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
        objc_registerClassPair(newClass);
    
        id instanceOfNewClass =
        [[newClass alloc] initWithDomain:@"someDomain" code:0 userInfo:nil];
        [instanceOfNewClass performSelector:@selector(report)];
        
        return YES;
    }
    
    
    void ReportFunction(id self, SEL _cmd)
    {
        NSLog(@"This object is %p.", self);
        NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);
        NSLog(@"Class is %p, and super is %p.", [self class], [self superclass]);
        
        Class currentClass = [self class];
        Class superClass = class_getSuperclass(currentClass);
    
        for (int i = 1; i < 5; i++)
        {
            NSLog(@"Following the isa pointer %d times gives %p,super Class is %@:%p", i, currentClass,superClass,superClass);
            currentClass = object_getClass(currentClass);
            superClass = class_getSuperclass(superClass);
        }
        
        NSLog(@"NSObject's class is %p", [NSObject class]);
        NSLog(@"NSObject's meta class is %p", object_getClass([NSObject class]));
        NSLog(@"NSObject's meta class's super class is %p", class_getSuperclass(object_getClass([NSObject class])));
        NSLog(@"NSObject's super class is %p", class_getSuperclass([NSObject class]));
    }
    

    打印结果:

    2017-08-17 14:56:09.581 ISATest[58151:9427754] This object is 0x60800004ac50.
    2017-08-17 14:56:09.581 ISATest[58151:9427754] Class is RuntimeErrorSubclass, and super is NSError.
    2017-08-17 14:56:09.581 ISATest[58151:9427754] Class is 0x60800004add0, and super is 0x1026b19f0.
    2017-08-17 14:56:09.582 ISATest[58151:9427754] Following the isa pointer 1 times gives 0x60800004add0,super Class is NSError:0x1026b19f0
    2017-08-17 14:56:09.582 ISATest[58151:9427754] Following the isa pointer 2 times gives 0x60800004aec0,super Class is NSObject:0x102c1ee58
    2017-08-17 14:56:09.582 ISATest[58151:9427754] Following the isa pointer 3 times gives 0x102c1ee08,super Class is (null):0x0
    2017-08-17 14:56:09.582 ISATest[58151:9427754] Following the isa pointer 4 times gives 0x102c1ee08,super Class is (null):0x0
    2017-08-17 14:56:09.582 ISATest[58151:9427754] NSObject's class is 0x102c1ee58
    2017-08-17 14:56:09.582 ISATest[58151:9427754] NSObject's meta class is 0x102c1ee08
    2017-08-17 14:56:09.583 ISATest[58151:9427754] NSObject's meta class's super class is 0x102c1ee58
    2017-08-17 14:56:09.583 ISATest[58151:9427754] NSObject's super class is 0x0
    
    

    Lightweight Generics

    typedef NSMutableDictionary<NSString *, id> SDOperationsDictionary;
    

    oc升级版本的用法,可以指定容器内对象的类型,这样编译器可以检查到一些编码错误。
    涉及知识点oc升级特性

    判断当前是否在主线程

    判断当前是否在主线程,如果不在,就切换到主线程执行任务。

    #ifndef dispatch_main_async_safe
    #define dispatch_main_async_safe(block)\
        if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
            block();\
        } else {\
            dispatch_async(dispatch_get_main_queue(), block);\
        }
    #endif
    
    dispatch_main_async_safe(^{
                   
                });
    

    strcmp是一个比较函数,当第一个参数和第二个参数比较结果一致的时候,返回值是0;
    dispatch_queue_get_label获取传入线程的标识。
    这里是方法的具体说明

    UIActivityIndicatorView

    self.activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:[self sd_getIndicatorStyle]];
    

    苹果sdk提供的一个加载指示器

    多线程NSOperation、GCD、锁

    此处涉及知识点较多,详细见:
    NSOperation
    NSOperation Queue
    GCD

    判断是否实现了协议

    if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
                [(id<SDWebImageOperation>) operations cancel];
    }
    

    NS_ENUM VS NS_OPTIONS

    typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
        SDWebImageDownloaderLowPriority = 1 << 0,
        SDWebImageDownloaderProgressiveDownload = 1 << 1,
    

    enum的每个值都是独立的,互斥的。
    options的值是字节里位的状态,可以做逻辑运算。
    bibibi ns_enum和ns_options区别

    dispatch_barrier_sync

    _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_barrier_async(self.barrierQueue, ^{
            SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
            BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
            if (canceled) {
                [self.URLOperations removeObjectForKey:token.url];
            }
        });
    
    dispatch_barrier_sync(self.barrierQueue, ^{
            SDWebImageDownloaderOperation *operation = self.URLOperations[url];
            if (!operation) {
                operation = createCallback();
                self.URLOperations[url] = operation;
    
                __weak SDWebImageDownloaderOperation *woperation = operation;
                operation.completionBlock = ^{
                  SDWebImageDownloaderOperation *soperation = woperation;
                  if (!soperation) return;
                  if (self.URLOperations[url] == soperation) {
                      [self.URLOperations removeObjectForKey:url];
                  };
                };
            }
            id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
            token = [SDWebImageDownloadToken new];
            token.url = url;
            token.downloadOperationCancelToken = downloadOperationCancelToken;
        });
    
    1. 用dispatch_barrier_* 加进队列的operation(a)会等之前队列里的operation执行完成后再执行,等a执行完成后再执行后续加进来的operation。
      目的就是a访问一个共享资源的时候,确保其他线程不会访问这个共享资源,在上面的例子里就是用来保护URLOperations。
    2. dispatch_barrier_async和dispatch_barrier_sync的区别就是后者会阻塞当前调用线程,等任务执行完成后再执行。
    3. dispatch_barrier的理解

    后台下载

    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
            // Clean up any unfinished task business by marking where you
            // stopped or ending the task outright.
            [application endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
        }];
    

    beginBackgroundTaskWithExpirationHandler这个方法可以申请短时间内(大约3~10分钟)在系统限制下在后台运行app,避免突然结束 app引发某些错误,用以提升用户体验。这个方法可以在任何时候多次调用,不会有问题。参见apple文档。
    相关的知识点:ios后台运行技术

    位图提前异步解压

    if (self.config.shouldDecompressImages) {
                image = [UIImage decodedImageWithImage:image];
            }
    

    jpg和png都是位图,从磁盘取出来后赋值给imageView的时候会进行一次自动解压,渲染。这个过程都是在主线程做的,如果主线程进行大量类似操作,会造成卡顿。
    我们可以把位图解压操作提前放到其他线程处理,等到渲染的时候,直接渲染就行了,解压后的image会比原来的image大很多,所以这是典型的空间换时间的做法。
    需要注意的是如果图片本身已经很大,多张图片同时这样处理,可能会造成内存使用过多,闪退。

    同样图片链接,资源更新的问题

    首先client调用sdwebimage的时候那将options设置为SDWebImageRefreshCached,这种设置下,会从文件cache中找到图片,返回到client,并且会继续去下载,下载获取到结果后再次返回给client。
    检查了源码,发现下载的cachePolicy会设置成NSURLRequestUseProtocolCachePolicy

    if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
    
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
    

    这种情况下,如果服务器返回图片时response header没有设置must-revalidata,客户端下次请求图片就会直接读取网络缓存(不确定服务器设置must-revalidata后有没有效果), 所以客户端下次请求图片时要手动带上etag或者If-Modified-Since,这样就可以通过服务器返回的是否是304去判断重新请求网络还是从网络缓存取图片。
    代码如下:

    + (void)downloaderRegister {
        SDWebImageDownloader *imgDownloader = SDWebImageManager.sharedManager.imageDownloader;
        imgDownloader.headersFilter  = ^NSDictionary *(NSURL *url, NSDictionary *headers) {
            //找图片
            NSFileManager *fm = [[NSFileManager alloc] init];
            NSString *imgKey = [SDWebImageManager.sharedManager cacheKeyForURL:url];
            NSString *imgPath = [SDWebImageManager.sharedManager.imageCache defaultCachePathForKey:imgKey];
            //图片本地修改时间
            NSDictionary *fileAttr = [fm attributesOfItemAtPath:imgPath error:nil];
            NSMutableDictionary *mutableHeaders = [headers mutableCopy];
            NSDate *lastModifiedDate = nil;
            if (fileAttr.count > 0) {
                lastModifiedDate = (NSDate *)fileAttr[NSFileModificationDate];
            }
            //格式化时间
            NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
            formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
            formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
            formatter.dateFormat = @"EEE, dd MMM yyyy HH:mm:ss z";
            NSString *lastModifiedStr = [formatter stringFromDate:lastModifiedDate];
            //设置header
            lastModifiedStr = lastModifiedStr.length > 0 ? lastModifiedStr : @"";
            [mutableHeaders setValue:lastModifiedStr forKey:@"If-Modified-Since"];
            
            return mutableHeaders;
        };
    }
    

    runloop

    一个线程可以有一个runloop,一个runloop可以有几种mode。
    runloop就是一个循环,主线程的runloop是自动创建的。
    runloop详解

    构建缓存时选用NSCache而非NSDictionary

    NSCache胜过NSDictionary之处在于,当系统资源将要耗尽时,它可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统发出“低内存”通知时手工删减缓存。

    NSCache并不会“拷贝”键,而是会“保留”它。此行为用NSDictionary也可以实现,然而需要编写相当复杂的代码。NSCache对象不拷贝键的原因在于:很多时候,键都是不支持拷贝操作的对象来充当的。因此,NSCache不会自动拷贝键,所以说,在键不支持拷贝操作的情况下,该类用起来比字典更方便。另外,NSCache是线程安全的,而NSDictionary则绝对不具备此优势。

    作者:Crazy2015
    链接:http://www.jianshu.com/p/48e1326e9a0d
    來源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    CADisplayLink(4.0版本已不使用)

    做手机界面刷新相关的内容适合使用,CADisplayLink vs NSTimer;

    @synchronized

    @synchronized (self.failedURLs) {
                isFailedUrl = [self.failedURLs containsObject:url];
            }
    
    @synchronized (self.failedURLs) {
                                [self.failedURLs addObject:url];
                            }
    
    @synchronized (self.failedURLs) {
                                [self.failedURLs removeObject:url];
                            }
    

    这是sdk提供的简写的锁,传入的参数是一个标识,所有相同标识的地方,大括号里的内容同时只允许一个线程访问。
    因为只是一个标识,也就是一个内存地址,本身传的内容不代表任何意义。也就是说这里统一传其他内容也是可以的。
    为什么不传self?是因为同一个类中可以通过不同标识进行区分。比如这个类中还有以下代码。

    - (void)cancelAll {
        @synchronized (self.runningOperations) {
            NSArray<SDWebImageCombinedOperation *> *copiedOperations = [self.runningOperations copy];
            [copiedOperations makeObjectsPerformSelector:@selector(cancel)];
            [self.runningOperations removeObjectsInArray:copiedOperations];
        }
    }
    
    - (BOOL)isRunning {
        BOOL isRunning = NO;
        @synchronized (self.runningOperations) {
            isRunning = (self.runningOperations.count > 0);
        }
        return isRunning;
    }
    
    - (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
        @synchronized (self.runningOperations) {
            if (operation) {
                [self.runningOperations removeObject:operation];
            }
        }
    }
    

    oc动态添加加属性

    我们知道可以通过category给类添加方法,但是怎么在不直接改类的前提下直接给一个类添加属性呢?
    答案是使用runtime方法。
    UIView+WebCache中大量使用了这个方法

    - (UIActivityIndicatorView *)activityIndicator {
        return (UIActivityIndicatorView *)objc_getAssociatedObject(self, &TAG_ACTIVITY_INDICATOR);
    }
    
    - (void)setActivityIndicator:(UIActivityIndicatorView *)activityIndicator {
        objc_setAssociatedObject(self, &TAG_ACTIVITY_INDICATOR, activityIndicator, OBJC_ASSOCIATION_RETAIN);
    }
    

    相关文章

      网友评论

        本文标题:bibibi SDWebImage 4.0源码

        本文链接:https://www.haomeiwen.com/subject/wjrarxtx.html