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