diff --git a/devtools/devtools-integration/native/include/devtools/devtools_utils.h b/devtools/devtools-integration/native/include/devtools/devtools_utils.h index 384a7c06e58..cce16310167 100644 --- a/devtools/devtools-integration/native/include/devtools/devtools_utils.h +++ b/devtools/devtools-integration/native/include/devtools/devtools_utils.h @@ -52,6 +52,8 @@ class DevToolsUtil { static void PostDomTask(const std::weak_ptr& weak_dom_manager, std::function func); + static bool ShouldAvoidPostDomManagerTask(const std::string& event_name); + private: static std::shared_ptr GetHitNode(const std::shared_ptr& root_node, const std::shared_ptr& node, double x, double y); static bool IsLocationHitNode(const std::shared_ptr& root_node, const std::shared_ptr& dom_node, double x, double y); diff --git a/devtools/devtools-integration/native/src/devtools_utils.cc b/devtools/devtools-integration/native/src/devtools_utils.cc index 058e118ea11..8a49d3ff605 100644 --- a/devtools/devtools-integration/native/src/devtools_utils.cc +++ b/devtools/devtools-integration/native/src/devtools_utils.cc @@ -413,4 +413,13 @@ void DevToolsUtil::PostDomTask(const std::weak_ptr& weak_dom_manager dom_manager->PostTask(hippy::dom::Scene(std::move(ops))); } } + +/** + * Specific methods like getLocationOnScreen should wait in the dom manager task runner. To avoid a deadlock, the + * callback must not be posted in the same task runner. + */ +bool DevToolsUtil::ShouldAvoidPostDomManagerTask(const std::string& event_name) { + return event_name == kGetLocationOnScreen; +} + } // namespace hippy::devtools diff --git a/docs/development/_sidebar.md b/docs/development/_sidebar.md index 84034059b38..8869e2a432c 100644 --- a/docs/development/_sidebar.md +++ b/docs/development/_sidebar.md @@ -2,6 +2,7 @@ * [Demo体验](development/demo.md) * [前端接入](development/web-integration.md) * [环境搭建](development/native-integration.md) +* [3.0升级指引](development/android-3.0-upgrade-guidelines.md) * [自定义组件](development/native-component.md) * [自定义模块](development/native-module.md) * [事件](development/native-event.md) diff --git a/docs/development/android-3.0-upgrade-guidelines.md b/docs/development/android-3.0-upgrade-guidelines.md new file mode 100644 index 00000000000..75cc494ca64 --- /dev/null +++ b/docs/development/android-3.0-upgrade-guidelines.md @@ -0,0 +1,89 @@ +# Android 3.0 SDK 升级指引 + +>这篇教程,主要介绍Hippy 2.0升级3.0版本如何进行适配以及2.0和3.0在使用上的一些差异化。 +
+ +1. 废弃HippyImageLoader相关实现 + + HippyImageLoader在2.0中作为引擎初始化必设项是不合理的,在3.0版本中由于图片数据的网络拉取和解码解耦为不同的子模块,HippyImageLoader已经被移除,图片请求会和其它所有IO相关的资源请求统一走vfs模块进行分发,网络请求vfs最终会分发到HttpAdapter完成请求的处理。 + 获取到图片数据后,解码模块新增加ImageDecoderAdapter可选项设置(引擎初始化时候新增imageDecoderAdapter参数设置),用于支持开发者有自定义格式图片的解码需求,ImageDecoderAdapter的具体接口描述如下: + + ```java + // 解码image原始数据,解码的结果可以通过 image data holder提供的setBitmap或者setDrawable接口 + // 置到holder中,如果宿主decode adapter不处理,返回false由SDK走默认解码逻辑 + boolean preDecode(@NonNull byte[] data, + @Nullable Map initProps, + @NonNull ImageDataHolder imageHolder, + @NonNull BitmapFactory.Options options); + + // 解码结束后,宿主通过该接口回调还可以获得二次处理bitmap的机会,比如要对bitmap做高斯模糊。 + void afterDecode(@Nullable Map initProps, + @NonNull ImageDataHolder imageHolder, + @NonNull BitmapFactory.Options options); + + // 引擎退出销毁时调用,释放adapter可能占用的资源 + void destroyIfNeeded(); + ``` + +2. 引擎初始化完成callback线程变更 + + 2.0中initEngine初始化结果SDK内部会切换到UI线程再callback onInitialized给宿主,但我们发现在很多APP内业务反馈的使用场景下,callback切UI线程执行具有很大的延迟,所以3.0中callback onInitialized直接在子线程回调并继续执行loadModule会有更好的效率,之前2.0在callback中对hippyRootView相关的UI操作需要开发者自己来切UI线程保证。 + +3. 引擎销毁 + + 3.0中destroyModule增加了回调接口,destroyEngine需要等destroyModule执行完成回调以后才能调用,否则可能有CRASH的风险,宿主可以参考下面代码示例进行引擎销毁: + + ```java + fun destroyEngine(hippyEngine: HippyEngine?, hippyRootView: ViewGroup?) { + hippyEngine?.destroyModule(hippyRootView, + Callback { result, e -> hippyEngine.destroyEngine() }) + } + ``` + +4. HippyEngine中的接口不再直接引用HippyRootView + + destroyModule接口参数以及loadModule接口返回值均使用系统ViewGroup类型替代,尽量减少对SDK的耦合。 + +5. loadModule接口参数ModuleListener接口有所变更 + - 我们发现之前2.0在onLoadCompleted回调接口中返回的root view参数其实在各多业务场景都不会去用到,所以在3.0中我们简化了这个接口,移除了root view参数的返回 + - 增加onFirstViewAdded接口回调,返回第一view挂载到Hippy root view的回调时机 + +6. 引擎初始化参数增加资源请求自定义processor的设置 + + ```java + public List processors; + ``` + + 关于vfs特性以及Processor接口使用的介绍可以详见 [VFS](feature/feature3.0/vfs.md)。 + +7. 关于UI Component事件发送 + Hippy终端事件的发送分为全局事件和UI Component事件2种,全局事件和2.0保持一致,使用HippyEngine中暴露的sendEvent接口发送,而UI Component事件的发送可以使用在3.0新增工具类EventUtils中封装的事件发送接口: + + ```java + @MainThread + public static void sendComponentEvent(@Nullable View view, + @NonNull String eventName, + @Nullable Object params); + ``` + +8. HippyInstanceContext已经被废弃 + 2.0中基于系统ContextWrapper封了Hippy自己的HippyInstanceContext,并将其作为所有Hippy view的初始化参数,随着3.0 framework和renderer两个子模块的解耦,我们发现HippyInstanceContext设计过于臃肿,已经不再适用于最新的3.0架构,所以我们在最新的3.0版本中废弃了HippyInstanceContext,改用更加轻量化的NativeRenderContext取代,也就是说3.0中所有Hippy相关的view中保存的context都是NativeRenderContext类型。 + +9. HippyEngine中新增render node缓存特性接口 + 2.0中我们支持了dom node缓存特性,但dom node缓存针对复杂页面场景性能还是存在一定的性能瓶颈,所有我们在3.0重新实现了性能更好的render node缓存特性,关于render node缓存特性与接口使用的介绍可以详见 [RenderNode Snapshot](feature/feature3.0/render-node-snapshot.md)。 + +10. 关于自定义UI组件的Controller中dispatchFunction参数说明 + 在2.0中dispatchFunction接收事件属性的参数类型为HippyArray类型,由于在2.0的后续版本中HippyMap和HippyArray就已经被标记为@Deprecated,所以在3.0的重构中,SDK内部也逐渐替换一些使用HippyMap或HippyArray类型参数的接口,所以针对Controller的dispatchFunction接口SDK内部默认替换成List类型参数 + + ```java + public void dispatchFunction(@NonNull T view, + @NonNull String functionName, + @NonNull List params); + + public void dispatchFunction(@NonNull T view, + @NonNull String functionName, + @NonNull List params, + @NonNull Promise promise); + ``` + + 为了减低3.0升级的成本原来使用HippyArray类型的接口还是保留,只是标记为@Deprecated,所以升级3.0对于原来定义的dispatchFunction接口不需要做任何修改,但建议后续升级到3.0版本的同学,定义新UI组件的时候,直接Override使用List参数类型的新接口。 diff --git a/docs/development/native-integration.md b/docs/development/native-integration.md index b8bda19ed27..e0ec4e4ce33 100644 --- a/docs/development/native-integration.md +++ b/docs/development/native-integration.md @@ -58,31 +58,6 @@ 4. 在宿主 APP 工程中增加引擎初始化与 `hippyRootView` 挂载逻辑,具体可以参考 [Demo](https://github.com/Tencent/Hippy/tree/v3.0-dev/framework/examples/android-demo) 工程中 `HippyEngineWrapper` 实现 -## 3.0与2.0的接入区别 - -1. 引擎初始化参数 - - HippyImageLoader在2.0中是必设项,在最新3.0版本中由于图片数据的网络拉取和图片解码解耦为不同的子模块,HippyImageLoader已经被移除,新增加ImageDecoderAdapter可选项设置,用于支持开发者有自定义格式图片的解码需求,ImageDecoderAdapter的具体接口用法可以参考native renderer文档介绍 - -2. 引擎初始化完成callback线程变更 - - 2.0中initEngine初始化结果SDK内部会切换到UI线程再callback给宿主,但我们发现在部分APP启动就使用Hippy的场景下,callback切UI线程执行具有很大的延迟,所以3.0中callback直接在子线程回调,之前2.0在callback中对hippyRootView相关的UI操作需要开发者自己来切UI线程保证 - -3. 引擎销毁 - - 3.0中destroyModule增加了回调接口,destroyEngine需要等destroyModule执行完成回调以后才能调用,否则可能有CRASH的风险 - -4. HippyEngine中不再直接引用HippyRootView - - destroyModule接口参数以及loadModule接口返回值均使用系统ViewGroup类型替代,尽量减少对SDK的耦合 - -5. loadModule接口参数ModuleListener接口有所变更 - - onLoadCompleted回调接口remove root view参数 - - 增加onFirstViewAdded接口回调 - -
-
- # iOS >注:以下文档都是假设您已经具备一定的 iOS 开发经验。 @@ -777,4 +752,3 @@ engine.start({ }, }); ``` - diff --git a/framework/android/src/main/java/com/tencent/mtt/hippy/adapter/http/DefaultHttpAdapter.java b/framework/android/src/main/java/com/tencent/mtt/hippy/adapter/http/DefaultHttpAdapter.java index 6ffb13a173a..a0403b4f569 100644 --- a/framework/android/src/main/java/com/tencent/mtt/hippy/adapter/http/DefaultHttpAdapter.java +++ b/framework/android/src/main/java/com/tencent/mtt/hippy/adapter/http/DefaultHttpAdapter.java @@ -337,6 +337,7 @@ public HttpTaskCallbackImpl(@NonNull ResourceDataHolder holder, @Override public void onTaskSuccess(HippyHttpRequest request, HippyHttpResponse response) throws Exception { + mDataHolder.resultCode = FetchResultCode.OK.ordinal(); mDataHolder.addResponseHeaderProperty(HTTP_RESPONSE_STATUS_CODE, response.getStatusCode().toString()); mDataHolder.addResponseHeaderProperty(HTTP_RESPONSE_RESPONSE_MESSAGE, @@ -355,7 +356,6 @@ public void onTaskSuccess(HippyHttpRequest request, HippyHttpResponse response) } mDataHolder.errorMessage = sb.toString(); } - mDataHolder.resultCode = FetchResultCode.ERR_REMOTE_REQUEST_FAILED.ordinal(); mCallback.onHandleCompleted(); return; } @@ -365,7 +365,6 @@ public void onTaskSuccess(HippyHttpRequest request, HippyHttpResponse response) } mDataHolder.readResourceDataFromStream(inputStream); } catch (IOException e) { - mDataHolder.resultCode = FetchResultCode.ERR_REMOTE_REQUEST_FAILED.ordinal(); mDataHolder.errorMessage = e.getMessage(); mCallback.onHandleCompleted(); return; @@ -397,7 +396,6 @@ public void onTaskSuccess(HippyHttpRequest request, HippyHttpResponse response) cookieManager.flush(); } } - mDataHolder.resultCode = FetchResultCode.OK.ordinal(); mCallback.onHandleCompleted(); } diff --git a/framework/android/src/main/java/com/tencent/mtt/hippy/modules/nativemodules/network/NetworkModule.java b/framework/android/src/main/java/com/tencent/mtt/hippy/modules/nativemodules/network/NetworkModule.java index 945e1cc31fa..80ad9057a14 100644 --- a/framework/android/src/main/java/com/tencent/mtt/hippy/modules/nativemodules/network/NetworkModule.java +++ b/framework/android/src/main/java/com/tencent/mtt/hippy/modules/nativemodules/network/NetworkModule.java @@ -102,10 +102,11 @@ protected void normalizeRequest(@NonNull HippyMap request, } } - protected void handleFetchResponse(@NonNull ResourceDataHolder dataHolder, Promise promise) + @NonNull + protected JSObject handleFetchResponse(@NonNull ResourceDataHolder dataHolder) throws IllegalStateException { JSObject responseObject = new JSObject(); - int statusCode = 0; + int statusCode = -1; String responseMessage = null; JSObject headerObject = new JSObject(); if (dataHolder.responseHeaders != null) { @@ -113,7 +114,7 @@ protected void handleFetchResponse(@NonNull ResourceDataHolder dataHolder, Promi statusCode = Integer.parseInt( dataHolder.responseHeaders.get(HTTP_RESPONSE_STATUS_CODE)); } catch (NumberFormatException e) { - throw new IllegalStateException(e.getMessage()); + throw new IllegalStateException("parse status code error!"); } responseMessage = dataHolder.responseHeaders.get(HTTP_RESPONSE_RESPONSE_MESSAGE); for (Entry entry : dataHolder.responseHeaders.entrySet()) { @@ -125,8 +126,11 @@ protected void handleFetchResponse(@NonNull ResourceDataHolder dataHolder, Promi headerObject.set(key, value); } } + if (responseMessage == null) { + responseMessage = (dataHolder.errorMessage == null) ? "" : dataHolder.errorMessage; + } responseObject.set(HTTP_RESPONSE_STATUS_CODE, statusCode); - responseObject.set("statusLine", (responseMessage == null) ? "" : responseMessage); + responseObject.set("statusLine", responseMessage); responseObject.set("respHeaders", headerObject); String body = ""; try { @@ -138,7 +142,22 @@ protected void handleFetchResponse(@NonNull ResourceDataHolder dataHolder, Promi throw new IllegalStateException(e.getMessage()); } responseObject.set("respBody", body); - promise.resolve(responseObject); + return responseObject; + } + + protected void handleFetchResult(@NonNull ResourceDataHolder dataHolder, final Promise promise) { + try { + if (dataHolder.resultCode == ResourceDataHolder.RESOURCE_LOAD_SUCCESS_CODE) { + JSObject responseObject = handleFetchResponse(dataHolder); + promise.resolve(responseObject); + } else { + String errorMessage = + (dataHolder.errorMessage == null) ? "Load remote resource failed!" : dataHolder.errorMessage; + promise.reject(errorMessage); + } + } catch (IllegalStateException e) { + promise.reject("Handle response failed: " + e.getMessage()); + } } @SuppressWarnings("deprecation") @@ -162,19 +181,7 @@ public void fetch(final HippyMap request, final Promise promise) { new FetchResourceCallback() { @Override public void onFetchCompleted(@NonNull ResourceDataHolder dataHolder) { - if (dataHolder.resultCode - == ResourceDataHolder.RESOURCE_LOAD_SUCCESS_CODE) { - try { - handleFetchResponse(dataHolder, promise); - } catch (IllegalStateException e) { - promise.reject( - "Handle response failed: " + e.getMessage()); - } - } else { - String error = TextUtils.isEmpty(dataHolder.errorMessage) - ? "Load remote resource failed!" : dataHolder.errorMessage; - promise.resolve(error); - } + handleFetchResult(dataHolder, promise); dataHolder.recycle(); } diff --git a/framework/examples/ios-demo/HippyDemo/RenderPage/HippyDemoViewController.mm b/framework/examples/ios-demo/HippyDemo/RenderPage/HippyDemoViewController.mm index 87183a5e03b..43481a05867 100644 --- a/framework/examples/ios-demo/HippyDemo/RenderPage/HippyDemoViewController.mm +++ b/framework/examples/ios-demo/HippyDemo/RenderPage/HippyDemoViewController.mm @@ -148,8 +148,6 @@ - (void)runHippyDemo { moduleProvider:nil launchOptions:launchOptions executorKey:uniqueEngineKey]; - _hippyBridge.contextName = uniqueEngineKey; - _hippyBridge.moduleName = @"Demo"; _hippyBridge.methodInterceptor = self; [_hippyBridge setInspectable:YES]; diff --git a/framework/ios/base/HippyDisplayLink.m b/framework/ios/base/HippyDisplayLink.m index c4be3f76b8e..991d18a2e69 100644 --- a/framework/ios/base/HippyDisplayLink.m +++ b/framework/ios/base/HippyDisplayLink.m @@ -23,12 +23,13 @@ #import "HippyDisplayLink.h" #import -#import +#import #import "HippyAssert.h" #import "HippyBridgeModule.h" #import "HippyFrameUpdate.h" #import "HippyModuleData.h" +#import "HippyWeakProxy.h" #define HippyAssertRunLoop() HippyAssert(_runLoop == [NSRunLoop currentRunLoop], @"This method must be called on the CADisplayLink run loop") @@ -41,7 +42,8 @@ @implementation HippyDisplayLink { - (instancetype)init { if ((self = [super init])) { _frameUpdateObservers = [NSMutableSet new]; - _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; + HippyWeakProxy *weakProxy = [HippyWeakProxy weakProxyForObject:self]; + _jsDisplayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(_jsThreadUpdate:)]; } return self; diff --git a/framework/ios/base/bridge/HippyBridge.h b/framework/ios/base/bridge/HippyBridge.h index 8e810d12bd0..5563a61ac91 100644 --- a/framework/ios/base/bridge/HippyBridge.h +++ b/framework/ios/base/bridge/HippyBridge.h @@ -139,7 +139,7 @@ HIPPY_EXTERN NSString *HippyBridgeModuleNameForClass(Class bridgeModuleClass); * @discussion Context name will be shown on safari development menu. * only for JSC engine */ -@property(nonatomic, copy)NSString *contextName; +@property (nonatomic, copy) NSString *contextName; /** * Set module name diff --git a/framework/ios/base/executors/HippyJSExecutor.mm b/framework/ios/base/executors/HippyJSExecutor.mm index d395cef9e67..cc38925acaa 100644 --- a/framework/ios/base/executors/HippyJSExecutor.mm +++ b/framework/ios/base/executors/HippyJSExecutor.mm @@ -363,10 +363,14 @@ - (void)setContextName:(NSString *)contextName { if (!contextName) { return; } - WeakCtxPtr weak_ctx = self.pScope->GetContext(); + __weak __typeof(self)weakSelf = self; [self executeBlockOnJavaScriptQueue:^{ @autoreleasepool { - SharedCtxPtr context = weak_ctx.lock(); + __strong __typeof(weakSelf)strongSelf = weakSelf; + if (!strongSelf.pScope) { + return; + } + SharedCtxPtr context = strongSelf.pScope->GetContext(); if (!context) { return; } diff --git a/framework/ios/debug/devtools/HippyDevInfo.m b/framework/ios/debug/devtools/HippyDevInfo.m index 7ce2689edb7..dc07711cb31 100644 --- a/framework/ios/debug/devtools/HippyDevInfo.m +++ b/framework/ios/debug/devtools/HippyDevInfo.m @@ -63,7 +63,7 @@ - (void)parseWsURLWithURLQuery:(NSString *)query { _wsURL = [debugWsURL substringFromIndex:range.location + range.length]; } -- (NSString *)assembleFullWSURLWithClientId:(NSString *)clientId contextName:(NSString *) contextName{ +- (NSString *)assembleFullWSURLWithClientId:(NSString *)clientId contextName:(NSString *)contextName { if (self.port.length <= 0) { self.port = [self.scheme isEqualToString:HippyDevWebSocketSchemeWs] ? @"80" : @"443"; } diff --git a/renderer/native/android/src/main/cpp/src/renderer/native_render_jni.cc b/renderer/native/android/src/main/cpp/src/renderer/native_render_jni.cc index d82e958ecac..1106f8e8855 100644 --- a/renderer/native/android/src/main/cpp/src/renderer/native_render_jni.cc +++ b/renderer/native/android/src/main/cpp/src/renderer/native_render_jni.cc @@ -33,6 +33,9 @@ #include "jni/jni_env.h" #include "renderer/native_render_manager.h" +#ifdef ENABLE_INSPECTOR +#include "devtools/devtools_utils.h" +#endif using DomArgument = hippy::dom::DomArgument; using DomEvent = hippy::dom::DomEvent; @@ -291,6 +294,12 @@ void DoCallBack(JNIEnv *j_env, jobject j_object, callback(std::make_shared(*params)); }}; +#ifdef ENABLE_INSPECTOR + if (hippy::devtools::DevToolsUtil::ShouldAvoidPostDomManagerTask(func_name)) { + ops[0](); + return; + } +#endif dom_manager->PostTask(Scene(std::move(ops))); } diff --git a/renderer/native/ios/renderer/HippyFont.m b/renderer/native/ios/renderer/HippyFont.mm similarity index 76% rename from renderer/native/ios/renderer/HippyFont.m rename to renderer/native/ios/renderer/HippyFont.mm index b327884667f..0f33cea1114 100644 --- a/renderer/native/ios/renderer/HippyFont.m +++ b/renderer/native/ios/renderer/HippyFont.mm @@ -48,75 +48,84 @@ typedef CGFloat NativeRenderFontWeight; static NativeRenderFontWeight weightOfFont(UIFont *font) { - static NSDictionary *nameToWeight; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - nameToWeight = @{ - @"normal": @(UIFontWeightRegular), - @"bold": @(UIFontWeightBold), - @"ultralight": @(UIFontWeightUltraLight), - @"thin": @(UIFontWeightThin), - @"light": @(UIFontWeightLight), - @"regular": @(UIFontWeightRegular), - @"medium": @(UIFontWeightMedium), - @"semibold": @(UIFontWeightSemibold), - @"heavy": @(UIFontWeightHeavy), - @"black": @(UIFontWeightBlack), - }; - }); - - NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; - NativeRenderFontWeight weight = [traits[UIFontWeightTrait] doubleValue]; - if (weight == 0.0) { - @autoreleasepool { - for (NSString *name in nameToWeight) { - if ([font.fontName.lowercaseString hasSuffix:name]) { - return [nameToWeight[name] doubleValue]; - } - } + static const struct SuffixWeight{ + NSString *suffix; + UIFontWeight weight; + } suffixToWeight[] = { + {@"normal", UIFontWeightRegular}, + {@"bold", UIFontWeightBold}, + {@"ultralight", UIFontWeightUltraLight}, + {@"thin", UIFontWeightThin}, + {@"light", UIFontWeightLight}, + {@"regular", UIFontWeightRegular}, + {@"medium", UIFontWeightMedium}, + {@"semibold", UIFontWeightSemibold}, + {@"heavy", UIFontWeightHeavy}, + {@"black", UIFontWeightBlack}, + }; + + NSString *fontName = font.fontName; + CFStringCompareFlags options = kCFCompareCaseInsensitive | kCFCompareAnchored | kCFCompareBackwards; + for(int i = 0; i < sizeof(suffixToWeight) / sizeof(suffixToWeight[0]); ++i){ + struct SuffixWeight item = suffixToWeight[i]; + if(CFStringFind((CFStringRef)fontName, (CFStringRef)item.suffix, options).location != kCFNotFound){ + return item.weight; } } - return weight; + + NSDictionary *traits = (__bridge_transfer NSDictionary *)CTFontCopyTraits((CTFontRef)font); + return (NativeRenderFontWeight)[traits[UIFontWeightTrait] doubleValue]; } static BOOL isItalicFont(UIFont *font) { - NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; - UIFontDescriptorSymbolicTraits symbolicTraits = [traits[UIFontSymbolicTrait] unsignedIntValue]; - return (symbolicTraits & UIFontDescriptorTraitItalic) != 0; + return (CTFontGetSymbolicTraits((CTFontRef)font) & kCTFontTraitItalic) != 0; } static BOOL isCondensedFont(UIFont *font) { - NSDictionary *traits = [font.fontDescriptor objectForKey:UIFontDescriptorTraitsAttribute]; - UIFontDescriptorSymbolicTraits symbolicTraits = [traits[UIFontSymbolicTrait] unsignedIntValue]; - return (symbolicTraits & UIFontDescriptorTraitCondensed) != 0; + return (CTFontGetSymbolicTraits((CTFontRef)font) & kCTFontTraitCondensed) != 0; } static UIFont *cachedSystemFont(CGFloat size, NativeRenderFontWeight weight) { - NSString *cacheKey = [NSString stringWithFormat:@"%.1f/%.2f", size, weight]; + struct __attribute__((__packed__)) CacheKey { + CGFloat size; + NativeRenderFontWeight weight; + }; + CacheKey key{size, weight}; + NSValue *cacheKey = [[NSValue alloc] initWithBytes:&key objCType:@encode(CacheKey)]; UIFont *font = [fontCache objectForKey:cacheKey]; if (!font) { - // Only supported on iOS8.2 and above - if (@available(iOS 8.2, *)) { - font = [UIFont systemFontOfSize:size weight:weight]; - } else { - if (weight >= UIFontWeightBold) { - font = [UIFont boldSystemFontOfSize:size]; - } else if (weight >= UIFontWeightMedium) { - font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:size]; - } else if (weight <= UIFontWeightLight) { - font = [UIFont fontWithName:@"HelveticaNeue-Light" size:size]; - } else { - font = [UIFont systemFontOfSize:size]; - } - } - + font = [UIFont systemFontOfSize:size weight:weight]; [fontCache setObject:font forKey:cacheKey]; } return font; } +// Caching wrapper around expensive +[UIFont fontNamesForFamilyName:] +static NSArray *fontNamesForFamilyName(NSString *familyName) +{ + static NSCache *> *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [NSCache new]; + [NSNotificationCenter.defaultCenter + addObserverForName:(NSNotificationName)kCTFontManagerRegisteredFontsChangedNotification + object:nil + queue:nil + usingBlock:^(NSNotification *) { + [cache removeAllObjects]; + }]; + }); + + NSArray *names = [cache objectForKey:familyName]; + if (!names) { + names = [UIFont fontNamesForFamilyName:familyName] ?: [NSArray new]; + [cache setObject:names forKey:familyName]; + } + return names; +} + @implementation HippyConvert (NativeRenderFont) + (UIFont *)UIFont:(id)json { @@ -182,14 +191,14 @@ + (NativeRenderFontVariantDescriptor *)NativeRenderFontVariantDescriptor:(id)jso NativeRenderFontVariantDescriptor *value = mapping[json]; if (HIPPY_DEBUG && !value && [json description].length > 0) { HippyLogError(@"Invalid NativeRenderFontVariantDescriptor '%@'. should be one of: %@", json, - [[mapping allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]); + [[mapping allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]); } return value; } HP_ARRAY_CONVERTER(NativeRenderFontVariantDescriptor) +@end - @end @implementation HippyFont @@ -265,7 +274,7 @@ + (UIFont *)updateFont:(UIFont *)font // Gracefully handle being given a font name rather than font family, for // example: "Helvetica Light Oblique" rather than just "Helvetica". - if (!didFindFont && [familyName length] > 0 && [UIFont fontNamesForFamilyName:familyName].count == 0) { + if (!didFindFont && familyName.length > 0 && fontNamesForFamilyName(familyName).count == 0) { familyName = font.familyName; fontWeight = weight ? fontWeight : weightOfFont(font); isItalic = style ? isItalic : isItalicFont(font); @@ -291,22 +300,18 @@ + (UIFont *)updateFont:(UIFont *)font } else { // Not a valid font or family HippyLogError(@"Unrecognized font family '%@'", familyName); - if (@available(iOS 8.2, *)) { - font = [UIFont systemFontOfSize:fontSize weight:fontWeight]; - } else if (fontWeight > UIFontWeightRegular) { - font = [UIFont boldSystemFontOfSize:fontSize]; - } else { - font = [UIFont systemFontOfSize:fontSize]; - } + font = [UIFont systemFontOfSize:fontSize weight:fontWeight]; } } - // Get the closest font that matches the given weight for the fontFamily - CGFloat closestWeight = INFINITY; - if ([familyName length] > 0) { - for (NSString *name in [UIFont fontNamesForFamilyName:familyName]) { + NSArray *names; + if (!didFindFont && familyName.length > 0) { + names = fontNamesForFamilyName(familyName); + // Get the closest font that matches the given weight for the fontFamily + CGFloat closestWeight = INFINITY; + for (NSString *name in names) { UIFont *match = [UIFont fontWithName:name size:fontSize]; - if (isItalic == isItalicFont(match) && (isCondensed == isCondensedFont(match) || !font)) { + if (isItalic == isItalicFont(match) && isCondensed == isCondensedFont(match)) { CGFloat testWeight = weightOfFont(match); if (ABS(testWeight - fontWeight) < ABS(closestWeight - fontWeight)) { font = match; @@ -315,13 +320,20 @@ + (UIFont *)updateFont:(UIFont *)font } } } + + if (!font && names.count > 0) { + font = [UIFont fontWithName:names[0] size:fontSize]; + } + // Apply font variants to font object if (variant) { NSArray *fontFeatures = [HippyConvert NativeRenderFontVariantDescriptorArray:variant]; UIFontDescriptor *fontDescriptor = - [font.fontDescriptor fontDescriptorByAddingAttributes:@{ UIFontDescriptorFeatureSettingsAttribute: fontFeatures }]; + [font.fontDescriptor fontDescriptorByAddingAttributes: + @{UIFontDescriptorFeatureSettingsAttribute: fontFeatures}]; font = [UIFont fontWithDescriptor:fontDescriptor size:fontSize]; } + return font; } diff --git a/renderer/native/ios/renderer/HippyRootView.mm b/renderer/native/ios/renderer/HippyRootView.mm index 00aa0c71a5e..33f77e015a0 100644 --- a/renderer/native/ios/renderer/HippyRootView.mm +++ b/renderer/native/ios/renderer/HippyRootView.mm @@ -100,6 +100,9 @@ - (instancetype)initWithBridge:(HippyBridge *)bridge if (!_bridge.moduleName) { _bridge.moduleName = moduleName; } + if (!_bridge.contextName) { + _bridge.contextName = moduleName; + } _moduleName = moduleName; _appProperties = [initialProperties copy]; _delegate = delegate; diff --git a/renderer/native/ios/renderer/component/view/HippyViewManager.mm b/renderer/native/ios/renderer/component/view/HippyViewManager.mm index 0a0621426f6..af593c712c1 100644 --- a/renderer/native/ios/renderer/component/view/HippyViewManager.mm +++ b/renderer/native/ios/renderer/component/view/HippyViewManager.mm @@ -70,7 +70,8 @@ - (HippyViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(__unused NSDict HIPPY_EXPORT_METHOD(getBoundingClientRect:(nonnull NSNumber *)hippyTag options:(nullable NSDictionary *)options callback:(HippyPromiseResolveBlock)callback ) { - if (options && [[options objectForKey:HippyViewManagerGetBoundingRelToContainerKey] boolValue]) { + if (options && [options isKindOfClass:NSDictionary.class] && + [[options objectForKey:HippyViewManagerGetBoundingRelToContainerKey] boolValue]) { [self measureInWindow:hippyTag withErrMsg:YES callback:callback]; } else { [self measureInAppWindow:hippyTag withErrMsg:YES callback:callback]; @@ -118,7 +119,7 @@ - (void)measureInWindow:(NSNumber *)componentTag } HIPPY_EXPORT_METHOD(measureInAppWindow:(NSNumber *)componentTag - callback:(HippyPromiseResolveBlock)callback) { + callback:(HippyPromiseResolveBlock)callback) { [self measureInAppWindow:componentTag withErrMsg:NO callback:callback]; } @@ -141,8 +142,8 @@ - (void)measureInAppWindow:(NSNumber *)componentTag } HIPPY_EXPORT_METHOD(getScreenShot:(nonnull NSNumber *)componentTag - params:(NSDictionary *__nonnull)params - callback:(HippyPromiseResolveBlock)callback) { + params:(NSDictionary *__nonnull)params + callback:(HippyPromiseResolveBlock)callback) { [self.bridge.uiManager addUIBlock:^(__unused HippyUIManager *uiManager, NSDictionary *viewRegistry) { UIView *view = viewRegistry[componentTag]; if (view == nil) { @@ -181,8 +182,8 @@ - (void)measureInAppWindow:(NSNumber *)componentTag } HIPPY_EXPORT_METHOD(getLocationOnScreen:(nonnull NSNumber *)componentTag - params:(NSDictionary *__nonnull)params - callback:(HippyPromiseResolveBlock)callback) { + params:(NSDictionary *__nonnull)params + callback:(HippyPromiseResolveBlock)callback) { [self.bridge.uiManager addUIBlock:^(__unused HippyUIManager *uiManager, NSDictionary *viewRegistry) { UIView *view = viewRegistry[componentTag]; if (view == nil) {