要看懂YYImage框架,最好先了解熱身部分(具體的自行百度),若是懶得看,直接跨過該部分,等到下面部分有疑問,再回過頭看這部分的知識,也是能夠。html
移動端圖片格式調研ios
Image I/O官方文檔github
GIF圖添加文字Demoweb
使用
CGBitmapContextCreate
函數建立一個位圖上下文; 使用CGContextDrawImage
函數將原始位圖繪製到上下文中; 使用CGBitmapContextCreateImage
函數建立一張新的解壓縮後的位圖。數組
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
信號量的講解bash
/* 注意,正常的使用順序是先下降而後再提升,這兩個函數一般成對使用。 */
dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); //等待下降信號量
// to do
dispatch_semaphore_signal(_framesLock); //提升信號量
複製代碼
所用到的知識: 複合賦值運算符、Image I/O、CADisplayLink、willChangeValueForKey:、併發
它是一個徹底兼容的「UIImage」子類。它擴展了UIImage 支持動畫WebP, APNG和GIF格式的圖像數據解碼。它還 支持NSCoding協議,以存檔和反存檔多幀圖像數據。app
a、animatedImageMemorySize
若是全部幀圖像都被加載到內存中,那麼總內存使用(以字節爲單位)。 若是圖像不是從多幀圖像數據建立的,則該值爲0。
b、preloadAllAnimatedImageFrames
將此屬性設置爲「YES」將阻塞要解碼的調用線程 全部動畫幀圖像到內存,設置爲「NO」將釋放預裝幀。 若是圖像被許多圖像視圖(如emoticon)共享,則預加載全部視圖 幀將下降CPU成本。
用於顯示動畫圖像的圖像視圖。 能夠用來播放多幀動畫以及普通動畫,能夠控制、暫停動畫 當設備有足夠的空閒內存時,這個視圖及時請求幀數據。 這個視圖能夠在內部緩衝區中緩存一些或全部將來的幀,以下降CPU成本。
從磁盤中加載一張圖片,並將它顯示到屏幕上,這個過程其實經歷不少,很是耗性能。隨着顯示的圖片增長,性能降低尤爲明顯。無論是 JPEG 仍是 PNG 等圖片,都是一種編碼後(壓縮)的位圖圖形格式。咱們先看下顯示到屏幕這個過程的工做流:
一、咱們使用
+[UIImage imageWithContentsOfFile:]
方法從磁盤中加載一張圖片。此時,圖片尚未被解碼,仍舊是編碼狀態下。 二、返回的圖片被分配給UIImageView
三、接着一個隱式的CATransaction
捕獲到了圖層樹的變化; 四、在主線程的下一個run loop
到來時,Core Animation
提交了這個隱式的事務,可能會涉及copy這些圖片(已經成爲圖層樹中的圖層內容的圖片)。這個 copy 操做可能會涉及如下部分或所有步驟:a.分配緩衝區來管理文件IO和解壓縮操做。 b.文件數據從磁盤讀取到內存。 c.將壓縮的圖片數據解碼成未壓縮的位圖形式,這是一個很是耗時的 CPU 操做; d.最後
Core Animation
使用未壓縮的位圖數據渲染UIImageView
的圖層
圖層樹:(我的理解)洋蔥看過去有不少層,這就是洋蔥的圖層,而屏幕上顯示的文字、圖片啊,均可以理解成爲圖層,不少圖層就造成了一個結構,這個不少圖層的結構就叫作圖層樹。
所以,在將磁盤中的圖片渲染到屏幕以前,必須先要獲得圖片的原始像素數據,才能執行後續的繪製操做,這就是爲何須要對圖片解碼的緣由。
一、YYImage *image = [YYImage imageNamed:name];
//傳入圖片名建立YYImage對象
二、[[self alloc] initWithData:data scale:scale];
//用重寫的方法初始化圖像數據
三、YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
//建立解碼類 YYImageDecoder 對象,緊接着更新數據
四、result = [self _updateData:data final:final];
//根據圖像的data算出圖片的type以及其餘信息,再根據不一樣type 的圖像去分別更新數據
五、[self _updateSourceImageIO];
// 計算出PNG、GIF等圖片信息(圖片的每一幀的屬性,包括寬、高、方向、動畫重複次數(gif類型)、持續時間(gif類型))
六、 YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
//把圖片添加到 UIImageView 的子類,這個子類後面講(第7點後都是它的核心),這裏暫且當它爲普通 ImageView 那樣看。
七、[self setImage:image withType:YYAnimatedImageTypeImage];
// 設置圖片,相似Setter方法
八、[self imageChanged];
//判斷當前圖片類型以及幀數,由CATransaction支持的顯示事務去更新圖層的 contentsRect
,以及重置動畫的參數,後面詳解該方法。
九、[self resetAnimated];
//重置動畫多種參數;[self calcMaxBufferCount];
// 動態調整當前內存的緩衝區大小。
十、[self didMoved];
// 窗口對象或者父視圖對象改變,則開始控制動畫的啓動(中止),這是動畫得以顯示的關鍵
一、UIImage *image = [[YYFrameImage alloc] initWithImagePaths:paths oneFrameDuration:0.1 loopCount:0];
//傳入圖片組的路徑、每個幀(每個圖片)的時間以及循環多少次,計算出總的durations 二、[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框架完畢了,但願你們都能從大神源碼學到知識。
一、是否模擬器
- (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"];
}
複製代碼
二、根據不一樣的系統 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;
}
複製代碼
咋一看,這不是單例嗎?保證初始化代碼只執行一次,可移步單例相關文章
三、判斷圖片後綴
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圖的鋸齒問題