美文网首页iOS开发经验总结iOS
读SDWebImage源码记录(一)

读SDWebImage源码记录(一)

作者: 一剑书生 | 来源:发表于2015-06-08 02:02 被阅读1811次

    断断续续看了SDWebImage的源码,下面按照当初阅读思路,写写SDWebImage中的一些实现逻辑以及基础技术。


    SDWebImage框架主要用于加载网络图片,主要是调用UIImageView+WebCache.h中的类别方法即可:

      - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
    
    

    它主要做了两个事情:

    1. 异步下载图片
    2. 异步缓存到本地

    那就来看看怎么实现第一点的,在xcode中点击上述方法,在UIImageView+WebCache.m中找到以下代码

    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
        [self sd_cancelCurrentImageLoad];
        ...省略...
       
        if (url) {
            __weak UIImageView *wself = self; //避免循环引用
            id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                if (!wself) return;
                dispatch_main_sync_safe(^{
                    if (!wself) return;
                    if (image) {    //若图片存在,在主线程中刷新UI
                        wself.image = image;
                        [wself setNeedsLayout];
                    } else {
                        if ((options & SDWebImageDelayPlaceholder)) {
                            wself.image = placeholder;
                            [wself setNeedsLayout];
                        }
                    }
                    if (completedBlock && finished) {
                        completedBlock(image, error, cacheType, url);
                    }
                });
            }];
            [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
        } else {
            ...省略...
        }
    }
    

    这里插播一点,上面的代码用到了宏定义的dispatch_main_sync_safe:

    #define dispatch_main_sync_safe(block)\
        if ([NSThread isMainThread]) {\
            block();\
        } else {\
            dispatch_sync(dispatch_get_main_queue(), block);\
        }
    

    目的是想在主线程中执行操作,为什么要判断当前线程是否就是主线程中呢?
    如果在主线程中执行dispatch_sync(dispatch_get_main_queue(), block) 同步操作时,会出现死锁问题,因为主线程正在执行当前代码,根本无法将block添加到主队列中。
    另外,由于宏定义的原因,断点是跳不进宏定义里面的block。

    继续看,先忽略掉一些细节,在上面代码可以看到是调用了SDWebImageManager中的方法:

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
    

    点击跟进去,看到以下代码:

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
        // Invoking this method without a completedBlock is pointless
        NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
    
        ...省略...
    
        __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        __weak SDWebImageCombinedOperation *weakOperation = operation;
    
        ...省略...
        @synchronized (self.runningOperations) {
            [self.runningOperations addObject:operation];
        }
        NSString *key = [self cacheKeyForURL:url];
    
        //查询本地缓存是否有该URL地址对应的图片
        operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
             ...省略...
            
            //当图片不存在或标志位设置了需要更新图片缓存,下载图片
            if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            
                ...省略....
    
                //调用SDImageDownloader的方法请求下载图片
                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) { //下载出错
                        dispatch_main_sync_safe(^{
                            if (!weakOperation.isCancelled) {
                                completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                            }
                        });
                        //如果不是因为网络异常,那么该URL地址下载图片失败,添加到failedURLs中
                        if (error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut) {
                            @synchronized (self.failedURLs) {
                                if (![self.failedURLs containsObject:url]) {
                                    [self.failedURLs addObject:url];
                                }
                            }
                        }
                    }
                    else {
    
                        ...进行一些处理,主要是缓存图片...
    
                    if (finished) {
                        @synchronized (self.runningOperations) {
                            [self.runningOperations removeObject:operation];
                        }
                    }
                }];
                operation.cancelBlock = ^{
                    [subOperation cancel];
                    
                    @synchronized (self.runningOperations) {
                        [self.runningOperations removeObject:weakOperation];
                    }
                };
            }
            else if (image) {  //查询本地缓存后发现图片已经存在
                dispatch_main_sync_safe(^{
                    if (!weakOperation.isCancelled) {
                        completedBlock(image, nil, cacheType, YES, url);
                    }
                });
                @synchronized (self.runningOperations) {
                    [self.runningOperations removeObject:operation];
                }
            }
            else {  //缓存没有对应图片,且代理不允许下载图片(代理可决定当图片不在缓存时,是否下载该图片)
                // Image not in cache and download disallowed by delegate
                dispatch_main_sync_safe(^{
                    if (!weakOperation.isCancelled) {
                        completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
                    }
                });
                @synchronized (self.runningOperations) {
                    [self.runningOperations removeObject:operation];
                }
            }
        }];
    
        return operation;
    }
    
    

    上面省略掉了判断该URL地址是否有缓存图片相关功能的代码,暂时忽略缓存部分。从上面看到,它调用了专门负责网络请求下载图片的SDImageDownloader类的方法,去请求下载图片

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageDownloaderOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
    

    该方法和SDWebImageManager类的下载方法表面上还长的一模一样,额,不多说,接着看

    
    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
        __block SDWebImageDownloaderOperation *operation;
        __weak SDWebImageDownloader *wself = self;
    
        [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
            NSTimeInterval timeoutInterval = wself.downloadTimeout;
            if (timeoutInterval == 0.0) {
                timeoutInterval = 15.0;  //下载超时时间
            }
    
            // 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:url 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 = [[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];
                                                                 });
                                                                 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;
                                                                __block NSArray *callbacksForURL;
                                                                //下载完成回调,要等到barrierQueue队列中的进度block执行完后才能执行,并从URLCallbacks字典中移除该URL对应的block信息
                                                                dispatch_barrier_sync(sself.barrierQueue, ^{
                                                                    callbacksForURL = [sself.URLCallbacks[url] copy];
                                                                    if (finished) {
                                                                        [sself.URLCallbacks removeObjectForKey:url];
                                                                    }
                                                                });
                                                                for (NSDictionary *callbacks in callbacksForURL) {
                                                                    SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                                    if (callback) callback(image, data, error, finished);
                                                                }
                                                            }
                                                            cancelled:^{
                                                                SDWebImageDownloader *sself = wself;
                                                                if (!sself) return;
                                                                //下载取消回调,移除该URL对应的block信息
                                                                dispatch_barrier_async(sself.barrierQueue, ^{
                                                                    [sself.URLCallbacks removeObjectForKey:url];
                                                                });
                                                            }];
            operation.shouldDecompressImages = wself.shouldDecompressImages; //是否解压图片
            
            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;
            }
    
            //添加操作到队列中
            [wself.downloadQueue addOperation:operation];
            //通过设置操作的依赖关系将下载顺序改为后进先出
            if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { 
                // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
                [wself.lastAddedOperation addDependency:operation]; //最后一个操作依赖于当前操作的完成,这样就后进先出了
                wself.lastAddedOperation = operation;
            }
        }];
    
        return operation;
    }
    

    上面的代码主要功能是创建下载operation,把它添加到下载队列,那么下载队列是什么时候创建的呢?
    SDWebImageDownloader类在初始化的时候,初始化了一个下载队列,并设置了最大并发数,请看:

    - (id)init {
        if ((self = [super init])) {
            _operationClass = [SDWebImageDownloaderOperation class]; //下载操作类
            _shouldDecompressImages = YES; //默认解压图片
            _executionOrder = SDWebImageDownloaderFIFOExecutionOrder; //默认下载顺序是先进先出
            _downloadQueue = [NSOperationQueue new];  //下载队列
            _downloadQueue.maxConcurrentOperationCount = 6; //最大并发数
            _URLCallbacks = [NSMutableDictionary new];  
            _HTTPHeaders = [NSMutableDictionary dictionaryWithObject:@"image/webp,image/*;q=0.8" forKey:@"Accept"]; //http头部参数
            _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);  //自定义GCD队列
            _downloadTimeout = 15.0; //下载超时时间
        }
        return self;
    }
    

    其中_URLCallbacks是用来保存图片下载的回调信息,一般是一个URL对应一张图片下载,包含了下载进度的block和完成的block;来看看上面代码中调用的私有方法,你就会明白了:

    - (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
        ...省略...
    
        //使用dispatch_barrier_sync来保证同一时间只有一个线程能对URLCallbacks进行操作
        dispatch_barrier_sync(self.barrierQueue, ^{
            BOOL first = NO;
            if (!self.URLCallbacks[url]) { //该URL没有对应的回调信息,则是第一次下载
                self.URLCallbacks[url] = [NSMutableArray new];
                first = YES;
            }
    
            // Handle single download of simultaneous download request for the same URL
            NSMutableArray *callbacksForURL = self.URLCallbacks[url];
            NSMutableDictionary *callbacks = [NSMutableDictionary new];
            if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; //进度回调
            if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; //完成回调
            [callbacksForURL addObject:callbacks];
            self.URLCallbacks[url] = callbacksForURL;
    
            if (first) { //第一次下载
                createCallback();
            }
        });
    }
    

    在以上代码中可以看到,凡是对URLCallbacks的添加移除操作都是使用dispatch_barrier_sync 函数,保证了线程安全性。
    接下来看看SDWebImageDownloaderOperation类是怎么发起网络请求下载数据的:

    - (void)start {
        @synchronized (self) { //加锁,因为多线程并发执行
            if (self.isCancelled) { //该操作取消后需要设置finished状态为YES
                self.finished = YES;
                [self reset];
                return;
            }
    
    #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
            //在后台执行
            if ([self shouldContinueWhenAppEntersBackground]) {
                __weak __typeof__ (self) wself = self;  //声明一个weak变量指向self,避免循环引用
                self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
                    __strong __typeof (wself) sself = wself; //block中不允许self被释放了,所以强引用;block执行完,strongself会被释放
    
                    if (sself) {
                        [sself cancel];
    
                        [[UIApplication sharedApplication] endBackgroundTask:sself.backgroundTaskId];
                        sself.backgroundTaskId = UIBackgroundTaskInvalid;
                    }
                }];
            }
    #endif
    
            self.executing = YES;
            self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
            self.thread = [NSThread currentThread];
        }
        //NSUrlConnection开始请求加载数据
        [self.connection start];
    
        if (self.connection) {
            if (self.progressBlock) {
                self.progressBlock(0, NSURLResponseUnknownLength);
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                [[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 { //启动当前线程的runloop,去接收异步回调事件,否则执行完start方法,线程结束,delegate(也就是当前对象)接收不到返回的数据了
                CFRunLoopRun();
            }
    
            if (!self.isFinished) {
                [self.connection cancel];
                [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
            }
        }
        else { //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
        if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
            [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
            self.backgroundTaskId = UIBackgroundTaskInvalid;
        }
    #endif
    }
    

    SDWebImageDownloaderOperation:下载操作类,继承了NSOperation,并重写了start方法,所以必须手动管理操作的状态(executing与finished属性),检测isCancelled的状态。
    对于图片的下载,使用的是NSUrlConnection,它实现了NSURLConnectionDataDelegate协议的几个:

    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; //收到响应
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data; //接收数据
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection; //完成加载
    

    下面看看是怎么接收处理数据的:

    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
        [self.imageData appendData:data]; //追加数据
    
        if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
            ...省略...
            // Get the total bytes downloaded (获取数据总大小)
            const NSInteger totalSize = self.imageData.length;
    
            // Update the data source, we must pass ALL the data, not just the new bytes
            //更新数据源,传入目前接收到的所有数据
            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);
    
                    // When we draw to Core Graphics, we lose orientation information,
                    // which means the image below born of initWithCGIImage will be
                    // oriented incorrectly sometimes. (Unlike the image born of initWithData
                    // in connectionDidFinishLoading.) So save it here and pass it on later.
                    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); //还没下载完
                        }
                    });
                }
            }
    
            CFRelease(imageSource);
        }
    
        if (self.progressBlock) { //下载进度回调
            self.progressBlock(self.imageData.length, self.expectedSize);
        }
    }
    

    再来看看结束加载数据的方法:

    - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
        SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
        @synchronized(self) {
            //停止当前线程的runloop,与上面的CFRunLoopRun配套使用
            CFRunLoopStop(CFRunLoopGetCurrent());
            self.thread = nil;
            self.connection = nil;
            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:nil];
            });
        }
        
        if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
            responseFromCached = NO;
        }
        
        if (completionBlock) {
            if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
                completionBlock(nil, nil, nil, YES);
            }
            else {
                UIImage *image = [UIImage sd_imageWithData:self.imageData];
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                image = [self scaledImageForKey:key image:image]; //缩放
                
                // Do not force decoding animated GIFs (GIF图片不解码)
                if (!image.images) {
                    if (self.shouldDecompressImages) { 
                        image = [UIImage decodedImageWithImage:image];
                    }
                }
                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);
                }
            }
        }
        self.completionBlock = nil;
        [self done];
    }
    

    上面涉及到了对图片的解码操作,因为我们用到的图片格式一般为PNG或JPG,都是经过编码压缩过的,而显示图片的时候,要进行解码,耗时较大,在这里直接解码的话,显示的时候就不需要再解码了,节省了时间,不过需要占用较多空间。
    至此,也就了解了SDWebImage异步下载图片的大概流程。

    以上个人见解,水平有限,如有错漏,欢迎指出,就酱~~~

    相关文章

      网友评论

        本文标题:读SDWebImage源码记录(一)

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