From 979ea2094e74ae8da0288fd5038f1f2838bd7763 Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Fri, 22 Jun 2018 07:28:38 -0700 Subject: [PATCH] Fabric: Introducing ImageManager Summary: @public ImageManager coordinates all work related to loading image bitmaps for component. The particular iOS implementation uses RCTImageLoader from RCTImage module under the hood. Reviewed By: fkgozali Differential Revision: D8526571 fbshipit-source-id: a0d927972d30113eed6e0cd169fceee17610181d --- React/Fabric/RCTScheduler.mm | 7 + ReactCommon/fabric/imagemanager/BUCK | 121 ++++++++++++++++++ .../fabric/imagemanager/ImageManager.h | 38 ++++++ .../fabric/imagemanager/ImageRequest.cpp | 49 +++++++ .../fabric/imagemanager/ImageRequest.h | 87 +++++++++++++ .../fabric/imagemanager/ImageResponse.cpp | 21 +++ .../fabric/imagemanager/ImageResponse.h | 28 ++++ .../platform/android/ImageManager.cpp | 26 ++++ .../imagemanager/platform/ios/ImageManager.mm | 32 +++++ .../platform/ios/RCTImageManager.h | 28 ++++ .../platform/ios/RCTImageManager.mm | 68 ++++++++++ .../ios/RCTImagePrimitivesConversions.h | 59 +++++++++ ReactCommon/fabric/imagemanager/primitives.h | 55 ++++++++ .../imagemanager/tests/ImageManagerTest.cpp | 18 +++ 14 files changed, 637 insertions(+) create mode 100644 ReactCommon/fabric/imagemanager/BUCK create mode 100644 ReactCommon/fabric/imagemanager/ImageManager.h create mode 100644 ReactCommon/fabric/imagemanager/ImageRequest.cpp create mode 100644 ReactCommon/fabric/imagemanager/ImageRequest.h create mode 100644 ReactCommon/fabric/imagemanager/ImageResponse.cpp create mode 100644 ReactCommon/fabric/imagemanager/ImageResponse.h create mode 100644 ReactCommon/fabric/imagemanager/platform/android/ImageManager.cpp create mode 100644 ReactCommon/fabric/imagemanager/platform/ios/ImageManager.mm create mode 100644 ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.h create mode 100644 ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.mm create mode 100644 ReactCommon/fabric/imagemanager/platform/ios/RCTImagePrimitivesConversions.h create mode 100644 ReactCommon/fabric/imagemanager/primitives.h create mode 100644 ReactCommon/fabric/imagemanager/tests/ImageManagerTest.cpp diff --git a/React/Fabric/RCTScheduler.mm b/React/Fabric/RCTScheduler.mm index 79cae1c13..0e1d5313b 100644 --- a/React/Fabric/RCTScheduler.mm +++ b/React/Fabric/RCTScheduler.mm @@ -7,9 +7,12 @@ #import "RCTScheduler.h" +#import #import #import #import +#import +#import #import "RCTConversions.h" @@ -44,6 +47,10 @@ private: _delegateProxy = std::make_shared((__bridge void *)self); SharedContextContainer contextContainer = std::make_shared(); + + void *imageLoader = (__bridge void *)[[RCTBridge currentBridge] imageLoader]; + contextContainer->registerInstance(typeid(ImageManager), std::make_shared(imageLoader)); + _scheduler = std::make_shared(contextContainer); _scheduler->setDelegate(_delegateProxy.get()); } diff --git a/ReactCommon/fabric/imagemanager/BUCK b/ReactCommon/fabric/imagemanager/BUCK new file mode 100644 index 000000000..0b83274c9 --- /dev/null +++ b/ReactCommon/fabric/imagemanager/BUCK @@ -0,0 +1,121 @@ +load("//configurations/buck/apple:flag_defs.bzl", "OBJC_ARC_PREPROCESSOR_FLAGS", "get_application_ios_flags", "get_debug_preprocessor_flags") +load("//ReactNative:DEFS.bzl", "ANDROID", "APPLE", "IS_OSS_BUILD", "get_apple_inspector_flags", "react_native_xplat_target", "rn_xplat_cxx_library") + +APPLE_COMPILER_FLAGS = [] + +if not IS_OSS_BUILD: + load("@xplat//configurations/buck/apple:flag_defs.bzl", "flags", "get_static_library_ios_flags") + + APPLE_COMPILER_FLAGS = flags.get_flag_value(get_static_library_ios_flags(), "compiler_flags") + +rn_xplat_cxx_library( + name = "imagemanager", + srcs = glob( + [ + "*.cpp", + ], + exclude = glob(["tests/**/*.cpp"]), + ), + header_namespace = "", + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++14", + "-Wall", + ], + fbandroid_exported_headers = subdir_glob( + [ + ("", "*.h"), + ], + prefix = "fabric/imagemanager", + ), + fbandroid_headers = subdir_glob( + [ + ("", "*.h"), + ("platform/android", "**/*.h"), + ], + prefix = "", + ), + fbandroid_srcs = glob( + [ + "platform/android/**/*.cpp", + ], + ), + fbobjc_compiler_flags = APPLE_COMPILER_FLAGS, + fbobjc_preprocessor_flags = get_debug_preprocessor_flags() + get_apple_inspector_flags(), + fbobjc_tests = [ + ":tests", + ], + force_static = True, + frameworks = [ + "$SDKROOT/System/Library/Frameworks/CoreGraphics.framework", + "$SDKROOT/System/Library/Frameworks/Foundation.framework", + "$SDKROOT/System/Library/Frameworks/UIKit.framework", + ], + ios_deps = [ + "xplat//js:RCTImage", + "xplat//js/react-native-github:RCTCxxBridge", + ], + ios_exported_headers = subdir_glob( + [ + ("", "*.h"), + ("platform/ios", "RCTImagePrimitivesConversions.h"), + ], + prefix = "fabric/imagemanager", + ), + ios_headers = subdir_glob( + [ + ("", "*.h"), + ("platform/ios", "**/*.h"), + ], + prefix = "", + ), + ios_srcs = glob( + [ + "platform/ios/**/*.cpp", + "platform/ios/**/*.mm", + ], + ), + macosx_tests_override = [], + platforms = (ANDROID, APPLE), + preprocessor_flags = [ + "-DLOG_TAG=\"ReactNative\"", + "-DWITH_FBSYSTRACE=1", + ], + tests = [], + visibility = ["PUBLIC"], + deps = [ + "xplat//fbsystrace:fbsystrace", + "xplat//folly:futures", + "xplat//folly:headers_only", + "xplat//folly:memory", + "xplat//folly:molly", + "xplat//third-party/glog:glog", + "xplat//yoga:yoga", + react_native_xplat_target("fabric/core:core"), + react_native_xplat_target("fabric/debug:debug"), + react_native_xplat_target("fabric/graphics:graphics"), + ], +) + +if not IS_OSS_BUILD: + load("@xplat//build_defs:fb_xplat_cxx_test.bzl", "fb_xplat_cxx_test") + + fb_xplat_cxx_test( + name = "tests", + srcs = glob(["tests/**/*.cpp"]), + headers = glob(["tests/**/*.h"]), + contacts = ["oncall+react_native@xmail.facebook.com"], + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++14", + "-Wall", + ], + platforms = APPLE, + deps = [ + "xplat//folly:molly", + "xplat//third-party/gmock:gtest", + ":imagemanager", + ], + ) diff --git a/ReactCommon/fabric/imagemanager/ImageManager.h b/ReactCommon/fabric/imagemanager/ImageManager.h new file mode 100644 index 000000000..d3f37d0ff --- /dev/null +++ b/ReactCommon/fabric/imagemanager/ImageManager.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include + +namespace facebook { +namespace react { + +class ImageManager; + +using SharedImageManager = std::shared_ptr; + +/* + * Cross platform facade for iOS-specific RCTImageManager. + */ +class ImageManager { +public: + + ImageManager(void *platformSpecificCounterpart); + ~ImageManager(); + + ImageRequest requestImage(const ImageSource &imageSource) const; + +private: + void *self_; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/imagemanager/ImageRequest.cpp b/ReactCommon/fabric/imagemanager/ImageRequest.cpp new file mode 100644 index 000000000..e023f444d --- /dev/null +++ b/ReactCommon/fabric/imagemanager/ImageRequest.cpp @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ImageRequest.h" + +namespace facebook { +namespace react { + +class ImageRequest::ImageNoLongerNeededException: + public std::logic_error { +public: + ImageNoLongerNeededException(): + std::logic_error("Image no longer needed.") {} +}; + +ImageRequest::ImageRequest(const ImageSource &imageSource, folly::Future &&responseFuture): + imageSource_(imageSource), + responseFutureSplitter_(folly::splitFuture(std::move(responseFuture))) {} + +ImageRequest::ImageRequest(ImageRequest &&other) noexcept: + imageSource_(std::move(other.imageSource_)), + responseFutureSplitter_(std::move(other.responseFutureSplitter_)) { + other.moved_ = true; + }; + +ImageRequest::~ImageRequest() { + if (!moved_) { + auto future = responseFutureSplitter_.getFuture(); + if (!future.isReady()) { + future.raise(ImageNoLongerNeededException()); + } + } +} + +folly::Future ImageRequest::getResponseFuture() const { + if (moved_) { + abort(); + } + + std::lock_guard lock(mutex_); + return responseFutureSplitter_.getFuture(); +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/imagemanager/ImageRequest.h b/ReactCommon/fabric/imagemanager/ImageRequest.h new file mode 100644 index 000000000..7d593f7be --- /dev/null +++ b/ReactCommon/fabric/imagemanager/ImageRequest.h @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +namespace facebook { +namespace react { + +/* + * Represents ongoing request for an image resource. + * The separate object must be constructed for every single separate + * image request. The object cannot be copied because it would make managing of + * event listeners hard and inefficient; the object can be moved though. + * To subscribe for notifications use `getResponseFuture()` method. + * Destroy to cancel the underlying request. + */ +class ImageRequest final { + +public: + + /* + * The exception which is thrown when `ImageRequest` is being deallocated + * if the future is not ready yet. + */ + class ImageNoLongerNeededException; + + /* + * `ImageRequest` is constructed with `ImageSource` and + * `ImageResponse` future which must be moved in inside the object. + */ + ImageRequest(const ImageSource &imageSource, folly::Future &&responseFuture); + + /* + * The move constructor. + */ + ImageRequest(ImageRequest &&other) noexcept; + + /* + * `ImageRequest` does not support copying by design. + */ + ImageRequest(const ImageRequest &) = delete; + + ~ImageRequest(); + + /* + * Creates and returns a *new* future object with promised `ImageResponse` + * result. Multiple consumers can call this method many times to create + * their own subscriptions to promised value. + */ + folly::Future getResponseFuture() const; + +private: + + /* + * Mutext to protect an access to the future. + */ + mutable std::mutex mutex_; + + /* + * Image source assosiated with the request. + */ + ImageSource imageSource_; + + /* + * Future splitter powers factory-like `getResponseFuture()` method. + */ + mutable folly::FutureSplitter responseFutureSplitter_; + + /* + * Indicates that the object was moved and hence cannot be used anymore. + */ + bool moved_ {false}; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/imagemanager/ImageResponse.cpp b/ReactCommon/fabric/imagemanager/ImageResponse.cpp new file mode 100644 index 000000000..39cea6218 --- /dev/null +++ b/ReactCommon/fabric/imagemanager/ImageResponse.cpp @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ImageResponse.h" + +namespace facebook { +namespace react { + +ImageResponse::ImageResponse(const std::shared_ptr &image): + image_(image) {} + +std::shared_ptr ImageResponse::getImage() const { + return image_; +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/imagemanager/ImageResponse.h b/ReactCommon/fabric/imagemanager/ImageResponse.h new file mode 100644 index 000000000..1ce66af6c --- /dev/null +++ b/ReactCommon/fabric/imagemanager/ImageResponse.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +namespace facebook { +namespace react { + +/* + * Represents retrieved image bitmap and any assotiated platform-specific info. + */ +class ImageResponse final { + +public: + ImageResponse(const std::shared_ptr &image); + + std::shared_ptr getImage() const; + +private: + std::shared_ptr image_; +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/imagemanager/platform/android/ImageManager.cpp b/ReactCommon/fabric/imagemanager/platform/android/ImageManager.cpp new file mode 100644 index 000000000..b1381eb0c --- /dev/null +++ b/ReactCommon/fabric/imagemanager/platform/android/ImageManager.cpp @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ImageManager.h" + +namespace facebook { +namespace react { + +ImageManager::ImageManager(void *platformSpecificCounterpart) { + // Not implemented. +} + +ImageManager::~ImageManager() { + // Not implemented. +} + +ImageRequest ImageManager::requestImage(const ImageSource &imageSource) const { + // Not implemented. +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/imagemanager/platform/ios/ImageManager.mm b/ReactCommon/fabric/imagemanager/platform/ios/ImageManager.mm new file mode 100644 index 000000000..55281935c --- /dev/null +++ b/ReactCommon/fabric/imagemanager/platform/ios/ImageManager.mm @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ImageManager.h" + +#import + +#import "RCTImageManager.h" + +namespace facebook { +namespace react { + +ImageManager::ImageManager(void *platformSpecificCounterpart) { + self_ = (__bridge_retained void *)[[RCTImageManager alloc] initWithImageLoader:(__bridge_transfer RCTImageLoader *)platformSpecificCounterpart]; +} + +ImageManager::~ImageManager() { + CFRelease(self_); + self_ = nullptr; +} + +ImageRequest ImageManager::requestImage(const ImageSource &imageSource) const { + RCTImageManager *imageManager = (__bridge RCTImageManager *)self_; + return [imageManager requestImage:imageSource]; +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.h b/ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.h new file mode 100644 index 000000000..45d6d0a1a --- /dev/null +++ b/ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class RCTImageLoader; + +/** + * iOS-specific ImageManager. + */ +@interface RCTImageManager : NSObject + +- (instancetype)initWithImageLoader:(RCTImageLoader *)imageLoader; + +- (facebook::react::ImageRequest)requestImage:(const facebook::react::ImageSource &)imageSource; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.mm b/ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.mm new file mode 100644 index 000000000..4adc90b72 --- /dev/null +++ b/ReactCommon/fabric/imagemanager/platform/ios/RCTImageManager.mm @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "RCTImageManager.h" + +#import +#import + +#import + +#import "RCTImagePrimitivesConversions.h" + +using namespace facebook::react; + +@implementation RCTImageManager +{ + RCTImageLoader *_imageLoader; +} + +- (instancetype)initWithImageLoader:(RCTImageLoader *)imageLoader +{ + if (self = [super init]) { + _imageLoader = imageLoader; + } + + return self; +} + +- (ImageRequest)requestImage:(const ImageSource &)imageSource +{ + __block auto promise = folly::Promise(); + + NSURLRequest *request = NSURLRequestFromImageSource(imageSource); + + auto completionBlock = ^(NSError *error, UIImage *image) { + auto imageResponse = ImageResponse(std::shared_ptr((__bridge_retained void *)image, CFRelease)); + promise.setValue(std::move(imageResponse)); + }; + + auto interruptBlock = ^(const folly::exception_wrapper &exceptionWrapper) { + if (!promise.isFulfilled()) { + promise.setException(exceptionWrapper); + } + }; + + RCTImageLoaderCancellationBlock cancellationBlock = + [_imageLoader loadImageWithURLRequest:request + size:CGSizeMake(imageSource.size.width, imageSource.size.height) + scale:imageSource.scale + clipped:YES + resizeMode:RCTResizeModeStretch + progressBlock:nil + partialLoadBlock:nil + completionBlock:completionBlock]; + + promise.setInterruptHandler([cancellationBlock, interruptBlock](const folly::exception_wrapper &exceptionWrapper) { + cancellationBlock(); + interruptBlock(exceptionWrapper); + }); + + return ImageRequest(imageSource, promise.getFuture()); +} + +@end diff --git a/ReactCommon/fabric/imagemanager/platform/ios/RCTImagePrimitivesConversions.h b/ReactCommon/fabric/imagemanager/platform/ios/RCTImagePrimitivesConversions.h new file mode 100644 index 000000000..8683a0089 --- /dev/null +++ b/ReactCommon/fabric/imagemanager/platform/ios/RCTImagePrimitivesConversions.h @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import + +using namespace facebook::react; + +inline static RCTResizeMode RCTResizeModeFromImageResizeMode(ImageResizeMode imageResizeMode) { + switch (imageResizeMode) { + case ImageResizeMode::Cover: return RCTResizeModeCover; + case ImageResizeMode::Contain: return RCTResizeModeContain; + case ImageResizeMode::Stretch: return RCTResizeModeStretch; + case ImageResizeMode::Center: return RCTResizeModeCenter; + case ImageResizeMode::Repeat: return RCTResizeModeRepeat; + } +} + +inline std::string toString(const ImageResizeMode &value) { + switch (value) { + case ImageResizeMode::Cover: return "cover"; + case ImageResizeMode::Contain: return "contain"; + case ImageResizeMode::Stretch: return "stretch"; + case ImageResizeMode::Center: return "center"; + case ImageResizeMode::Repeat: return "repeat"; + } +} + +inline static NSURLRequest *NSURLRequestFromImageSource(const ImageSource &imageSource) { + + NSString *urlString = [NSString stringWithCString:imageSource.uri.c_str() + encoding:NSASCIIStringEncoding]; + + if (!imageSource.bundle.empty()) { + NSString *bundle = [NSString stringWithCString:imageSource.bundle.c_str() + encoding:NSASCIIStringEncoding]; + urlString = [NSString stringWithFormat:@"%@.bundle/%@", bundle, urlString]; + } + + NSURL *url = [[NSURL alloc] initWithString:urlString]; + + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; + + /* + // TODO(shergin): To be implemented. + request.HTTPBody = ...; + request.HTTPMethod = ...; + request.cachePolicy = ...; + request.allHTTPHeaderFields = ...; + */ + + return [request copy]; +} diff --git a/ReactCommon/fabric/imagemanager/primitives.h b/ReactCommon/fabric/imagemanager/primitives.h new file mode 100644 index 000000000..21932693a --- /dev/null +++ b/ReactCommon/fabric/imagemanager/primitives.h @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include + +namespace facebook { +namespace react { + +class ImageSource { + +public: + enum class Type { + Invalid, + Remote, + Local + }; + + Type type {}; + std::string uri {}; + std::string bundle {}; + Float scale {3}; + Size size {0}; + + bool operator==(const ImageSource &rhs) const { + return + std::tie(this->type, this->uri) == + std::tie(rhs.type, rhs.uri); + } + + bool operator!=(const ImageSource &rhs) const { + return !(*this == rhs); + } +}; + +using ImageSources = std::vector; + +enum class ImageResizeMode { + Cover, + Contain, + Stretch, + Center, + Repeat, +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/imagemanager/tests/ImageManagerTest.cpp b/ReactCommon/fabric/imagemanager/tests/ImageManagerTest.cpp new file mode 100644 index 000000000..b872b698a --- /dev/null +++ b/ReactCommon/fabric/imagemanager/tests/ImageManagerTest.cpp @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#include + +#include + +using namespace facebook::react; + +TEST(ImageManagerTest, testSomething) { + // TODO: +}