在Cocoa Touch中实现Debounced / Coalesced模式,如`layoutSubviews`
许多Cocoa Touch类利用了合并事件的设计模式。 例如, setNeedsLayout
有一个方法setNeedsLayout
,它可以在不久的将来调用layoutSubviews
。 这在许多属性影响布局的情况下尤其有用。 在每个属性的setter中,您可以调用[self setNeedsLayout]
,这将确保布局将更新,但如果一次更改多个属性,或者即使单个属性被修改多个,也会阻止布局的许多(可能是昂贵的)更新运行循环的一次迭代中的时间。 其他昂贵的操作,如setNeedsDisplay
和drawRect:
方法对遵循相同的模式。
实现这样的模式的最佳方法是什么? 具体来说,我想将一些依赖属性绑定到一个昂贵的方法,如果属性发生了变化,每次迭代运行循环都需要调用一次。
可能解决方案
使用CADisplayLink
或NSTimer
你可以得到这样的工作,但两者似乎都比必要的更多涉及我不确定将这个添加到许多对象(尤其是计时器)的性能影响。 毕竟,性能是做这样的事情的唯一原因。
在某些情况下我使用过这样的东西:
- (void)debounceSelector:(SEL)sel withDelay:(CGFloat)delay { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:sel object:nil]; [self performSelector:sel withObject:nil afterDelay:delay]; }
这在用户输入应该仅在连续动作或类似事件时触发某些事件的情况下工作得很好。 当我们想要确保触发事件没有延迟时,我们只想在同一个运行循环中合并调用,这似乎很笨拙。
NSNotificationQueue
就是你想要的东西。 请参阅合并通知的文档
这是UIViewController中的一个简单示例:
- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)viewDidLoad { [super viewDidLoad]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(configureView:) name:@"CoalescingNotificationName" object:self]; [self setNeedsReload:@"viewDidLoad1"]; [self setNeedsReload:@"viewDidLoad2"]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self setNeedsReload:@"viewWillAppear1"]; [self setNeedsReload:@"viewWillAppear2"]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self setNeedsReload:@"viewDidAppear1"]; [self setNeedsReload:@"viewDidAppear2"]; } - (void)setNeedsReload:(NSString *)context { NSNotification *notification = [NSNotification notificationWithName:@"CoalescingNotificationName" object:self userInfo:@{@"context":context}]; [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName|NSNotificationCoalescingOnSender forModes:nil]; } - (void)configureView:(NSNotification *)notification { NSString *text = [NSString stringWithFormat:@"configureView called: %@", notification.userInfo]; NSLog(@"%@", text); self.detailDescriptionLabel.text = text; }
您可以签出文档并使用postingStyle来获取所需的行为。 在此示例中,使用NSPostASAP
将为我们提供输出:
configureView called: { context = viewDidLoad1; } configureView called: { context = viewDidAppear1; }
这意味着已经合并了对setNeedsReload
的背靠背调用。
我使用自定义调度源实现了类似的function。 基本上,您使用DISPATCH_SOURCE_TYPE_DATA_OR设置调度源,如下所示:
dispatch_source_t source = dispatch_source_create( DISPATCH_SOURCE_TYPE_DATA_OR, 0, 0, dispatch_get_main_queue() ); dispatch_source_set_event_handler( source, ^{ // UI update logic goes here }); dispatch_resume( source );
之后,每当您想要通知更新时,您都会致电:
dispatch_source_merge_data( __source, 1 );
事件处理程序块是不可重入的,因此在事件处理程序运行时发生的更新将合并。
这是我在我的框架中使用的模式,Conche( https://github.com/djs-code/Conche )。 如果您正在寻找其他示例,请查看CNCHStateMachine.m和CNCHObjectFeed.m。
这接近“主要基于意见”,但我会抛弃我通常的处理方法:
设置一个标志,然后使用performSelector进行队列处理。
在你的@interface中:
@property (nonatomic, readonly) BOOL needsUpdate;
然后在你的@implementation中:
-(void)setNeedsUpdate { if(!_needsUpdate) { _needsUpdate = true; [self performSelector:@selector(_performUpdate) withObject:nil afterDelay:0.0]; } } -(void)_performUpdate { if(_needsUpdate) { _needsUpdate = false; [self performUpdate]; } } -(void)performUpdate { }
_needsUpdate
的双重检查有点多余,但便宜。 真正的偏执狂会将所有相关的部分包装在@synchronized中,但是只有在可以从主线程以外的线程调用setNeedsUpdate
时才需setNeedsUpdate
。 如果你要这样做,你还需要在调用performSelector之前对setNeedsUpdate进行更改以获取主线程。
我的理解是调用performSelector:withObject:afterDelay:
使用延迟值0会导致在下一次通过事件循环时调用该方法。
如果您希望您的操作排队,直到下一次通过事件循环,那应该可以正常工作。
如果你想要合并多个不同的动作并且只想要一个“完成自上一次通过事件循环以来累积的所有事情”调用,你可以在你的app委托(或其他一些单个实例)中为performSelector:withObject:afterDelay:
添加单个调用performSelector:withObject:afterDelay:
object)在启动时,并在每次调用结束时再次调用您的方法。 然后,您可以添加要执行的操作的NSMutableSet,并在每次触发要合并的操作时向该集添加条目。 如果您创建了一个自定义操作对象并覆盖了操作对象上的isEqual(和哈希)方法,则可以对其进行设置,以便在您的操作集中只有一个操作对象。 在通过事件循环的过程中多次添加相同的操作类型将添加该类型中的一个且仅一个操作。
您的方法可能如下所示:
- (void) doCoalescedActions; { for (CustomActionObject *aCustomAction in setOfActions) { //Do whatever it takes to handle coalesced actions } [setOfActions removeAllObjects]; [self performSelector: @selector(doCoalescedActions) withObject: nil afterDelay: 0]; }
如果没有您想要做的具体细节,很难详细了解如何做到这一点。