目不能两视而明,耳不能两听而聪。----------《荀子·劝学》
技术点归纳
与图片相关的,这些知识都是与图片相关的,无需一定要掌握。
1、PNG
图片的判断。可以看SDImageCache
的ImageDataHasPNGPreffix
方法。
2、Image Data
判断图片类型以及根据data
创建图片。可以查看NSData+ImageContentType
和UIImage+MultiFormat
类。
3、图片解压(以及解压带来内存问题)。SDWebImageDecoder
类中的decodedImageWithImage:
方法的实现,牵扯到底层图片相关的操作。
4、gif
图片的处理。虽然SD
对GIF
的支持比较差劲。但是老外的纯技术精神,不得不佩服。请看issue#945
iOS开发技术,这些东西都是项目中比较常用的技术,一定要掌握。
1、NSFileManager
的操作。在获取缓存大小相关内容时,需要我们熟练掌握NSFileManager
类的相关用法。
2、NSCache
类。在SD
的issue
上面,由NSCache
缓存引起的问题(如内存警告等)还是有很多的,后面才得到改善。
3、NSOperation
、NSOperationQueue
、NSThread
、@synchronized
线程及操作队列相关类。
4、GCD
。dispatch_barrier_sync
、dispatch_apply
等函数。
5、NSURLRequest
、NSURLResponse
、NSURLConnection
、NSURLCache
等网络请求相关类。
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对代码上锁,保证代码的原子性,从而保证多线程的安全。@synchronized 结构在工作时为传入的对象分配了一个递归锁,防止同一个线程重复调用时产生死锁。
另外,还有很多锁,如:互斥锁、递归锁、自旋锁、条件锁等。我在之前的文章中有分析,就不一一解释了。传送门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
@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的原理和实现