UITableView的优化方向:缓存高度、异步绘制、减少层级、hide、预渲染。
缓存高度:
我们一般在网络请求结束后,更新界面之前就把每个 cell 的高度算好,缓存到相对应的 model 中。异步绘制:
在Cell上添加系统控件的时候,实质上系统都需要调用底层的接口进行绘制,当我们大量添加控件时,对资源的开销也会很大,所以我们可以索性直接绘制,提高效率。
//异步绘制
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGRect rect = CGRectMake(0, 0, 100, 100);
UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
[[UIColor lightGrayColor] set];
CGContextFillRect(context, rect);
//将绘制的内容以图片的形式返回,并调主线程显示
UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 回到主线程
dispatch_async(dispatch_get_main_queue(), ^{
//code
});
});
减少层级:
减少SubViews的数量, 在滑动的列表上,多层次的view会导致帧数的下降。例如: 绘制 cell 不建议使用 UIView,建议使用 CALayer。
从形式来说:UIView 的绘制是建立在 CoreGraphic 上的,使用的是 CPU。CALayer 使用的是 Core Animation,CPU,GPU 通吃,由系统决定使用哪个。View的绘制使用的是自下向上的一层一层的绘制,然后渲染。Layer处理的是 Texure,利用 GPU 的 Texture Cache 和独立的浮点数计算单元加速 纹理 的处理。
从事件的响应来说:UIView是 CALayer 的代理,layer本身并不能响应事件,因为layer是直接继承自NSObject,不具备处理事件的能力。而 UIView 是继承了UIResponder 的,这也是事件转发的角度上说明,view要比单纯的layer复杂的多。多层次的view再加上各种手势的处理势必导致帧数的下降。
Hide:
尽量在绘制cell的时候把视图绘制完整,不需要显示的设置hide属性为YES。预渲染:
参考链接利用预渲染加速iOS设备的图像显示。链接内容:
最近在做一个UITableView的例子,发现滚动时的性能还不错。但来回滚动时,第一次显示的图像不如再次显示的图像流畅,出现前会有稍许的停顿感。于是我猜想显示过的图像肯定是被缓存起来了,查了下文档后发现果然如此。
后来在别的文章中找到了一些提示:原来在显示图像时,解压和重采样会消耗很多CPU时间;而如果预先在一个bitmap context里画出图像,再缓存这个图像,就能省去这些繁重的工作了。
接着我就写了个例子程序来验证:
// 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
控制器的代码我就不列出了,就是点按钮时,更新view(调用[self.view setNeedsDisplayInRect:imageRect]),画出一张图,并在label中显示消耗的时间。
值得一提的是,在模拟器上可以直接用clock()函数获得微秒级的精度,但iOS设备上精度为10毫秒。于是我找到了mach_absolute_time(),它在Mac和iOS设备上都有纳秒级的精度。
测试用的是一张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,因为它是iOS 4.0才加入的。
由于JPEG图像是不透明的,所以第二个参数就设为YES。
第三个参数是缩放比例,iPhone 4是2.0,其他是1.0。虽然这里可以用[UIScreen mainScreen].scale来获取,但实际上设为0后,系统就会自动设置正确的比例了。
值得一提的是,图像本身也有缩放比例,普通的图像是1.0(除了UIImage imageNamed:外,大部分API都只能获得这种图像,而且缩放比例是不可更改的),高清图像是2.0。图像的点和屏幕的像素就是依靠2者的缩放比例来计算的,例如普通图像在视网膜显示屏上是1:4,而高清图像在视网膜显示屏上则是1:1。
接下来的drawInRect:把图像画到了当前的image context里,这时就完成了解压缩和重采样的工作了。然后再从image context里获取新的image,这个image的缩放比例也能正确地和设备匹配。
再点下按钮,发现时间已经缩短到12微秒左右了,之后的画图稳定在15微秒左右。
还能更快吗?让我们来试试Core Graphics。
先定义一个全局的CGImageRef变量:
static CGImageRef imageRef;
再在awakeFromNib中设置一下它的值:
imageRef = self.image.CGImage;
最后在drawRect:中绘制:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawImage(context, imageRect, imageRef);
搞定运行一下,发现时间增加到33微秒左右了,而且图像还上下颠倒了⋯
这个原因是UIKit和Core Graphics的坐标系y轴是相反的,于是加上2行代码来修正:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, 0, 100);
CGContextScaleCTM(context, 1, -1);
CGContextDrawImage(context, imageRect, imageRef);
这下图像终于正常显示了,时间缩短到了14微秒左右,成效不大,看来直接用-drawAtPoint:和-drawInRect:也足够好了。当然,这个例子正确的做法是用viewDidLoad或loadView,不过我懒得列出控制器代码,所以就放awakeFromNib里了。