YYImage框架瞧一瞧
建議檢視原文: ofollow,noindex">https://www.jianshu.com/p/83edaeeb5851 (不定時更新)
前言:
要看懂YYImage框架,最好先了解熱身部分(具體的自行百度),如果懶得看,直接跨過該部分,等到下面部分有疑問,再回過頭看這部分的知識,也是可以。
熱身部分
1、Image I/O
使用 CGBitmapContextCreate
函式建立一個位圖上下文;
使用 CGContextDrawImage
函式將原始點陣圖繪製到上下文中;
使用 CGBitmapContextCreateImage
函式建立一張新的解壓縮後的點陣圖。
2、 CGBitmapContextCreate
中的引數
-
data
:如果不為NULL
,那麼它應該指向一塊大小至少為bytesPerRow * height
位元組的記憶體;如果 為NULL
,那麼系統就會為我們自動分配和釋放所需的記憶體,所以一般指定NULL
即可; -
width
和height
:點陣圖的寬度和高度,分別賦值為圖片的畫素寬度和畫素高度即可; -
bitsPerComponent
:畫素的每個顏色分量使用的 bit 數,在 RGB 顏色空間下指定 8 即可; -
bytesPerRow
:點陣圖的每一行使用的位元組數,大小至少為width * bytes per pixel
位元組。有意思的是,當我們指定 0 時,系統不僅會為我們自動計算,而且還會進行 cache line alignment 的優化 -
space
: 顏色空間 ,一般使用 RGB 即可; -
bitmapInfo
:點陣圖的佈局資訊。當圖片不包含 alpha 的時候使用kCGImageAlphaNoneSkipFirst
,否則使用kCGImageAlphaPremultipliedFirst
3、訊號量
/* 注意,正常的使用順序是先降低然後再提高,這兩個函式通常成對使用。 */ dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); //等待降低訊號量 // to do dispatch_semaphore_signal(_framesLock); //提高訊號量
所用到的知識:
複合賦值運算子、Image I/O、CADisplayLink、willChangeValueForKey:、
一、YYImage總體介紹

YImage 結構
1、YYImage 功能
- 顯示動畫型別的圖片
- 播放幀動畫
- 播放 sprite sheet 動畫
- 圖片型別探測
- 圖片解碼、編碼(最核心功能)
2、YYImage 主要類介紹
YYImage 類
它是一個完全相容的“UIImage”子類。它擴充套件了UIImage
支援動畫WebP, APNG和GIF格式的影象資料解碼。它還
支援NSCoding協議,以存檔和反存檔多幀影象資料。
a、animatedImageMemorySize
如果所有幀影象都被載入到記憶體中,那麼總記憶體使用(以位元組為單位)。
如果影象不是從多幀影象資料建立的,則該值為0。
b、preloadAllAnimatedImageFrames
將此屬性設定為“YES”將阻塞要解碼的呼叫執行緒
所有動畫幀影象到記憶體,設定為“NO”將釋放預裝幀。
如果影象被許多影象檢視(如emoticon)共享,則預載入所有檢視
幀將降低CPU成本。
YYAnimatedImageView 類
用於顯示動畫影象的影象檢視。
可以用來播放多幀動畫以及普通動畫,可以控制、暫停動畫
當裝置有足夠的空閒記憶體時,這個檢視及時請求幀資料。
這個檢視可以在內部緩衝區中快取一些或所有未來的幀,以降低CPU成本。
3、YYImage 的意義(圖片解碼的原因)
從磁碟中載入一張圖片,並將它顯示到螢幕上,這個過程其實經歷很多,非常耗效能。隨著顯示的圖片增加,效能下降尤其明顯。不管是 JPEG 還是 PNG 等圖片,都是一種編碼後(壓縮)的點陣圖圖形格式。我們先看下顯示到螢幕這個過程的工作流:
1、我們使用 +[UIImage imageWithContentsOfFile:]
方法從磁碟中載入一張圖片。此時,圖片還沒有被解碼,仍舊是編碼狀態下。
UIImageView
3、接著一個隱式的 CATransaction
捕獲到了圖層樹的變化;
4、在主執行緒的下一個 run loop
到來時, Core Animation
提交了這個隱式的事務,可能會涉及copy這些圖片(已經成為圖層樹中的圖層內容的圖片)。這個 copy 操作可能會涉及以下部分或全部步驟:
a.分配緩衝區來管理檔案IO和解壓縮操作。
b.檔案資料從磁碟讀取到記憶體。
c.將壓縮的圖片資料解碼成未壓縮的點陣圖形式,這是一個非常耗時的 CPU 操作;
d.最後 Core Animation
使用未壓縮的點陣圖資料渲染 UIImageView
的圖層
圖層樹:(個人理解)洋蔥看過去有很多層,這就是洋蔥的圖層,而螢幕上顯示的文字、圖片啊,都可以理解成為圖層,很多圖層就形成了一個結構,這個很多圖層的結構就叫做圖層樹。
因此,在將磁碟中的圖片渲染到螢幕之前,必須先要得到圖片的原始畫素資料,才能執行後續的繪製操作,這就是為什麼需要對圖片解碼的原因。
二、YYImage主要類呼叫邏輯
A、渲染GIF/WebP/PNG(APNG)方法呼叫順序
1、 YYImage *image = [YYImage imageNamed:name];
//傳入圖片名建立YYImage物件
2、 [[self alloc] initWithData:data scale:scale];
//用重寫的方法初始化影象資料
3、 YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
//建立解碼類 YYImageDecoder 物件,緊接著更新資料
4、 result = [self _updateData:data final:final];
//根據影象的data算出圖片的type以及其他資訊,再根據不同type 的影象去分別更新資料
5、 [self _updateSourceImageIO];
// 計算出PNG、GIF等圖片資訊(圖片的每一幀的屬性,包括寬、高、方向、動畫重複次數(gif型別)、持續時間(gif型別))
6、 YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
//把圖片新增到 UIImageView 的子類,這個子類後面講(第7點後都是它的核心),這裡暫且當它為普通 ImageView 那樣看。
7、 [self setImage:image withType:YYAnimatedImageTypeImage];
// 設定圖片,類似Setter方法
8、 [self imageChanged];
//判斷當前圖片型別以及幀數,由CATransaction支援的顯示事務去更新圖層的 contentsRect
,以及重置動畫的引數,後面詳解該方法。
9、 [self resetAnimated];
//重置動畫多種引數; [self calcMaxBufferCount];
// 動態調整當前記憶體的緩衝區大小。
10、 [self didMoved];
// 視窗物件或者父檢視物件改變,則開始控制動畫的啟動(停止),這是動畫得以顯示的關鍵
B、渲染幀動畫方法呼叫順序
1、 UIImage *image = [[YYFrameImage alloc] initWithImagePaths:paths oneFrameDuration:0.1 loopCount:0];
//傳入圖片組的路徑、每一個幀(每一個圖片)的時間以及迴圈多少次,計算出總的durations
2、 [self initWithImagePaths:paths frameDurations:durations loopCount:loopCount];
// 把第一張圖片解碼後返回,並求出第一幀的大小,作為每一幀的大小
YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
後面步驟跟 渲染GIF/WebP/PNG(APNG)方法呼叫順序 第7點開始幾乎一樣
注意:由於程式碼過多,不可能面面俱到,所以下面只會摘取核心進行講解。這樣,讀者看完此文以及看完我標註過的原始碼(),,去讀原始碼,也更容易理解。
三、核心程式碼
// 它接受一個原始的點陣圖引數 imageRef ,最終返回一個新的解壓縮後的點陣圖 newImage CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) { if (!imageRef) return NULL; size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); if (width == 0 || height == 0) return NULL; // 重新繪製解碼(可能會失去一些精度) if (decodeForDisplay) { //decode with redraw (may lose some precision) CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask; BOOL hasAlpha = NO; if (alphaInfo == kCGImageAlphaPremultipliedLast || alphaInfo == kCGImageAlphaPremultipliedFirst || alphaInfo == kCGImageAlphaLast || alphaInfo == kCGImageAlphaFirst) { hasAlpha = YES; } // BGRA8888 (premultiplied) or BGRX8888 // same as UIGraphicsBeginImageContext() and -[UIView drawRect:] CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo); if (!context) return NULL; CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode CGImageRef newImage = CGBitmapContextCreateImage(context); CFRelease(context); return newImage; // 返回一個新的解壓縮後的點陣圖 newImage } else { } }
YYCGImageCreateDecodedCopy
是解壓縮的核心,也就是渲染圖片效能顯著的原因。該方法首先求出圖片的寬高,注意,這裡的圖片是指編碼前的圖片的每一幀圖片。
- (void)imageChanged { YYAnimatedImageType newType = [self currentImageType]; id newVisibleImage = [self imageForType:newType]; NSUInteger newImageFrameCount = 0; BOOL hasContentsRect = NO; if ([newVisibleImage isKindOfClass:[UIImage class]] && [newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) { // 求出有多少幀(如果是幀動畫(由多張圖組合的),相當於有多少張圖) newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount; if (newImageFrameCount > 1) { // 動態圖 hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)]; } } // 由CATransaction支援的顯示事務去更新圖層的 contentsRect, 但一般不用走這段程式碼。大都走的是 CATransaction 的隱式事務自己更新 if (!hasContentsRect && _curImageHasContentsRect) { if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) { [CATransaction begin]; [CATransaction setDisableActions:YES]; self.layer.contentsRect = CGRectMake(0, 0, 1, 1); [CATransaction commit]; } } _curImageHasContentsRect = hasContentsRect; // YYSpriteSheetImage 類用到,先不理 if (hasContentsRect) { CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0]; [self setContentsRect:rect forImage:newVisibleImage]; } if (newImageFrameCount > 1) { [self resetAnimated]; // 重置動畫多種引數,包括在後臺釋放影象,下面再賦值已經被重置過的動畫引數 _curAnimatedImage = newVisibleImage; // 當前動畫圖片 _curFrame = newVisibleImage; // 當前幀 _totalLoop = _curAnimatedImage.animatedImageLoopCount; // 總迴圈次數 _totalFrameCount = _curAnimatedImage.animatedImageFrameCount; // 總幀數 [self calcMaxBufferCount]; // 動態調整當前記憶體的緩衝區大小。 } [self setNeedsDisplay]; // 標誌需要重繪,會在下一個迴圈到來時重新整理 [self didMoved]; // 視窗物件或者父檢視物件改變,則開始控制動畫的啟動(停止),這是動畫得以顯示的關鍵 }
圖片改變的處理核心
主要做了以下幾點:
- 初始化動畫引數
resetAniamted
- 初始化或者重置後求出動畫播放迴圈次數、當前幀、總幀數
- 呼叫動態調整緩衝區方法
calcMaxBufferCount
、呼叫控制動畫方法didMoved
// init the animated params. - (void)resetAnimated { if (!_link) { _lock = dispatch_semaphore_create(1); _buffer = [NSMutableDictionary new]; // 新增到這種佇列中的操作,就會自動放到子執行緒中執行。 _requestQueue = [[NSOperationQueue alloc] init]; /* maxConcurrentOperationCount 預設情況下為-1,表示不進行限制,可進行併發執行。 為1時,佇列為序列佇列。只能序列執行。大於1時,佇列為併發佇列 */ _requestQueue.maxConcurrentOperationCount = 1; /* 初始化一個新的 CADisplayLink 物件,在螢幕更新時呼叫。為了使顯示迴圈與顯示同步,應用程式使用addToRunLoop:forMode:方法將其新增到執行迴圈中 一個計時器物件,允許應用程式將其繪圖同步到顯示的重新整理率。 */ _link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)]; if (_runloopMode) { [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode]; } // 禁用通知 _link.paused = YES; // 接受記憶體警告的通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; // 接受返回後臺的通知,返回後臺時,記錄即將顯示的下一幀 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; } [_requestQueue cancelAllOperations]; LOCK( if (_buffer.count) { NSMutableDictionary *holder = _buffer; _buffer = [NSMutableDictionary new]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ // Capture the dictionary to global queue, // release these images in background to avoid blocking UI thread. [holder class];// 捕獲字典到全域性佇列,在後臺釋放這些影象以避免阻塞UI執行緒。 }); } ); _link.paused = YES; _time = 0; if (_curIndex != 0) { [self willChangeValueForKey:@"currentAnimatedImageIndex"]; _curIndex = 0; // 把索引值重置為0 [self didChangeValueForKey:@"currentAnimatedImageIndex"]; } _curAnimatedImage = nil; // 當前影象為空 _curFrame = nil; // 當前幀 _curLoop = 0; //當前迴圈次數 _totalLoop = 0; // 總迴圈次數 _totalFrameCount = 1; // 總幀數 _loopEnd = NO; // 是否迴圈結尾 _bufferMiss = NO; // 是否丟幀 _incrBufferCount = 0; // 當前允許的快取 }
重置圖片的引數;
記憶體警告時釋放記憶體;
初始化一個新的 CADisplayLink 物件,在螢幕更新時呼叫。
// 只有螢幕重新整理累加時間不小於當前幀的動畫播放時間才顯示圖片,播放下一幀。 // 播放 GIF 的關鍵 - (void)step:(CADisplayLink *)link { UIImage <YYAnimatedImage> *image = _curAnimatedImage; NSMutableDictionary *buffer = _buffer; // 下一張的圖片 UIImage *bufferedImage = nil; // 下一張要顯示的索引 NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount; BOOL bufferIsFull = NO; // // 當前無影象顯示 返回 if (!image) return; if (_loopEnd) { // view will keep in last frame // 結束迴圈 停留在最後幀 [self stopAnimating]; // 如果動畫播放迴圈結束了,就停止動畫 return; } NSTimeInterval delay = 0; if (!_bufferMiss) { // 螢幕重新整理時間的累加 _time += link.duration; // link.duration 螢幕重新整理的時間,預設1/60 s delay = [image animatedImageDurationAtIndex:_curIndex]; // 返回當前幀的持續時間 if (_time < delay) return; _time -= delay; // 減去上一幀播放的時間 if (nextIndex == 0) { _curLoop++; // 增加一輪迴圈次數 if (_curLoop >= _totalLoop && _totalLoop != 0) { // 已經到了迴圈次數,停止播放 _loopEnd = YES; [self stopAnimating]; [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep return; // stop at last frame } } delay = [image animatedImageDurationAtIndex:nextIndex]; // 返回下一幀的的持續時間 /***/ if (_time > delay) _time = delay; // do not jump over frame } LOCK( bufferedImage = buffer[@(nextIndex)]; if (bufferedImage) { if ((int)_incrBufferCount < _totalFrameCount) { [buffer removeObjectForKey:@(nextIndex)]; } [self willChangeValueForKey:@"currentAnimatedImageIndex"]; _curIndex = nextIndex; // 用KVO改變 當前索引值 [self didChangeValueForKey:@"currentAnimatedImageIndex"]; _curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage; // 實現YYSpriteSheetImage 的協議方法,才會進入該 if 語句 if (_curImageHasContentsRect) { _curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex]; [self setContentsRect:_curContentsRect forImage:_curFrame]; } nextIndex = (_curIndex + 1) % _totalFrameCount; _bufferMiss = NO; if (buffer.count == _totalFrameCount) { bufferIsFull = YES; // 緩衝區已經滿 } } else { // 丟幀,某一幀沒有辦法找到顯示 _bufferMiss = YES; } )//LOCK if (!_bufferMiss) { // 重新整理顯示影象 [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep } /* _YYAnimatedImageViewFetchOperation 為 NSOperation 的子類 還未獲取完所有影象,交給它獲取下一張影象 */ if (!bufferIsFull && _requestQueue.operationCount == 0) { // if some work not finished, wait for next opportunity _YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new]; operation.view = self; operation.nextIndex = nextIndex; operation.curImage = image; [_requestQueue addOperation:operation]; // } }
這是動畫播放的關鍵,是 CADisplayLink物件 的方法,每 1/60s 也就是螢幕重新整理一次就呼叫一次
- (void)calcMaxBufferCount { int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame; // 求出每一幀的位元組數 if (bytes == 0) bytes = 1024; // 如果為0,則給定1024 int64_t total = _YYDeviceMemoryTotal(); // 獲取裝置的CPU實體記憶體 int64_t free = _YYDeviceMemoryFree(); // 獲取裝置的容量 int64_t max = MIN(total * 0.2, free * 0.6); // 比較記憶體的0.2倍以及容量的0.6倍最小值 max = MAX(max, BUFFER_SIZE); // 如果不夠 10 M,則以 10 M 作為最大緩衝區大小 /** _maxBufferSize 內部幀緩衝區大小 * 當裝置有足夠的空閒記憶體時,這個檢視將請求並解碼一些或所有未來的幀影象進入一個內部緩衝區。 * 預設值為0 如果這個屬性的值是0,那麼最大緩衝區大小將根據當前的狀態進行動態調整裝置釋放記憶體。否則,緩衝區大小將受到此值的限制。 * 當收到記憶體警告或應用程式進入後臺時,緩衝區將被立即釋放 */ if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max; //得出緩衝區的最大值 double maxBufferCount = (double)max / (double)bytes; if (maxBufferCount < 1) maxBufferCount = 1; else if (maxBufferCount > 512) maxBufferCount = 512; _maxBufferCount = maxBufferCount; // 最大緩衝數 }
動態求出最大緩衝數--->參考
/* 從自定義的 start 方法中呼叫 main 方法 呼叫[self didMoved]; 從而呼叫此方法 */ - (void)main { __strong YYAnimatedImageView *view = _view; if (!view) return; if ([self isCancelled]) return; view->_incrBufferCount++; //動態調整當前記憶體的緩衝區大小。 if (view->_incrBufferCount == 0) [view calcMaxBufferCount]; if (view->_incrBufferCount > (NSInteger)view->_maxBufferCount) { view->_incrBufferCount = view->_maxBufferCount; } NSUInteger idx = _nextIndex; // 獲取 Operation 中傳過來的 下一個索引值 NSUInteger max = view->_incrBufferCount < 1 ? 1 : view->_incrBufferCount; // 當前的緩衝區計數 NSUInteger total = view->_totalFrameCount; // 總圖片幀數 view = nil; for (int i = 0; i < max; i++, idx++) { @autoreleasepool { if (idx >= total) idx = 0; if ([self isCancelled]) break; __strong YYAnimatedImageView *view = _view; if (!view) break; LOCK_VIEW(BOOL miss = (view->_buffer[@(idx)] == nil)); //拿索引值去當前緩衝區取圖片 // 如果沒有取到圖片,則在子執行緒重新解碼,得到解碼後的圖片 if (miss) { // 等到當前還未解碼的圖片 UIImage *img = [_curImage animatedImageFrameAtIndex:idx]; NSLog(@"當前執行緒---%@", [NSThread currentThread]); // 列印當前執行緒,每次列印都是 name = (null),說明在非同步執行緒 // 在非同步執行緒再次呼叫解碼圖片,如果無法解碼或已經解碼就返回self img = img.yy_imageByDecoded; if ([self isCancelled]) break; LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]); // 每次新增一張圖片到 _buffer 陣列 view = nil; } } } }
該方法負責把圖片存入緩衝區中。(過程:取未解碼圖片-->解碼存入緩衝區)
在此,對YYImage框架完畢了,希望大家都能從大神原始碼學到知識。
其他額外收穫:
1、是否模擬器
- (BOOL)isSimulator { size_t size; sysctlbyname("hw.machine", NULL, &size, NULL, 0); char *machine = malloc(size); sysctlbyname("hw.machine", machine, &size, NULL, 0); NSString *model = [NSString stringWithUTF8String:machine]; free(machine); return [model isEqualToString:@"x86_64"] || [model isEqualToString:@"i386"]; }
2、根據不同的系統 scale 選擇圖片
/** 一個NSNumber物件陣列,根據不同的系統scale返回陣列內部不同順序的數字 e.g. iPhone3GS:@[@1,@2,@3] iPhone5:@[@2,@3,@1]iPhone6 Plus:@[@3,@2,@1] */ static NSArray *_NSBundlePreferredScales() { static NSArray *scales; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ CGFloat screenScale = [UIScreen mainScreen].scale; if (screenScale <= 1) { scales = @[@1,@2,@3]; } else if (screenScale <= 2) { scales = @[@2,@3,@1]; } else { scales = @[@3,@2,@1]; } }); return scales; }
咋一看,這不是單例嗎?保證初始化程式碼只執行一次,可移步單例相關文章
3、判斷圖片字尾
NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"]; NSArray *scales = _NSBundlePreferredScales(); for (int s = 0; s < scales.count; s++) { scale = ((NSNumber *)scales[s]).floatValue; NSString *scaledName = _NSStringByAppendingNameScale(res, scale); for (NSString *e in exts) { path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e]; if (path) break; } if (path) break; }
如果圖片沒標明後綴,則遍歷字尾陣列,並新增字尾到傳進來的圖片名,最後到 mainBundle
裡面取圖片路徑,取到地址則停止
CF_RETURNS_RETAINED
標記返回CF型別的函式,該型別需要呼叫方釋放
NSDefaultRunLoopMode
保持gif 圖在scrollView 拉動時不停止
|=為按位或運算子 eg: a|=b;
相當於 a=a|b;
參考:
快速解決GIF圖的鋸齒問題