利用預(yù)渲染加速iOS設(shè)備的圖像顯示
最近在做一個(gè)UITableView的例子,發(fā)現(xiàn)滾動(dòng)時(shí)的性能還不錯(cuò)。但來(lái)回滾動(dòng)時(shí),第一次顯示的圖像不如再次顯示的圖像流暢,出現(xiàn)前會(huì)有稍許的停頓感。
于是我猜想顯示過(guò)的圖像肯定是被緩存起來(lái)了,查了下文檔后發(fā)現(xiàn)果然如此。
后來(lái)在別的文章中找到了一些提示:原來(lái)在顯示圖像時(shí),解壓和重采樣會(huì)消耗很多CPU時(shí)間;而如果預(yù)先在一個(gè)bitmap context里畫出圖像,再緩存這個(gè)圖像,就能省去這些繁重的工作了。
接著我就寫了個(gè)例子程序來(lái)驗(yàn)證:
- // ImageView.h
- #import <UIKit/UIKit.h>
- @interface ImageView : UIView {
- UIImage *image;
- }
- @property (retain, nonatomic) UIImage *image;
- @end
- // ImageView.m
- #include <mach/mach_time.h>
- #import "ImageView.h"
- @implementation ImageView
- #define LABEL_TAG 1
- static const CGRect imageRect = {{0, 0}, {100, 100}};
- static const CGPoint imagePoint = {0, 0};
- @synthesize image;
- - (void)awakeFromNib {
- if (!self.image) {
- self.image = [UIImage imageNamed:@"random.jpg"];
- }
- }
- - (void)drawRect:(CGRect)rect {
- if (CGRectEqualToRect(rect, imageRect)) {
- uint64_t start = mach_absolute_time();
- [image drawAtPoint:imagePoint];
- uint64_t drawTime = mach_absolute_time() - start;
- NSString *text = [[NSString alloc] initWithFormat:@"%ld", drawTime];
- UILabel *label = (UILabel *)[self viewWithTag:LABEL_TAG];
- label.text = text;
- [text release];
- }
- }
- - (void)dealloc {
- [super dealloc];
- [image release];
- }
- @end
控制器的代碼我就不列出了,就是點(diǎn)按鈕時(shí),更新view(調(diào)用[self.view setNeedsDisplayInRect:imageRect]),畫出一張圖,并在label中顯示消耗的時(shí)間。
值得一提的是,在模擬器上可以直接用clock()函數(shù)獲得微秒級(jí)的精度,但iOS設(shè)備上精度為10毫秒。于是我找到了mach_absolute_time(),它在Mac和iOS設(shè)備上都有納秒級(jí)的精度。
測(cè)試用的是一張200x200像素的JPEG圖像,命名時(shí)加了@2x,在iPhone 4上第一次顯示時(shí)花了約300微秒,再次顯示約65微秒。
接下來(lái)就是見(jiàn)證奇跡的時(shí)刻了,把這段代碼加入程序:
- static const CGSize imageSize = {100, 100};
- - (void)awakeFromNib {
- if (!self.image) {
- self.image = [UIImage imageNamed:@"random.jpg"];
- if (NULL != UIGraphicsBeginImageContextWithOptions)
- UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0);
- else
- UIGraphicsBeginImageContext(imageSize);
- [image drawInRect:imageRect];
- self.image = UIGraphicsGetImageFromCurrentImageContext();
- UIGraphicsEndImageContext();
- }
- }
這里需要判斷一下UIGraphicsBeginImageContextWithOptions是否為NULL,因?yàn)樗莍OS 4.0才加入的。
由于JPEG圖像是不透明的,所以第二個(gè)參數(shù)就設(shè)為YES。
第三個(gè)參數(shù)是縮放比例,iPhone 4是2.0,其他是1.0。雖然這里可以用[UIScreen mainScreen].scale來(lái)獲取,但實(shí)際上設(shè)為0后,系統(tǒng)就會(huì)自動(dòng)設(shè)置正確的比例了。
值得一提的是,圖像本身也有縮放比例,普通的圖像是1.0(除了UIImage imageNamed:外,大部分API都只能獲得這種圖像,而且縮放比例是不可更改的),高清圖像是2.0。圖像的點(diǎn)和屏幕的像素就是依靠2者的縮放比例來(lái)計(jì)算的,例如普通圖像在視網(wǎng)膜顯示屏上是1:4,而高清圖像在視網(wǎng)膜顯示屏上則是1:1。
接下來(lái)的drawInRect:把圖像畫到了當(dāng)前的image context里,這時(shí)就完成了解壓縮和重采樣的工作了。然后再?gòu)膇mage context里獲取新的image,這個(gè)image的縮放比例也能正確地和設(shè)備匹配。
再點(diǎn)下按鈕,發(fā)現(xiàn)時(shí)間已經(jīng)縮短到12微秒左右了,之后的畫圖穩(wěn)定在15微秒左右。
還能更快嗎?讓我們來(lái)試試Core Graphics。
先定義一個(gè)全局的CGImageRef變量:
- static CGImageRef imageRef;
再在awakeFromNib中設(shè)置一下它的值:
- imageRef = self.image.CGImage;
最后在drawRect:中繪制:
- CGContextRef context = UIGraphicsGetCurrentContext();
- CGContextDrawImage(context, imageRect, imageRef);
搞定運(yùn)行一下,發(fā)現(xiàn)時(shí)間增加到33微秒左右了,而且圖像還上下顛倒了⋯
這個(gè)原因是UIKit和Core Graphics的坐標(biāo)系y軸是相反的,于是加上2行代碼來(lái)修正:
- CGContextRef context = UIGraphicsGetCurrentContext();
- CGContextTranslateCTM(context, 0, 100);
- CGContextScaleCTM(context, 1, -1);
- CGContextDrawImage(context, imageRect, imageRef);
這下圖像終于正常顯示了,時(shí)間縮短到了14微秒左右,成效不大,看來(lái)直接用-drawAtPoint:和-drawInRect:也足夠好了。當(dāng)然,這個(gè)例子正確的做法是用viewDidLoad或loadView,不過(guò)我懶得列出控制器代碼,所以就放awakeFromNib里了。