Easy UITableView 优化






3.70/5 (4投票s)
Easy UITableView optimizations.
现在这可能是我写过的最简单的博客文章,但我认为这是每个 iOS 开发者都应该利用的东西。UITableView
是 iOS 中一个极其基础的视图,在许多许多应用程序中使用。 然而,我注意到,尽管有很多出色的应用程序使用了 table view,但它们在 table view cell 内容的数据加载方面存在一个令人不适的效果:直到 cell 可见时才加载数据。 我想通过这篇博文来解决 3 个影响 table view 体验的不足之处。
在UITableView
滚动时加载NSURLConnection
数据,
- 缓冲
UITableView
,以便内容在显示在屏幕上之前加载,以及 - 在后台线程渲染网络图形,这样当网络图形解压缩和渲染时,
UITableView
就不会卡顿(主线程被阻塞)。
流畅的 Table Views,就像 1, 2, 3 一样简单...
1. NSURLConnection 在滚动运行循环源上
默认情况下,NSURLConnection
在其创建的线程上运行,并被安排在该线程主运行循环的NSDefaultRunLoopMode
中执行。 现在,一个非常常见的情况是,NSURLConnection
在主线程上创建和运行,问题在于UIScrollView
(以及像UITableView
这样的子类)在不同的运行循环源上执行滚动,这会阻止默认运行循环模式下的源执行,包括我们的NSURLConnection
。其效果是,在视图滚动期间没有任何下载完成。
有三种简单的解决方案,每一种都完全可行,选择最适合你用例的一种。
- 在后台线程运行
NSURLConnection
。注意:由于一个 bug,NSURLConnection
不能在 iOS 4 的后台线程上运行,所以此解决方案仅适用于 iOS 5 及以上版本。 - 使用
NSRunLoopCommonModes
运行循环模式运行NSURLConnection
。你很可能想使用initWithRequest:delegate:startImmediately:
创建连接,并将 startImmediately 设置为NO
,而不是让它自动启动。然后,在启动连接之前,使用scheduleInRunLoop:forMode:
并使用NSRunLoopCommonModes
。你通常可以使用[NSRunLoop currentRunLoop]
作为运行循环。 - 最后,你可以使用由 Mattt Thompson(NSHipster 博客的作者)编写的、非常棒的开源库AFNetworking 。这个库允许你为每个
AFURLConnectionOperation
配置运行循环模式,甚至将其运行循环模式默认为NSRunLoopCommonModes
。尽管我从未在生产环境中使用过这个库,但在一些边项目中使用过,我必须说它是 iOS 网络请求的黄金标准。使用它,然后永不回头。
现在,随着NSURLConnection
在滚动视图滚动时加载数据,你将不再有必须停止 table view 滚动才能显示 cell 内容的烦人行为。 不过,有一个重要的性能考虑:将可执行代码添加到与滚动并行运行(如我们的NSURLConnection
)会占用大量处理器资源,因此如果你在旧设备上使用选项 2 或 3,你可能会注意到卡顿。出于这个原因,我要求设备必须是 iPad 2 或更新的型号、iPhone 4 或更新的型号,以及 iPod 4 代或更新的型号才能启用此功能,否则这种卡顿是不值得的。
2. UITableView 缓冲
这可能是你能做的最简单的改进任何 iOS 应用程序的感知性能的改动之一。 简单来说,如果你想预加载 table view 超出 table view 可见边界的数据,你只需要扩展 table view 本身就可以达到所需的缓冲效果。 例如,你有一个 iPhone 应用,它有一个全屏的 table view(320x480 或 320x568)。 如果你想让 table view 缓冲额外高度的数据,你只需要将 table view 的高度增加一倍(320x960 或 320x1136),然后使用UIScrollView
的contentInset
和scrollIndicatorInsets
属性来抵消高度的增加。 就是这么简单。 通过对 table view 进行这个简单的更改,你现在就能够避免在内容下载时显示具有空白内容的 table view cell,然后又出现令人不适的内容。
3. 后台图像渲染
现在,让 table view 运行流畅的最不明显但最困难的优化就是后台渲染图像。 让我先退一步,在尝试解决问题之前,先剖析一下问题所在。
当一个网络图形(PNG 或 JPG)被下载时,它必须做两件事才能在屏幕上显示为图像。 首先,也是最耗时的一部分,图形必须解压缩成一个位图。 其次,这个位图必须渲染到图形上下文中,才能为显示在设备屏幕上做好准备。如果你没有后台图像渲染,并且开始滚动一个包含大量网络下载图形的 table view(特别是如果图形是 Retina 图像并且你的设备性能较慢),你会注意到卡顿。 使用 Xcode 的 Instruments 进行进一步调查将表明,减速几乎完全可以归因于这些网络图形的解压缩。
解决方案是在我们下载网络图形时添加一个间接层。 你不是直接将网络响应返回的NSData
转换为UIImage
,然后将其添加到你的视图层级的UIImageView
中,而是希望异步地将该数据转换为一个预渲染的UIImage
,然后将其抛回主线程,以便设置到你的UIImageView
上。请记住,UIImage
是一个不透明的对象,它封装了各种图像数据表示,这些表示会根据UIImage
的需求而变化,这就是为什么我们希望在主线程上使用图像之前,确保图像处于我们想要的最终状态(解压缩和渲染)。
这是一段代码片段,实现了我们刚才讨论的功能
// UIImage+ASyncRendering.h
#import <UIKit/UIKit.h>
typedef void(^UIImageASyncRenderingCompletionBlock)(UIImage*);
typedef NS_ENUM(NSInteger, NSPUIImageType)
{
NSPUIImageType_JPEG,
NSPUIImageType_PNG
};
@interface UIImage (ASyncRendering)
/**
Renders the provided image data asynchronously so as to not block the main thread. Very useful when desiring to
keep the UI responsive while still generating UI from downloaded and compressed images.
@param imageData the data to render as a UIImage.
@param imageType the image type decode the \a imageData as. Can be \c NSPUIImageType_JPEG or \c NSPUIImageType_PNG.
@param block the completion block to be called once the \a imageData is rendered as a \c UIImage.
*/
+ (void) imageByRenderingData:(NSData*)imageData
ofImageType:(NSPUIImageType)imageType
completion:(UIImageASyncRenderingCompletionBlock)block;
@end
/********************************************************/
// UIImage+ASyncRendering.m
#import "UIImage+ASyncRendering.h"
@implementation UIImage (ASyncRendering)
+ (void) imageByRenderingData:(NSData*)imageData
ofImageType:(NSPUIImageType)imageType
completion:(UIImageASyncRenderingCompletionBlock)block
{
// NOTE: though I create a dispatch queue specifically for serializing
// image rendering, this is just my choice. You can easily just use the dispatch_get_global_queue.
static dispatch_queue_t s_imageRenderQ;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
s_imageRenderQ =
dispatch_queue_create("UIImage+ASyncRendering_Queue",
DISPATCH_QUEUE_SERIAL);
});
dispatch_async(s_imageRenderQ, ^() {
UIImage* imageObj = nil;
if (imageData)
{
CGDataProviderRef dataProvider =
CGDataProviderCreateWithCFData((__bridge CFDataRef)imageData);
if (dataProvider)
{
CGImageRef image = NULL;
if (NSPUIImageType_PNG == imageType)
{
image =
CGImageCreateWithPNGDataProvider(dataProvider,
NULL,
NO,
kCGRenderingIntentDefault);
}
else
{
image =
CGImageCreateWithJPEGDataProvider(dataProvider,
NULL,
NO,
kCGRenderingIntentDefault);
}
if (image)
{
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
unsigned char* imageBuffer =
(unsigned char*)malloc(width*height*4);
CGColorSpaceRef colorSpace =
CGColorSpaceCreateDeviceRGB();
CGContextRef imageContext =
CGBitmapContextCreate(imageBuffer,
width,
height,
8,
width*4,
colorSpace,
(kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little));
if (imageContext)
{
CGContextDrawImage(imageContext,
CGRectMake(0, 0, width, height),
image);
CGImageRef outputImage =
CGBitmapContextCreateImage(imageContext);
if (outputImage)
{
imageObj =
[UIImage imageWithCGImage:outputImage
scale:[UIScreen mainScreen].scale
orientation:UIImageOrientationUp];
CGImageRelease(outputImage);
}
CGContextRelease(imageContext);
}
CGColorSpaceRelease(colorSpace);
free(imageBuffer);
CGImageRelease(image);
}
CGDataProviderRelease(dataProvider);
}
}
dispatch_async(dispatch_get_main_queue(), ^() {
block(imageObj);
});
});
}
@end
通过将此类别添加到你的项目中,你可以这样做
// Custom cell implementation
- (void) loadImagery
{
// ... download logic
}
- (void) completeLoadImagery:(NSData*)imageData
{
// _imageView.image = [UIImage imageWithData:imageData]; <-- don't do this anymore
[UIImage imageByRenderingData:imageData
ofImageType:NSPUIImageType_JPEG
completion:^(UIImage* image) {
_imageView.image = image;
}];
}
或者,如果你使用AFNetworking ,你可以利用UIImageView(AFNetworking)
类别。
[编辑] 我已经更新了我的github 代码以支持 NSPUIImageType_Auto,它将自动从提供的 NSData 中检测图像类型。
[编辑] 我添加了一个github 演示项目来展示正在发生的事情(NSPDTableViewOptimizations)。老实说,看到优化的最好方法是在 iOS 6(或 5)的 iPhone 4 上运行该演示。这将是最慢的带有 Retina 屏幕的设备,并且真正能显示出令人不适的体验以及它如何得到改善。
结论
看!就像 1, 2, 3 一样简单! 你也可以从我的github 项目下载异步图像渲染代码以及其他有用的代码。