在“现实”中,有很多不同的方法。没有“一种真正的方法”。什么适合您实际上取决于lot关于您在问题中没有讨论的因素,但无论如何我都会尝试。我也不确定如何CADisplayLink
就是你想要的。我通常认为这对于需要帧同步(即音频和视频口型同步)的事情很有用,这听起来不像您所需要的,但让我们看看几种不同的方法可以做到这一点。我认为你问题的关键在于模型和视图之间是否需要第二个“层”。
背景:单线程(即仅主线程)示例
让我们首先考虑一个普通的单线程应用程序如何工作:
- 用户事件进入主线程
- 事件处理程序触发对控制器方法的调用。
- 控制器方法更新模型状态。
- 模型状态的更改会使视图状态无效。 (IE。
-setNeedsDisplay
)
- 当下一帧到来时,窗口服务器将触发从当前模型状态重新渲染视图状态并显示结果
请注意,步骤 1-4 可以在步骤 5 发生之间发生多次,但是,由于这是一个单线程应用程序,当步骤 5 发生时,步骤 1-4 不会发生,并且用户事件正在排队等待步骤 5 完成。假设步骤 1-4“非常快”,这通常会以预期的方式丢帧。
将渲染与主线程解耦
现在,让我们考虑一下您想要将渲染卸载到后台线程的情况。在这种情况下,序列应如下所示:
- 用户事件进入主线程
- 事件处理程序触发对控制器方法的调用。
- 控制器方法更新模型状态。
- 模型状态的更改会将异步渲染任务排入队列以供后台执行。
- 如果异步渲染任务完成,它将生成的位图放置在视图已知的位置,并调用
-setNeedsDisplay
在视图上。
- 当下一帧到来时,窗口服务器将触发对
-drawRect
在视图上,现在的实现是从“已知共享位置”获取最近完成的位图并将其复制到视图中。
这里有一些细微差别。让我们首先考虑这样一种情况:您只是尝试将渲染与主线程分离(暂时忽略多核的利用——稍后会详细介绍):
几乎可以肯定,您永远不会希望同时运行多个渲染任务。一旦开始渲染一帧,您可能不想取消/停止渲染它。您可能希望将未来未启动的渲染操作排队到单个插槽队列中,该队列始终包含最后一个排队的未启动渲染操作。这应该为您提供合理的丢帧行为,这样您就不会出现“落后”的渲染帧,您应该直接丢掉帧。
如果存在一个完全渲染但尚未显示的框架,我想你always想要显示该框架。考虑到这一点,您不想打电话-setNeedsDisplay
直到位图完成并位于已知位置。
您将需要跨线程同步访问。例如,当您将渲染操作排入队列时,最简单的方法是获取模型状态的只读快照,并将其传递给渲染操作,渲染操作只会从快照中读取。这使您不必与“实时”游戏模型同步(该模型可能会通过控制器方法在主线程上发生变化,以响应未来的用户事件。)另一个同步挑战是将已完成的位图传递到视图和召唤-setNeedsDisplay
。最简单的方法可能是让图像成为视图上的一个属性,并分派该属性的设置(带有完整的图像)并调用-setNeedsDisplay
转到主线程。
这里有一个小问题:如果用户事件以很高的速率传入,并且您能够在单个显示帧的持续时间内(1/60 秒)渲染多个帧,那么您could最终渲染的位图掉落在地板上。这种方法的优点是始终在显示时向视图提供最新的帧(减少感知延迟),但它的缺点是它会产生渲染从未获得的帧的所有计算成本。显示(即功率)。这里的正确权衡因每种情况而异,并且可能包括更细粒度的调整。
利用多核——本质上的并行渲染
假设您已经按照上面的讨论将渲染与主线程分离,并且您的渲染操作本身本质上是可并行的,那么只需并行化您的一个渲染操作,同时继续以相同的方式与视图交互,您就应该获得多核并行性免费。也许您可以将每个帧划分为 N 个图块,其中 N 是核心数量,然后一旦所有 N 个图块完成渲染,您就可以将它们拼凑在一起并将它们传递到视图,就好像渲染操作是整体的一样。如果您正在使用模型的只读快照,则 N 个切片任务的设置成本应该是最小的(因为它们都可以使用相同的源模型。)
利用多核——固有的串行渲染
如果您的渲染操作本质上是串行的(根据我的经验,大多数情况下),您使用多个核心的选择是在飞行中进行与核心一样多的渲染操作。当一帧完成时,它会向任何已排队或仍在飞行中但在之前的渲染操作发出信号,表明它们可能放弃并取消,然后它将自身设置为由视图显示,就像仅解耦示例中一样。
正如仅解耦情况中提到的,这总是在显示时向视图提供最新的帧,但它会产生渲染从未显示的帧的所有计算(即功耗)成本。
当模型速度缓慢时...
我还没有解决实际上是基于用户事件的模型更新太慢的情况,因为从某种意义上来说,如果是这样的话,在很多方面,你就不再关心渲染了。如果渲染如何跟上model甚至跟不上?此外,假设您找到了一种将渲染和模型计算互锁的方法,渲染总是会抢夺模型计算的周期,根据定义,模型计算总是落后的。换句话说,当某个东西本身无法每秒更新 N 次时,你就不能希望每秒渲染该东西 N 次。
我可以设想一些情况,您可以将连续运行的物理模拟之类的东西卸载到后台线程。这样的系统必须自行管理其实时性能,并且假设它这样做,那么您将面临将该系统的结果与传入的用户事件流同步的挑战。一团糟。
在常见情况下,您really希望事件处理和模型突变是way比实时更快,并且渲染是“困难的部分”。我很难想象一个有意义的情况,其中模型更新是限制因素,但您仍然关心解耦渲染以提高性能。
换句话说:如果您的模型只能以 10Hz 更新,那么以超过 10Hz 的速度更新视图就没有意义。当用户事件的发生速度远快于 10Hz 时,这种情况的主要挑战就出现了。该挑战将是有意义地丢弃、采样或合并传入事件,以保持有意义并提供良好的用户体验。
一些代码
下面是一个基于 Xcode 中的 Cocoa 应用程序模板的简单示例,展示了解耦背景渲染的外观。 (我意识到after编写这个基于 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 这样的电池供电环境中,它可能会将您的应用程序/游戏变成耗电大户。
对于模型更新(而不是渲染)成为限制因素的任何情况,正确的方法将在很大程度上取决于情况的具体细节,并且与渲染相比,更难概括。