本文概述
打造出色的应用程序不仅涉及外观或功能, 还涉及其性能。尽管移动设备的硬件规格正在迅速提高, 但是性能不佳, 在每次屏幕切换时停滞或像幻灯片一样滚动的应用程序可能会破坏其用户的体验, 并使其感到沮丧。在本文中, 我们将看到如何衡量iOS应用程序的性能并对其进行调整以提高效率。就本文而言, 我们将构建一个包含大量图像和文本的简单应用程序。
为了测试性能, 我建议使用真实设备。如果你认真构建应用程序并针对平滑的iOS动画对其进行优化, 那么模拟器就不会削减它。模拟有时可能与现实不同步。例如, 模拟器可能正在Mac上运行, 这可能意味着CPU(中央处理单元)比iPhone上的CPU功能强大得多。相反, 你的设备和Mac之间的GPU(图形处理单元)差异如此之大, 以至于Mac实际上可以模拟设备的GPU。结果, 在你的模拟器上, 与CPU绑定的操作趋向于更快, 而与GPU绑定的操作趋向于更慢。
以60 FPS动画
感知性能的一个关键方面是确保动画以60 FPS(每秒帧)的速度运行, 这是屏幕的刷新率。有一些基于计时器的动画, 我们在这里不再讨论。一般来说, 如果你以高于50 FPS的速度运行, 则你的应用程序将看起来流畅且性能良好。如果你的动画停留在20到40 FPS之间, 则会出现明显的卡顿现象, 并且用户会在过渡中检测到”粗糙度”。低于20 FPS的任何内容都会严重影响应用程序的可用性。
在开始之前, 可能值得讨论CPU绑定和GPU绑定操作之间的区别。 GPU是专门用于绘制图形的专用芯片。虽然CPU也可以, 但速度要慢得多。这就是为什么我们希望将大部分图形渲染(从2D或3D模型生成图像的过程)卸载到GPU。但是我们需要小心, 因为当GPU耗尽处理能力时, 即使CPU相对空闲, 与图形相关的性能也会下降。
核心动画(Core Animation)是一个功能强大的框架, 可以在你的应用程序内外处理动画。它将过程分为6个关键步骤:
-
布局:在其中布置图层并设置其属性(例如颜色及其相对位置)的位置
-
显示:这是将背景图像绘制到上下文的位置。你在drawRect:或drawLayer:inContext:中编写的任何例程都可以在此处访问。
-
准备:在此阶段, Core Animation将要向渲染器发送上下文以进行绘制, 因此将执行一些必要的任务, 例如解压缩图像。
-
提交:此处, Core Animation将所有这些数据发送到渲染服务器。
-
反序列化:前面的4个步骤全部在你的应用程序中, 现在动画正在应用程序外部进行处理, 打包的图层被反序列化为渲染服务器可以理解的树。一切都转换为OpenGL几何。
-
绘制:渲染形状(实际上是三角形)。
你可能已经猜到了过程1-4是CPU运算和5-6是GPU运算。在现实中, 你只需要在第一2个步骤的控制。 GPU的最大杀手是其中GPU具有以填充每一帧的同一像素多次半透明层。此外, 任何屏幕外绘图(阴影, 蒙版, 圆角或图层栅格化等多个图层效果都将迫使Core Animation绘制屏幕外)也会影响性能。太大而无法由GPU处理的图像将由速度慢得多的CPU处理。尽管阴影可以通过直接在层上设置两个属性可以轻松实现, 他们可以, 如果你有阴影在屏幕上的许多对象轻易杀死性能。有时它是值得考虑加入这些阴影的图像。
衡量iOS动画效果
我们将从一个带有5个PNG图像和表格视图的简单应用开始。在此应用中, 我们实际上将加载5张图像, 但将在10, 000行以上重复。我们将在图像和图像旁边的标签上添加阴影:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath];
NSInteger index = (indexPath.row % [self.images count]);
NSString *imageName = [self.images objectAtIndex:index];
NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
cell.customCellImageView.image = image;
cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5);
cell.customCellImageView.layer.shadowOpacity = 0.8f;
cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)];
cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3);
cell.customCellMainLabel.layer.shadowOpacity = 0.5f;
return cell;
}
图像只是被回收, 而标签总是不同的。结果是:
垂直滑动时, 你很可能会在视图滚动时注意到结结。此时, 你可能会认为问题在于我们正在主线程中加载图像。可能是如果我们将其移至后台线程, 则所有问题都将得到解决。
让我们尝试一下并评估效果, 而不是盲目猜测。是时候开始使用乐器了。
要使用乐器, 你需要从”运行”更改为”配置文件”。而且, 你还应该连接到真实的设备, 并非模拟器上所有仪器都可用(这是你不应该针对模拟器的性能进行优化的另一个原因!)。我们将主要使用” GPU驱动程序”, “核心动画”和”时间分析器”模板。一个鲜为人知的事实是, 你不必拖放并在另一台仪器上运行, 而可以拖放多台仪器并同时运行多台仪器。
现在我们已经设置好仪器, 让我们进行测量。首先, 让我们看看我们的FPS是否真的有问题。
kes子, 我想我们这里的速度是18 FPS。从捆绑包中的映像加载到主线程上真的那么昂贵且昂贵吗?注意我们的渲染器利用率几乎达到极限。我们的tiler利用率也是如此。两者都在95%以上。这与在主线程中从捆绑包中加载图像无关, 因此在这里不要寻找解决方案。
效率调整
有一个名为shouldRasterize的属性, 人们可能会建议你在这里使用它。 Rasterize应该做什么?它将你的图层缓存为展平图像。所有这些昂贵的图层绘制都需要发生一次。万一你的框架频繁更改, 则无需使用缓存, 因为无论如何每次都需要重新生成它。
快速修改我们的代码, 我们得到:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath];
NSInteger index = (indexPath.row % [self.images count]);
NSString *imageName = [self.images objectAtIndex:index];
NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
cell.customCellImageView.image = image;
cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5);
cell.customCellImageView.layer.shadowOpacity = 0.8f;
cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)];
cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3);
cell.customCellMainLabel.layer.shadowOpacity = 0.5f;
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return cell;
}
我们再次测量:
仅用两条线, 我们的FPS就提高了2倍。我们现在平均高于40 FPS。但是如果我们将图像加载移至后台线程会有所帮助吗?
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath];
NSInteger index = (indexPath.row % [self.images count]);
NSString *imageName = [self.images objectAtIndex:index];
cell.tag = indexPath.row;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
dispatch_async(dispatch_get_main_queue(), ^{
if (indexPath.row == cell.tag) {
cell.customCellImageView.image = image;
}
});
});
cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5);
cell.customCellImageView.layer.shadowOpacity = 0.8f;
cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)];
cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3);
cell.customCellMainLabel.layer.shadowOpacity = 0.5f;
// cell.layer.shouldRasterize = YES;
// cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return cell;
}
经过测量, 我们发现性能平均约为18 FPS:
没有什么值得庆祝的。换句话说, 它没有改善我们的帧速率。那是因为即使阻塞主线程是错误的, 渲染也不是我们的瓶颈。
回到更好的例子, 我们平均高于40FPS, 性能明显更平滑。但实际上我们可以做得更好。
在Core Animation Tool上检查” Color Blended Layers”, 我们看到:
屏幕上显示” Color Blended Layers”, 你的GPU在其中进行大量渲染。绿色表示最少的渲染活动, 而红色表示最多。但是我们确实将shouldRasterize设置为YES。值得指出的是, “颜色混合层”与”颜色命中绿色而未命中红色”不同。稍后, 基本上会在重新生成缓存时以红色突出显示栅格化的图层(这是查看你是否未正确使用缓存的好工具)。将shouldRasterize设置为YES不会影响非透明图层的初始渲染。
这是重要的一点, 我们需要暂停片刻思考。无论是否应将Rasterize设置为YES, 渲染框架都需要检查所有视图, 并根据子视图是透明还是不透明来混合(或不混合)。虽然UILabel不透明是很有意义的, 但它可能一文不值, 并且会降低性能。例如, 在白色背景上的透明UILabel可能一文不值。让它变得不透明:
这样可以产生更好的性能, 但是我们对应用程序的外观和感觉有所改变。现在, 由于我们的标签和图像是不透明的, 因此阴影已在图像周围移动。没有人会喜欢这种变化, 如果我们想保持一流的性能来保持原始的外观和感觉, 就不会失去希望。
为了在保留原始外观的同时挤出一些额外的FPS, 重要的是重新访问到目前为止我们所忽略的两个核心动画阶段。
- 准备
- 承诺
这些似乎完全是我们无法控制的, 但事实并非如此。我们知道要加载图像, 需要对其进行解压缩。减压时间根据图像格式而变化。对于PNG, 解压缩比JPEG快很多(尽管加载时间更长, 并且这也取决于图像大小), 所以我们在使用PNG的道路上走了正确的路, 但是我们对解压缩过程并没有做任何事情, 并且这种解压缩发生在”绘图点”!这是我们可能会浪费时间的最糟糕的地方-在主线程上。
有一种方法可以强制减压。我们可以立即将其设置为UIImageView的image属性。但这仍然会解压缩主线程上的图像。有什么更好的办法吗?
有一个。将其绘制到CGContext中, 在此之前需要先解压缩图像。我们可以在后台线程中执行此操作(使用CPU), 并根据图像视图的大小根据需要为其设置界限。这将通过不在主线程上执行来优化我们的图像绘制过程, 并使我们不必在主线程上进行不必要的”准备”计算。
在处理图像时, 为什么不在绘制图像时添加阴影?然后, 我们可以将图像捕获(并将其缓存)为一张静态的不透明图像。代码如下:
- (UIImage*)generateImageFromName:(NSString*)imageName {
//define a boudns for drawing
CGRect imgVwBounds = CGRectMake(0, 0, 48, 48);
//get the image
NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"];
UIImage *image = [UIImage imageWithContentsOfFile:filePath];
//draw in the context
UIGraphicsBeginImageContextWithOptions(imgVwBounds.size, NO, 0); {
//get context
CGContextRef context = UIGraphicsGetCurrentContext();
//shadow
CGContextSetShadowWithColor(context, CGSizeMake(0, 3.0f), 3.0f, [UIColor blackColor].CGColor);
CGContextBeginTransparencyLayer (context, NULL);
[image drawInRect:imgVwBounds blendMode:kCGBlendModeNormal alpha:1.0f];
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
CGContextEndTransparencyLayer(context);
}
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
最后:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath];
// NSInteger index = (indexPath.row % [self.images count]);
// NSString *imageName = [self.images objectAtIndex:index];
//
// cell.tag = indexPath.row;
//
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"];
// UIImage *image = [UIImage imageWithContentsOfFile:filePath];
//
// dispatch_async(dispatch_get_main_queue(), ^{
// if (indexPath.row == cell.tag) {
// cell.customCellImageView.image = image;
// }
// });
// });
cell.customCellImageView.image = [self getImageByIndexPath:indexPath];
cell.customCellImageView.clipsToBounds = YES;
// cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5);
// cell.customCellImageView.layer.shadowOpacity = 0.8f;
cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)];
cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3);
cell.customCellMainLabel.layer.shadowOpacity = 0.5f;
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return cell;
}
结果是:
现在, 我们的平均速度高于55 FPS, 而渲染利用率和tileer利用率几乎是原来的一半。
本文总结
万一你想知道我们还能采取什么措施每秒增加几帧, 那就别无所求。 UILabel使用WebKit HTML呈现文本。我们可以直接转到CATextLayer并在那里也可以处理阴影。
你可能已经注意到, 在我们的上述实现中, 我们并未在后台线程中加载图片, 而是对其进行了缓存。由于只有5张图片, 因此效果非常快, 并且似乎并没有影响整体性能(尤其是因为所有5张图片都是在滚动之前加载到屏幕上的)。但是你可能想要尝试将此逻辑移至后台线程以提高性能。
效率调整是世界一流的应用程序与业余应用程序之间的区别。性能优化, 尤其是当它涉及到iOS动画, 可以是一个艰巨的任务。但随着仪器的帮助下, 可以很容易地诊断瓶颈iOS上的动画性能。
评论前必须登录!
注册