關(guān)于iOS中圖像顯示的一些優(yōu)化處理

這篇文章是博主學(xué)習(xí)了幾位大牛的關(guān)于圖片處理和顯示的文章后,結(jié)合自己的總結(jié)和實(shí)踐后的記錄。幾位大牛的文章鏈接會(huì)在后面參上。因?yàn)樗接邢?,可能?huì)有錯(cuò)誤。

圖片的加載

在iOS中從磁盤讀取一張圖片并顯示在屏幕上,大概需要下面幾個(gè)步驟

  1. 從磁盤拷貝數(shù)據(jù)到內(nèi)核緩沖區(qū)
  1. 從內(nèi)核緩沖區(qū)復(fù)制數(shù)據(jù)到用戶空間
  2. 生成UIImageView,把圖像數(shù)據(jù)賦值給UIImageView
  3. 如果圖像數(shù)據(jù)為未解碼的PNG/JPG,解碼為位圖數(shù)據(jù)
  4. CATransaction捕獲到UIImageView layer樹的變化
  5. 主線程Runloop提交CATransaction,開始進(jìn)行圖像渲染
    6.1. 果數(shù)據(jù)沒有字節(jié)對(duì)齊,Core Animation會(huì)再拷貝一份數(shù)據(jù),進(jìn)行字節(jié)對(duì)齊。
    6.2. GPU處理位圖數(shù)據(jù),進(jìn)行渲染。

簡(jiǎn)單理解上面幾部的話大概就是把圖片從磁盤讀入內(nèi)存,然后解碼,然后渲染。其中讀入內(nèi)存的操作可以由子線程執(zhí)行,解碼和渲染的過程系統(tǒng)默認(rèn)是一定要在主線程執(zhí)行的。這也是很多時(shí)候顯示圖片發(fā)生卡頓的原因。不過在開發(fā)中多數(shù)情況下我們都是使用圖片處理庫來處理圖片,這些庫已經(jīng)解決了主線程解碼的問題(將解碼操作放在子線程)。渲染是一定要在主線程的,這個(gè)不需要解釋了。

關(guān)于卡頓的產(chǎn)生

說到卡頓的產(chǎn)生就不得不提到圖像的顯示原理,關(guān)于這部分內(nèi)容在ibireme大神寫的文章中有很詳細(xì)的講解,這里我只簡(jiǎn)單描述下(湊個(gè)字)。

在計(jì)算機(jī)系統(tǒng)中顯示器要想顯示畫面首先需要CPU計(jì)算好顯示內(nèi)容,然后將計(jì)算好的結(jié)果交給GPU進(jìn)行渲染,在雙緩存+垂直同步機(jī)制下(iOS始終是雙緩存+垂直同步)GPU會(huì)等到顯示器發(fā)送垂直同步信號(hào)(VSync)后將渲染后的結(jié)果更新到幀緩沖區(qū)等待視頻控制器讀取數(shù)據(jù)。

解釋了圖像顯示原理后再描述產(chǎn)生卡頓的原因就很好理解了,iOS設(shè)備的屏幕大概每秒刷新60次(這個(gè)值取決設(shè)備硬件,比如 iPhone 真機(jī)上通常是 59.97),也就是在這1/60秒內(nèi)要完成CPU執(zhí)行計(jì)算,GPU執(zhí)行渲染變換等等然后提交幀緩存這些操作,等待下一次VSync信號(hào)到來后把結(jié)果顯示在屏幕上。

如果在1/60秒內(nèi)這些操作沒有執(zhí)行完成呢?這踏馬就尷尬了。那么這一幀將會(huì)被丟棄,等待下一次VSync信號(hào)。體現(xiàn)在屏幕上就是界面什么都沒做,還是顯示上一幀的內(nèi)容。這在肉眼看來就是卡了。

我嘗試著在工程里存入了幾張平均大小在1.5m的png圖片,然后把這些圖片放在tableView里面以每個(gè)cell一張圖片的方式顯示。在這里還需要提一下系統(tǒng)加載jpeg圖片時(shí)速度會(huì)比png圖片要快,但是因?yàn)閄Code會(huì)在引入png圖片時(shí)對(duì)png圖片進(jìn)行解碼優(yōu)化,所以解碼操作上png要比jpeg更快,因?yàn)閖peg圖片的解壓算法更復(fù)雜。主要代碼如下:

#define  Width  [UIScreen mainScreen].bounds.size.width
#define  Height  [UIScreen mainScreen].bounds.size.height
@interface TableViewController ()<UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView *table;
@property (nonatomic, strong) NSMutableArray *dataArr;
@property (nonatomic, strong) UIScrollView *scroll;
@end

@implementation TableViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  self.view.backgroundColor = [UIColor whiteColor];

  self.dataArr = [NSMutableArray arrayWithCapacity:0];
  for (NSInteger i = 1; i < 6; i++) {
      NSString *path = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"b%ld", i] ofType:@"png"];
      [self.dataArr addObject:path];
  }
  [self.view addSubview:self.table];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
  static NSString *identifier = @"cell";
  const NSInteger imageTag = 99;
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
  if (!cell) {
      cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier];
  }

  UIImageView *imageView = (UIImageView *)[cell viewWithTag:imageTag];
  if (!imageView) {
      imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, Width, Height)];
      imageView.tag = imageTag;
      [cell.contentView addSubview:imageView];
  }
  cell.tag = indexPath.row;


  NSString *imagePath = self.dataArr[indexPath.row];
  imageView.image = [UIImage imageWithContentsOfFile:imagePath];
  return cell;
}

運(yùn)行這段代碼發(fā)現(xiàn)已經(jīng)發(fā)生了很嚴(yán)重的卡頓,原因在于這里將圖片的加載,解碼和渲染操作全部放在了主線程。在1/60秒內(nèi)根本無法完成這些操作。所以需要將任務(wù)分散來緩解主線程壓力。至于為什么使用imageWithContentsOfFile:方法而不是imageNamed:,為了演示效果,我不希望系統(tǒng)將解碼后的圖片進(jìn)行緩存所以沒有使用imageNamed方法。

關(guān)于UIImage的這兩個(gè)方法,他們的相同點(diǎn)是都只是將數(shù)據(jù)讀入內(nèi)存而不進(jìn)行解碼,只有當(dāng)圖片將要顯示之前才會(huì)被解碼(事實(shí)上UIImage的幾個(gè)創(chuàng)建方法都是這樣的)。不同點(diǎn)是imageNamed:方法會(huì)在第一次解碼顯示之后將解碼后的位圖進(jìn)行全局緩存,只有在程序退入后臺(tái)或者接收到內(nèi)存警告時(shí)才會(huì)將位圖釋放。這也是為什么在第一次滑動(dòng)tableVIew的時(shí)候會(huì)卡,之后再反復(fù)滑動(dòng)就不會(huì)卡頓的原因。imageWithContentsOfFile:方法雖然在64位設(shè)備是默認(rèn)也會(huì)緩存(緩存到CGImage內(nèi)部),但是一旦圖片被釋放,緩存的數(shù)據(jù)也會(huì)被釋放。

imageWithContentsOfFile:這個(gè)方法的底層實(shí)現(xiàn)是調(diào)用了ImageIO框架的CGImageSourceCreateWithData()方法,該方法在有一個(gè)ShouldCache參數(shù),64位設(shè)備上參數(shù)值默認(rèn)是YES。(這句話99.9%抄襲自ibireme的文章)。

當(dāng)我在看《iOS Core Animation: Advanced Techniques》這本書的時(shí)候,書中提到可以使用CGImageSourceCreateWithURL()方法生成image來避免延時(shí)解碼,不知道是我看的這版年代太久還是理解有誤。使用這個(gè)方法實(shí)質(zhì)上和CGImageSourceCreateWithData()沒有什么區(qū)別,都達(dá)不到解碼的效果。并且在實(shí)際的代碼中測(cè)試發(fā)現(xiàn)確實(shí)沒有解決延時(shí)解碼的問題。測(cè)試代碼如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *identifier = @"cell";
const NSInteger imageTag = 99;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier];
}

  UIImageView *imageView = (UIImageView *)[cell viewWithTag:imageTag];
    if (!imageView) {
      imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, Width, Height)];
      imageView.tag = imageTag;
      [cell.contentView addSubview:imageView];
  }
  cell.tag = indexPath.row;
  cell.tag = indexPath.row;
  imageView.image = nil;
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      NSInteger index = indexPath.row;
      NSURL *imageURL = [NSURL fileURLWithPath:self.dataArr[index]];
      NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES};
      CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, NULL);
      CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0,(__bridge CFDictionaryRef)options);
      UIImage *image = [UIImage imageWithCGImage:imageRef];
      CGImageRelease(imageRef);
      CFRelease(source);
      dispatch_async(dispatch_get_main_queue(), ^{
          if (index == cell.tag) {
              imageView.image = image;
          }
      });
  });
  return cell;
}

將代碼修改為這樣后卡頓依然存在。

這里可以簡(jiǎn)單實(shí)現(xiàn)一下異步解碼的操作,將cellForRow方法里面的部分代碼改成這樣:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    NSInteger index = indexPath.row;
    NSString *imagePath = self.dataArr[index];
    UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
    //redraw image using device context
    UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 0);
    [image drawInRect:imageView.bounds];
    image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    //set image on main thread, but only if index still matches up
    dispatch_async(dispatch_get_main_queue(), ^{
        if (index == cell.tag) {
            imageView.image = image;
        }
    });
});

將圖片繪制到畫布上,然后從畫布取出圖片。這樣做的好處是圖像的繪制完全可以放在后臺(tái)執(zhí)行,我們只需在繪制完成后取出圖片,然后在主線程上賦值給UIImageView就可以,這個(gè)時(shí)候的圖片因?yàn)槭浅绦蚶L制的就已經(jīng)不再是jpg或者png或者其他格式了,所以自然也不需要解碼操作。這只是簡(jiǎn)單地實(shí)現(xiàn),具體的可是學(xué)習(xí)YYWebImage或者SDWebImageDecoder。

緩存和異步解碼只是緩解CPU壓力的方法之一,除此之外還有很多地方可以優(yōu)化CPU和GPU資源,比如之前提到的對(duì)于圓角和陰影的處理。

另外還有一種比較有意思的方式用來顯示很大的圖片,就是使用CATiledLayer,在iOS6以前系統(tǒng)的地圖就是使用它來實(shí)現(xiàn)的。這個(gè)類的出現(xiàn)也是為了解決加載大圖造成的性能問題,它會(huì)將一張大圖分解成多張小圖碎片,然后分開顯示。關(guān)于CATiledLayer的使用我也是從《iOS Core Animation: Advanced Techniques》這本書中看到的,書中給了一個(gè)例子,將一張20482048的圖片分割成64張小圖,然后將CATiledLayer添加在一個(gè)大小是256 * 256的UIScrollView上,contentSize為20482048,開始的時(shí)候顯示第一片圖片,然后根據(jù)手勢(shì)的滑動(dòng)方向以及當(dāng)前的位置,CATiledLayer的代理方法- (void)drawLayer: inContext: 會(huì)加載相應(yīng)的圖片碎片,就像是地圖應(yīng)用中地圖會(huì)一塊一塊的加載出來一樣。

這種方法也可以使用到前面的例子當(dāng)中,我們把代碼改成這樣:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
  static NSString *identifier = @"cell";
  const NSInteger imageTag = 99;
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
  if (!cell) {
      cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier];
  }

  UIImageView *imageView = (UIImageView *)[cell viewWithTag:imageTag];
  if (!imageView) {
      imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, Width, Height)];
      imageView.tag = imageTag;
      [cell.contentView addSubview:imageView];
  }
  cell.tag = indexPath.row;

  CATiledLayer *tileLayer = (CATiledLayer *)[cell.contentView.layer.sublayers lastObject];
  if (!tileLayer) {
      tileLayer = [CATiledLayer layer];
      tileLayer.frame = CGRectMake(0, 0, Width, Height);
      tileLayer.contentsScale = [UIScreen mainScreen].scale;
      tileLayer.tileSize = CGSizeMake(cell.bounds.size.width * [UIScreen mainScreen].scale, cell.bounds.size.height * [UIScreen mainScreen].scale);
      tileLayer.delegate = self;
      [tileLayer setValue:@(indexPath.row) forKey:@"index"];
      [cell.contentView.layer addSublayer:tileLayer];
  }
  tileLayer.contents = nil;
  [tileLayer setValue:@(indexPath.row) forKey:@"index"];
  [tileLayer setNeedsDisplay];

return cell;
}

可以看到當(dāng)滑動(dòng)屏幕的時(shí)候圖片的顯示會(huì)呈現(xiàn)碎片式的淡入淡出效果。

參考的文章:
iOS圖片加載速度極限優(yōu)化—FastImageCache解析
iOS 處理圖片的一些小 Tip
iOS 保持界面流暢的技巧
《iOS Core Animation: Advanced Techniques》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容