ComponentKit/StatefulViews/CKStatefulViewReusePool.mm (164 lines of code) (raw):
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "CKStatefulViewComponentController.h"
#import <RenderCore/RCAssert.h>
#import <ComponentKit/CKCollection.h>
#import <ComponentKit/RCDispatch.h>
#import "CKStatefulViewReusePool.h"
#import <unordered_map>
struct FBStatefulReusePoolItemEntry {
UIView *view;
CKStatefulViewReusePoolPendingMayRelinquishBlock block;
};
class FBStatefulReusePoolItem {
public:
UIView *viewWithPreferredSuperview(UIView *preferredSuperview) noexcept
{
if (_entries.empty()) {
return nil;
}
// Preferentially return the parent view.
auto preferIt = CK::find_if(_entries,
[preferredSuperview](const FBStatefulReusePoolItemEntry &entry)->bool {
return entry.view == preferredSuperview;
});
if (preferIt != _entries.end()) {
FBStatefulReusePoolItemEntry entry = *preferIt;
_entries.erase(preferIt);
if (entry.block == NULL || entry.block()) {
return entry.view;
}
}
// We didn't find the item preferentially. Time to fall back to going from start to finish.
auto it = _entries.begin();
while (it != _entries.end()) {
FBStatefulReusePoolItemEntry entry = *it;
// erase returns the next iterator
it = _entries.erase(it);
if (entry.block == NULL || entry.block()) {
// The block tells us it's OK to reuse this view
return entry.view;
}
}
return nil;
};
NSUInteger viewCount() noexcept
{
return _entries.size();
};
void addEntry(const FBStatefulReusePoolItemEntry &entry) noexcept
{
_entries.push_back(entry);
};
void absorbPendingPool(const FBStatefulReusePoolItem &otherPool, NSInteger maxEntries) noexcept
{
for (const FBStatefulReusePoolItemEntry &entry : otherPool._entries) {
// In the future, we should consider not evaluating the block here immediately, and letting it move into the
// normal reuse pool. That way we can let stateful components hold onto their own views without any
// reconfiguration for a longer period of time.
if (entry.block == NULL || entry.block()) {
// The stateful view component can decide not to allow reuse of its view if the component has re-mounted before
// the block is evaluated.
if (maxEntries < 0 || viewCount() < maxEntries) {
_entries.push_back({entry.view});
}
}
}
}
private:
std::vector<FBStatefulReusePoolItemEntry> _entries;
};
struct PoolKeyHasher {
std::size_t operator()(const std::pair<__unsafe_unretained Class, id> &pair) const
{
return [pair.first hash] ^ [pair.second hash];
}
};
@implementation CKStatefulViewReusePool
{
std::unordered_map<std::pair<__unsafe_unretained Class, id>, FBStatefulReusePoolItem, PoolKeyHasher> _pool;
std::unordered_map<std::pair<__unsafe_unretained Class, id>, FBStatefulReusePoolItem, PoolKeyHasher> _pendingPool;
BOOL _enqueuedPendingPurge;
BOOL _clearingPendingPool;
}
+ (instancetype)sharedPool
{
static CKStatefulViewReusePool *pool;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
pool = [[CKStatefulViewReusePool alloc] init];
});
return pool;
}
- (UIView *)dequeueStatefulViewForControllerClass:(Class)controllerClass
preferredSuperview:(UIView *)preferredSuperview
context:(id)context
{
RCAssertMainThread();
RCAssertNotNil(controllerClass, @"Must provide a controller class");
const std::pair<__unsafe_unretained Class, id> key = std::make_pair(controllerClass, context);
const auto it = _pool.find(key);
if (it == _pool.end()) { // Avoid overhead of creating the item unless it already exists
return nil;
}
if (![it->first.first isContextValid:it->first.second]) {
// Context is invalid. This generally should not happen
// since invalid context == non reacreatable context
// But we still handle it to make sure that behaviour is
// well defined
// Remove views with invalid context from reuse pool
_pool.erase(it);
return nil;
}
UIView *candidate = it->second.viewWithPreferredSuperview(preferredSuperview);
if (candidate) {
return candidate;
}
const auto pendingIt = _pendingPool.find(key);
if (pendingIt == _pendingPool.end()) {
return nil;
}
return pendingIt->second.viewWithPreferredSuperview(preferredSuperview);
}
- (void)enqueueStatefulView:(UIView *)view
forControllerClass:(Class)controllerClass
context:(id)context
mayRelinquishBlock:(CKStatefulViewReusePoolPendingMayRelinquishBlock)mayRelinquishBlock
{
RCAssertMainThread();
RCAssertNotNil(view, @"Must provide a view");
RCAssertNotNil(controllerClass, @"Must provide a controller class");
RCAssertNotNil(mayRelinquishBlock, @"Must provide a relinquish block");
// Shortcut for invalid contexts. If context is invalid
// by the time when it is being added to the pool, we should
// drop it immediately
if (![controllerClass isContextValid:context]) {
return;
}
auto const addEntry = ^{
auto &poolItem = _pendingPool[std::make_pair(controllerClass, context)];
poolItem.addEntry({view, mayRelinquishBlock});
};
if (!_clearingPendingPool) {
addEntry();
} else {
// Using this function instead of dispatch_async to make sure there are no ordering issues with regard to enqueueing
// the pending purge below.
RCDispatchMainDefaultMode(addEntry);
}
if (_enqueuedPendingPurge) {
return;
}
_enqueuedPendingPurge = YES;
// Wait for the run loop to turn over before trying to relinquish the view. That ensures that if we are remounted on
// a different root view, we reuse the same view (since didMount will be called immediately after didUnmount).
RCDispatchMainDefaultMode(^{
self->_enqueuedPendingPurge = NO;
[self purgePendingPool];
[self dropViewsWithInvalidContext];
});
}
- (void)purgePendingPool
{
RCAssertMainThread();
for (const auto &it : _pendingPool) {
// Ignore items that already can't be reused to save some cycles
BOOL isContextValid = [it.first.first isContextValid:it.first.second];
if (!isContextValid) {
continue;
}
// maximumPoolSize will be -1 by default
NSInteger maximumPoolSize = [it.first.first maximumPoolSize:it.first.second];
FBStatefulReusePoolItem &poolItem = _pool[it.first];
poolItem.absorbPendingPool(it.second, maximumPoolSize);
}
_clearingPendingPool = YES;
_pendingPool.clear();
_clearingPendingPool = NO;
}
- (void)dropViewsWithInvalidContext {
RCAssertMainThread();
for (auto it = _pool.begin(); it != _pool.end();) {
if (![it->first.first isContextValid:it->first.second]) {
it = _pool.erase(it);
} else {
++it;
}
}
}
@end