SDWebImage知识点总结一

目不能两视而明,耳不能两听而聪。----------《荀子·劝学》

SDWebImage框架图

技术点归纳

与图片相关的,这些知识都是与图片相关的,无需一定要掌握。

1、PNG图片的判断。可以看SDImageCacheImageDataHasPNGPreffix方法。
2、Image Data判断图片类型以及根据data创建图片。可以查看NSData+ImageContentTypeUIImage+MultiFormat类。
3、图片解压(以及解压带来内存问题)。SDWebImageDecoder类中的decodedImageWithImage:方法的实现,牵扯到底层图片相关的操作。
4、gif图片的处理。虽然SDGIF的支持比较差劲。但是老外的纯技术精神,不得不佩服。请看issue#945

iOS开发技术,这些东西都是项目中比较常用的技术,一定要掌握。

1、NSFileManager的操作。在获取缓存大小相关内容时,需要我们熟练掌握NSFileManager类的相关用法。
2、NSCache类。在SDissue上面,由NSCache缓存引起的问题(如内存警告等)还是有很多的,后面才得到改善。
3、NSOperationNSOperationQueueNSThread@synchronized线程及操作队列相关类。
4、GCDdispatch_barrier_syncdispatch_apply等函数。
5、NSURLRequestNSURLResponseNSURLConnectionNSURLCache等网络请求相关类。
6、后台操作。
7、Runloop
8、Runtime
9、KVO
下面我们就主要的知识点来进行剖析:

一、SDWebImage中@autoreleasepool的应用

dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });
/*
@autoreleasepool {
    // code do something, creates some autoreleases objects
}
*/

1、autoreleasepool在什么时候使用?

  • autorelease机制是基于UIFrameWork。因此写非UI框架程序时需要自己管理对象的生命周期,如AppKit等Cocoa框架、命令行工具。。
  • autorelease触发时机发生在下一次RunLoop的时候。因此如果在下一个大循环里不断创建临时对象,那么这些对象在下一次RunLoop回来之前将没有机会被释放,十分消耗内存。在种情况下,我们在循环内部使用@autoreleasepool {},将autorelease对象释放掉,从而避免内存峰值的出现。
for (int i = 0; i < 1000000; i++) {
        @autoreleasepool {
            NSString *str = @"abc";
            str = [str stringByAppendingString:@"zhangsan"];
        }        
}

下面的方法中,对象不断被创建,在for循环结束之前,这些对象都会被系统放到最近的自动释放池里面,等待回收,因此就会消耗大量的内存,可能造成内存枯竭。而等到循环结束之后,内存的用量会突然下降。
而如果把循环内的代码包裹在我们创建的@autoreleasepool {}中,那么在循环中创建的对象就会放到这个池子中,而不是在线程的主池里面。@autoreleasepool {}的作用范围是在{}内部,所以创建的对象会得到及时的释放,防止内存的暴涨。

for (int i = 0; i < 10000; i++) {
   NSString *str = @"abc";
}
  • 创建新的线程。Cocoa的应用都会维护自己autoreleasepool。因此,非Cocoa程序创建线程时,需要显式添加autoreleasepool。
  • 长时间在后台运行
扩展:
1、在什么线程下面需要使用autoreleasepool呢?
  • GCD开辟子线程不需要手动创建autoreleasepool,因为GCD的每个队列都会自行创建管理autoreleasepool。但是,凡事都有例外,我们无法保证什么时候它drain(文档中没有说明),有可能在一个Block执行结束后,也可能很多个Block执行结束后。因此如果仅仅生成少量对象,那就没有必要去自己生成NSAutoreleasePool;否则就自己生成一个NSAutoreleasePool来控制drain pool(排水池)。传送门
 dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }
        @autoreleasepool {
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });
  • NSThread开辟子线程需要手动创建autoreleasepool。
  • 自定义NSOperationQueue需要在重写的main()方法中创建一个@autoreleasepool,将要做的操作放到@autoreleasepool{}中.
    tips:创建自动释放池的原因是因为在异步执行的情况下,不能访问主线程的自动释放池,所以应该自己创建一个自动释放池。
2、autorelease的对象何时被释放?
  • 对象执行autorelease方法会将对象添加到自动释放池中。
  • 当自动释放池销毁时,自动释放池中的所有对象作release操作。
  • 对象执行autorelease方法后,自身引用计数不会改变,而且会返回对象本身。

2、autoreleasepool原理是什么?

AutoreleasePool(自动释放池)是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。在正常情况下,创建的变量会在超出其作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将延迟执行。

自动释放池的执行过程:objc_autoreleasePoolPush() —> [object autorelease] —> objc_autoreleasePoolPop(void *)。

int main(int argc, const char * argv[]) {

    /* @autoreleasepool */ {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

    return 0;
}
每一个线程都会维护自己的 autoreleasepool 堆栈。换句话说 autoreleasepool 是与线程紧密相关的,每一个 autoreleasepool 只对应一个线程,子线程退出的时候会清空autoreleasepool。
tips:子线程的autoreleasepool也需要手动获取,但区分情况,一般系统提供的block如usingBlock和GCD提供的block内部都会自动包裹一个autoreleasepool,不用手动加。但是你自己通过其他方式创建的子线程,在线程内部需要手动获取autoreleasepool,防止局部内存使用峰值过高或发生其他内存问题,最后,autoreleasepool释放时,也会对其管理的对象发送release消息。

MarkDown语法扩展

二、SDWebImage中的@synchronized(同步锁)

//SDWebImageDownloaderOperation中取消线程的执行
- (void)cancel {
    @synchronized (self) {
        if (self.thread) {
            [self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
        }
        else {
            [self cancelInternal];
        }
    }
}
  • 锁是如何与你传入 @synchronized 的对象关联上的?
    你调用 sychronized 的每个对象,Objective-C runtime 都会为其分配一个递归锁并存储在哈希表中。

  • @synchronized会保持(retain,增加引用计数)被锁住的对象么?
    使用@synchronized不会导致此对象的引用计数增加

  • 假如传入 @synchronized 的对象在 @synchronized 的 block 里面被释放或者被赋值为 nil 将会怎么样?
    如果在 sychronized 内部对象被释放或被设为 nil 看起来都 OK。不过这没在文档中说明,所以我不会再生产代码中依赖这条。

  • 如果传入@synchronized 的对象值为 nil 将会怎么样?
    @synchronized(nil)不会有任何作用,hash计算为空,加锁失败,代码块不是线程安全的。你可以通过在 objc_sync_nil 上加断点来查看是否发生了这样的事情。

  • @synchronized对代码上锁,保证代码的原子性,从而保证多线程的安全。@synchronized 结构在工作时为传入的对象分配了一个递归锁,防止同一个线程重复调用时产生死锁。
    @synchronized的原理, @synchronized使用传入的object的内存地址作key,通过hash map对应的一个系统维护的递归锁。所以不管是传入什么类型的object,只要是有内存地址,就能启动同步代码块的效果。如果传入nil, 那就相当于没有加锁.
    另外,还有很多锁,如:互斥锁、递归锁、自旋锁、条件锁等。我在之前的文章中有分析,就不一一解释了。传送门1

  • 正确使用同步锁。传送门2
    精准粒度控制:
    虽然说@synchronized比较慢,但@synchronized和其他同步锁的性能相比并没有很夸张的慢,对于使用者来说几乎忽略不计。 慢的原因其实是没有做好粒度控制。锁的本质是为了让我们的一段代码获得原子性,不同的数据要使用不同的锁,尽量将粒度控制在最细的程度。

@synchronized (tokenA) {
    [arrA addObject:obj];
}

@synchronized (tokenB) {
    [arrB addObject:obj];
}

三、SDWebImage的内存警告处理

  • 使用通知来观察内存的警告,收到UIApplicationDidReceiveMemoryWarningNotification内存警告通知,执行clearMenory方法,清理内存缓存。
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(clearMemory)
                                             name:UIApplicationDidReceiveMemoryWarningNotification
                                           object:nil];
@property (strong, nonatomic) NSCache *memCache;

- (void)clearMemory {
    [self.memCache removeAllObjects];
}
知识点扩展NSCache简介:
  • 应用场景:
    iOS中需要频繁读取的数据,都可以用NSCache把数据缓存到内存中提高读取性能。
  • 特点:
    1、NSCache具有自动删除的功能,以减少系统占用的内存;
    2、NSCache是线程安全的,不需要加线程锁;
    3、键对象不会像 NSMutableDictionary 中那样被复制。(键不需要实现 NSCopying 协议)。
    NSCache知识点总结链接

四、SDWebImage的Disk缓存时长、清理操作时间点以及清理原则。

  • SDWebImage缓存方式枚举
    1、不允许SDWebImage缓存,而是从web上下载。
    2、从磁盘缓存。
    3、从内存缓存。
typedef NS_ENUM(NSInteger, SDImageCacheType) {
    /**
     * The image wasn't available the SDWebImage caches, but was downloaded from the web.
     */
    SDImageCacheTypeNone,
    /**
     * The image was obtained from the disk cache.
     */
    SDImageCacheTypeDisk,
    /**
     * The image was obtained from the memory cache.
     */
    SDImageCacheTypeMemory
};
  • Disk缓存时长默认为一周
//SDImageCache.m
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
  • Disk清理操作时间点
    应用被终结以及应用进入后台。
//SDImageCache.m
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(deleteOldFiles)
                                             name:UIApplicationWillTerminateNotification
                                           object:nil];

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

由于进入后台之后执行清除数据操作,但是应用在很短时间内就会被挂起导致操作无法完成,所及就涉及到申请后台运行时间来执行操作的问题了。

- (void)backgroundCleanDisk {
    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 cleanDiskWithCompletionBlock:^{
        //结束后台任务
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}

申请后台运行时间在iOS 7.0之前是600s,在iOS 7.0之后就变成180s了,但是这些时间已经足够我们完成一般的操作了。UIBackgroundTaskIdentifier知识点传送门

  • 清理磁盘的原则
    清理缓存的规则分两步,第一步先清除掉过期的缓存文件,如果清除过期的文件之后磁盘空间还是不够,那么就继续按照文件存储的事件从早到晚排序删除文件,直到剩余空间达到要求。
扩展知识点:
1、static const与#define(宏定义)

之前常用的字符串常量,一般是抽成宏,但是苹果不推荐我们抽成宏,推荐我们使用const常量,那么为什么呢?。

  • 优缺点:
    编译时刻:宏是预编译(编译之前处理),const是编译阶段。
    编译检查:宏不做检查,不会报编译错误,只是替换,const会编译检查,会报编译错误。
    宏的好处:宏能定义一些函数,方法。 const不能。
    宏的坏处:使用大量宏,容易造成编译时间久,每次都需要重新替换
  • 异同点:
    相同点:
    a:都不能再被修改
    不同点:
    a:static const 修饰变量只有一份内存,高效。
    b:宏定义只是简单的替换,每次使用都需要创建一份内存,比较消耗内存。
    扩展知识点传送门
    宏(define)与常量(const)

五、SDWebImage存储在那里嫩?

  • 缓存在沙盒目录library/Caches
  • 默认情况下,二级目录为
    ~/Library/Caches/default/com.hackemist.SDWebImageCache.default
  • 我们也可以自定义目录
- (id)init {
    return [self initWithNamespace:@"default"];
}

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

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

        // initialise PNG signature data
        kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];

        // Create IO serial queue
        _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

        // Init default values
        _maxCacheAge = kDefaultCacheMaxCacheAge;

        //......
    return self;
}

六、SDWebImage用到的回调设计

  • Block
    当使用次数不多,且联系紧密时,推荐使用Block
typedef void(^SDWebImageQueryCompletedBlock)(UIImage *image, SDImageCacheType cacheType);

typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);

typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);
/**
 * Remove all expired cached image from disk. Non-blocking method - returns immediately.
 * @param completionBlock An block that should be executed after cache expiration completes (optional)
 */
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;

/**
 * Asynchronously calculate the disk cache's size.
 */
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;

/**
 *  Async check if image exists in disk cache already (does not load the image)
 *
 *  @param key             the key describing the url
 *  @param completionBlock the block to be executed when the check is done.
 *  @note the completion block will be always executed on the main queue
 */
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;

  • Delegate
    方法会被多用多次,推荐使用Delegate,可以将方法常驻,随时监听方法的执行。
@protocol SDWebImagePrefetcherDelegate <NSObject>

@optional

/**
 * Called when an image was prefetched.
 *
 * @param imagePrefetcher The current image prefetcher
 * @param imageURL        The image url that was prefetched
 * @param finishedCount   The total number of images that were prefetched (successful or not)
 * @param totalCount      The total number of images that were to be prefetched
 */
- (void)imagePrefetcher:(SDWebImagePrefetcher *)imagePrefetcher didPrefetchURL:(NSURL *)imageURL finishedCount:(NSUInteger)finishedCount totalCount:(NSUInteger)totalCount;

/**
 * Called when all images are prefetched.
 * @param imagePrefetcher The current image prefetcher
 * @param totalCount      The total number of images that were prefetched (whether successful or not)
 * @param skippedCount    The total number of images that were skipped
 */
- (void)imagePrefetcher:(SDWebImagePrefetcher *)imagePrefetcher didFinishWithTotalCount:(NSUInteger)totalCount skippedCount:(NSUInteger)skippedCount;

@end
  • 项目常用举例
    1、UITableView的delegate中的监控滑动,代理方法需要被不断地执行。
    2、UIView动画、GCD、NSBlockOperation,都是执行一次就释放。
    以上两点可以作为我们日常使用的参考原则。

七、SDWebImage中用到的枚举

  • NS_ENUM 通用枚举
    NS_ENUM定义的枚举不能几个同时存在,只能选择一个。
doneBlock(diskImage, SDImageCacheTypeDisk);
  • NS_OPTIONS 位移枚举
    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) {
 
            }];

SDWebImage示例如下:

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0,     // 值为2的0次方

    SDWebImageDownloaderProgressiveDownload = 1 << 1,  // 值为2的1次方

    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,

    SDWebImageDownloaderContinueInBackground = 1 << 4,

    SDWebImageDownloaderHandleCookies = 1 << 5,

    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    SDWebImageDownloaderHighPriority = 1 << 7,
};
typedef NS_ENUM(NSInteger, SDImageCacheType) {
    /**
     * The image wasn't available the SDWebImage caches, but was downloaded from the web.
     */
    SDImageCacheTypeNone,
    /**
     * The image was obtained from the disk cache.
     */
    SDImageCacheTypeDisk,
    /**
     * The image was obtained from the memory cache.
     */
    SDImageCacheTypeMemory
};

SDWebImage源码链接
参考资料1
SDWebImage4.0源码探究1
SDWebImage4.0源码探究2
autoreleasepool使用
AutoreleasePool的原理和实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • “宜未雨而绸缪;毋临渴而掘井。”凡事早做打算,方为上上之策!------前言 一、SDWebImage中的weak...
    woniu阅读 382评论 0 2
  • # 运行时 ``` 1、程序中任何代码都会被转化成runtime的C代码执行,如[target doSomeThi...
    _叮叮当当__阅读 155评论 0 0
  • 1、为什么说Objective-C是一门动态的语言? 静态、动态是相对的,这里动态语言指的是不需要在编译时确定所有...
    River_YYH阅读 1,958评论 0 14
  • Objective-C的内存管理 引用计数机制 Objective-C 主要采用引用计数机制来管理对象的内存。 运...
    渐z阅读 1,592评论 0 4
  • 参考地址:MrPeak大神的中级面试题及针对Mrpeak面试题的解答 首先必须了解 iOS程序的启动执行顺序 Ap...
    轻尘_小吕阅读 331评论 0 1