在“现实”中有很多不同的方法。没有“一种真正的方式”。什么适合你真的取决于你在问题中没有讨论的因素很多,但无论如何我都会试一试。我也不确定CADisplayLink 是你想要的。我通常会认为这对于需要帧同步(即口型同步音频和视频)的事情很有用,这听起来不像你需要的,但让我们看看你可能会这样做的几种不同方法。我认为您问题的症结在于模型和视图之间是否需要第二个“层”。
背景:单线程(即仅主线程)示例
让我们首先考虑一个普通的单线程应用程序如何工作:
- 用户事件进入主线程
- 事件处理程序触发对控制器方法的调用。
- 控制器方法更新模型状态。
- 对模型状态的更改使视图状态无效。 (即
-setNeedsDisplay)
- 当下一帧到来时,窗口服务器将触发从当前模型状态重新渲染视图状态并显示结果
请注意,在第 5 步之间,第 1-4 步可能会发生多次,但是,由于这是一个单线程应用程序,当第 5 步正在执行时,第 1-4 步不会发生,并且用户事件正在排队up 等待第 5 步完成。这通常会以预期的方式丢帧,假设步骤 1-4“非常快”。
从主线程中解耦渲染
现在,让我们考虑一下您希望将渲染卸载到后台线程的情况。在这种情况下,序列应该如下所示:
- 用户事件进入主线程
- 事件处理程序触发对控制器方法的调用。
- 控制器方法更新模型状态。
- 对模型状态的更改将异步渲染任务排入队列以供后台执行。
- 如果异步渲染任务完成,它会将生成的位图放在视图已知的位置,并在视图上调用
-setNeedsDisplay。
- 当下一帧出现时,窗口服务器将触发对视图上的
-drawRect 的调用,现在实现为从“已知共享位置”获取最近完成的位图并将其复制到视图中。
这里有一些细微差别。让我们首先考虑您只是试图将渲染与主线程分离的情况(暂时忽略多核的利用——稍后再讨论):
几乎可以肯定,您永远不希望同时运行多个渲染任务。一旦开始渲染帧,您可能不想取消/停止渲染它。您可能希望将未来的未启动渲染操作排队到单个插槽队列中,该队列始终包含最后一个入队的未启动渲染操作。这应该为您提供合理的丢帧行为,这样您就不会“落后”渲染帧,而应该丢掉。
如果存在完全渲染但尚未显示的帧,我认为您总是想要显示该帧。考虑到这一点,在位图完成并位于已知位置之前,您不希望在视图上调用 -setNeedsDisplay。
您需要跨线程同步访问。例如,当您将渲染操作排入队列时,最简单的方法是获取模型状态的只读快照,并将其传递给渲染操作,渲染操作只会从快照中读取。这使您不必与“实时”游戏模型同步(控制器方法可能会在主线程上改变它以响应未来的用户事件。)另一个同步挑战是将完成的位图传递给视图和-setNeedsDisplay 的电话。最简单的方法可能是将图像作为视图上的一个属性,并将该属性的设置(连同完成的图像)和-setNeedsDisplay 的调用分派给主线程。
这里有一个小问题:如果用户事件以高速率进入,并且您能够在单个显示帧的持续时间内(1/60 秒)渲染多个帧,您可以 最终渲染掉在地板上的位图。这种方法的优点是始终在显示时向视图提供最新的帧(减少感知延迟),但它的*缺点*优点是它会产生渲染永远不会得到的帧的所有计算成本显示(即功率)。此处的正确权衡将因每种情况而异,并且可能包括更细粒度的调整。
利用多核——固有的并行渲染
假设您已经将渲染与上面讨论的主线程分离,并且您的渲染操作本身是可并行的,那么只需并行化您的一个渲染操作,同时继续以相同的方式与视图交互,您应该得到多个-免费的核心并行性。或许您可以将每一帧分成 N 块,其中 N 是内核数,然后一旦所有 N 块完成渲染,您就可以将它们拼凑在一起并将它们交付给视图,就好像渲染操作是单片的一样。如果您使用的是模型的只读快照,则 N 个切片任务的设置成本应该是最低的(因为它们都可以使用相同的源模型。)
利用多核——固有的串行渲染
如果您的渲染操作本质上是串行的(根据我的经验,大多数情况下),您使用多个内核的选择是让渲染操作与内核一样多。当一帧完成时,它会发出任何已入队或仍在飞行中的信号,但在此之前,渲染操作可能会放弃并取消,然后它将自己设置为由视图显示,就像在仅解耦示例中一样。
正如在仅解耦的情况中提到的,这总是在显示时为视图提供最新的帧,但它会产生渲染从未显示的帧的所有计算(即功率)成本。
当模型慢时...
我没有解决实际上是基于用户事件的模型更新太慢的情况,因为从某种意义上说,如果是这种情况,在许多方面,您不再关心渲染。如果 model 甚至跟不上,渲染怎么可能跟上?此外,假设您找到了一种将渲染和模型计算互锁的方法,那么渲染总是从模型计算中抢夺周期,根据定义,这些周期总是落后的。换句话说,当某事物本身不能每秒更新 N 次时,您就不能希望每秒渲染 N 次。
我可以设想一些情况,您可以将诸如连续运行的物理模拟之类的东西卸载到后台线程。这样的系统必须自行管理其实时性能,并且假设它这样做了,那么您将面临将来自该系统的结果与传入的用户事件流同步的挑战。真是一团糟。
在一般情况下,您真的希望事件处理和模型变化比实时快方式,并且渲染是“困难的部分”。我很难想象一个有意义的案例,其中模型更新是限制因素,但您仍然关心解耦渲染以提高性能。
换一种说法:如果您的模型只能以 10Hz 更新,那么以超过 10Hz 的速度更新您的视图将毫无意义。这种情况的主要挑战来自于用户事件的发生速度远快于 10Hz。该挑战将是有意义地丢弃、采样或合并传入的事件,以保持有意义并提供良好的用户体验。
一些代码
这是一个基于 Xcode 中的 Cocoa 应用程序模板的解耦背景渲染的简单示例。 (我意识到在对这个基于 OS X 的示例进行编码后,这个问题被标记为ios,所以我猜这是“不管它值多少钱”)
@class MyModel;
@interface NSAppDelegate : NSObject <NSApplicationDelegate>
@property (assign) IBOutlet NSWindow *window;
@property (nonatomic, readwrite, copy) MyModel* model;
@end
@interface MyModel : NSObject <NSMutableCopying>
@property (nonatomic, readonly, assign) CGPoint lastMouseLocation;
@end
@interface MyMutableModel : MyModel
@property (nonatomic, readwrite, assign) CGPoint lastMouseLocation;
@end
@interface MyBackgroundRenderingView : NSView
@property (nonatomic, readwrite, assign) CGPoint coordinates;
@end
@interface MyViewController : NSViewController
@end
@implementation NSAppDelegate
{
MyViewController* _vc;
NSTrackingArea* _trackingArea;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
self.window.acceptsMouseMovedEvents = YES;
int opts = (NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingMouseMoved);
_trackingArea = [[NSTrackingArea alloc] initWithRect: [self.window.contentView bounds]
options:opts
owner:self
userInfo:nil];
[self.window.contentView addTrackingArea: _trackingArea];
_vc = [[MyViewController alloc] initWithNibName: NSStringFromClass([MyViewController class]) bundle: [NSBundle mainBundle]];
_vc.representedObject = self;
_vc.view.frame = [self.window.contentView bounds];
[self.window.contentView addSubview: _vc.view];
}
- (void)mouseEntered:(NSEvent *)theEvent
{
}
- (void)mouseExited:(NSEvent *)theEvent
{
}
- (void)mouseMoved:(NSEvent *)theEvent
{
// Update the model for mouse movement.
MyMutableModel* mutableModel = self.model.mutableCopy ?: [[MyMutableModel alloc] init];
mutableModel.lastMouseLocation = theEvent.locationInWindow;
self.model = mutableModel;
}
@end
@interface MyModel ()
// Re-declare privately so the setter exists for the mutable subclass to use
@property (nonatomic, readwrite, assign) CGPoint lastMouseLocation;
@end
@implementation MyModel
@synthesize lastMouseLocation;
- (id)copyWithZone:(NSZone *)zone
{
if ([self isMemberOfClass: [MyModel class]])
{
return self;
}
MyModel* copy = [[MyModel alloc] init];
copy.lastMouseLocation = self.lastMouseLocation;
return copy;
}
- (id)mutableCopyWithZone:(NSZone *)zone
{
MyMutableModel* copy = [[MyMutableModel alloc] init];
copy.lastMouseLocation = self.lastMouseLocation;
return copy;
}
@end
@implementation MyMutableModel
@end
@interface MyViewController (Downcast)
- (MyBackgroundRenderingView*)view; // downcast
@end
@implementation MyViewController
static void * const MyViewControllerKVOContext = (void*)&MyViewControllerKVOContext;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])
{
[self addObserver: self forKeyPath: @"representedObject.model.lastMouseLocation" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context: MyViewControllerKVOContext];
}
return self;
}
- (void)dealloc
{
[self removeObserver: self forKeyPath: @"representedObject.model.lastMouseLocation" context: MyViewControllerKVOContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (MyViewControllerKVOContext == context)
{
// update the view...
NSValue* oldCoordinates = change[NSKeyValueChangeOldKey];
oldCoordinates = [oldCoordinates isKindOfClass: [NSValue class]] ? oldCoordinates : nil;
NSValue* newCoordinates = change[NSKeyValueChangeNewKey];
newCoordinates = [newCoordinates isKindOfClass: [NSValue class]] ? newCoordinates : nil;
CGPoint old = CGPointZero, new = CGPointZero;
[oldCoordinates getValue: &old];
[newCoordinates getValue: &new];
if (!CGPointEqualToPoint(old, new))
{
self.view.coordinates = new;
}
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
@end
@interface MyBackgroundRenderingView ()
@property (nonatomic, readwrite, retain) id toDisplay; // doesn't need to be atomic because it should only ever be used on the main thread.
@end
@implementation MyBackgroundRenderingView
{
// Pointer sized reads/
intptr_t _lastFrameStarted;
intptr_t _lastFrameDisplayed;
CGPoint _coordinates;
}
@synthesize coordinates = _coordinates;
- (void)setCoordinates:(CGPoint)coordinates
{
_coordinates = coordinates;
// instead of setNeedDisplay...
[self doBackgroundRenderingForPoint: coordinates];
}
- (void)setNeedsDisplay:(BOOL)flag
{
if (flag)
{
[self doBackgroundRenderingForPoint: self.coordinates];
}
}
- (void)doBackgroundRenderingForPoint: (CGPoint)value
{
NSAssert(NSThread.isMainThread, @"main thread only...");
const intptr_t thisFrame = _lastFrameStarted++;
const NSSize imageSize = self.bounds.size;
const NSRect imageRect = NSMakeRect(0, 0, imageSize.width, imageSize.height);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// If another frame is already queued up, don't bother starting this one
if (_lastFrameStarted - 1 > thisFrame)
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"Not rendering a frame because there's a more recent one queued up already."); });
return;
}
// introduce an arbitrary fake delay between 1ms and 1/15th of a second)
const uint32_t delays = arc4random_uniform(65);
for (NSUInteger i = 1; i < delays; i++)
{
// A later frame has been displayed. Give up on rendering this old frame.
if (_lastFrameDisplayed > thisFrame)
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"Aborting rendering a frame that wasn't ready in time"); });
return;
}
usleep(1000);
}
// render image...
NSImage* image = [[NSImage alloc] initWithSize: imageSize];
[image lockFocus];
NSString* coordsString = [NSString stringWithFormat: @"%g,%g", value.x, value.y];
[coordsString drawInRect: imageRect withAttributes: nil];
[image unlockFocus];
NSArray* toDisplay = @[ image, @(thisFrame) ];
dispatch_async(dispatch_get_main_queue(), ^{
self.toDisplay = toDisplay;
[super setNeedsDisplay: YES];
});
});
}
- (void)drawRect:(NSRect)dirtyRect
{
NSArray* toDisplay = self.toDisplay;
if (!toDisplay)
return;
NSImage* img = toDisplay[0];
const int64_t frameOrdinal = [toDisplay[1] longLongValue];
if (frameOrdinal < _lastFrameDisplayed)
return;
[img drawInRect: self.bounds];
_lastFrameDisplayed = frameOrdinal;
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"Displayed a frame"); });
}
@end
结论
抽象地说,只是将渲染与主线程解耦,但不一定并行化,(即第一种情况)可能就足够了。要从那里走得更远,您可能想要研究并行化每帧渲染操作的方法。并行绘制多帧具有一些优势,但在像 iOS 这样的电池供电环境中,它可能会将您的应用程序/游戏变成耗电大户。
对于模型更新而不是渲染是限制因素的任何情况,正确的方法将在很大程度上取决于情况的具体细节,并且与渲染相比更难概括。