bibibi SDWebImage 4.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
链接://www.greatytc.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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容