SDWebImage异步下载和缓存的源码剖析
来源:互联网 发布:定时短信软件 编辑:程序博客网 时间:2024/04/20 21:16
前言:
在移动app开发过程中,考虑到手机流量、网速以及内存等因素,当我们的app需要频繁的访问网络时,对网络优化这块的要求就显得尤为重要。
比如某个app需要经常重复显示很多张网络图片的时候,如果在每次需要显示同一张网络图片,都要访问网络进行下载操作,那就显得很不合理了。
因为那样会相当耗时,且耗流量。这就需要对已下载好的网络图片进行缓存操作。
我目前开发的一个项目属于展示类app,一个需要频繁处理网络图片的应用,涉及复杂的异步下载和异步缓存等处理。在使用SDWebImage类库进行网络图片处理之前,我们的项目写了一套对于网络图片的异步下载以及缓存处理的操作方法。
我们的项目对于网络图片的处理逻辑如下:
- 根据图片URL查找内存是否有这张图片,有则返回;
- 如果内存中不存在该图片,则查找磁盘是否有这张图片,有则返回;
如果磁盘中没有该图片,则开始进行网络下载处理,下载成功后保存到内存和磁盘中,并返回图片。
上面是对网络图片最基本的处理逻辑,而这里面会涉及到复杂的缓存处理,以及异步下载操作,类库SDWebImage对这些操作都处理得非常好。
接下来,我们来剖析一下类库SDWebImage的实现原理吧。
一、重点关注这几个作用
- 通过类别提供接口,用户不需要关注内部复杂的逻辑操作;
- 可以取消任务处理队列中的任务;
- 可以设置最大的并发数;
- 可以设置operation之间的依赖关系,能实现后进先出的需要;
- 能够确保同一个图片URL不会被重复下载;
- 能够确保一个错误的图片URL不会被重复的请求;
- 通过多线程异步操作的方式,确保主线程UI不会被阻塞;
- 通过在后台对图片进行解码处理,避免在主线程设置setImage的时候由CPU解码,而占用太多主线程的时间;
二、了解一下各个类的作用
1、定义通用宏和方法
SDWebImageCompat:宏定义和C语言的一些工具方法
SDWebImageOperation:定义通用的operation协议,主要是一个取消方法cancel
了解NSOperation点这里
2、下载处理
SDWebImageDownloader:实际的下载功能和配置提供者。
通过URLCallbacks字典来存储回调callback(下载进度的回调、完成的回调);
将下载任务NSOperation添加到下载队列NSOperationQueue中;设置下载任务NSOperation之间的依赖关系;
通过dispatch_barrier_sync来保证后续提交的block等待当前的block执行完毕后再执行,因为下载任务是在并行队列中执行,这样确保线程安全。
SDWebImageDownloaderOperation:继承自NSOperation。
这是一个异步的下载任务,封装了NSURLConnection进行实际的下载任务。
里面涉及到run loop的一些用法。
了解RunLoop点这里
3、缓存处理
SDImageCache:实际的缓存处理者(内存和磁盘cache)。
同样是通过dispatch_barrier_sync来确保线程安全,因为缓存处理跟下载处理一样,都是放到并发队列里面去执行,通过dispatch_barrier_sync可以确保对一些全局资源操作时,不会有资源抢占问题,比如对一个全局字典的增删操作,就可以通过它来确保安全。
而这个类会管理一个任务NSOperation,通过它可以实现由外部cancel掉正在执行的缓存操作任务。实现方式,是在每次执行并发队列中的缓存操作时,先判断NSOperation的状态量isCancelled是否为YES,如果是则return,不进行下面的缓存操作。
memCache:NSCache的子类,用户内存缓存处理,会在收到内存警告的时候,自动清空内存。
4、功能类
SDWebImageManager:管理整个框架的核心类。
通过这个单例来管理一个下载操作,以及一个缓存操作,每次收到URL请求,则返回一个NSOperation对象,由外部控制这个对象是否需要取消cancel。
SDWebImageDecoder:图片的解码类。
通过在后台对下载好的图片进行解码操作,为什么需要解码呢?后面会进行解释。
SDWebImagePrefetcher:图片的预加载管理。
5、类别
UIView+WebCacheOperation:为UIView添加了一个字典属性operationDictionary。
通过这个字典可以拿到对应UIView控件上的下载/缓存操作任务对象NSOperation,然后通过这个对象可以cancel掉当前控件上面的下载/缓存操作任务。
UIImageView+WebCache:为UIImageView添加了一个字符串属性imageURLKey。对应的就是当前UIImageView控件上的图片URL。
UIImageView+WebCache:为UIImageView添加了一个loading功能,使得每个UIImageView都具备显示正在加载的状态。
三、分析一下处理逻辑步骤
1、取消正在加载的图片
[self sd_cancelCurrentImageLoad];
如何做到取消UIImageView上面URL的下载任务呢?
UIImageView的类别通过runtime为其添加了一个dic属性,对于静态图而言,某个value值就是一个“遵循某个协议”cancel方法的具体类(我们不需要关心这个类,只需要关心这个协议的方法即可-面向协议编程),而这个cancel方法正好就是调用下载任务NSOperation的cancel方法来取消下载任务。
2、查找内存中是否有需要加载的图片
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {//异步返回查询的结果}
返回的operation.cacheOperation这个对象有什么用?
返回的这个任务对象是一个缓存处理任务NSOperation,之所以返回这个任务,是因为从磁盘或者内存查询的过程是异步的,后面可能需要对缓存操作进行取消cancel。
从下面的代码可以看出,内存和磁盘的查询任务,是通过异步的方式,放到串行队列中执行IO处理。
NSOperation *operation = [NSOperation new];dispatch_async(self.ioQueue, ^{ //切换到io队列上,进行磁盘操作 //回归到主线程行,进行doneBlock操作 dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); });}
3、创建下载任务
由于有各种各样的block回调,例如下载进度的回调,完成的回调,所以需要一个数据结构来存储这些回调;
用来存储回调的数据结构是一个URLCallbacks字典,其中key是图片的URL,value是回调的数组(用数组是因为有gif图的下载处理),数组里面是字典(对应下载进度的回调,完成的回调)。
[[wself.operationClass alloc] initWithRequest:request options:options progress:^(NSInteger receivedSize, NSInteger expectedSize){ //Progress 回调} completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished){ //Completion回调} cancelled:^{ //Cancel 回调}
在回调方法中,用到了dispatch_barrier_async。
为什么要用dispatch_barrier_async呢?
_barrierQueue是个并行队列,意味着队列上的任务可以并行执行。用dispatch_barrier_async来保证后续提交的block等待当前的block执行完毕后再执行,这样确保线程安全。
SDWebImageDownloader *sself = wself;if (!sself) return;//阻碍barrierQueue,dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url];});
了解GCD点这里
4、下载成功后,根据图片解码和处理图片格式,回调给UIImageView显示。
5、总结整个调用过程:
1、取消上一次下载任务;2、设置占位默认图;3、保存回调;4、cache查询:先内存再磁盘;5、通过NSURLConnection进行图片下载,解码,回调显示,并缓存。
四、了解里面的线程管理
一共有4个队列
1. mainQueue 主队列。 在这个队列上进行UI更新,发送notification2. barrierQueue 并行队列。 在这个队列上处理数据回调,比如缓存的并行操作,以及下载的并行操作,在并行队列中,对全局资源进行操作,需要保证线程安全,一致使用dispatch_barrier_sync确保后续执行的block等待当前block执行完再执行。3. ioQueue 串行队列。 用在图片的磁盘操作串行执行。4. downloadQueue(NSOperationQueue),用来全局的管理下载的任务
五、为什么要对下载好的图片进行解码?
传统的UIImage进行解码都是在主线程上进行的,比如
UIImage * image = [UIImage imageNamed:@"123.jpg"]self.imageView.image = image;
在这个时候,图片其实并没有解码。而是,当图片实际需要显示到屏幕上的时候,CPU才会进行解码,绘制成纹理什么的,交给GPU渲染。这其实是很占用主线程CPU时间的,而众所周知,主线程的时间真的很宝贵
六、为什么通过类别来提供接口?
其实好处很明显:将对象依赖转变成接口依赖、开闭原则。
它将复杂的网络图片的异步下载、异步缓存等操作封装起来,用户不用考虑内部的这些逻辑,通过类别提供的外部接口,就能很好的完成下载、缓存以及回调等操作。
如果后面需求导致SDWebImage类库不满足需求,需要更换别的下载管理类库的话,只需要对UIImageView+WebCache这个类库新增接口,而外部不需要太大的改变。
七、通过类别添加的属性分别有什么作用?
UIView和UIImageView的类别Category,通过运行时机制runtime为它们添加了一些属性property。下面讲解一下这些新添加的属性的作用。
1、UIView+WebCacheOperation这个类别,为UIView添加了一个属性operationDictionary。
1.1 这个字典有什么用呢? 它使得每个UIView控件都有一个它自己的一组管理下载任务的对象(遵循SDWebImageOperation协议的下载管理器对象)。 即每个UIView控件都可以通过这个属性,获得它上面的网络图片(UIView的派生类UIImageView控件)URL的下载任务的管理器。
2.2 这个URL下载任务的管理器有什么用呢? 这个遵循SDWebImageOperation协议的下载管理器对象,通过NSOperation和NSOperationQueue来处理下载的任务, 可以通过cancel来取消当前下载队列中的任务。 当每个UIImageView控件都具备一个属性,就是属于它的网络图片的下载任务的管理器, 那么它就可以通过这个属性去实现当前下载任务的取消操作。
1.3 举个例子吧。 比如某个UIImageView控件需要下载URL1,正在下载的时候,突然改变下载的图片,换成下载URL2,这个时候, 如果短时间内对这个UIImageView控件所要下载的图片URL进行多次改变URL3,URL4,URL5... 那么当URLn才是这个UIImageView真正想要下载显示的URL的话,前面n-1个URL没有取消下载,就相当浪费资源,也不合理。 这个问题在通过UITableViewCell来显示网络图片的时候就有很明显的体现,因为cell的重用机制。
- UIImageView+WebCache这个类别,为UIImageView添加了一个属性imageURLKey。
2.1 这个字符串有什么用呢? 它使得每个UIImageView控件都有一个它自己的网络图片URL,通过这个属性可以获取属于UIImageView控件的网络图片URL。 很多时候,我们外部是需要知道这个UIImageView对应的URL的。
2.2 举个例子吧。 像上面提到的UITableViewCell来显示网络图片的例子,因为cell的重用,所以cell上面的UIImaageView也是重用的。 即imageURLKey也是不断改变的,而上面提到SDWebImage的做法是: 当UIImageView对应的URL改变了,会cancel掉它之前旧的URL的下载任务。 如果我们希望下载策略变成: 每个URL都会执行下载直到完成不会中途cancel,当回调成功的时候,通过iamgeURLKey来匹配对应的控件。 回调的时候,不能通过UIImageView来匹配,因为会重用,而imageURLKey会一直赋值,所以应该通过imageURLKey来匹配。
2、 UIImageView+WebCache这个类别,为UIImageView添加了一个loading功能,使得每个UIImageView都具备显示正在加载的状态。
//下面来看看每个UIView控件是怎么实现取消它上面的网络图片的下载任务的- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key { // 取消(停止)下载队列中正在进行下载的任务 NSMutableDictionary *operationDictionary = [self operationDictionary]; id operations = [operationDictionary objectForKey:key]; 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]; }}
八、UITableviewCell中处理图片异步下载
我们知道,tableViewCell是有重用机制的,也就是说,内存中只有当前可见的cell数目的实例,滑动的时候,新显示cell会重用被滑出的cell对象。
这样就存在一个问题:
一般情况下在我们会在cellForRow方法里面设置cell的图片数据源,也就是说如果一个cell的imageview对象开启了一个下载任务,这个时候该cell对象发生了重用,新的image数据源会开启另外的一个下载任务。
由于他们关联的imageview对象实际上是同一个cell实例的imageview对象,就会发生2个下载任务回调给同一个imageview对象,这个时候就有必要做一些处理,避免回调发生时,错误的image数据源刷新了UI。
方案一: SDWebImage提供的UIImageView扩展的解决方案
imageView对象会关联一个下载列表(列表是给AnimationImages用的,这个时候会下载多张图片),当tableview滑动,imageView重设数据源(url)时,会cancel掉下载列表中所有的任务,然后开启一个新的下载任务。
这样子就保证了只有当前可见的cell对象的imageView对象关联的下载任务能够回调,不会发生image错乱。
同时,SDWebImage管理了一个全局下载队列(在DownloadManager中),默认并发量设置为6,也就是说如果可见cell的数目是大于6的,就会有部分下载队列处于等待状态。
而且,在添加下载任务到全局的下载队列中去的时候,SDWebImage默认是采取LIFO策略的,具体是在添加下载任务的时候,将上次添加的下载任务添加依赖为新添加的下载任务。
//把下载操作添加到下载队列中,该方法会调用operation内部的start方法开启图片的下载任务[wself.downloadQueue addOperation:operation];//判断任务的执行优先级,如果是后进先出,则调整任务的依赖关系,优先执行当前的(最后添加)任务if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { [wself.lastAddedOperation addDependency:operation]; //设置当前下载操作为最后一个操作 wself.lastAddedOperation = operation;}
方案二: 非SDWebImage的方案
imageView对象和图片的url相关联,在滑动时,不取消旧的下载任务,而是在下载任务完成回调时,进行url匹配,只有匹配成功的image会刷新imageView对象,而其他的image则只做缓存操作,而不刷新UI。
同时,仍然管理一个执行队列,为了避免占用太多的资源,通常会对执行队列设置一个最大的并发量。
此外,为了保证LIFO的下载策略,可以自己维持一个等待队列,每次下载任务开始的时候,将后进入的下载任务插入到等待队列的前面。
九、开始对源码进行剖析
1、首先通过UIImageView的一个类别来实现网络图片的下载以及显示功能,核心还是通过SDWebImageManager这个单列来管理网络图片的异步下载和缓存处理。
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock { // 取消当前图像下载 [self sd_cancelCurrentImageLoad]; // 利用运行时retain url objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); //判断,如果传入的下载策略不是延迟显示占位图片,那么在主线程中设置占位图片 if (!(options & SDWebImageDelayPlaceholder)) { dispatch_main_async_safe(^{ // 设置占位图像 self.image = placeholder; }); } //如果url不为空 if (url) { //检查activityView是否可用 if ([self showActivityIndicatorView]) { [self addActivityIndicator]; } __weak __typeof(self)wself = self; // 实例化 SDWebImageOperation 操作 id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { //移除UIActivityIndicatorView [wself removeActivityIndicator]; if (!wself) return; //下面block中的操作在主线程中处理 dispatch_main_sync_safe(^{ if (!wself) return; //如果图片下载完成,且传入的下载选项为手动设置图片则直接执行completedBlock回调,并返回 if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) { completedBlock(image, error, cacheType, url); return; } else if (image) { //否则,如果图片存在,则设置图片到UIImageView上面,并刷新重绘视图 wself.image = image; [wself setNeedsLayout]; } else { //如果没有得到图像 //如果传入的下载选项为延迟显示占位图片,则设置占位图片到UIImageView上面,并刷新重绘视图 if ((options & SDWebImageDelayPlaceholder)) { wself.image = placeholder; [wself setNeedsLayout]; } } if (completedBlock && finished) { completedBlock(image, error, cacheType, url); } }); }]; [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; } else { //如果url为空,则在主线中处理下面的操作 dispatch_main_async_safe(^{ //移除UIActivityIndicatorView [self removeActivityIndicator]; //处理错误信息,并执行任务结束回调,把错误信息作为参数传递出去 NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}]; if (completedBlock) { completedBlock(nil, error, SDImageCacheTypeNone, url); } }); }}
2、核心类SDWebImageManager单例是如何通过一个缓存器SDImageCache和一个下载器SDWebImageDownloader来实现图片的缓存和下载器功能的。
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock { //没有completedblock,那么调用这个方法是毫无意义的 NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead"); //检查用户传入的URL是否正确,如果该URL是NSString类型的,那么尝试转换 if ([url isKindOfClass:NSString.class]) { url = [NSURL URLWithString:(NSString *)url]; } //防止因参数类型错误而导致应用程序崩溃,判断URL是否是NSURL类型的,如果不是则直接设置为nil if (![url isKindOfClass:NSURL.class]) { url = nil; } //初始化一个SDWebImageCombinedOperationBlock块 __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; __weak SDWebImageCombinedOperation *weakOperation = operation; BOOL isFailedUrl = NO; //初始化设定该URL是正确的 //加互斥锁,检索请求图片的URL是否在曾下载失败的集合中(URL黑名单) @synchronized (self.failedURLs) { isFailedUrl = [self.failedURLs containsObject:url]; } //如果url不正确或者 选择的下载策略不是『下载失败尝试重新下载』且该URL存在于黑名单中,那么直接返回,回调任务完成block块,传递错误信息 if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) { //该宏保证了completedBlock回调在主线程中执行 dispatch_main_sync_safe(^{ NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]; completedBlock(nil, error, SDImageCacheTypeNone, YES, url); }); return operation; } //加互斥锁,把当前的下载任务添加到『当前正在执行任务数组』中 @synchronized (self.runningOperations) { [self.runningOperations addObject:operation]; } //得到该URL对应的缓存KEY NSString *key = [self cacheKeyForURL:url]; //该方法查找URLKEY对应的图片缓存是否存在,查找完毕之后把该图片(存在|不存在)和该图片的缓存方法以block的方式传递,缓存情况查找完毕之后,在block块中进行后续处理(如果该图片没有缓存·下载|如果缓存存在|如果用户设置了下载的缓存策略是刷新缓存如何处理等等) operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { //先判断该下载操作是否已经被取消,如果被取消则把当前操作从runningOperations数组中移除,并直接返回 if (operation.isCancelled) { @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } return; } //(图片不存在||下载策略为刷新缓存)且(shouldDownloadImageForURL不能响应||该图片存在缓存) if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { //从此处开始,一直在处理downloaderOptions(即下载策略) //如果图像存在,但是下载策略为刷新缓存,则通知缓存图像并尝试重新下载 if (image && options & SDWebImageRefreshCached) { dispatch_main_sync_safe(^{ completedBlock(image, nil, cacheType, YES, url); }); } SDWebImageDownloaderOptions downloaderOptions = 0; //如果下载策略为SDWebImageLowPriority 那么downloaderOptions = 其本身 if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority; if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload; if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache; if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground; if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies; if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates; if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority; //如果图片存在,且下载策略为刷新刷新缓存 if (image && options & SDWebImageRefreshCached) { //如果图像已缓存,但需要刷新缓存,那么强制进行刷新 downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload; //忽略从NSURLCache读取图片 downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse; } //到此处位置,downloaderOptions(即下载策略)处理操作结束 //核心方法:使用下载器,下载图片 id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { if (weakOperation.isCancelled) { //如果此时操作被取消,那么什么也不做 } else if (error) { //如果下载失败,则处理结束的回调,在合适的情况下把对应图片的URL添加到黑名单中 dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(nil, error, SDImageCacheTypeNone, finished, url); } }); if ( error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut && error.code != NSURLErrorInternationalRoamingOff && error.code != NSURLErrorDataNotAllowed && error.code != NSURLErrorCannotFindHost && error.code != NSURLErrorCannotConnectToHost) { @synchronized (self.failedURLs) { [self.failedURLs addObject:url]; } } } else { //下载成功, 先判断当前的下载策略是否是SDWebImageRetryFailed,如果是那么把该URL从黑名单中删除 if ((options & SDWebImageRetryFailed)) { @synchronized (self.failedURLs) { [self.failedURLs removeObject:url]; } } //是否要进行磁盘缓存? BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly); //如果下载策略为SDWebImageRefreshCached且该图片缓存中存在且未下载下来,那么什么都不做 if (options & SDWebImageRefreshCached && image && !downloadedImage) { } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) { //否则,如果下载图片存在且(不是可动画图片数组||下载策略为SDWebImageTransformAnimatedImage&&transformDownloadedImage方法可用) //开子线程处理 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ //在下载后立即将图像转换,并进行磁盘和内存缓存 UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url]; if (transformedImage && finished) { BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage]; [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk]; } //在主线程中回调completedBlock dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url); } }); }); } else { //得到下载的图片且已经完成,则进行缓存处理 if (downloadedImage && finished) { [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; } dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url); } }); } } if (finished) { @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } }]; //处理cancelBlock operation.cancelBlock = ^{ [subOperation cancel]; @synchronized (self.runningOperations) { [self.runningOperations removeObject:weakOperation]; } }; } else if (image) { //如果图片存在,且操作没有被取消,那么在主线程中回调completedBlock,并把当前操作移除 dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(image, nil, cacheType, YES, url); } }); @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } else { //图片缓存不存在且不允许代理下载,那么在主线程中回调completedBlock,并把当前操作移除 dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(nil, nil, SDImageCacheTypeNone, YES, url); } }); @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } }]; return operation;}
3、SDImageCache管理着SDWebImage的缓存,其中内存缓存采用NSCache,同时会创建一个ioQueue负责对硬盘的读写,并且会添加观察者,在收到内存警告、关闭或进入后台时完成对应的处理。同时在后台完成磁盘文件的清理、创建等工作。
/* 1.先检查是否有内存缓存 2.如果没有内存缓存则检查是否有沙盒缓存 3.如果有沙盒缓存,则把该图片做内存缓存并处理doneBlock回调 */- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock { //如果回调不存在,则直接返回 if (!doneBlock) { return nil; } //如果缓存对应的key为空,则直接返回,并把存储方式(无缓存)通过block块以参数的形式传递 if (!key) { doneBlock(nil, SDImageCacheTypeNone); return nil; } //检查该KEY对应的内存缓存,如果存在内存缓存,则直接返回,并把图片和存储方式(内存缓存)通过block块以参数的形式传递 UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { doneBlock(image, SDImageCacheTypeMemory); return nil; } NSOperation *operation = [NSOperation new]; //创建一个操作 //使用异步函数,添加任务到串行队列中(会开启一个子线程处理block块中的任务) dispatch_async(self.ioQueue, ^{ //如果当前的操作被取消,则直接返回 if (operation.isCancelled) { return; } @autoreleasepool { //检查该KEY对应的磁盘缓存 UIImage *diskImage = [self diskImageForKey:key]; //如果存在磁盘缓存,且应该把该图片保存一份到内存缓存中,则先计算该图片的cost(成本)并把该图片保存到内存缓存中 if (diskImage && self.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); [self.memCache setObject:diskImage forKey:key cost:cost]; } //线程间通信,在主线程中回调doneBlock,并把图片和存储方式(磁盘缓存)通过block块以参数的形式传递 dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); }); } }); return operation;}
4、SDWebImageDownloader主要实现下载功能和下载回调,他通过自定义的操作SDWebImageDownloaderOperation来处理具体的下载,并且管理操作之间的依赖关系为LIFO(后进先出)。
//核心方法:下载图片的操作- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { __block SDWebImageDownloaderOperation *operation; __weak __typeof(self)wself = self; //为了避免block的循环引用 //处理进度回调|完成回调等,如果该url在self.URLCallbacks并不存在,则调用createCallback block块 [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{ //处理下载超时,如果没有设置过则初始化为15秒 NSTimeInterval timeoutInterval = wself.downloadTimeout; if (timeoutInterval == 0.0) { timeoutInterval = 15.0; } //根据给定的URL和缓存策略创建可变的请求对象,设置请求超时 //请求策略:如果是SDWebImageDownloaderUseNSURLCache则使用NSURLRequestUseProtocolCachePolicy,否则使用NSURLRequestReloadIgnoringLocalCacheData /* NSURLRequestUseProtocolCachePolicy:默认的缓存策略 1)如果缓存不存在,直接从服务端获取。 2)如果缓存存在,会根据response中的Cache-Control字段判断下一步操作,如: Cache-Control字段为must-revalidata, 则询问服务端该数据是否有更新,无更新的话直接返回给用户缓存数据,若已更新,则请求服务端. NSURLRequestReloadIgnoringLocalCacheData:忽略本地缓存数据,直接请求服务端。 */ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; //设置是否使用Cookies(采用按位与) /* 关于cookies参考:http://blog.csdn.net/chun799/article/details/17206907 */ request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); //开启HTTP管道,这可以显著降低请求的加载时间,但是由于没有被服务器广泛支持,默认是禁用的 request.HTTPShouldUsePipelining = YES; //设置请求头信息(过滤等) if (wself.headersFilter) { request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]); } else { request.allHTTPHeaderFields = wself.HTTPHeaders; } //核心方法:创建下载图片的操作 operation = [[wself.operationClass alloc] initWithRequest:request options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; }); //遍历callbacksForURL数组中的所有字典,执行SDWebImageDownloaderProgressBlock回调 for (NSDictionary *callbacks in callbacksForURL) { //说明:SDWebImageDownloaderProgressBlock作者可能考虑到用户拿到进度数据后会进行刷新处理,因此在主线程中处理了回调 dispatch_async(dispatch_get_main_queue(), ^{ SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; if (callback) callback(receivedSize, expectedSize); }); } } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_barrier_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; //如果完成,那么把URL从URLCallbacks字典中删除 if (finished) { [sself.URLCallbacks removeObjectForKey:url]; } }); //遍历callbacksForURL数组中的所有字典,执行SDWebImageDownloaderCompletedBlock回调 for (NSDictionary *callbacks in callbacksForURL) { SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; if (callback) callback(image, data, error, finished); } } cancelled:^{ SDWebImageDownloader *sself = wself; if (!sself) return; //把当前的url从URLCallbacks字典中移除 dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url]; }); }]; //设置是否需要解码 operation.shouldDecompressImages = wself.shouldDecompressImages; //身份认证 if (wself.urlCredential) { operation.credential = wself.urlCredential; } else if (wself.username && wself.password) { //设置 https 访问时身份验证使用的凭据 operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]; } //判断下载策略是否是高优先级的或低优先级,以设置操作的队列优先级 if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; } //把下载操作添加到下载队列中,该方法会调用operation内部的start方法开启图片的下载任务 [wself.downloadQueue addOperation:operation]; //判断任务的执行优先级,如果是后进先出,则调整任务的依赖关系,优先执行当前的(最后添加)任务 if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation;//设置当前下载操作为最后一个操作 } }]; return operation;}
5、SDWebImageDownloaderOperation是自定义的并发队列,最直接的负责图片的下载。通过NSURLConnection接口来实现。实现SDWebImageOperation来处理取消下载操作。在下载过程中会发送四个通知用于表示开始下载、停止下载、接收到数据、下载完成。图片下载由 NSURLConnection 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。如果子线程进需要不断处理一些事件,那么设置一个Run Loop是最好的处理方式。
//SDWebImageDownloaderOperation是NSOperation的派生类//核心方法:在该方法中处理图片下载操作- (void)start { @synchronized (self) { //判断当前操作是否被取消,如果被取消了,则标记任务结束,并处理后续的block和清理操作 if (self.isCancelled) { self.finished = YES; [self reset]; return; } //条件编译,如果是iphone设备且大于4.0#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 Class UIApplicationClass = NSClassFromString(@"UIApplication"); BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)]; //程序即将进入后台 if (hasApplication && [self shouldContinueWhenAppEntersBackground]) { __weak __typeof__ (self) wself = self; //获得UIApplication单例对象 UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)]; //UIBackgroundTaskIdentifier:通过UIBackgroundTaskIdentifier可以实现有限时间内在后台运行程序 //在后台获取一定的时间去指行我们的代码 self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{ __strong __typeof (wself) sself = wself;#warning 3 if (sself) { [sself cancel]; //取消当前下载操作 [app endBackgroundTask:sself.backgroundTaskId]; //结束后台任务 sself.backgroundTaskId = UIBackgroundTaskInvalid; } }]; }#endif //当前任务正在执行 self.executing = YES; //创建NSURLConnection对象,并设置代理(没有马上发送请求) self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; //获得当前线程 self.thread = [NSThread currentThread]; } //发送网络请求 [self.connection start]; if (self.connection) { if (self.progressBlock) { //进度block的回调 self.progressBlock(0, NSURLResponseUnknownLength); } //注册通知中心,在主线程中发送通知SDWebImageDownloadStartNotification【任务开始下载】 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self]; }); //开启线程对应的Runloop if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) { //确保后台线程的runloop跑起来 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false); } else { //开启Runloop CFRunLoopRun(); } if (!self.isFinished) { //取消网络连接 [self.connection cancel]; //处理错误信息 [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]]; } } else { //执行completedBlock回调,打印Connection初始化失败 if (self.completedBlock) { self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES); } }#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 Class UIApplicationClass = NSClassFromString(@"UIApplication"); if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { return; } if (self.backgroundTaskId != UIBackgroundTaskInvalid) { UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)]; [app endBackgroundTask:self.backgroundTaskId]; self.backgroundTaskId = UIBackgroundTaskInvalid; }#endif}
6、NSOperation和NSOperationQueue:可以取消任务处理队列中的任务,设置最大并发数,设置operation之间的依赖关系。
//取消- (void)cancel { @synchronized (self) { if (self.thread) { //线程间通信,在self.thread线程中调用cancelInternalAndStop方法执行取消和停止操作 [self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO]; } else { [self cancelInternal]; } }}//取消和停止- (void)cancelInternalAndStop { if (self.isFinished) return; //如果已经完成则直接返回 [self cancelInternal]; //处理取消操作 CFRunLoopStop(CFRunLoopGetCurrent()); //关停当前的runloop}//取消网络- (void)cancelInternal { if (self.isFinished) return; [super cancel]; if (self.cancelBlock) self.cancelBlock(); //执行cancelBlock块 //如果连接对象存在,则取消网络请求 if (self.connection) { [self.connection cancel]; //注册通知中心,在主线程中发送通知SDWebImageDownloadStopNotification【下载任务停止】 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; }); //处理当前正在执行和是否已经完成 if (self.isExecuting) self.executing = NO; if (!self.isFinished) self.finished = YES; } //执行清理操作 [self reset];}//任务执行完毕之后,修改当前任务的结束状态(YES)和执行状态(NO),执行清理操作- (void)done { self.finished = YES; self.executing = NO; [self reset];}//清理操作- (void)reset { self.cancelBlock = nil; self.completedBlock = nil; self.progressBlock = nil; self.connection = nil; self.imageData = nil; self.thread = nil;}
7、 connection:didReceiveResponse:当接收到服务器响应的时候调用该方法
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { //'304 Not Modified' is an exceptional one if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) { //获得下载图片的总大小,并执行进度回调 NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0; self.expectedSize = expected; if (self.progressBlock) { self.progressBlock(0, expected); } //初始化可变的Data用来接收图片数据 self.imageData = [[NSMutableData alloc] initWithCapacity:expected]; //得到请求的响应头信息 self.response = response; //注册通知中心,在主线程中发送通知SDWebImageDownloadReceiveResponseNotification【接收到服务器的响应】 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self]; }); } else { //请求出现问题则执行该代码块 NSUInteger code = [((NSHTTPURLResponse *)response) statusCode]; if (code == 304) { [self cancelInternal]; //执行取消操作 } else { [self.connection cancel]; //取消请求 } //注册通知中心,在主线程中发送通知SDWebImageDownloadStopNotification【下载任务停止】 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; }); //执行任务结束block回调,传递错误信息 if (self.completedBlock) { self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES); } //关停当前runloop CFRunLoopStop(CFRunLoopGetCurrent()); [self done]; }}
8、connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。
//当接收到服务器返回数据的时候调用该方法,可能会调用多次- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { //不断拼接接收到的图片数据(二进制数据) [self.imageData appendData:data]; //如果下载图片设置的策略是SDWebImageDownloaderProgressiveDownload,那么处理图片UI if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) { //获得当前已经接收到的二进制数据大小 const NSInteger totalSize = self.imageData.length; // 把图片的二进制数据转换为CGImageSourceRef CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL); //如果是第一次(即接收到第一部分的图片数据) if (width + height == 0) { CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); if (properties) { NSInteger orientationValue = -1; CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); if (val) CFNumberGetValue(val, kCFNumberLongType, &height); val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); if (val) CFNumberGetValue(val, kCFNumberLongType, &width); val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue); CFRelease(properties); orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; } } //接收数据中期(之前接收过一部分,但为完全) if (width + height > 0 && totalSize < self.expectedSize) { // Create the image CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);#ifdef TARGET_OS_IPHONE // Workaround for iOS anamorphic image if (partialImageRef) { const size_t partialHeight = CGImageGetHeight(partialImageRef); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); if (bmContext) { CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef); CGImageRelease(partialImageRef); partialImageRef = CGBitmapContextCreateImage(bmContext); CGContextRelease(bmContext); } else { CGImageRelease(partialImageRef); partialImageRef = nil; } }#endif if (partialImageRef) { UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; UIImage *scaledImage = [self scaledImageForKey:key image:image]; if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:scaledImage]; } else { image = scaledImage; } CGImageRelease(partialImageRef); dispatch_main_sync_safe(^{ if (self.completedBlock) { self.completedBlock(image, nil, nil, NO); } }); } } //释放imageSource对象 CFRelease(imageSource); } //执行progressBlock,不断更新进度信息 if (self.progressBlock) { self.progressBlock(self.imageData.length, self.expectedSize); }}
9、connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
//当请求结束的时候会调用该方法- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection { SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock; @synchronized(self) { //关停当前的runloop CFRunLoopStop(CFRunLoopGetCurrent()); //把线程和连接对象清空 self.thread = nil; self.connection = nil; //在主线程中发出通知: dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];//任务停止 [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];//任务完成 }); } //请求头是否是从缓存获取? if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) { responseFromCached = NO; } if (completionBlock) { //如果下载策略是SDWebImageDownloaderIgnoreCachedResponse&&responseFromCached为真,执行completionBlock if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) { completionBlock(nil, nil, nil, YES); } else if (self.imageData) { //如果得到图片的二进制数据, 把二进制数据转换为图片 UIImage *image = [UIImage sd_imageWithData:self.imageData]; //返回指定URL的缓存键值,即URL字符串 NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; //处理图片的缩放问题 image = [self scaledImageForKey:key image:image]; if (!image.images) { //如果需要,那么对图片进行解压缩处理 if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:image]; } } //如果发现转换之后图片的Size为0,则执行completionBlock,图片参数传nil if (CGSizeEqualToSize(image.size, CGSizeZero)) { completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES); } else { completionBlock(image, self.imageData, nil, YES); } } else { completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES); } } self.completionBlock = nil; //结束后的处理 [self done]; }
十、图片的解压缩
后续补上。
参考文献
- SDWebImage异步下载和缓存的源码剖析
- SDWebImage下载和缓存图片(UIImge)
- Android下载图片 图片的异步加载 和缓存存取
- 关于SDWebImage中下载图片和缓存图片的实现原理
- SDWebImage 图片加载和缓存
- 图片的异步加载和缓存
- 使用SDWebImage进行简单的图片下载和缓存
- SDWebImage使用,图片加载和缓存
- SDWebImage使用,图片加载和缓存
- SDWebImage使用,图片加载和缓存
- SDWebImage使用,图片加载和缓存
- SDWebImage使用,图片加载和缓存
- SDWebImage使用,图片加载和缓存
- SDWebImage使用,图片加载和缓存
- SDWebImage使用,图片加载和缓存
- SDWebImage使用,图片加载和缓存
- SDWebImage使用,图片加载和缓存
- SDWebImage使用,图片加载和缓存
- 安卓小案例收集五(内容提供者、动画)
- 2.Enable ADB integration' to be enabled.
- 从头学android_GET 和 POST 网络请求
- Android实现正方系统的登录以及课程表,成绩获取和空课室的查询(二)
- tp框架多个OR与And连用查询笔记
- SDWebImage异步下载和缓存的源码剖析
- notification复用中抛出can not parcel recyle’s bitmap
- 10、正则表达式匹配练习
- CF Watchmen 【思维+数学】
- 自动化发布项目之jenkins + git + maven 自动化部署一个web项目
- linux下的进程控制块task_struct详解
- Android Studio详细使用教程
- HYSBZ 1036 树的统计Count(树链剖分)
- hdu1349(题解) Minimum Inversion Number