Skip to content

Commit

Permalink
fix(ios): bridge and rootView's memory issue when dealloc
Browse files Browse the repository at this point in the history
  • Loading branch information
wwwcg committed Oct 15, 2023
1 parent 5198dce commit 8a4b599
Show file tree
Hide file tree
Showing 14 changed files with 334 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ typedef NS_ENUM(NSUInteger, RenderType) {
RenderTypeNative,
};

@class HippyBridge;
@class HippyBridge, HippyRootView;
@class HippyPageCacheManager, HippyPageCache;

@protocol HippyPageCacheManagerObserverProtocol <NSObject>
Expand All @@ -56,7 +56,7 @@ typedef NS_ENUM(NSUInteger, RenderType) {
@interface HippyPageCache : NSObject

@property(nonatomic, strong) HippyBridge *hippyBridge;
@property(nonatomic, strong) UIView *rootView;
@property(nonatomic, strong) HippyRootView *rootView;
@property(nonatomic, strong, nullable) UIImage *snapshot;

@property(nonatomic, assign) DriverType driverType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/

#import "HippyPageCache.h"
#import <hippy/HippyRootView.h>

@implementation HippyPageCache

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ @interface HippyDemoViewController () <HippyMethodInterceptorProtocol, HippyBrid
NSURL *_debugURL;

HippyBridge *_hippyBridge;
UIView *_hippyRootView;
HippyRootView *_hippyRootView;
BOOL _fromCache;
}

Expand Down Expand Up @@ -82,6 +82,7 @@ - (instancetype)initWithPageCache:(HippyPageCache *)pageCache {
- (void)dealloc {
[_hippyRootView removeObserver:self forKeyPath:@"frame"];
[[HippyPageCacheManager defaultPageCacheManager] addPageCache:[self toPageCache]];
NSLog(@"%@ dealloc", self.class);
}

- (void)viewDidLoad {
Expand Down Expand Up @@ -132,12 +133,11 @@ - (void)mountConnector:(HippyBridge *)hippyBridge {
isSimulator = YES;
#endif

#if USE_NEW_LOAD
HippyRootView *rootView = [[HippyRootView alloc] initWithBridge:hippyBridge
moduleName:@"Demo"
initialProperties:@{@"isSimulator": @(isSimulator)}
delegate:self];
rootView.frame = self.contentAreaView.bounds;
rootView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;

if (_isDebugMode) {
hippyBridge.sandboxDirectory = [_debugURL URLByDeletingLastPathComponent];
Expand All @@ -157,6 +157,35 @@ - (void)mountConnector:(HippyBridge *)hippyBridge {
}];
}

#else
HippyRootView *rootView = nil;

if (_isDebugMode) {
hippyBridge.sandboxDirectory = [_debugURL URLByDeletingLastPathComponent];
rootView = [[HippyRootView alloc] initWithBridge:hippyBridge
businessURL:_debugURL
moduleName:@"Demo"
initialProperties:@{@"isSimulator": @(isSimulator)}
delegate:self];
} else {
NSURL *vendorBundleURL = [self vendorBundleURL];
NSURL *indexBundleURL = [self indexBundleURL];
[hippyBridge loadBundleURL:vendorBundleURL completion:^(NSURL * _Nullable, NSError * _Nullable) {
NSLog(@"url %@ load finish", vendorBundleURL);
}];
hippyBridge.sandboxDirectory = [indexBundleURL URLByDeletingLastPathComponent];
rootView = [[HippyRootView alloc] initWithBridge:hippyBridge
businessURL:indexBundleURL
moduleName:@"Demo"
initialProperties:@{@"isSimulator": @(isSimulator)}
delegate:self];
}

#endif

rootView.frame = self.contentAreaView.bounds;
rootView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;

[self.contentAreaView addSubview:rootView];
if (_hippyRootView) {
[_hippyRootView removeObserver:self forKeyPath:@"frame" context:NULL];
Expand Down
33 changes: 13 additions & 20 deletions framework/ios/base/bridge/HippyBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,17 +81,19 @@ HIPPY_EXTERN NSString *const HippyDidInitializeModuleNotification;
*/
HIPPY_EXTERN NSString *HippyBridgeModuleNameForClass(Class bridgeModuleClass);

/**
* Async batched bridge used to communicate with the JavaScript application.
*/


/// Async bridge used to communicate with the JavaScript application.
@interface HippyBridge : NSObject <HippyInvalidating>

/// The bridge delegate
@property (nonatomic, weak, readonly) id<HippyBridgeDelegate> delegate;

/// SDK launch config
/// TODO: 优化 launchOptions 参数
@property (nonatomic, copy, readonly) NSDictionary *launchOptions;



/// Create A HippyBridge instance, without load/execute any js bundle.
///
/// @param delegate bridge delegate
Expand Down Expand Up @@ -159,6 +161,7 @@ HIPPY_EXTERN NSString *HippyBridgeModuleNameForClass(Class bridgeModuleClass);
- (void)loadBundleURL:(NSURL *)bundleURL
completion:(void (^_Nullable)(NSURL * _Nullable, NSError * _Nullable))completion;


@property(nonatomic, assign)std::weak_ptr<VFSUriLoader> VFSUriLoader;

/**
Expand Down Expand Up @@ -213,11 +216,13 @@ HIPPY_EXTERN NSString *HippyBridgeModuleNameForClass(Class bridgeModuleClass);

- (void)handleBuffer:(id _Nullable)buffer batchEnded:(BOOL)batchEnded;


/// <#Description#>
/// - Parameter isInspectable: <#isInspectable description#>
- (void)setInspectable:(BOOL)isInspectable;

/**
* All registered bridge module classes.
*/

/// All registered bridge module classes.
@property (nonatomic, copy, readonly) NSArray<Class> *moduleClasses;

- (NSString *)moduleConfig;
Expand Down Expand Up @@ -302,7 +307,7 @@ HIPPY_EXTERN NSString *HippyBridgeModuleNameForClass(Class bridgeModuleClass);



- (void)setRootView:(HippyRootView *)rootView;
- (void)setRootView:(UIView *)rootView;

- (void)resetRootSize:(CGSize)size;

Expand All @@ -322,18 +327,6 @@ HIPPY_EXTERN NSString *HippyBridgeModuleNameForClass(Class bridgeModuleClass);
@end


@interface HippyBridge (RedBoxDebug)

/// The last current active bridge instance.
+ (instancetype)currentBridge;

/// Record the last active bridge instance.
/// - Parameter currentBridge: bridge instance, pass nil to reset.
+ (void)setCurrentBridge:(nullable HippyBridge *)currentBridge;

@end


HIPPY_EXTERN void HippyBridgeFatal(NSError *, HippyBridge *);

HIPPY_EXTERN void HippyBridgeHandleException(NSException *exception, HippyBridge *bridge);
Expand Down
27 changes: 1 addition & 26 deletions framework/ios/base/bridge/HippyBridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -937,9 +937,6 @@ - (void)invalidate {
_startTime = footstone::TimePoint::SystemNow();
self.moduleSemaphore = nil;

if ([HippyBridge currentBridge] == self) {
[HippyBridge setCurrentBridge:nil];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[jsExecutor executeBlockOnJavaScriptQueue:^{
@autoreleasepool {
Expand Down Expand Up @@ -1142,7 +1139,7 @@ - (void)setSnapShotData:(NSData *)data {


//FIXME: 调整优化
- (void)setRootView:(HippyRootView *)rootView {
- (void)setRootView:(UIView *)rootView {
auto engineResource = [[HippyJSEnginesMapper defaultInstance] JSEngineResourceForKey:self.engineKey];
auto domManager = engineResource->GetDomManager();
NSNumber *rootTag = [rootView hippyTag];
Expand Down Expand Up @@ -1205,25 +1202,3 @@ void HippyBridgeHandleException(NSException *exception, HippyBridge *bridge) {
HippyHandleException(exception);
}


#pragma mark -

@implementation HippyBridge (RedBoxDebug)

static HippyBridge *HippyCurrentBridgeInstance = nil;

/**
* The last current active bridge instance. This is set automatically whenever
* the bridge is accessed. It can be useful for static functions or singletons
* that need to access the bridge for purposes such as logging, but should not
* be relied upon to return any particular instance, due to race conditions.
*/
+ (instancetype)currentBridge {
return HippyCurrentBridgeInstance;
}

+ (void)setCurrentBridge:(nullable HippyBridge *)currentBridge {
HippyCurrentBridgeInstance = currentBridge;
}

@end
9 changes: 9 additions & 0 deletions framework/ios/module/dev/HippyRedBox.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,13 @@

@property (nonatomic, readonly) HippyRedBox *redBox;

/// The last current active bridge instance.
+ (nullable id)currentBridge;

/// Record the last active bridge instance.
/// - Parameter currentBridge: bridge instance, pass nil to reset.
+ (void)setCurrentBridge:(nullable HippyBridge *)currentBridge;

@end


24 changes: 23 additions & 1 deletion framework/ios/module/dev/HippyRedBox.mm
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
#import "HippyErrorInfo.h"
#import "HippyRedBox.h"
#import "HippyUtils.h"

#import "HippyWeakProxy.h"
#import "HippyAsserts.h"
#import "HippyConvert.h"
#import "HippyJSStackFrame.h"
Expand Down Expand Up @@ -428,14 +428,36 @@ - (void)reloadFromRedBoxWindow:(__unused HippyRedBoxWindow *)redBoxWindow {

@end


#pragma mark -


@implementation HippyBridge (HippyRedBox)

- (HippyRedBox *)redBox {
return [self moduleForClass:[HippyRedBox class]];
}

static HippyWeakProxy *HippyCurrentBridgeInstance = nil;

/**
* The last current active bridge instance. This is set automatically whenever
* the bridge is accessed. It can be useful for static functions or singletons
* that need to access the bridge for purposes such as logging, but should not
* be relied upon to return any particular instance, due to race conditions.
*/
+ (instancetype)currentBridge {
return (id)HippyCurrentBridgeInstance;
}

+ (void)setCurrentBridge:(nullable HippyBridge *)currentBridge {
HippyCurrentBridgeInstance = [HippyWeakProxy weakProxyForObject:currentBridge];
}


@end


#else // Disabled

@implementation HippyRedBox
Expand Down
33 changes: 33 additions & 0 deletions framework/ios/utils/HippyWeakProxy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*!
* iOS SDK
*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface HippyWeakProxy : NSProxy

+ (instancetype)weakProxyForObject:(id)target;

@end

NS_ASSUME_NONNULL_END
67 changes: 67 additions & 0 deletions framework/ios/utils/HippyWeakProxy.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*!
* iOS SDK
*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2019 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#import "HippyWeakProxy.h"

@interface HippyWeakProxy ()

@property (nonatomic, weak) id target;

@end

@implementation HippyWeakProxy

+ (instancetype)weakProxyForObject:(id)target {
HippyWeakProxy *proxy = [HippyWeakProxy alloc];
proxy.target = target;
return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
return _target;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}

#pragma mark Handling Unimplemented Methods

- (void)forwardInvocation:(NSInvocation *)invocation {
// Fallback for when target is nil. Don't do anything, just return 0/NULL/nil.
// The method signature we've received to get here is just a dummy to keep `doesNotRecognizeSelector:` from firing.
// We can't really handle struct return types here because we don't know the length.
void *nullPointer = NULL;
[invocation setReturnValue:&nullPointer];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
// We only get here if `forwardingTargetForSelector:` returns nil.
// In that case, our weak target has been reclaimed. Return a dummy method signature to keep `doesNotRecognizeSelector:` from firing.
// We'll emulate the Obj-c messaging nil behavior by setting the return value to nil in `forwardInvocation:`, but we'll assume that the return
// value is `sizeof(void *)`. Other libraries handle this situation by making use of a global method signature cache, but that seems heavier than
// necessary and has issues as well. See https://www.mikeash.com/pyblog/friday-qa-2010-02-26-futures.html and
// https://github.com/steipete/PSTDelegateProxy/issues/1 for examples of using a method signature cache.
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}

@end
2 changes: 1 addition & 1 deletion modules/ios/base/HippyLog.mm
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ void HippyLogNativeInternal(HippyLogLevel level, const char *fileName, int lineN
dispatch_async(dispatch_get_main_queue(), ^{
// red box is thread safe, but by deferring to main queue we avoid a startup
// race condition that causes the module to be accessed before it has loaded
[[HippyBridge currentBridge].redBox showErrorMessage:message withStack:stack];
[((HippyBridge *)[HippyBridge currentBridge]).redBox showErrorMessage:message withStack:stack];
});
}
#endif
Expand Down
Loading

0 comments on commit 8a4b599

Please sign in to comment.