RenderCore/View/ComponentViewManager.mm (325 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. * */ #include "ComponentViewManager.h" #import <objc/runtime.h> #import <unordered_map> #import <RenderCore/RCAssert.h> #import <RenderCore/RCAssociatedObject.h> #import <RenderCore/CKGlobalConfig.h> #import <RenderCore/CKMutex.h> #import <RenderCore/ComponentViewReuseUtilities.h> #import "CKMountedObjectForView.h" using namespace CK::Component; namespace CK { namespace Component { struct PersistentAttributeShapeKey { /** Identifiers in sorted order. For the small sizes we use, this is faster than set or unordered_set. */ std::vector<std::string> identifiers; /** Cumulative hash of all identifiers */ size_t hash; /** Initialize the hash field to 0; remember that C++ doesn't initialize POD fields by default. */ PersistentAttributeShapeKey() : hash(0) {}; void addString(const std::string &s) { identifiers.insert(std::lower_bound(identifiers.begin(), identifiers.end(), s), s); // XOR'ing hashes is often a bad idea. Two of the reasons why are that two identical values will cancel each // other out, and that ordering information is lost (a ^ b == b ^ a). Since we don't expect identical values and // ordering doesn't matter (indeed *must not* matter), go ahead and use XOR here. hash ^= std::hash<std::string>()(s); } bool operator==(const CK::Component::PersistentAttributeShapeKey &k) const { return identifiers == k.identifiers; } }; struct ActionDisabler { ActionDisabler() : _originalValue([CATransaction disableActions]) { [CATransaction setDisableActions:YES]; } ~ActionDisabler() { [CATransaction setDisableActions:_originalValue]; } private: BOOL _originalValue; }; } } namespace std { template <> struct hash<const CK::Component::PersistentAttributeShapeKey> { size_t operator()(const CK::Component::PersistentAttributeShapeKey &k) const { return k.hash; } }; } int32_t PersistentAttributeShape::computeIdentifier(const CKViewComponentAttributeValueMap &attributes) noexcept { CK::Component::PersistentAttributeShapeKey key; for (const auto &it : attributes) { if (it.first.unapplicator == nil) { key.addString(it.first.identifier); } } static CK::StaticMutex lock = CK_MUTEX_INITIALIZER; // protects identifierMap CK::StaticMutexLocker l(lock); static auto *identifierMap = new std::unordered_map<const CK::Component::PersistentAttributeShapeKey, const int32_t>(); static int32_t nextIdentifier = 0; const auto it = identifierMap->find(key); if (it == identifierMap->end()) { // We don't need fancy atomic here because we're already under the StaticMutex (for identifierMap). int32_t identifier = nextIdentifier++; identifierMap->emplace(std::move(key), identifier); return identifier; } else { return it->second; } } @interface CKOptimisticViewMutationTokenWrapper : NSObject { @public int _serial; __weak id _view; } @end @interface CKComponentAttributeSetWrapper : NSObject { @public int _tokenSerial; bool _suppressApply; int _tokenSerialBaseline; std::shared_ptr<const CKViewComponentAttributeValueMap> _attributes; std::vector<CK::Component::OptimisticViewMutationInfo> _optimisticViewMutations; std::vector<CKOptimisticViewMutationTeardown> _optimisticViewMutationTeardowns_Old; } @end @interface CKComponentViewReusePoolMapWrapper : NSObject { @public ViewReusePoolMap _viewReusePoolMap; } @end UIView *ViewReusePool::viewForClass(const CKComponentViewClass &viewClass, UIView *container, CK::Component::MountAnalyticsContext *mountAnalyticsContext) noexcept { if (position == pool.end()) { UIView *v = viewClass.createView(); RCCAssertNotNil(v, @"Expected non-nil view to be created for view class %s", viewClass.getIdentifier().description().c_str()); [container addSubview:v]; pool.push_back(v); position = pool.end(); ViewReuseUtilities::createdView(v, viewClass, container); if (auto mac = mountAnalyticsContext) { mac->viewAllocations++; } return v; } else { if (auto mac = mountAnalyticsContext) { mac->viewReuses++; } return *position++; } } void ViewReusePool::reset(CK::Component::MountAnalyticsContext *mountAnalyticsContext) noexcept { for (auto it = pool.begin(); it != position; ++it) { ViewReuseUtilities::willUnhide(*it, mountAnalyticsContext); [*it setHidden:NO]; } for (auto it = position; it != pool.end(); ++it) { [*it setHidden:YES]; CK::Component::AttributeApplicator::resetOptimisticViewMutations(*it); ViewReuseUtilities::didHide(*it, mountAnalyticsContext); } position = pool.begin(); } const char kComponentViewReusePoolMapAssociatedObjectKey = ' '; void ViewReusePool::hideAll(UIView *view, MountAnalyticsContext *mountAnalyticsContext) noexcept { CKComponentViewReusePoolMapWrapper *wrapper = RCGetAssociatedObject_MainThreadAffined(view, &kComponentViewReusePoolMapAssociatedObjectKey); if (!wrapper) { return; } const auto hide = [&](ViewReusePool &viewReusePool) { viewReusePool.position = viewReusePool.pool.begin(); viewReusePool.reset(mountAnalyticsContext); }; auto &viewReusePoolMap = wrapper->_viewReusePoolMap; for (auto &it : viewReusePoolMap.dictionary) { hide(it.second); } } ViewReusePoolMap::ViewReusePoolMap() {} ViewReusePoolMap &ViewReusePoolMap::viewReusePoolMapForView(UIView *v) noexcept { CKComponentViewReusePoolMapWrapper *wrapper = RCGetAssociatedObject_MainThreadAffined(v, &kComponentViewReusePoolMapAssociatedObjectKey); if (!wrapper) { wrapper = [[CKComponentViewReusePoolMapWrapper alloc] init]; RCSetAssociatedObject_MainThreadAffined(v, &kComponentViewReusePoolMapAssociatedObjectKey, wrapper); } return wrapper->_viewReusePoolMap; } void ViewReusePoolMap::reset(UIView *container, CK::Component::MountAnalyticsContext *mountAnalyticsContext) noexcept { for (auto &it : dictionary) { it.second.reset(mountAnalyticsContext); } // Now we need to ensure that the ordering of container.subviews matches vendedViews. NSMutableArray *subviews = [[container subviews] mutableCopy]; std::vector<UIView *>::const_iterator nextVendedViewIt = vendedViews.cbegin(); // Can't use NSFastEnumeration since we mutate subviews during enumeration. for (NSUInteger i = 0; i < [subviews count]; i++) { UIView *subview = subviews[i]; // We use linear search here. We could create a std::unordered_set of vended views, but given the typical size of // the list of vended views, I guessed a linear search would probably be faster considering constant factors. const auto &vendedViewIt = std::find(nextVendedViewIt, vendedViews.cend(), subview); if (vendedViewIt == vendedViews.cend()) { // Ignore subviews not created by components infra, or that were not vended during this pass (they are hidden). continue; } if (vendedViewIt != nextVendedViewIt) { NSUInteger swapIndex = [subviews indexOfObjectIdenticalTo:*nextVendedViewIt]; // This check can cause some z-ordering issue if views vended by the framework are manipulated outside of the framework, if (swapIndex != NSNotFound) { // This naive algorithm does not do the minimal number of swaps. But it's simple, and swaps should be relatively // rare in any case, so let's go with it. [subviews exchangeObjectAtIndex:i withObjectAtIndex:swapIndex]; [container exchangeSubviewAtIndex:i withSubviewAtIndex:swapIndex]; } RCCAssertWithCategory(swapIndex != NSNotFound, [CKMountedObjectForView(*nextVendedViewIt) class], @"Expected to find subview %@ (mounted object: %@) in %@ (mounted object: %@)", [*nextVendedViewIt class], [CKMountedObjectForView(*nextVendedViewIt) class], [container class], [CKMountedObjectForView(container) class]); } ++nextVendedViewIt; } vendedViews.clear(); } static char kPersistentAttributesViewKey = ' '; static CKComponentAttributeSetWrapper *attributeSetWrapperForView(UIView *view) { CKComponentAttributeSetWrapper *wrapper = RCGetAssociatedObject_MainThreadAffined(view, &kPersistentAttributesViewKey); if (wrapper == nil) { wrapper = [[CKComponentAttributeSetWrapper alloc] init]; RCSetAssociatedObject_MainThreadAffined(view, &kPersistentAttributesViewKey, wrapper); } return wrapper; } void AttributeApplicator::applyAttributes(UIView *view, std::shared_ptr<const CKViewComponentAttributeValueMap> attributes) noexcept { CK::Component::ActionDisabler actionDisabler; // We never want implicit animations when applying attributes // Avoid the static destructor fiasco, use a pointer: static const auto *empty = new CKViewComponentAttributeValueMap(); CKComponentAttributeSetWrapper *const wrapper = attributeSetWrapperForView(view); const bool useNewStyleOptimisticMutations = CKReadGlobalConfig().useNewStyleOptimisticMutations; const bool hasNewStyleOptimisticViewMutations = useNewStyleOptimisticMutations && !wrapper->_optimisticViewMutations.empty(); if (!useNewStyleOptimisticMutations) { // Reset optimistic mutations so that applicators see they see the state they expect. if (!wrapper->_optimisticViewMutationTeardowns_Old.empty()) { const auto copiedTeardowns = wrapper->_optimisticViewMutationTeardowns_Old; wrapper->_optimisticViewMutationTeardowns_Old.clear(); for (CKOptimisticViewMutationTeardown teardown : copiedTeardowns) { if (teardown) { teardown(view); } } } } else { for (auto it = wrapper->_optimisticViewMutations.rbegin(); it != wrapper->_optimisticViewMutations.rend(); ++it) { if (it->undo) { it->undo(view); } } } const CKViewComponentAttributeValueMap &oldAttributes = wrapper->_attributes ? *wrapper->_attributes : *empty; const CKViewComponentAttributeValueMap &newAttributes = *attributes; // First, tear down any attributes that appear in the *old* set but not the new set, and *do* have an unapplicator. for (const auto &oldAttr : oldAttributes) { if (oldAttr.first.unapplicator) { const auto &newAttr = newAttributes.find(oldAttr.first); if (newAttr == newAttributes.end()) { // There is no new attribute, so we always must call "unapplicator". oldAttr.first.unapplicator(view, oldAttr.second); } else if (!RCObjectIsEqual(newAttr->second, oldAttr.second)) { // If the attribute has an updater, don't call the unapplicator; instead, the updater will be called below. if (newAttr->first.updater == nil) { oldAttr.first.unapplicator(view, oldAttr.second); } } } } // Now apply the applicators for all attributes in the *new* set, except those that haven't changed in value. for (const auto &newAttr : newAttributes) { const auto &oldAttr = oldAttributes.find(newAttr.first); if (oldAttr == oldAttributes.end()) { // There is no old attribute, so we always must call "applicator". newAttr.first.applicator(view, newAttr.second); } else if (!RCObjectIsEqual(oldAttr->second, newAttr.second)) { // If the attribute has an "updater", call that. Otherwise, call the applicator. if (newAttr.first.updater) { newAttr.first.updater(view, oldAttr->second, newAttr.second); } else { newAttr.first.applicator(view, newAttr.second); } } } if (hasNewStyleOptimisticViewMutations) { auto copiedOptimisticMutations = wrapper->_optimisticViewMutations; wrapper->_suppressApply = true; for (auto optimisticMutation : copiedOptimisticMutations) { optimisticMutation.load(view); } wrapper->_suppressApply = false; if (copiedOptimisticMutations.size() != wrapper->_optimisticViewMutations.size()) { copiedOptimisticMutations = wrapper->_optimisticViewMutations; } for (auto optimisticMutation : copiedOptimisticMutations) { optimisticMutation.apply(view); } } // Update the wrapper to reference the new attributes. Don't do this before now since it changes oldAttributes. wrapper->_attributes = std::move(attributes); } void AttributeApplicator::addOptimisticViewMutationTeardown_Old(UIView *view, CKOptimisticViewMutationTeardown teardown) noexcept { // We must tear down the mutations in the *reverse* order in which they were applied, // or we could end up restoring the wrong value. CKComponentAttributeSetWrapper *const wrapper = attributeSetWrapperForView(view); wrapper->_optimisticViewMutationTeardowns_Old.insert(wrapper->_optimisticViewMutationTeardowns_Old.begin(), teardown); } CKOptimisticMutationToken AttributeApplicator::addOptimisticViewMutation(UIView *view, CKOptimisticViewMutationOperation undo, CKOptimisticViewMutationOperation apply, CKOptimisticViewMutationOperation load) noexcept { CKComponentAttributeSetWrapper *const wrapper = attributeSetWrapperForView(view); for (auto it = wrapper->_optimisticViewMutations.rbegin(); it != wrapper->_optimisticViewMutations.rend(); ++it) { if (it->undo) { it->undo(view); } } load(view); auto token = [[CKOptimisticViewMutationTokenWrapper alloc] init]; token->_serial = wrapper->_tokenSerial++; token->_view = view; wrapper->_optimisticViewMutations.push_back({ .serial = token->_serial, .undo = undo, .apply = apply, .load = load }); for (auto optimisticMutation : wrapper->_optimisticViewMutations) { optimisticMutation.apply(view); } return token; } void AttributeApplicator::removeOptimisticViewMutation(CKOptimisticMutationToken token) noexcept { if (token == CKOptimisticMutationTokenNull) { return; } UIView *view = ((CKOptimisticViewMutationTokenWrapper *)token)->_view; if (view == nullptr) { return; } CKComponentAttributeSetWrapper *const wrapper = attributeSetWrapperForView(view); if (wrapper->_tokenSerialBaseline > ((CKOptimisticViewMutationTokenWrapper *)token)->_serial) { // Token no longer valid or relevant because the associated view recycled return; } for (auto it = wrapper->_optimisticViewMutations.begin(); it != wrapper->_optimisticViewMutations.end(); ++it) { if (it->serial == ((CKOptimisticViewMutationTokenWrapper *)token)->_serial) { if (wrapper->_optimisticViewMutations.size() == 1) { if (it->undo) { it->undo(view); } } wrapper->_optimisticViewMutations.erase(it); break; } } if (!wrapper->_suppressApply) { for (auto optimisticMutation : wrapper->_optimisticViewMutations) { optimisticMutation.apply(view); } } } void AttributeApplicator::resetOptimisticViewMutations(UIView *view) noexcept { CKComponentAttributeSetWrapper *const wrapper = attributeSetWrapperForView(view); if (wrapper->_tokenSerial > 0) { for (auto it = wrapper->_optimisticViewMutations .rbegin(); it != wrapper->_optimisticViewMutations.rend(); ++it) { if (it->undo) { it->undo(view); } } wrapper->_optimisticViewMutations.clear(); wrapper->_tokenSerial++; wrapper->_tokenSerialBaseline = wrapper->_tokenSerial; } } @implementation CKComponentAttributeSetWrapper @end @implementation CKComponentViewReusePoolMapWrapper @end @implementation CKOptimisticViewMutationTokenWrapper @end