12

为什么在 iOS 4.3.5 上,“大”(960 x 1380)自定义 UIView 执行 CABackingStoreUpdate 的效率如此低下,我该如何提高绘图操作的性能?

不完全确定我的意思?继续阅读...

笔记:

随着我对这个问题的理解不断发展,这个问题也在不断发展。因此,问题本身是相似的,但自首次提出问题以来,以下正文中的代码示例和基本细节/推理发生了显着变化。

语境

我有一个非常基本的应用程序(底部的代码),它在 drawRect: 自定义 UIView 的方法中绘制单个省略号。当绘制的椭圆的大小保持不变但自定义 UIView 的大小变大时,应用程序演示了性能差异:

小 UIView

大 UIView

我在运行 iOS 4.3.5 的第 4 代 iPod 和运行 iOS 5.1.1 的第 1 代 iPad 上使用不同大小的自定义 UIView 多次运行该应用程序。
下表显示了从时间分析器仪器获取的结果:

时间分析器结果

以下仪器轨迹显示了每个设备的两个极端的详细信息:

iOS 5.1.1 - (自定义 UIView 大小 320 x 460)

iOS 5.1.1 - 自定义 UIView 大小 320 x 460

iOS 5.1.1 - (自定义 UIView 尺寸 960 x 1380)

iOS 5.1.1 - 自定义 UIView 尺寸 960 x 1380

iOS 4.3.5 - (自定义 UIView 大小 320 x 460)

iOS 4.3.5 - 自定义 UIView 大小 320 x 460

iOS 4.3.5 - (自定义 UIView 尺寸 960 x 1380)

iOS 4.3.5 - 自定义 UIView 尺寸 960 x 1380

正如您(希望)所见,在 4 个案例中的 3 个案例中,我们得到了我们期望的结果:大部分时间都花在了执行自定义 UIViews drawRect: 方法上,每个案例都保持 10fps。
但是第四个案例显示了应用程序在仅绘制单个形状时难以保持 7fps 的性能下降。在 UIView 的 CALayer 的显示方法中,大部分时间都花在了复制内存上,具体来说:

[CALayer 显示] >
[CALayer _display] >
CABackingStoreUpdate >
CA::Render::ShmemBitmap::copy_pixels(CA::Render::ShmemBitmap const*, CGSRegionObject*) >
memcpy$VARIANT$CortexA8

现在不需要天才就可以从这些数字中看出这里有严重的错误。使用大小为 960 x 1380 的自定义 UIView,iOS 4.3.5 复制内存的时间是绘制整个视图内容的时间的 4 倍多。

问题

现在,鉴于上下文,我再次提出我的问题:

为什么在 iOS 4.3.5 上,“大”(960 x 1380)自定义 UIView 执行 CABackingStoreUpdate 的效率如此低下,我该如何提高绘图操作的性能?

很感谢任何形式的帮助。

我还在Apple Developer forums上发布了这个问题。

真正的交易

现在,显然,为了这个问题,我已经将我的真正问题简化为最简单的可重现案例。我实际上是在尝试为位于 UIScrollView 内的 960 x 1380 自定义 UIView 的一部分设置动画。

虽然我很欣赏当他们没有通过 Quartz 2D 达到他们想要的性能水平时引导任何人使用 OpenGL ES 的诱惑,但我要求任何采取这条路线的人至少解释一下为什么 Quartz 2D 甚至在iOS 4.3.5 上最基本的绘图操作,iOS 5.1.1 没有问题。正如你可以想象的那样,我对为这个基石案例重写所有内容的想法并不感到兴奋。这也适用于建议使用 Core Animation 的人。虽然为了简单起见,我在演示中使用了改变颜色的椭圆(非常适合核心动画的任务),但我实际想要执行的绘图操作是大量随时间扩展的线条,这是一个绘图任务Quartz 2D 非常适合(当它是高性能的时候!)。另外,再次,

代码

TViewController.m(标准视图控制器的实现)

#import "TViewController.h"
#import "TCustomView.h"

// VERSION 1 features the custom UIView the same size as the screen.
// VERSION 2 features the custom UIView nine times the size of the screen.
#define VERSION 2


@interface TViewController ()
@property (strong, nonatomic) TCustomView *customView;
@property (strong, nonatomic) NSTimer *animationTimer;
@end


@implementation TViewController

- (void)viewDidLoad
{
    // Custom subview.
    TCustomView *customView = [[TCustomView alloc] init];
    customView.backgroundColor = [UIColor whiteColor];
#if VERSION == 1
    customView.frame = CGRectMake(0.0f, 0.0f, 320.0f, 460.0f);
#else
    customView.frame = CGRectMake(0.0f, 0.0f, 960.0f, 1380.0f);
#endif

    [self.view addSubview:customView];

    UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
    [customView addGestureRecognizer:singleTap];

    self.customView = customView;
}

#pragma mark - Timer Loop

- (void)handleTap:(UITapGestureRecognizer *)tapGesture
{
    self.customView.value = 0.0f;

    if (!self.animationTimer  || !self.animationTimer.isValid) {
        self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(animationLoop) userInfo:nil repeats:YES];
    }
}

#pragma mark - Timer Loop

- (void)animationLoop
{
    // Update model here. For simplicity, increment a single value.
    self.customView.value += 0.01f;

    if (self.customView.value >= 1.0f)
    {
        self.customView.value = 1.0f;
        [self.animationTimer invalidate];
    }

    [self.customView setNeedsDisplayInRect:CGRectMake(0.0f, 0.0f, 320.0f, 460.0f)];
}

@end

-

TCustomView.h(自定义视图标题)

#import <UIKit/UIKit.h>

@interface TCustomView : UIView
@property (assign) CGFloat value;
@end

-

TCustomView.m(自定义视图实现)

#import "TCustomView.h"

@implementation TCustomView

- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();

    // Draw ellipses.
    CGContextSetRGBFillColor(context, self.value, self.value, self.value, 1.0f);
    CGContextFillEllipseInRect(context, rect);

    // Draw value itself.
    [[UIColor redColor] set];
    NSString *value = [NSString stringWithFormat:@"%f", self.value];
    [value drawAtPoint:rect.origin withFont:[UIFont fontWithName:@"Arial" size:15.0f]];
}

@end
4

2 回答 2

5

由于 iPod Touch 第 4 代和 iPad 第 1 代具有相似的硬件(相同数量的内存/相同的 GPU),这表明您看到的问题是由于 iOS4 中未优化的代码路径。

如果您查看导致 iOS4 上(负)性能峰值的视图大小,它们的一侧都比 1024 长。最初 1024x1024 是 UIView 可能的最大尺寸,虽然这个限制已经取消,但它完全是可能比这更大的视图只在 iOS5 及更高版本中变得有效。

我猜想您在 iOS4 中看到的过多内存复制是由于 UIKit 为大型 UIView 使用了完整大小的内存缓冲区,但是在合成之前必须复制适当大小的图块;并且在 iOS5 和更高版本中,他们要么删除了对可以合成的图块大小的任何限制,要么改变了 UIKit 为如此大的 UIView 呈现的方式。

在解决 iOS4 上的这个瓶颈方面,您可以尝试使用较小的 UIView 平铺您想要覆盖的区域。如果您将其结构为:

  父视图 - 包含绘图和事件相关代码
    平铺视图 1 - 包含 drawRect
    ...
    平铺视图 n - 包含 drawRect

在每个平铺视图中,您可以要求父视图在适当调整图形上下文的变换后渲染其内容。这意味着您不必更改绘图代码,它只会被多次调用(这会有一点开销,但请记住每次调用将只绘制整个视图的一部分)。

请注意,父视图没有 drawRect 方法很重要 - 否则 UIKit 会认为您想直接在其中绘制,它会创建一个后备存储,从而使您回到相同的情况。

您还可以查看 CATiledLayer - 这会为您进行平铺,但是是异步的;这意味着您的绘图代码等必须处理从一个或多个后台线程执行的问题。

于 2013-03-09T10:02:08.257 回答
0

正如您所观察到的,时间主要用于传输一些数据。我认为在 iOS 4.3.5 中,CoreGraphics 不使用 GPU 和图形内存来实现 CGContextFillEllipseInRect 等原始绘图功能......

然后每次你需要绘制一些东西时,它会被 CPU 绘制在主内存中,以计算所需的所有内容,然后复制到图形内存中。这当然需要很长时间,因为公共汽车很慢。

我猜从 iOS 5. 或 5.1 开始,原始绘图函数调用一些 GPU 着色器(GPU 内的程序),然后所有繁重的工作都在那里完成。

然后只有少数数据(参数和程序代码)从主 RAM 存储器传输到图形存储器。

于 2013-03-09T18:26:48.247 回答