您的位置:首页 > 其它

SDWebImage扩展二次加载图片

2016-03-08 12:28 471 查看
随着网络安全的意识加重,相信很多同学都遇到过图片二次加载的需求,即第一次请求获得图片的URL,第二次请求获得图片。在iOS中图片的加载库很多,而SDWebImage绝对是其中的翘楚,也是广大iOSer们开发时候的首选。这里简单介绍一下自己在项目中在SDWebImage基础上扩展的图片二次加载请求。为了不破坏SDWebImage原有的线程控制和加载逻辑,所以获取图片的URL的线程仍然使用继承SDWebImageOperation的方式来开展,这里取名为:SDWebImageFetchURLOperation,代码如下:

SDWebImageFetchURLOperation.h

/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

#import <Foundation/Foundation.h>
#import "SDWebImageDownloader.h"
#import "SDWebImageOperation.h"

@interface SDWebImageFetchURLOperation : NSOperation <SDWebImageOperation>

/**
* The request used by the operation's connection.
*/
@property (strong, nonatomic) NSMutableURLRequest *request;

/**
* Whether the URL connection should consult the credential storage for authenticating the connection. `YES` by default.
*
* This is the value that is returned in the `NSURLConnectionDelegate` method `-connectionShouldUseCredentialStorage:`.
*/
@property (nonatomic, assign) BOOL shouldUseCredentialStorage;

/**
* The credential used for authentication challenges in `-connection:didReceiveAuthenticationChallenge:`.
*
* This will be overridden by any shared credentials that exist for the username or password of the request URL, if present.
*/
@property (nonatomic, strong) NSURLCredential *credential;

/**
*  Initializes a `SDWebImageFetchURLOperation` object
*
*  @see SDWebImageDownloaderOperation
*
*  @param request        the URL request
*  @param completedBlock the block executed when the download is done.
*                        @note the completed block is executed on the main queue for success. If errors are found, there is a chance the block will be executed on a background queue
*  @param cancelBlock    the block executed if the download (operation) is cancelled
*
*  @return the initialized instance
*/
- (id)initWithRequest:(NSMutableURLRequest *)request
completed:(SDWebImageFetchURLCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock;

@end


SDWebImageFetchURLOperation.m

/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

#import "SDWebImageFetchURLOperation.h"
#import "SDWebImageDecoder.h"
#import "UIImage+MultiFormat.h"
#import <ImageIO/ImageIO.h>
#import "SDWebImageManager.h"

@interface SDWebImageFetchURLOperation () <NSURLConnectionDataDelegate>

@property (copy, nonatomic) SDWebImageFetchURLCompletedBlock completedBlock;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;

@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
@property (assign, nonatomic) NSInteger expectedSize;
@property (strong, nonatomic) NSMutableData *urlData;
@property (strong, nonatomic) NSURLConnection *connection;
@property (strong, atomic) NSThread *thread;

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
#endif

@end

@implementation SDWebImageFetchURLOperation {
//    size_t width, height;
UIImageOrientation orientation;
BOOL responseFromCached;
}

@synthesize executing = _executing;
@synthesize finished = _finished;

- (id)initWithRequest:(NSMutableURLRequest *)request
completed:(SDWebImageFetchURLCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock {
if ((self = [super init])) {
self.request = request;
_shouldUseCredentialStorage = YES;
_completedBlock = [completedBlock copy];
_cancelBlock = [cancelBlock copy];
_executing = NO;
_finished = NO;
_expectedSize = 0;
responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called
}
return self;
}

- (void)start {

@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}

self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
}

[self.connection start];

if (self.connection) {

[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];

if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
// Make sure to run the runloop in our background thread so it can process downloaded data
// Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
//       not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466) CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
CFRunLoopRun();
}

if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else {
if (self.completedBlock) {
self.completedBlock(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
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}

- (void)cancel {
@synchronized (self) {
if (self.thread) {
[self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
}
else {
[self cancelInternal];
}
}
}

- (void)cancelInternalAndStop {
if (self.isFinished) return;
[self cancelInternal];
CFRunLoopStop(CFRunLoopGetCurrent());
}

- (void)cancelInternal {
if (self.isFinished) return;
[super cancel];
if (self.cancelBlock) self.cancelBlock();

if (self.connection) {
[self.connection cancel];
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];

// As we cancelled the connection, its callback won't be called and thus won't
// maintain the isFinished and isExecuting flags.
if (self.isExecuting) self.executing = NO;
if (!self.isFinished) self.finished = YES;
}

[self reset];
}

- (void)done {
self.finished = YES;
self.executing = NO;
[self reset];
}

- (void)reset {
self.cancelBlock = nil;
self.completedBlock = nil;
self.connection = nil;
self.urlData = nil;
self.thread = nil;
}

- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isConcurrent {
return YES;
}

#pragma mark NSURLConnection (delegate)

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
if (![response respondsToSelector:@selector(statusCode)] || [((NSHTTPURLResponse *)response) statusCode] < 400) {
NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
self.expectedSize = expected;

self.urlData = [[NSMutableData alloc] initWithCapacity:expected];
}
else {
[self.connection cancel];

[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:nil];

if (self.completedBlock) {
self.completedBlock(nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
}
CFRunLoopStop(CFRunLoopGetCurrent());
[self done];
}
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.urlData appendData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
SDWebImageFetchURLCompletedBlock completionBlock = self.completedBlock;
@synchronized(self) {
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
self.connection = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:nil];
}

if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
responseFromCached = NO;
}

if (completionBlock)
{
NSString *url = [[NSString alloc] initWithData:self.urlData encoding:NSUTF8StringEncoding];
if (!url) {
completionBlock(nil, [NSError errorWithDomain:@"SDWebImageErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey : @"fetch image url failed"}], YES);
}
else {
completionBlock(url, nil, YES);
}
}
self.completionBlock = nil;
[self done];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
CFRunLoopStop(CFRunLoopGetCurrent());
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:nil];

if (self.completedBlock) {
self.completedBlock(nil, error, YES);
}

[self done];
}

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
responseFromCached = NO; // If this method is called, it means the response wasn't read from cache
if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
// Prevents caching of responses
return nil;
}
else {
return cachedResponse;
}
}

- (BOOL)shouldContinueWhenAppEntersBackground {
return SDWebImageDownloaderContinueInBackground;
}

- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection {
return self.shouldUseCredentialStorage;
}

- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
} else {
if ([challenge previousFailureCount] == 0) {
if (self.credential) {
[[challenge sender] useCredential:self.credential forAuthenticationChallenge:challenge];
} else {
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
} else {
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
}
}

@end


其实这部分代码基本属于比葫芦画瓢,就是模仿SDWebImageDownloaderOperation来写的。写好了获取图片URL的Operation,就要处理逻辑方面的东西了。这部分的逻辑主要在SDWebImageDownloader中做修改,修改思路就是在每个下载图片的operation前面都添加一个获取图片的operation,然后让下载的operation依赖于获取图片URL的operation,但是相信大家使用SDWebImage的情形大部分还是在UITabView等列表中,而且图片下载的数据传输远远大于获取一个URL的数据量,所以按照上面的思路来做的话很可能造成大量的获取图片URL的operation占用资源造成图片下载的不及时,所以这里把获取图片URL的operation优先级降低,把图片下载operation的优先级提高,而图片下载又依赖于图片URL的获取,这样就基本符合原始SDWebImage的加载逻辑了。下面是逻辑处理代码。不对的地方欢迎指正交流。

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__block SDWebImageFetchURLOperation *fetchOperation;
__weak SDWebImageDownloader *wself = self;

[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{

NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}

__block NSURL *mURL = [url copy];

if ([mURL.description hasPrefix:@"xxxxxxxxxxxxxxx"]) {//这里判断是不是需要进行二次加载的URL
NSMutableURLRequest *prerequest = [[NSMutableURLRequest alloc] initWithURL:mURL cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:5.0];
[prerequest setHTTPMethod:@"GET"];

fetchOperation = [[SDWebImageFetchURLOperation alloc] initWithRequest:prerequest completed:^(NSString *url, NSError *error, BOOL finished) {
if (!url) {
return ;
}
if (operation) {
[operation changeURL:[NSURL URLWithString:url]];
}
} cancelled:^{
return ;
}];
fetchOperation.queuePriority = NSOperationQueuePriorityLow;//降低获取URL线程优先级
}

// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:mURL cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}

operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
NSArray *callbacksForURL = [sself callbacksForURL:url];
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
NSArray *callbacksForURL = [sself callbacksForURL:url];
if (finished) {
[sself removeCallbacksForURL:url];
}
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
[sself removeCallbacksForURL:url];
}];

if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}

//        if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;//提高下载优先级
//        } else if (options & SDWebImageDownloaderLowPriority) {
//            operation.queuePriority = NSOperationQueuePriorityLow;
//        }

if (fetchOperation) {//如果有二次加载的operation存在,那么设置下载和获取operation的依赖关系并加入线程队列
[operation addDependency:fetchOperation];
[wself.downloadQueue addOperation:operation];
[wself.downloadQueue addOperation:fetchOperation];
}else{
[wself.downloadQueue addOperation:operation];
}
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
if (fetchOperation) {
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = fetchOperation;
}else{
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}
}];

return operation;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: