Pet*_*ter 8 performance cgcontext ios
我正在使用CGContextStrokePath为图表绘制~768点.问题是每一秒我都会得到一个新的数据点,从而重绘图形.目前这已经是繁忙的应用程序占用了50%的CPU.


图形绘制在UIView中的drawRect中完成.该图是基于时间的,因此新数据点总是到达右侧.
我正在考虑一些替代方法:
也有可能我错过了一些明显的东西,我看到这么糟糕的表现?
CGContextBeginPath(context);
CGContextSetLineWidth(context, 2.0);
UIColor *color = [UIColor whiteColor];
CGContextSetStrokeColorWithColor(context, [color CGColor]);
…
CGContextAddLines(context, points, index);
CGContextMoveToPoint(context, startPoint.x, startPoint.y);
CGContextClosePath(context);
CGContextStrokePath(context);
Run Code Online (Sandbox Code Playgroud)
rob*_*off 15
让我们实现一个图形视图,它使用一堆高大的瘦层来减少所需的重绘量.当我们添加样本时,我们会将图层向左滑动,因此在任何时候,我们可能会在视图的左边缘悬挂一个图层,而在视图的右边缘悬挂一个图层:

您可以在我的github帐户上找到以下代码的完整工作示例.
让我们每层32点宽:
#define kLayerWidth 32
Run Code Online (Sandbox Code Playgroud)
我们假设我们要沿X轴将样本按每个点一个样本间隔:
#define kPointsPerSample 1
Run Code Online (Sandbox Code Playgroud)
因此,我们可以推断出每层的样本数量.让我们称一块瓷砖的样品价值:
#define kSamplesPerTile (kLayerWidth / kPointsPerSample)
Run Code Online (Sandbox Code Playgroud)
当我们绘制图层时,我们不能只在图层内严格绘制样本.我们必须在每个边缘之后绘制一个或两个样本,因为这些样本的线穿过图层的边缘.我们将这些称为填充样本:
#define kPaddingSamples 2
Run Code Online (Sandbox Code Playgroud)
iPhone屏幕的最大尺寸为320点,因此我们可以计算出需要保留的最大样本数:
#define kMaxVisibleSamples ((320 / kPointsPerSample) + 2 * kPaddingSamples)
Run Code Online (Sandbox Code Playgroud)
(如果要在iPad上运行,则应更改320.)
我们需要能够计算哪个图块包含给定的样本.正如您将看到的,即使样本数为负数,我们也希望这样做,因为它会使以后的计算更容易:
static inline NSInteger tileForSampleIndex(NSInteger sampleIndex) {
// I need this to round toward -? even if sampleIndex is negative.
return (NSInteger)floorf((float)sampleIndex / kSamplesPerTile);
}
Run Code Online (Sandbox Code Playgroud)
现在,要实现GraphView,我们需要一些实例变量.我们需要存储我们用于绘制图形的图层.我们希望能够根据图形的哪个图块查找每个图层:
@implementation GraphView {
// Each key in _tileLayers is an NSNumber whose value is a tile number.
// The corresponding value is the CALayer that displays the tile's samples.
// There will be tiles that don't have a corresponding layer.
NSMutableDictionary *_tileLayers;
Run Code Online (Sandbox Code Playgroud)
在实际项目中,您需要将样本存储在模型对象中,并为视图提供对模型的引用.但是对于这个例子,我们只是将样本存储在视图中:
// Samples are stored in _samples as instances of NSNumber.
NSMutableArray *_samples;
Run Code Online (Sandbox Code Playgroud)
由于我们不想存储任意大量的样本,因此我们会在_samples变大时丢弃旧样本.但如果我们大多假装我们从不丢弃样本,它将简化实施.为此,我们会跟踪收到的样本总数.
// I discard old samples from _samples when I have more than
// kMaxTiles' worth of samples. This is the total number of samples
// ever collected, including discarded samples.
NSInteger _totalSampleCount;
Run Code Online (Sandbox Code Playgroud)
我们应该避免阻塞主线程,因此我们将在单独的GCD队列上进行绘制.我们需要跟踪需要在该队列上绘制哪些区块.为避免多次绘制待处理的图块,我们使用一个集合(消除重复)而不是数组:
// Each member of _tilesToRedraw is an NSNumber whose value
// is a tile number to be redrawn.
NSMutableSet *_tilesToRedraw;
Run Code Online (Sandbox Code Playgroud)
这是我们将要绘制的GCD队列.
// Methods prefixed with rq_ run on redrawQueue.
// All other methods run on the main queue.
dispatch_queue_t _redrawQueue;
}
Run Code Online (Sandbox Code Playgroud)
无论是在代码中还是在nib中创建此视图,我们都需要两种初始化方法:
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
[self commonInit];
}
return self;
}
- (void)awakeFromNib {
[self commonInit];
}
Run Code Online (Sandbox Code Playgroud)
两种方法都调用commonInit真正的初始化:
- (void)commonInit {
_tileLayers = [[NSMutableDictionary alloc] init];
_samples = [[NSMutableArray alloc] init];
_tilesToRedraw = [[NSMutableSet alloc] init];
_redrawQueue = dispatch_queue_create("MyView tile redraw", 0);
}
Run Code Online (Sandbox Code Playgroud)
ARC不会为我们清理GCD队列:
- (void)dealloc {
if (_redrawQueue != NULL) {
dispatch_release(_redrawQueue);
}
}
Run Code Online (Sandbox Code Playgroud)
要添加新样本,我们选择一个随机数并将其追加_samples.我们也增加了_totalSampleCount.如果_samples变大,我们会丢弃最老的样品.
- (void)addRandomSample {
[_samples addObject:[NSNumber numberWithFloat:120.f * ((double)arc4random() / UINT32_MAX)]];
++_totalSampleCount;
[self discardSamplesIfNeeded];
Run Code Online (Sandbox Code Playgroud)
Then, we check if we've started a new tile. If so, we find the layer that was drawing the oldest tile, and reuse it to draw the newly-created tile.
if (_totalSampleCount % kSamplesPerTile == 1) {
[self reuseOldestTileLayerForNewestTile];
}
Run Code Online (Sandbox Code Playgroud)
Now we recompute the layout of all the layers, which will to the left a bit so the new sample will be visible in the graph.
[self layoutTileLayers];
Run Code Online (Sandbox Code Playgroud)
Finally, we add tiles to the redraw queue.
[self queueTilesForRedrawIfAffectedByLastSample];
}
Run Code Online (Sandbox Code Playgroud)
We don't want to discard samples one at a time. That would be inefficient. Instead, we let the garbage build up for a while, then throw it away all at once:
- (void)discardSamplesIfNeeded {
if (_samples.count >= 2 * kMaxVisibleSamples) {
[_samples removeObjectsInRange:NSMakeRange(0, _samples.count - kMaxVisibleSamples)];
}
}
Run Code Online (Sandbox Code Playgroud)
To reuse a layer for the new tile, we need to find the layer of the oldest tile:
- (void)reuseOldestTileLayerForNewestTile {
// The oldest tile's layer should no longer be visible, so I can reuse it as the new tile's layer.
NSInteger newestTile = tileForSampleIndex(_totalSampleCount - 1);
NSInteger reusableTile = newestTile - _tileLayers.count;
NSNumber *reusableTileObject = [NSNumber numberWithInteger:reusableTile];
CALayer *layer = [_tileLayers objectForKey:reusableTileObject];
Run Code Online (Sandbox Code Playgroud)
Now we can remove it from the _tileLayers dictionary under the old key and store it under the new key:
[_tileLayers removeObjectForKey:reusableTileObject];
[_tileLayers setObject:layer forKey:[NSNumber numberWithInteger:newestTile]];
Run Code Online (Sandbox Code Playgroud)
By default, when we move the reused layer to its new position, Core Animation will animate it sliding over. We don't want that, because it will be a big empty orange rectangle sliding across our graph. We want to move it instantly:
// The reused layer needs to move instantly to its new position,
// lest it be seen animating on top of the other layers.
[CATransaction begin]; {
[CATransaction setDisableActions:YES];
layer.frame = [self frameForTile:newestTile];
} [CATransaction commit];
}
Run Code Online (Sandbox Code Playgroud)
When we add a sample, we'll always want to redraw the tile containing the sample. We also need to redraw the prior tile, if the new sample is within the padding range of the prior tile.
- (void)queueTilesForRedrawIfAffectedByLastSample {
[self queueTileForRedraw:tileForSampleIndex(_totalSampleCount - 1)];
// This redraws the second-newest tile if the new sample is in its padding range.
[self queueTileForRedraw:tileForSampleIndex(_totalSampleCount - 1 - kPaddingSamples)];
}
Run Code Online (Sandbox Code Playgroud)
Queuing a tile for redraw is just a matter of adding it to the redraw set and dispatching a block to redraw it on _redrawQueue.
- (void)queueTileForRedraw:(NSInteger)tile {
[_tilesToRedraw addObject:[NSNumber numberWithInteger:tile]];
dispatch_async(_redrawQueue, ^{
[self rq_redrawOneTile];
});
}
Run Code Online (Sandbox Code Playgroud)
The system will send layoutSubviews to the GraphView when it first appears, and any time its size changes (such as if a device rotation resizes it). And we only get the layoutSubviews message when we're really about to appear on the screen, with our final bounds set. So layoutSubviews is a good place to set up the tile layers.
First, we need to create or remove layers as necessary so we have the right layers for our size. Then we need to lay out the layers by setting their frames appropriately. Finally, for each layer, we need to queue its tile for redraw.
- (void)layoutSubviews {
[self adjustTileDictionary];
[CATransaction begin]; {
// layoutSubviews only gets called on a resize, when I will be
// shuffling layers all over the place. I don't want to animate
// the layers to their new positions.
[CATransaction setDisableActions:YES];
[self layoutTileLayers];
} [CATransaction commit];
for (NSNumber *key in _tileLayers) {
[self queueTileForRedraw:key.integerValue];
}
}
Run Code Online (Sandbox Code Playgroud)
Adjusting the tile dictionary means setting up a layer for each visible tile and removing layers for non-visible tiles. We'll just reset the dictionary from scratch each time, but we'll try to reuse the layer's we've already created. The tiles that need layers are the newest tile, and preceding tiles so we have enough layers to cover the view.
- (void)adjustTileDictionary {
NSInteger newestTile = tileForSampleIndex(_totalSampleCount - 1);
// Add 1 to account for layers hanging off the left and right edges.
NSInteger tileLayersNeeded = 1 + ceilf(self.bounds.size.width / kLayerWidth);
NSInteger oldestTile = newestTile - tileLayersNeeded + 1;
NSMutableArray *spareLayers = [[_tileLayers allValues] mutableCopy];
[_tileLayers removeAllObjects];
for (NSInteger tile = oldestTile; tile <= newestTile; ++tile) {
CALayer *layer = [spareLayers lastObject];
if (layer) {
[spareLayers removeLastObject];
} else {
layer = [self newTileLayer];
}
[_tileLayers setObject:layer forKey:[NSNumber numberWithInteger:tile]];
}
for (CALayer *layer in spareLayers) {
[layer removeFromSuperlayer];
}
}
Run Code Online (Sandbox Code Playgroud)
The first time through, and any time the view gets sufficiently wider, we need to create new layers. While we're creating the view, we'll tell it to avoid animating its contents or position. Otherwise it will animate them by default.
- (CALayer *)newTileLayer {
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor greenColor].CGColor;
layer.actions = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNull null], @"contents",
[NSNull null], @"position",
nil];
[self.layer addSublayer:layer];
return layer;
}
Run Code Online (Sandbox Code Playgroud)
Actually laying out the tile layers is just a matter of setting each layer's frame:
- (void)layoutTileLayers {
[_tileLayers enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
CALayer *layer = obj;
layer.frame = [self frameForTile:[key integerValue]];
}];
}
Run Code Online (Sandbox Code Playgroud)
Of course the trick is computing the frame for each layer. And the y, width, and height parts are easy enough:
- (CGRect)frameForTile:(NSInteger)tile {
CGRect myBounds = self.bounds;
CGFloat x = [self xForTile:tile myBounds:myBounds];
return CGRectMake(x, myBounds.origin.y, kLayerWidth, myBounds.size.height);
}
Run Code Online (Sandbox Code Playgroud)
To compute the x coordinate of the tile's frame, we compute the x coordinate of the first sample in the tile:
- (CGFloat)xForTile:(NSInteger)tile myBounds:(CGRect)myBounds {
return [self xForSampleAtIndex:tile * kSamplesPerTile myBounds:myBounds];
}
Run Code Online (Sandbox Code Playgroud)
Computing the x coordinate for a sample requires a little thought. We want the newest sample to be at the right edge of the view, and the second-newest to be kPointsPerSample points to the left of that, and so on:
- (CGFloat)xForSampleAtIndex:(NSInteger)index myBounds:(CGRect)myBounds {
return myBounds.origin.x + myBounds.size.width - kPointsPerSample * (_totalSampleCount - index);
}
Run Code Online (Sandbox Code Playgroud)
Now we can talk about how to actually draw tiles. We're going to do the drawing on a separate GCD queue. We can't safely access most Cocoa Touch objects from two threads simultaneously, so we need to be careful here. We'll use a prefix of rq_ on all the methods that run on _redrawQueue to remind ourselves that we're not on the main thread.
To redraw one tile, we need to get the tile number, the graphical bounds of the tile, and the points to draw. All of those things come from data structures that we might be modifying on the main thread, so we need to access them only on the main thread. So we dispatch back to the main queue:
- (void)rq_redrawOneTile {
__block NSInteger tile;
__block CGRect bounds;
CGPoint pointStorage[kSamplesPerTile + kPaddingSamples * 2];
CGPoint *points = pointStorage; // A block cannot reference a local variable of array type, so I need a pointer.
__block NSUInteger pointCount;
dispatch_sync(dispatch_get_main_queue(), ^{
tile = [self dequeueTileToRedrawReturningBounds:&bounds points:points pointCount:&pointCount];
});
Run Code Online (Sandbox Code Playgroud)
It so happens that we might not have any tiles to redraw. If you look back at queueTilesForRedrawIfAffectedByLastSample, you'll see that it usually tries to queue the same tile twice. Since _tilesToRedraw is a set (not an array), the duplicate was discarded, but rq_redrawOneTile was dispatched twice anyway. So we need to check that we actually have a tile to redraw:
if (tile == NSNotFound)
return;
Run Code Online (Sandbox Code Playgroud)
Now we need to actually draw the tile's samples:
UIImage *image = [self rq_imageWithBounds:bounds points:points pointCount:pointCount];
Run Code Online (Sandbox Code Playgroud)
Finally we need to update the tile's layer to show the new image. We can only touch a layer on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[self setImage:image forTile:tile];
});
}
Run Code Online (Sandbox Code Playgroud)
Here's how we actually draw the image for the layer. I will assume you know enough Core Graphics to follow this:
- (UIImage *)rq_imageWithBounds:(CGRect)bounds points:(CGPoint *)points pointCount:(NSUInteger)pointCount {
UIGraphicsBeginImageContextWithOptions(bounds.size, YES, 0); {
CGContextRef gc = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(gc, -bounds.origin.x, -bounds.origin.y);
[[UIColor orangeColor] setFill];
CGContextFillRect(gc, bounds);
[[UIColor whiteColor] setStroke];
CGContextSetLineWidth(gc, 1.0);
CGContextSetLineJoin(gc, kCGLineCapRound);
CGContextBeginPath(gc);
CGContextAddLines(gc, points, pointCount);
CGContextStrokePath(gc);
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
Run Code Online (Sandbox Code Playgroud)
But we still have to get the tile, the graphics bounds, and the points to draw. We dispatched back to the main thread to do it:
// I return NSNotFound if I couldn't dequeue a tile.
// The `pointsOut` array must have room for at least
// kSamplesPerTile + 2*kPaddingSamples elements.
- (NSInteger)dequeueTileToRedrawReturningBounds:(CGRect *)boundsOut points:(CGPoint *)pointsOut pointCount:(NSUInteger *)pointCountOut {
NSInteger tile = [self dequeueTileToRedraw];
if (tile == NSNotFound)
return NSNotFound;
Run Code Online (Sandbox Code Playgroud)
The graphics bounds are just the bounds of the tile, just like we computed earlier to set the frame of the layer:
*boundsOut = [self frameForTile:tile];
Run Code Online (Sandbox Code Playgroud)
I need to start graphing from the padding samples before the first sample of the tile. But, prior to having enough samples to fill the view, my tile number may actually be negative! So I need to be sure not to try to access a sample at a negative index:
NSInteger sampleIndex = MAX(0, tile * kSamplesPerTile - kPaddingSamples);
Run Code Online (Sandbox Code Playgroud)
We also need to make sure we don't try to run past the end of the samples when we compute the sample at which we stop graphing:
NSInteger endSampleIndex = MIN(_totalSampleCount, tile * kSamplesPerTile + kSamplesPerTile + kPaddingSamples);
Run Code Online (Sandbox Code Playgroud)
And when I actually access the sample values, I need to account for the samples I've discarded:
NSInteger discardedSampleCount = _totalSampleCount - _samples.count;
Run Code Online (Sandbox Code Playgroud)
现在我们可以计算出图形的实际点数:
CGFloat x = [self xForSampleAtIndex:sampleIndex myBounds:self.bounds];
NSUInteger count = 0;
for ( ; sampleIndex < endSampleIndex; ++sampleIndex, ++count, x += kPointsPerSample) {
pointsOut[count] = CGPointMake(x, [[_samples objectAtIndex:sampleIndex - discardedSampleCount] floatValue]);
}
Run Code Online (Sandbox Code Playgroud)
我可以返回点数和瓦片:
*pointCountOut = count;
return tile;
}
Run Code Online (Sandbox Code Playgroud)
这是我们如何从重绘队列中拉出一个磁贴.请记住,队列可能为空:
- (NSInteger)dequeueTileToRedraw {
NSNumber *number = [_tilesToRedraw anyObject];
if (number) {
[_tilesToRedraw removeObject:number];
return number.integerValue;
} else {
return NSNotFound;
}
}
Run Code Online (Sandbox Code Playgroud)
最后,这是我们如何实际将图块层的内容设置为新图像.请记住,我们已调度回主队列执行此操作:
- (void)setImage:(UIImage *)image forTile:(NSInteger)tile {
CALayer *layer = [_tileLayers objectForKey:[NSNumber numberWithInteger:tile]];
if (layer) {
layer.contents = (__bridge id)image.CGImage;
}
}
Run Code Online (Sandbox Code Playgroud)
如果你做了所有这些,它将正常工作.但实际上,当新样本进入时,可以通过动画重新定位图层来使其看起来更漂亮.这非常容易.我们只是修改,newTileLayer以便为position属性添加动画:
- (CALayer *)newTileLayer {
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor greenColor].CGColor;
layer.actions = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNull null], @"contents",
[self newTileLayerPositionAnimation], @"position",
nil];
[self.layer addSublayer:layer];
return layer;
}
Run Code Online (Sandbox Code Playgroud)
我们创建这样的动画:
- (CAAnimation *)newTileLayerPositionAnimation {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
animation.duration = 0.1;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
return animation;
}
Run Code Online (Sandbox Code Playgroud)
您需要设置持续时间以匹配新样本到达的速度.