ComponentKit/Core/CKComponentGenerator.mm (384 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 "CKComponentGenerator.h" #import <mutex> #import <ComponentKit/CKAnalyticsListener.h> #import <RenderCore/RCAssert.h> #import <ComponentKit/CKBuildComponent.h> #import <ComponentKit/CKComponentController.h> #import <ComponentKit/CKComponentControllerEvents.h> #import <ComponentKit/CKComponentControllerHelper.h> #import <ComponentKit/CKComponentScopeRoot.h> #import <ComponentKit/CKComponentScopeRootFactory.h> #import <ComponentKit/CKDelayedInitialisationWrapper.h> #import <ComponentKit/CKGlobalConfig.h> #import <ComponentKit/CKSystraceScope.h> #import <ComponentKit/CKTraitCollectionHelper.h> static void *kAffinedQueueKey = &kAffinedQueueKey; #define RCAssertAffinedQueue() RCCAssert(_isRunningOnAffinedQueue(), @"This method must only be called on the affined queue") struct CKComponentGeneratorInputs { CK::NonNull<CKComponentScopeRoot *> scopeRoot; CKComponentStateUpdateMap stateUpdates; BOOL forceReloadInNextGeneration{NO}; CKComponentGeneratorInputs(CK::NonNull<CKComponentScopeRoot *> scopeRoot) : scopeRoot(std::move(scopeRoot)) { } void updateModel(id<NSObject> model) noexcept { _didUpdateModelOrContext = _didUpdateModelOrContext || (model != _model); _model = model; } void updateContext(id<NSObject> context) noexcept { _didUpdateModelOrContext = _didUpdateModelOrContext || (context != _context); _context = context; } void updateTraitCollection(UITraitCollection *traitCollection) noexcept { _didUpdateTraitCollection = _didUpdateTraitCollection || !RCObjectIsEqual(traitCollection, _traitCollection); _traitCollection = [traitCollection copy]; } void updateAccessibilityStatus(BOOL isAccessibilityEnabled) noexcept { _didAccessibilityChange = _didAccessibilityChange || (isAccessibilityEnabled != _isAccessibilityEnabled); _isAccessibilityEnabled = isAccessibilityEnabled; } id<NSObject> model() const { return _model; } id<NSObject> context() const { return _context; } UITraitCollection *traitCollection() const { return _traitCollection; } BOOL didUpdateModelOrContext() const { return _didUpdateModelOrContext; } BOOL treeNeedsReflow() const { return forceReloadInNextGeneration || _didAccessibilityChange || _didUpdateTraitCollection; } bool operator==(const CKComponentGeneratorInputs &i) const { return scopeRoot == i.scopeRoot && _model == i._model && _context == i._context && stateUpdates == i.stateUpdates && forceReloadInNextGeneration == i.forceReloadInNextGeneration && _didUpdateModelOrContext == i._didUpdateModelOrContext && RCObjectIsEqual(_traitCollection, i._traitCollection) && _isAccessibilityEnabled == i._isAccessibilityEnabled; } void reset(CK::NonNull<CKComponentScopeRoot *> newScopeRoot) noexcept { scopeRoot = newScopeRoot; stateUpdates = {}; _didUpdateModelOrContext = NO; _didAccessibilityChange = NO; _didUpdateTraitCollection = NO; } CKReflowTrigger reflowTrigger(CKBuildTrigger buildTrigger) const { if ((buildTrigger & CKBuildTriggerEnvironmentUpdate) == 0) { return CKReflowTriggerNone; } CKReflowTrigger reflowTrigger = CKReflowTriggerNone; if (_didUpdateTraitCollection) { reflowTrigger |= CKReflowTriggerUIContext; } if (_didAccessibilityChange) { reflowTrigger |= CKReflowTriggerAccessibility; } if (forceReloadInNextGeneration) { reflowTrigger |= CKReflowTriggerReload; } return reflowTrigger; } private: id<NSObject> _model; id<NSObject> _context; UITraitCollection *_traitCollection; CK::Optional<BOOL> _isAccessibilityEnabled = CK::none; BOOL _didUpdateModelOrContext{NO}; BOOL _didUpdateTraitCollection{NO}; BOOL _didAccessibilityChange{NO}; }; /** This makes sure accessing `inputs` is either thread-safe or affined to a specific queue. */ struct CKComponentGeneratorInputsStore { CKComponentGeneratorInputsStore(dispatch_queue_t affinedQueue, CKComponentGeneratorInputs inputs) : _affinedQueue(affinedQueue), _inputs(std::move(inputs)) { if (_affinedQueue != nil && _affinedQueue != dispatch_get_main_queue()) { dispatch_queue_set_specific(_affinedQueue, kAffinedQueueKey, kAffinedQueueKey, NULL); } } template <typename T> T acquireInputs(NS_NOESCAPE T(^block)(CKComponentGeneratorInputs &)) { if (_affinedQueue) { RCAssertAffinedQueue(); return block(_inputs); } else { std::lock_guard<std::mutex> lock(_inputsMutex); return block(_inputs); } } private: dispatch_queue_t _affinedQueue; std::mutex _inputsMutex; CKComponentGeneratorInputs _inputs; BOOL _isRunningOnAffinedQueue() const { if (_affinedQueue == dispatch_get_main_queue()) { return [NSThread isMainThread]; } else { return (dispatch_get_specific(kAffinedQueueKey) == kAffinedQueueKey); } } }; @interface CKComponentGenerator () <CKComponentStateListener> @end @implementation CKComponentGenerator { CKComponentProviderFunc _componentProvider; __weak id<CKComponentGeneratorDelegate> _delegate; std::unique_ptr<CKComponentGeneratorInputsStore> _inputsStore; dispatch_queue_t _affinedQueue; } - (instancetype)initWithOptions:(const CKComponentGeneratorOptions &)options { if (self = [super init]) { _delegate = options.delegate; _componentProvider = options.componentProvider; _inputsStore = std::make_unique<CKComponentGeneratorInputsStore>(options.affinedQueue, CKComponentScopeRootWithPredicates(self, options.analyticsListener ?: CKReadGlobalConfig().defaultAnalyticsListener, options.componentPredicates, options.componentControllerPredicates) ); _affinedQueue = options.affinedQueue; } return self; } - (void)dealloc { const auto scopeRoot = _inputsStore->acquireInputs(^(CKComponentGeneratorInputs &inputs){ return inputs.scopeRoot; }); const auto invalidateController = ^{ CKComponentScopeRootAnnounceControllerInvalidation(scopeRoot); }; if ([NSThread isMainThread]) { invalidateController(); } else { dispatch_async(dispatch_get_main_queue(), invalidateController); } } - (void)updateTraitCollection:(UITraitCollection *)traitCollection { _inputsStore->acquireInputs(^(CKComponentGeneratorInputs &inputs) { inputs.updateTraitCollection(traitCollection); }); } - (void)updateAccessibilityStatus:(BOOL)accessibilityEnabled { _inputsStore->acquireInputs(^(CKComponentGeneratorInputs &inputs) { inputs.updateAccessibilityStatus(accessibilityEnabled); }); } - (void)updateModel:(id<NSObject>)model { _inputsStore->acquireInputs(^(CKComponentGeneratorInputs &inputs) { inputs.updateModel(model); }); } - (void)updateContext:(id<NSObject>)context { _inputsStore->acquireInputs(^(CKComponentGeneratorInputs &inputs) { inputs.updateContext(context); }); } - (CKBuildComponentResult)generateComponentSynchronously { return _inputsStore->acquireInputs(^(CKComponentGeneratorInputs &inputs) { const auto treeNeedsReflow = inputs.treeNeedsReflow(); auto const buildTrigger = CKBuildComponentTrigger(inputs.scopeRoot, inputs.stateUpdates, treeNeedsReflow, inputs.didUpdateModelOrContext()); auto const reflowReason = inputs.reflowTrigger(buildTrigger); inputs.forceReloadInNextGeneration = NO; __block CK::DelayedInitialisationWrapper<CKBuildComponentResult> result; CKPerformWithCurrentTraitCollection(inputs.traitCollection(), ^{ result = CKBuildComponent(inputs.scopeRoot, inputs.stateUpdates, ^{ return _componentProvider(inputs.model(), inputs.context()); }, buildTrigger, reflowReason); }); _applyResult(result, inputs, _addedComponentControllersBetweenScopeRoots(result.get().scopeRoot, inputs.scopeRoot), _invalidComponentControllersBetweenScopeRoots(result.get().scopeRoot, inputs.scopeRoot)); return result; }); } - (void)generateComponentAsynchronously { const auto inputs = _inputsStore->acquireInputs(^(CKComponentGeneratorInputs &_inputs){ return std::make_shared<const CKComponentGeneratorInputs>(_inputs); }); const auto asyncGeneration = CK::Analytics::willStartAsyncBlock(CK::Analytics::BlockName::ComponentGeneratorWillGenerate); // Avoid capturing `self` in global queue so that `CKComponentGenerator` does not have a chance to be deallocated outside affined queue. const auto componentProvider = _componentProvider; const auto affinedQueue = _affinedQueue; __weak const auto weakSelf = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ CKSystraceScope generationScope(asyncGeneration); __block std::shared_ptr<const CKBuildComponentResult> result = nullptr; auto const buildTrigger = CKBuildComponentTrigger(inputs->scopeRoot, inputs->stateUpdates, inputs->treeNeedsReflow(), inputs->didUpdateModelOrContext()); auto const reflowReason = inputs->reflowTrigger(buildTrigger); CKPerformWithCurrentTraitCollection(inputs->traitCollection(), ^{ result = std::make_shared<const CKBuildComponentResult>(CKBuildComponent( inputs->scopeRoot, inputs->stateUpdates, ^{ return componentProvider(inputs->model(), inputs->context()); }, buildTrigger, reflowReason )); }); const auto addedComponentControllers = std::make_shared<const std::vector<CKComponentController *>>(_addedComponentControllersBetweenScopeRoots(result->scopeRoot, inputs->scopeRoot)); const auto invalidComponentControllers = std::make_shared<const std::vector<CKComponentController *>>(_invalidComponentControllersBetweenScopeRoots(result->scopeRoot, inputs->scopeRoot)); const auto asyncApplication = CK::Analytics::willStartAsyncBlock(CK::Analytics::BlockName::ComponentGeneratorWillApply); const auto applyResult = ^{ CKSystraceScope applicationScope(asyncApplication); const auto strongSelf = weakSelf; if (!strongSelf) { return; } if (![strongSelf->_delegate componentGeneratorShouldApplyAsynchronousGenerationResult:strongSelf]) { return; } // If the inputs haven't changed, apply the result; otherwise, retry. const auto shouldRetry = strongSelf->_inputsStore->acquireInputs(^(CKComponentGeneratorInputs &_inputs){ if (_inputs == *inputs) { _inputs.forceReloadInNextGeneration = NO; _applyResult(*result, _inputs, addedComponentControllers != nullptr ? *addedComponentControllers : std::vector<CKComponentController *>{}, invalidComponentControllers != nullptr ? *invalidComponentControllers : std::vector<CKComponentController *>{}); return NO; } else { return YES; } }); if (shouldRetry) { [strongSelf generateComponentAsynchronously]; } else { if (CKReadGlobalConfig().clangCStructLeakWorkaroundEnabled) { id<CKComponentGeneratorDelegate> delegate = strongSelf->_delegate; if (delegate) { [delegate componentGenerator:strongSelf didAsynchronouslyGenerateComponentResult:*result]; } } else { [strongSelf->_delegate componentGenerator:strongSelf didAsynchronouslyGenerateComponentResult:*result]; } } }; if (affinedQueue) { dispatch_async(affinedQueue, applyResult); } else { applyResult(); } }); } - (void)forceReloadInNextGeneration { _inputsStore->acquireInputs(^(CKComponentGeneratorInputs &inputs){ inputs.forceReloadInNextGeneration = YES; }); } - (CKComponentScopeRoot *)scopeRoot { return _inputsStore->acquireInputs(^(CKComponentGeneratorInputs &inputs){ return inputs.scopeRoot; }); } - (void)setScopeRoot:(CK::NonNull<CKComponentScopeRoot *>)scopeRoot { _inputsStore->acquireInputs(^(CKComponentGeneratorInputs &inputs){ _notifyInitializationControllerEvents(_addedComponentControllersBetweenScopeRoots(scopeRoot, inputs.scopeRoot)); _notifyInvalidateControllerEvents(_invalidComponentControllersBetweenScopeRoots(scopeRoot, inputs.scopeRoot)); inputs.scopeRoot = scopeRoot; }); } #pragma mark - Private static void _applyResult(const CKBuildComponentResult &result, CKComponentGeneratorInputs &inputs, const std::vector<CKComponentController *> &addedComponentControllers, const std::vector<CKComponentController *> &invalidComponentControllers) { _notifyInitializationControllerEvents(addedComponentControllers); _notifyInvalidateControllerEvents(invalidComponentControllers); inputs.reset(result.scopeRoot); } static void _notifyInvalidateControllerEvents(const std::vector<CKComponentController *> &invalidComponentControllers) { const auto componentControllers = std::make_shared<std::vector<CKComponentController *>>(invalidComponentControllers); const auto invalidateControllers = ^{ for (auto componentController : *componentControllers) { [componentController invalidateController]; } }; if ([NSThread isMainThread]) { invalidateControllers(); } else { dispatch_async(dispatch_get_main_queue(), invalidateControllers); } } static void _notifyInitializationControllerEvents(const std::vector<CKComponentController *> &addedComponentControllers) { const auto componentControllers = std::make_shared<std::vector<CKComponentController *>>(addedComponentControllers); const auto didInitControllers = ^{ for (auto componentController : *componentControllers) { [componentController didInit]; } }; if ([NSThread isMainThread]) { didInitControllers(); } else { dispatch_async(dispatch_get_main_queue(), didInitControllers); } } static std::vector<CKComponentController *> _invalidComponentControllersBetweenScopeRoots(CKComponentScopeRoot *newRoot, CKComponentScopeRoot *previousRoot) { if (!previousRoot) { return {}; } return CKComponentControllerHelper::removedControllersFromPreviousScopeRootMatchingPredicate(newRoot, previousRoot, &CKComponentControllerInvalidateEventPredicate); } static std::vector<CKComponentController *> _addedComponentControllersBetweenScopeRoots(CKComponentScopeRoot *newRoot, CKComponentScopeRoot *previousRoot) { return CKComponentControllerHelper::addedControllersFromPreviousScopeRootMatchingPredicate(newRoot, previousRoot, &CKComponentControllerInitializeEventPredicate); } #pragma mark - CKComponentStateListener - (void)componentScopeHandle:(CKComponentScopeHandle *)handle rootIdentifier:(CKComponentScopeRootIdentifier)rootIdentifier didReceiveStateUpdate:(id (^)(id))stateUpdate metadata:(const CKStateUpdateMetadata &)metadata mode:(CKUpdateMode)mode { RCAssertMainThread(); const auto enqueueStateUpdate = ^{ _inputsStore->acquireInputs(^(CKComponentGeneratorInputs &inputs){ inputs.stateUpdates[handle].push_back(stateUpdate); [[inputs.scopeRoot analyticsListener] didReceiveStateUpdateFromScopeHandle:handle rootIdentifier:rootIdentifier]; }); [_delegate componentGenerator:self didReceiveComponentStateUpdateWithMode:mode]; }; if (_affinedQueue == dispatch_get_main_queue()) { enqueueStateUpdate(); } else if (!_affinedQueue) { // Dispatch to avoid potential deadlock. dispatch_async(dispatch_get_main_queue(), enqueueStateUpdate); } else { dispatch_async(_affinedQueue, enqueueStateUpdate); } } + (BOOL)requiresMainThreadAffinedStateUpdates { return YES; } @end