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