SDWebImage是ios上常用的一套网络图片加载的方案。
oc和swift语言都支持
探索了一下源码,受益良多,如下:
代码组织结构及调用关系
调用关系
从Cache中取到图片后,如果没有必要,就不会再去下载
代码组织结构
用到了哪些设计模式?(后续扩展)
涉及知识点
对象的含义
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
子类A的对象a调用父类B的方法时,此时在父类中调用[self class]方法,获取到的类是A。
这是因为当调用该方法时,a已经生成好了,地址也确定了,此时self就是a,取类名取出来的一定是A。
这块知识点的衍生有meta class的概念。
通过写代码打日志,可以看出来此图完全正确。
- (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;
});
- 用dispatch_barrier_* 加进队列的operation(a)会等之前队列里的operation执行完成后再执行,等a执行完成后再执行后续加进来的operation。
目的就是a访问一个共享资源的时候,确保其他线程不会访问这个共享资源,在上面的例子里就是用来保护URLOperations。 - dispatch_barrier_async和dispatch_barrier_sync的区别就是后者会阻塞当前调用线程,等任务执行完成后再执行。
- 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);
}