1 概述
SDWebImage基本是iOS項(xiàng)目的標(biāo)配。他以靈活簡(jiǎn)單的api,提供了圖片從加載、解析、處理、緩存、清理等一些列功能。讓我們專心于業(yè)務(wù)的處理。但是并不意味著會(huì)用就可以了,通過(guò)源碼分析和學(xué)習(xí),讓我們知道如何用好它。學(xué)習(xí)分析優(yōu)秀源碼也可以從潛移默化中給我們提供很多解決日常需求的思路。下面就是一張圖來(lái)概述SDWebImage的所有類:

通過(guò)對(duì)這個(gè)圖片的分析,我們可以把SDWebImage的源碼分為三種:
- 各種分類:
-
UIButton(WebCache)為UIButton類添加載圖片的方法。比如正常情況下、點(diǎn)擊情況下、的image屬性和背景圖片等。 -
MKAnnotationView(WebCache)為MKAnnotationView類添加各種加載圖片的方法。 -
UIImageView(WebCache)為UIImageView類添加加載圖片的方法。 -
UIImageView(HighlightedWebCache)為UIImageView類添加高亮狀態(tài)下加載圖片的方法。 -
FLAnimatedImageView(WebCache)為FLAnimatedImageView類添加加載動(dòng)態(tài)的方法,這個(gè)分類需要引入FLAnimatedImage框架。SDWebImage推薦使用這個(gè)框架來(lái)處理動(dòng)態(tài)圖片(GIF)的加載。 - UIImageView、UIButton、FLAnimatedImageView通過(guò)
sd_setImageWithURL等api來(lái)做圖片加載請(qǐng)求。這也是我們唯一需要做的。 - 上面的幾個(gè)UIView子類都會(huì)調(diào)用
UIView(WebCache)分類的sd_internalSetImageWithURL方法來(lái)做圖片加載請(qǐng)求。具體是通過(guò)SDWebImageManager調(diào)用來(lái)實(shí)現(xiàn)的。同時(shí)實(shí)現(xiàn)了Operation取消、ActivityIndicator的添加與取消。
-
- 各種工具類:
-
NSData+ImageContentType: 根據(jù)圖片數(shù)據(jù)獲取圖片的類型,比如GIF、PNG等。 -
SDWebImageCompat: 根據(jù)屏幕的分辨倍數(shù)成倍放大或者縮小圖片大小。 -
SDImageCacheConfig: 圖片緩存策略記錄。比如是否解壓縮、是否允許iCloud、是否允許內(nèi)存緩存、緩存時(shí)間等。默認(rèn)的緩存時(shí)間是一周。 -
UIImage+MultiFormat: 獲取UIImage對(duì)象對(duì)應(yīng)的data、或者根據(jù)data生成指定格式的UIImage,其實(shí)就是UIImage和NSData之間的轉(zhuǎn)換處理。 -
UIImage+GIF: 對(duì)于一張圖片是否GIF做判斷??梢愿鶕?jù)NSData返回一張GIF的UIImage對(duì)象,并且只返回GIF的第一張圖片生成的GIF。如果要顯示多張GIF,使用FLAnimatedImageView。 -
SDWebImageDecoder: 根據(jù)圖片的情況,做圖片的解壓縮處理。并且根據(jù)圖片的情況決定如何處理解壓縮。
-
- 核心類:
-
SDImageCache: 負(fù)責(zé)SDWebImage的整個(gè)緩存工作,是一個(gè)單列對(duì)象。緩存路徑處理、緩存名字處理、管理內(nèi)存緩存和磁盤(pán)緩存的創(chuàng)建和刪除、根據(jù)指定key獲取圖片、存入圖片的類型處理、根據(jù)緩存的創(chuàng)建和修改日期刪除緩存。 -
SDWebImageManager: 擁有一個(gè)SDWebImageCache和SDWebImageDownloader屬性分別用于圖片的緩存和加載處理。為UIView及其子類提供了加載圖片的統(tǒng)一接口。管理正在加載操作的集合。這個(gè)類是一個(gè)單列。還有就是各種加載選項(xiàng)的處理。 -
SDWebImageDownloader: 實(shí)現(xiàn)了圖片加載的具體處理,如果圖片在緩存存在則從緩存區(qū)。如果緩存不存在,則直接創(chuàng)建一個(gè)。SDWebImageDownloaderOperation對(duì)象來(lái)下載圖片。管理NSURLRequest對(duì)象請(qǐng)求頭的封裝、緩存、cookie的設(shè)置。加載選項(xiàng)的處理等功能。管理Operation之間的依賴關(guān)系。這個(gè)類是一個(gè)單列. -
SDWebImageDownloaderOperation: 一個(gè)自定義的并行Operation子類。這個(gè)類主要實(shí)現(xiàn)了圖片下載的具體操作、以及圖片下載完成以后的圖片解壓縮、Operation生命周期管理等。 -
UIView+WebCache: 所有的UIButton、UIImageView都回調(diào)用這個(gè)分類的方法來(lái)完成圖片加載的處理。同時(shí)通過(guò)UIView+WebCacheOperation分類來(lái)管理請(qǐng)求的取消和記錄工作。所有UIView及其子類的分類都是用這個(gè)類的sd_intemalSetImageWithURL:來(lái)實(shí)現(xiàn)圖片的加載。 -
FLAnimatedImageView: 動(dòng)態(tài)圖片的數(shù)據(jù)通過(guò)ALAnimatedImage對(duì)象來(lái)封裝。FLAnimatedImageView是UIImageView的子類。通過(guò)他完全可以實(shí)現(xiàn)動(dòng)態(tài)圖片的加載顯示和管理。并且比UIImageView做了流程優(yōu)化。
-
2 實(shí)現(xiàn)流程
SDWebImage為我們實(shí)現(xiàn)了圖片加載、數(shù)據(jù)處理、圖片緩存等一些列工作。通過(guò)下圖我們可以分析一下他的流程:

通過(guò)這個(gè)圖,我們發(fā)現(xiàn)SDWebImage加載的過(guò)程是首先從緩存中加載數(shù)據(jù)。而且緩存加載又是優(yōu)先從內(nèi)存緩存中加載,然后才是磁盤(pán)加載。最后如果緩存沒(méi)有,才從網(wǎng)絡(luò)上加載。同時(shí)網(wǎng)絡(luò)成功加載圖片以后,存入本地緩存。
3 UIView+WebCache分析
UIImageView、UIButton、FLAnimatedImageView都會(huì)調(diào)用UIView(WebCache)分類的sd_internalSetImageWithURL方法來(lái)做圖片加載請(qǐng)求。具體是通過(guò)SDWebImageManager調(diào)用來(lái)實(shí)現(xiàn)的。同時(shí)實(shí)現(xiàn)了Operation取消、ActivityIndicator的添加與取消。我們首先來(lái)看sd_internalSetImageWithURL方法的實(shí)現(xiàn):
/**
所有UIView及其子類都是通過(guò)這個(gè)方法來(lái)加載圖片
@param url 加載的url
@param placeholder 占位圖
@param options 加載選項(xiàng)
@param operationKey key
@param setImageBlock Block
@param progressBlock 進(jìn)度Block
@param completedBlock 回調(diào)Block
*/
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
//取消當(dāng)前類所對(duì)應(yīng)的所有下載Operation對(duì)象
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
/*
把UIImageView的加載圖片操作和他自身用關(guān)聯(lián)對(duì)象關(guān)聯(lián)起來(lái),方便后面取消等操作。關(guān)聯(lián)的key就是UIImageView對(duì)應(yīng)的類名
*/
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//如果有設(shè)置站位圖,則先顯示站位圖
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {
// check if activityView is enabled or not
//如果UIImageView對(duì)象有設(shè)置添加轉(zhuǎn)動(dòng)菊花數(shù)據(jù),加載的時(shí)候添加轉(zhuǎn)動(dòng)的菊花
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
__weak __typeof(self)wself = self;
/*
*operation是一個(gè)`SDWebImageCombinedOperation`對(duì)象。通過(guò)這個(gè)對(duì)象來(lái)獲取圖片
*/
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
//停止菊花
[sself sd_removeActivityIndicator];
if (!sself) {
return;
}
dispatch_main_async_safe(^{
if (!sself) {
return;
}
//如果設(shè)置了不自動(dòng)顯示圖片,則直接調(diào)用completedBlock,讓調(diào)用者處理圖片的顯示
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
completedBlock(image, error, cacheType, url);
return;
} else if (image) {
//自動(dòng)顯示圖片
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
} else {
//如果設(shè)置了延遲顯示占位圖,則圖片加載失敗的情況下顯示占位圖
if ((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
}
//完成回調(diào)
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
//關(guān)聯(lián)Operationkey與Operation對(duì)象。方便后面根據(jù)key取消operation操作等。
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
//加載失敗的情況
dispatch_main_async_safe(^{
//移除菊花
[self sd_removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
給UIView及其子類添加旋轉(zhuǎn)菊花是通過(guò)關(guān)聯(lián)對(duì)象來(lái)實(shí)現(xiàn)的。通過(guò)如下幾個(gè)方法來(lái)實(shí)現(xiàn):
#pragma mark 通過(guò)關(guān)聯(lián)對(duì)象來(lái)實(shí)現(xiàn)菊花的添加
- (UIActivityIndicatorView *)activityIndicator {
return (UIActivityIndicatorView *)objc_getAssociatedObject(self, &TAG_ACTIVITY_INDICATOR);
}
- (void)setActivityIndicator:(UIActivityIndicatorView *)activityIndicator {
objc_setAssociatedObject(self, &TAG_ACTIVITY_INDICATOR, activityIndicator, OBJC_ASSOCIATION_RETAIN);
}
#pragma mark 是否顯示旋轉(zhuǎn)菊花
- (void)sd_setShowActivityIndicatorView:(BOOL)show {
objc_setAssociatedObject(self, &TAG_ACTIVITY_SHOW, @(show), OBJC_ASSOCIATION_RETAIN);
}
- (BOOL)sd_showActivityIndicatorView {
return [objc_getAssociatedObject(self, &TAG_ACTIVITY_SHOW) boolValue];
}
#pragma mark 旋轉(zhuǎn)菊花的樣式
- (void)sd_setIndicatorStyle:(UIActivityIndicatorViewStyle)style{
objc_setAssociatedObject(self, &TAG_ACTIVITY_STYLE, [NSNumber numberWithInt:style], OBJC_ASSOCIATION_RETAIN);
}
- (int)sd_getIndicatorStyle{
return [objc_getAssociatedObject(self, &TAG_ACTIVITY_STYLE) intValue];
}
還有就是通過(guò)UIView+WebCacheOperation類來(lái)實(shí)現(xiàn)UIView的圖片下載Operation的關(guān)聯(lián)和取消。具體key的值可以從sd_internalSetImageWithURL中找到具體獲取方式,通過(guò)在這個(gè)方法中實(shí)現(xiàn)Operation的關(guān)聯(lián)與取消。
/**
關(guān)聯(lián)Operation對(duì)象與key對(duì)象
@param operation Operation對(duì)象
@param key key
*/
- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key {
if (key) {
[self sd_cancelImageLoadOperationWithKey:key];
if (operation) {
SDOperationsDictionary *operationDictionary = [self operationDictionary];
operationDictionary[key] = operation;
}
}
}
/**
取消當(dāng)前key對(duì)應(yīng)的所有實(shí)現(xiàn)了SDWebImageOperation協(xié)議的Operation對(duì)象
@param key Operation對(duì)應(yīng)的key
*/
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
// Cancel in progress downloader from queue
//獲取當(dāng)前View對(duì)應(yīng)的所有key
SDOperationsDictionary *operationDictionary = [self operationDictionary];
//獲取對(duì)應(yīng)的圖片加載Operation
id operations = operationDictionary[key];
//取消所有當(dāng)前View對(duì)應(yīng)的所有Operation
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}
4 FLAnimatedImageView分析
SDWebImage使用FLAnimatedImage框架來(lái)處理動(dòng)態(tài)圖片,它包含FLAnimatedImage和FLAnimatedImageView兩個(gè)雷。動(dòng)態(tài)圖片的數(shù)據(jù)通過(guò)ALAnimatedImage對(duì)象來(lái)封裝。FLAnimatedImageView是UIImageView的子類。通過(guò)他完全可以實(shí)現(xiàn)動(dòng)態(tài)圖片的加載顯示和管理。并且比UIImageView做了流程優(yōu)化。我們來(lái)看一下FLAnimatedImageView.h里面定義的接口:
/**
`FLAnimatedImageView`是一個(gè)`UIImageView`的子類。實(shí)現(xiàn)了`UIImageView`的`start/stop/isAnimating`方法。所以我們可以直接使用`FLAnimatedImageView`替代`UIImageView`。
通過(guò)`CADisplayLink`對(duì)象來(lái)處理當(dāng)前圖片幀和下一幀圖片的顯示。
*/
@interface FLAnimatedImageView : UIImageView
/**
動(dòng)態(tài)圖片的封裝對(duì)象。首先通過(guò)設(shè)置`[UIImageView.image]`為nil來(lái)清除已經(jīng)存在的動(dòng)態(tài)圖片。設(shè)置`animatedImage`屬性會(huì)自動(dòng)設(shè)置新的動(dòng)態(tài)圖片并且開(kāi)始顯示。而且會(huì)把當(dāng)前顯示的UIImage存入`currentFrame`中。
*/
@property (nonatomic, strong) FLAnimatedImage *animatedImage;
@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);
/**
當(dāng)前動(dòng)畫(huà)幀對(duì)應(yīng)的UIImage對(duì)象
*/
@property (nonatomic, strong, readonly) UIImage *currentFrame;
/**
當(dāng)前圖片鎮(zhèn)對(duì)應(yīng)的索引
*/
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;
/**
指定動(dòng)態(tài)圖片執(zhí)行所在的runloop的mode。NSRunLoopCommonMode
*/
@property (nonatomic, copy) NSString *runLoopMode;
@end
我們通過(guò)FLAnimatedImageView+WebCache這個(gè)分類的sd_setImageWithURL來(lái)加載動(dòng)態(tài)圖片:
/**
FLAnimatedImage+WebCache分類通過(guò)這個(gè)方法來(lái)加載動(dòng)態(tài)圖片
@param url 圖片的url
@param placeholder 占位圖
@param options 加載選項(xiàng)
@param progressBlock 進(jìn)度Block
@param completedBlock 完成Block
*/
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
__weak typeof(self)weakSelf = self;
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:^(UIImage *image, NSData *imageData) {
//根據(jù)NSData的類型獲取圖片的類型
SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
//如果是GIF,則處理
if (imageFormat == SDImageFormatGIF) {
//給FLAnimatedImageView的animatedImage屬性設(shè)置動(dòng)態(tài)圖片。這個(gè)setter方法被重寫(xiě)了
weakSelf.animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData];
weakSelf.image = nil;
} else {
//不是動(dòng)態(tài)圖片,則正常顯示
weakSelf.image = image;
weakSelf.animatedImage = nil;
}
}
progress:progressBlock
completed:completedBlock];
}
從上面可以看出,獲取圖片數(shù)據(jù)以后。首先通過(guò)SDImageFormat得到圖片的類型。如果是GIF類型,則先把圖片數(shù)據(jù)封裝成一個(gè)FLAnimatedImage對(duì)象。然后設(shè)置給animatedImage屬性。這個(gè)屬性的setter方法如下:
/**
animatedImage的setter方法。通過(guò)這個(gè)屬性setter方法來(lái)設(shè)置FLAnimatedImageView的數(shù)據(jù)。并且開(kāi)始動(dòng)態(tài)顯示
@param animatedImage animatedImage屬性
*/
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
{
if (![_animatedImage isEqual:animatedImage]) {
if (animatedImage) {
//清除UIImageView以前的圖片數(shù)據(jù)
super.image = nil;
super.highlighted = NO;
//先說(shuō)intrinsicContentSize,也就是控件的內(nèi)置大小。比如UILabel,UIButton等控件,他們都有自己的內(nèi)置大小??丶膬?nèi)置大小往往是由控件本身的內(nèi)容所決定的,比如一個(gè)UILabel的文字很長(zhǎng),那么該UILabel的內(nèi)置大小自然會(huì)很長(zhǎng)??丶膬?nèi)置大小可以通過(guò)UIView的intrinsicContentSize屬性來(lái)獲取內(nèi)置大小,也可以通過(guò)invalidateIntrinsicContentSize方法來(lái)在下次UI規(guī)劃事件中重新計(jì)算intrinsicContentSize。如果直接創(chuàng)建一個(gè)原始的UIView對(duì)象,顯然它的內(nèi)置大小為0。
[self invalidateIntrinsicContentSize];
} else {
//停止動(dòng)態(tài)圖片的動(dòng)態(tài)顯示
[self stopAnimating];
}
//賦值
_animatedImage = animatedImage;
//當(dāng)前動(dòng)態(tài)圖片數(shù)據(jù)幀
self.currentFrame = animatedImage.posterImage;
//當(dāng)前數(shù)據(jù)幀索引
self.currentFrameIndex = 0;
if (animatedImage.loopCount > 0) {
self.loopCountdown = animatedImage.loopCount;
} else {
self.loopCountdown = NSUIntegerMax;
}
self.accumulator = 0.0;
//更新對(duì)象的狀態(tài)。從而更新shouldAnimated這個(gè)屬性的值。
[self updateShouldAnimate];
if (self.shouldAnimate) {
//開(kāi)始動(dòng)態(tài)顯示
[self startAnimating];
}
[self.layer setNeedsDisplay];
}
}
/**
判斷當(dāng)前FLAnimatedImageView是否需要顯示動(dòng)畫(huà)
*/
- (void)updateShouldAnimate
{
BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
self.shouldAnimate = self.animatedImage && isVisible;
}
5 CADisplayLink
有趣的地方是FLAnimatedImageView通過(guò)過(guò)CADisplayLink來(lái)刷新動(dòng)態(tài)圖片幀的顯示。CADisplayLink是一個(gè)能讓我們以和屏幕刷新率相同的頻率將內(nèi)容畫(huà)到屏幕上的定時(shí)器。我們?cè)趹?yīng)用中創(chuàng)建一個(gè)新的CADisplayLink對(duì)象,把它添加到一個(gè)runloop中,并給它提供一個(gè)target和selector在屏幕刷新的時(shí)候調(diào)用。
一但CADisplayLink以特定的模式注冊(cè)到runloop之后,每當(dāng)屏幕需要刷新的時(shí)候runloop就會(huì)調(diào)用CADisplayLink綁定的target上的selector,這時(shí)target可以讀到CADisplayLink的每次調(diào)用的時(shí)間戳,用來(lái)準(zhǔn)備下一幀顯示需要的數(shù)據(jù)。例如一個(gè)視頻應(yīng)用使用時(shí)間戳來(lái)計(jì)算下一幀要顯示的視頻數(shù)據(jù)。在UI做動(dòng)畫(huà)的過(guò)程中,需要通過(guò)時(shí)間戳來(lái)計(jì)算UI對(duì)象在動(dòng)畫(huà)的下一幀要更新的大小等等。在添加進(jìn)runloop的時(shí)候我們應(yīng)該選用高一些的優(yōu)先級(jí),來(lái)保證動(dòng)畫(huà)的平滑??梢栽O(shè)想一下,我們?cè)趧?dòng)畫(huà)的過(guò)程中,runloop被添加進(jìn)來(lái)了一個(gè)高優(yōu)先級(jí)的任務(wù),那么,下一次的調(diào)用就會(huì)被暫停轉(zhuǎn)而先去執(zhí)行高優(yōu)先級(jí)的任務(wù),然后在接著執(zhí)行CADisplayLink的調(diào)用,從而造成動(dòng)畫(huà)過(guò)程的卡頓,使動(dòng)畫(huà)不流暢。duration屬性提供了每幀之間的時(shí)間,也就是屏幕每次刷新之間的的時(shí)間。我們可以使用這個(gè)時(shí)間來(lái)計(jì)算出下一幀要顯示的UI的數(shù)值。但是duration只是個(gè)大概的時(shí)間,如果CPU忙于其它計(jì)算,就沒(méi)法保證以相同的頻率執(zhí)行屏幕的繪制操作,這樣會(huì)跳過(guò)幾次調(diào)用回調(diào)方法的機(jī)會(huì)。frameInterval屬性是可讀可寫(xiě)的NSInteger型值,標(biāo)識(shí)間隔多少幀調(diào)用一次selector方法,默認(rèn)值是1,即每幀都調(diào)用一次。如果每幀都調(diào)用一次的話,對(duì)于iOS設(shè)備來(lái)說(shuō)那刷新頻率就是60HZ也就是每秒60次,如果將 frameInterval 設(shè)為2 那么就會(huì)兩幀調(diào)用一次,也就是變成了每秒刷新30次。我們通過(guò)pause屬性開(kāi)控制CADisplayLink的運(yùn)行。當(dāng)我們想結(jié)束一個(gè)CADisplayLink的時(shí)候,應(yīng)該調(diào)用-(void)invalidate從runloop中刪除并刪除之前綁定的 target跟selector。另外CADisplayLink 不能被繼承。
//每1/60秒都回調(diào)用一次displayDidRefresh方法來(lái)做UI處理
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
//把displayLink加入主線程的commomMode里面
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];