iOS面试题我爱编程iOS面试题

SDWebImage4.0源码探究(一)面试题

2018-04-17  本文已影响814人  lionsom_lin

目录

官方文档

一、UML图和时序图

[图片上传失败...(image-d49a02-1523894672845)]

[图片上传失败...(image-e4a15e-1523894672845)]


二、SDWebImage 中@autoreleasepool的应用

现考虑如下代码:

for (int i = 0; i < 10000; i++) {
    [self doSthWith:object];
}

这段代码和笔试题关键部分大同小异。如果"doSthWith:"方法要创建一个临时对象,那么这个对象很可能会放在自动释放池里。笔试题中最后stringByAppendingString方法很有可能属于上述的方法。因此如果涉及到了自动释放池,那么问题也应该就出在上面。

注意:即便临时对象在调用完方法后就不再使用了,它们也依然处于存活状态,因为目前它们都在自动释放池里,等待系统稍后进行回收。但自动释放池却要等到该线程执行下一次事件循环时才会清空,这就意味着在执行for循环时,会有持续不断的新的临时对象被创建出来,并加入自动释放池。要等到结束for循环才会释放。在for循环中内存用量会持续上涨,而等到结束循环后,内存用量又会突然下降。

而如果把循环内的代码包裹在“自动释放池”中,那么在循环中自动释放的对象就会放在这个池,而不是在线程的主池里面。如下:

for (int i = 0; i < 1000000; i++) {
        @autoreleasepool {
            NSString *str = @"abc";
            str = [str lowercaseString];
            str = [str stringByAppendingString:@"xyz"];
        }        
}

新增的自动释放池可以减少内存用量,因为系统会在块的末尾把这些对象回收掉。而上述这些临时对象,正在回收之列。

自动释放池的机制就像“栈”。系统创建好池之后,将其压入栈中,而清空自动释放池相当于将池从栈中弹出。在对象上执行自动释放操作,就等于将其放入位于栈顶的那个池。

结论:@autoreleasepool利于局部变量立刻释放


三、SDWebImage 支持GIF动图吗?

3.1、SDWebImage 4.0版本之前的UIImage+GIF类别

SDWebImage这个库里有一个UIImage+GIF的类别,里面为UIImage扩展了三个方法:

@interface UIImage (GIF)
+ (IImage *)sd_animatedGIFNamed:(NSString *)name;
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data;
- (UIImage *)sd_animatedImageByScalingAndCroppingToSize:(CGSize)size;
@end

具体使用 参考文章

NSString *path = [[NSBundle mainBundle] pathForResource:@"gifTest" ofType:@"gif"];
NSData *data = [NSData dataWithContentsOfFile:path];
UIImage *image = [UIImage sd_animatedGIFWithData:data];
gifImageView.image = image;

3.2、SDWebImage 4.0版本之后的UIImage+GIF类别

SDWebImage这个库里有一个UIImage+GIF的类别,其中的扩展方法只有一个sd_animatedGIFWithData :它只返回数据包含的第一帧的图像

@interface UIImage (GIF)
/**
 *  Compatibility method - creates an animated UIImage from an NSData, it will only contain the 1st frame image
 */
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data;

/**
 *  Checks if an UIImage instance is a GIF. Will use the `images` array
 */
- (BOOL)isGIF;
@end

具体使用

只返回一帧的图像

结论:SDWebImage 4.0版本之后,sd_animatedGIFWithData :没办法实现gif加载;

3.3、SDWebImage 4.0版本之后 加载gif新方法

官方文档:


Animated Images (GIF) support


结论:


四、SDWebImage 如何区分图片格式?

在分类"NSData+ImageContentType.h"中

typedef NS_ENUM(NSInteger, SDImageFormat) {
    SDImageFormatUndefined = -1,
    SDImageFormatJPEG = 0,
    SDImageFormatPNG,
    SDImageFormatGIF,
    SDImageFormatTIFF,
    SDImageFormatWebP
};

/**
 *  Return image format
 *
 *  @param data the input image data
 *
 *  @return the image format as `SDImageFormat` (enum)
 */
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data;
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    
    uint8_t c;
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return SDImageFormatJPEG;
        case 0x89:
            return SDImageFormatPNG;
        case 0x47:
            return SDImageFormatGIF;
        case 0x49:
        case 0x4D:
            return SDImageFormatTIFF;
        case 0x52:
            // R as RIFF for WEBP
            if (data.length < 12) {
                return SDImageFormatUndefined;
            }
            
            NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
            if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                return SDImageFormatWebP;
            }
    }
    return SDImageFormatUndefined;
}

实现思想:将数据data转为十六进制数据,取第一个字节数据进行判断。

查看图片

五、SDWebImage 缓存图片的名称如何避免重名

对『绝对路径』进行MD5


六、SDWebImage 中常量的定义

可参考之前的文章宏(define)与常量(const)

// SDWebImage-umbrella.h

#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
// 在SDWebImageCompat.h
FOUNDATION_EXPORT NSString *const SDWebImageErrorDomain;

// 在SDWebImageCompat.m
NSString *const SDWebImageErrorDomain = @"SDWebImageErrorDomain";
UIKIT_EXTERN NSString * const CHECK_SUM_MQ;

NSString * const CHECK_SUM_MQ = @"123";
// 在系统内部文件 UIKitDefines.h 中

#ifdef __cplusplus
#define UIKIT_EXTERN        extern "C" __attribute__((visibility ("default")))
#else
#define UIKIT_EXTERN            extern __attribute__((visibility ("default")))
#endif
// 系统内部文件 NSObjCRuntime.h 中

#if defined(__cplusplus)
#define FOUNDATION_EXTERN extern "C"
#else
#define FOUNDATION_EXTERN extern
#endif

#if TARGET_OS_WIN32

    #if defined(NSBUILDINGFOUNDATION)
        #define FOUNDATION_EXPORT FOUNDATION_EXTERN __declspec(dllexport)
    #else
        #define FOUNDATION_EXPORT FOUNDATION_EXTERN __declspec(dllimport)
    #endif

    #define FOUNDATION_IMPORT FOUNDATION_EXTERN __declspec(dllimport)

#else
    #define FOUNDATION_EXPORT  FOUNDATION_EXTERN
    #define FOUNDATION_IMPORT FOUNDATION_EXTERN
#endif

七、SDWebImage 如何保证UI操作放在主线程中执行?

iOS UI 操作在主线程不一定安全?

在SDWebImage的SDWebImageCompat.h中有这样一个宏定义,用来保证主线程操作,为什么要这样写?

// SDWebImageCompat.h 中

#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

在此之前见到最多的是这样的:

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

对比两段代码可以发现前者有两个地方改变了,一是多了 #ifndef,二是判断条件改变了。

显然,增加 #ifndef 是为了提高代码的严谨,防止重复定义 dispatch_main_async_safe

关于判断条件的改变的原因则是复杂得多了,可参考文档

GCD's Main Queue vs. Main Thread

Queues are not bound to any specific thread

分析:如何判断当前是否在main thread?

最简单的方法

检查我们当前在主线程上执行的最简单的方法是使用[NSThread isMainThread] - GCD缺少一个类似的方便的API来检查我们是否在主队列上运行,因此许多开发人员使用了NSThread API。如下:

if ([NSThread isMainThread]) {
    block();
} else {
    dispatch_async(dispatch_get_main_queue(), block);
}

这在大多数情况下是有效的,直到它出现了异常。下面是关于ReactiveCocoa repo问题的摘录:
ReactiveCocoa issue

image

潜在的问题是VektorKit API正在检查是否在主队列上调用它,而不是检查它在主线程上运行。

虽然每个应用程序都只有一个主线程,但是在这个主线程上执行许多不同的队列是可能的。

如果库(如VektorKit)依赖于在主队列上检查执行,那么从主线程上执行的非主队列调用API将导致问题。也就是说,如果在主线程执行非主队列调度的API,而这个API需要检查是否由主队列上调度,那么将会出现问题。

更安全的方法一

从技术上讲,我认为这是一个 MapKit / VektorKit 漏洞,苹果的UI框架通常保证在从主线程调用时正确工作,没有任何文档提到需要在主队列上执行代码。

但是,现在我们知道某些api不仅依赖于主线程上的运行,而且还依赖于主队列,因此检查当前队列而不是检查当前线程更安全。

检查当前队列还可以更好地利用GCD为线程提供的抽象。从技术上讲,我们不应该知道/关心主队列是一种总是绑定到主线程的特殊队列。

不幸的是,GCD没有一个非常方便的API来检查我们当前正在运行的队列(这很可能是许多开发人员首先使用NSThread.isMainThread()的原因)。

我们需要使用 dispatch_queue_set_specific 函数来将键值对与主队列相关联;稍后,我们可以使用 dispatch_queue_get_specific 来检查键和值的存在。

- (void)function {
    static void *mainQueueKey = "mainQueueKey";
    dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, &mainQueueKey, NULL);
    if (dispatch_get_specific(mainQueueKey)) {
        // do something in main queue
        //通过这样判断,就可以真正保证(我们在不主动搞事的情况下),任务一定是放在主队列中的
    } else {
        // do something in other queue
    }
}
更安全的方法二 (SDWebImage使用的方法)

我们知道在使用 GCD 创建一个 queue 的时候会指定 queue_label,可以理解为队列名,就像下面:

dispatch_queue_t myQueue = dispatch_queue_create("com.apple.threadQueue", DISPATCH_QUEUE_SERIAL);

而第一个参数就是 queue_label,根据官方文档解释,这个queueLabel 是唯一的,所以SDWebImage就采用了这个方式

//取得当前队列的队列名
dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)
   
//取得主队列的队列名
dispatch_queue_get_label(dispatch_get_main_queue())

然后通过 strcmp 函数进行比较,如果为0 则证明当前队列就是主队列。

SDWebImage中的实例 :判断当前是否是IOQueue

- (void)checkIfQueueIsIOQueue {
    const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
    const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue);
    if (strcmp(currentQueueLabel, ioQueueLabel) != 0) {
        NSLog(@"This method should be called from the ioQueue");
    }
}

结论

SDWebImage 就是从判断是否在主线程执行改为判断是否由主队列上调度。而由于主队列是一个串行队列,无论任务是异步同步都不会开辟新线程,所以当前队列是主队列等价于当前在主线程上执行。可以这样说,在主队列调度的任务肯定在主线程执行,而在主线程执行的任务不一定是由主队列调度的。


八、SDWebImage 的最大并发数 和 超时时长

// SDWebImageDownloader.m   -initWithSessionConfiguration:

_downloadQueue.maxConcurrentOperationCount = 6;
_downloadTimeout = 15.0;

九、SDWebImage 的Memory缓存和Disk缓存是用什么实现的?

9.1、Memory缓存实现 -- AutoPurgeCache

『AutoPurgeCache』类继承自 『NSCache』

SDWebImage 还专门实现了一个叫做 AutoPurgeCache 的类 继承自 NSCache ,相比于普通的 NSCache它提供了一个在内存紧张时候释放缓存的能力。

@interface AutoPurgeCache : NSCache
@end

@implementation AutoPurgeCache

- (nonnull instancetype)init {
    self = [super init];
    if (self) {
#if SD_UIKIT
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
    }
    return self;
}

- (void)dealloc {
#if SD_UIKIT
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}

@end

9.2、Disk缓存实现 -- NSFileManager

SDImageCache 的磁盘缓存是通过异步操作 NSFileManager 存储缓存文件到沙盒来实现的。


十、读取Memory和Disk的时候如何保证线程安全?

10.1、读取Memory

NScache是线程安全的,在多线程操作中,不需要对Cache加锁。
读取缓存的时候是在主线程进行。由于使用NSCache进行存储、所以不需要担心单个value对象的线程安全。

10.2、读取Disk

@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;


// Create IO serial queue
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
        
- (void)checkIfQueueIsIOQueue {
    const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
    const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue);
    if (strcmp(currentQueueLabel, ioQueueLabel) != 0) {
        NSLog(@"This method should be called from the ioQueue");
    }
}
// SDImageCache.m

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    // .........    
    
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{
            @autoreleasepool {
                NSData *data = imageData;
                if (!data && image) {
                    SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
                    data = [image sd_imageDataAsFormat:imageFormatFromData];
                }                
                [self storeImageDataToDisk:data forKey:key];
            }
            
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    }
    
    // .........
}

结论:

// SDImageCache.m

- (NSUInteger)getSize {
    __block NSUInteger size = 0;
    dispatch_sync(self.ioQueue, ^{
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
        for (NSString *fileName in fileEnumerator) {
            NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
            NSDictionary<NSString *, id> *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
            size += [attrs fileSize];
        }
    });
    return size;
}

分析:我们可以看见,不会创建新线程且切操作会顺序执行。你可能会疑惑:为什么同样都是在主线程执行,这样没有死锁。其实这个和线程没有关系,和队列有关系,只要不放在主队列就不会阻塞主队列上的操作(各种系统的UI方法),这个操作只是选择了合适的时机在主线程上跑了一下而已~

10.3、SD使用 @synchronized

@synchronized (self.failedURLs) {
    isFailedUrl = [self.failedURLs containsObject:url];
}

结论:所有可能引起资源抢夺的对象操作、全部有条件锁保护。
使用@synchronized来使得代码获得原子性,从而保证多线程安全。


十一、SDWebImage 的Memory警告是如何处理的!

利用通知中心观察

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(clearMemory)
                                             name:UIApplicationDidReceiveMemoryWarningNotification
                                           object:nil];

十二、SDWebImage Disk缓存时长? Disk清理操作时间点? Disk清理原则?

12.1、默认为一周

// SDImageCacheConfig.m

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

12.2、磁盘清理时间点

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(deleteOldFiles)
                                             name:UIApplicationWillTerminateNotification
                                           object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundDeleteOldFiles)
                                             name:UIApplicationDidEnterBackgroundNotification
                                           object:nil];

分别在『应用被杀死时』和 『应用进入后台时』进行清理操作

清理磁盘的方法

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

当应用进入后台时,会涉及到『Long-Running Task
正常程序在进入后台后、虽然可以继续执行任务。但是在时间很短内就会被挂起待机。
Long-Running可以让系统为app再多分配一些时间来处理一些耗时任务。

- (void)backgroundDeleteOldFiles {
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
// 后台任务标识--注册一个后台任务
    __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;
    }];

    // Start the long-running task and return immediately.
    [self deleteOldFilesWithCompletionBlock:^{
//结束后台任务
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}

12.3、磁盘清理原则

清理缓存的规则分两步进行。 第一步先清除掉过期的缓存文件。 如果清除掉过期的缓存之后,空间还不够。 那么就继续按文件时间从早到晚排序,先清除最早的缓存文件,直到剩余空间达到要求。

具体点,SDWebImage 是怎么控制哪些缓存过期,以及剩余空间多少才够呢? 通过两个属性:

@interface SDImageCacheConfig : NSObject

/**
 * The maximum length of time to keep an image in the cache, in seconds
 */
@property (assign, nonatomic) NSInteger maxCacheAge;

/**
 * The maximum size of the cache, in bytes.
 */
@property (assign, nonatomic) NSUInteger maxCacheSize;

maxCacheAge 和 maxCacheSize 有默认值吗?

[SDImageCache sharedImageCache].maxCacheSize = 1024 * 1024 * 50;    // 50M

maxCacheSize 是以字节来表示的,我们上面的计算代表 50M 的最大缓存空间。 把这行代码写在你的 APP 启动的时候,这样 SDWebImage 在清理缓存的时候,就会清理多余的缓存文件了。

十三、SDWebImage Disk目录位于哪里?

- (instancetype)init {
    return [self initWithNamespace:@"default"];
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns {
    NSString *path = [self makeDiskCachePath:ns];
    return [self initWithNamespace:ns diskCacheDirectory:path];
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory {
    if ((self = [super init])) {
        NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
   
   // .......
}

如何打开 真机和模拟器 沙盒文件

NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0];

通过 Finder -> 前往 -> 前往文件夹 -> 将路径输入即可!

image image

通过 查看包内容 查看即可!

十四、SDWebImage 的回调设计?

十五、SDWebImage 中 NS_OPTIONSNS_ENUM 的使用

/// SDWebImageManager.h      Line 14

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    SDWebImageRetryFailed = 1 << 0,                    // 值为2的0次方
    SDWebImageLowPriority = 1 << 1,                    // 值为2的1次方
    SDWebImageCacheMemoryOnly = 1 << 2,                // 值为2的2次方
    SDWebImageProgressiveDownload = 1 << 3,            // 值为2的3次方
    SDWebImageRefreshCached = 1 << 4,                  // 值为2的4次方
    SDWebImageContinueInBackground = 1 << 5,           // 值为2的5次方
    SDWebImageHandleCookies = 1 << 6,                  // 值为2的6次方
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,    // 值为2的7次方
    SDWebImageHighPriority = 1 << 8,
    SDWebImageDelayPlaceholder = 1 << 9,
    SDWebImageTransformAnimatedImage = 1 << 10,
    SDWebImageAvoidAutoSetImage = 1 << 11,
    SDWebImageScaleDownLargeImages = 1 << 12
};
/// SDImageCache.h     Line 13

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    SDImageCacheTypeNone,             // 默认从0开始
    SDImageCacheTypeDisk,             // 值为1
    SDImageCacheTypeMemory            // 值为2
};

NS_ENUM 定义 通用枚举
NS_OPTIONS 定义 位移枚举

位移枚举即是在你需要的地方可以同时存在多个枚举值如这样:

[gifImageView sd_setImageWithURL:url placeholderImage:image options:SDWebImageRefreshCached | SDWebImageRetryFailed progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {

            } completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
 
            }];

而NS_ENUM定义的枚举不能几个枚举项同时存在,只能选择其中一项,像这样:

// SDImageCache.m      Line 407

doneBlock(diskImage, diskData, SDImageCacheTypeDisk);

思考

/// SDWebImageManager.m    Line 157

SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;

分析

若 options = SDWebImageLowPriority | SDWebImageCacheMemoryOnly | SDWebImageProgressiveDownload 

| 运算规则:只要两个对应的二进制位有一个为1,结果位就为1,否则为0;
& 运算规则:只有两个对应的二进制位都为1时,结果位才为1,否则为0;

转换为二进制
options = 0001 | 0010 | 0100
SDWebImageLowPriority = 0001

options = 0111
SDWebImageLowPriority = 0001

if (options & SDWebImageLowPriority)
=== if(0111 & 0001)
=== if(0001)
=== if(2)

如果SDWebImageLowPriority = 1000
if (options & SDWebImageLowPriority)
=== if(0111 & 1000)
=== if(0000)
=== if(0)

十六、SDWebImage 中的工具类介绍

工具类深入研读


参考文档


完结

欢迎指正补充,可联系lionsom_lin@qq.com
原文地址:SDWebImage4-0源码探究(二)框架分析

上一篇下一篇

猜你喜欢

热点阅读