diff --git a/ios/sdk/component/listview/HippyBaseListView.m b/ios/sdk/component/listview/HippyBaseListView.m index e266b51cb1a..0d4bbc4fc22 100644 --- a/ios/sdk/component/listview/HippyBaseListView.m +++ b/ios/sdk/component/listview/HippyBaseListView.m @@ -47,6 +47,7 @@ @implementation HippyBaseListView { HippyHeaderRefresh *_headerRefreshView; HippyFooterRefresh *_footerRefreshView; NSArray *_previousVisibleCells; + NSMutableDictionary *_cachedItems; } @synthesize node = _node; @@ -59,6 +60,10 @@ - (instancetype)initWithBridge:(HippyBridge *)bridge { _isInitialListReady = NO; _preNumberOfRows = 0; _preloadItemNumber = 1; + _cachedItems = [NSMutableDictionary dictionaryWithCapacity:64]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didReceiveMemoryWarning) + name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; [self initTableView]; } @@ -169,6 +174,13 @@ - (void)insertHippySubview:(UIView *)subview atIndex:(NSInteger)atIndex { } } +- (void)removeHippySubview:(UIView *)subview { + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(purgeFurthestIndexPathsFromScreen) + object:nil]; + [self purgeFurthestIndexPathsFromScreen]; +} + #pragma mark -Scrollable - (void)setScrollEnabled:(BOOL)value { @@ -310,6 +322,7 @@ - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell NSAssert([cell isKindOfClass:[HippyBaseListViewCell class]], @"cell must be subclass of HippyBaseListViewCell"); if ([cell isKindOfClass:[HippyBaseListViewCell class]]) { HippyBaseListViewCell *hippyCell = (HippyBaseListViewCell *)cell; + [_cachedItems setObject:[hippyCell.cellView hippyTag] forKey:indexPath]; hippyCell.node.cell = nil; } } @@ -324,20 +337,15 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N cell = [[cls alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; cell.tableView = tableView; } - UIView *cellView = nil; - if (cell.node.cell) { - cellView = [_bridge.uiManager createViewFromNode:indexNode]; - } else { - cellView = [_bridge.uiManager updateNode:cell.node withNode:indexNode]; - if (nil == cellView) { - cellView = [_bridge.uiManager createViewFromNode:indexNode]; - } - } + UIView *cellView = [_bridge.uiManager createViewFromNode:indexNode]; HippyAssert([cellView conformsToProtocol:@protocol(ViewAppearStateProtocol)], @"subviews of HippyBaseListViewCell must conform to protocol ViewAppearStateProtocol"); cell.cellView = (UIView *)cellView; cell.node = indexNode; cell.node.cell = cell; + if (cellView) { + [_cachedItems removeObjectForKey:indexPath]; + } return cell; } @@ -381,7 +389,8 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [scrollViewListener scrollViewDidScroll:scrollView]; } } - + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(purgeFurthestIndexPathsFromScreen) object:nil]; + [self performSelector:@selector(purgeFurthestIndexPathsFromScreen) withObject:nil afterDelay:.5f]; [_headerRefreshView scrollViewDidScroll]; [_footerRefreshView scrollViewDidScroll]; } @@ -538,6 +547,13 @@ - (void)didMoveToSuperview { _rootView = nil; } +- (void)didMoveToWindow { + if (!self.window) { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(purgeFurthestIndexPathsFromScreen) object:nil]; + [self purgeFurthestIndexPathsFromScreen]; + } +} + - (BOOL)isManualScrolling { return _manualScroll; } @@ -558,7 +574,82 @@ - (BOOL)showScrollIndicator { return [_tableView showsVerticalScrollIndicator]; } +- (NSUInteger)maxCachedItemCount { + return NSUIntegerMax; +} + +- (NSUInteger)differenceFromIndexPath:(NSIndexPath *)indexPath1 againstAnother:(NSIndexPath *)indexPath2 { + NSAssert([NSThread mainThread], @"must be in main thread"); + long diffCount = 0; + for (NSUInteger index = MIN([indexPath1 section], [indexPath2 section]); index < MAX([indexPath1 section], [indexPath2 section]); index++) { + diffCount += [_tableView numberOfRowsInSection:index]; + } + diffCount = diffCount + [indexPath1 row] - [indexPath2 row]; + return labs(diffCount); +} + +- (NSInteger)differenceFromIndexPath:(NSIndexPath *)indexPath + againstVisibleIndexPaths:(NSArray *)visibleIndexPaths { + NSIndexPath *firstIndexPath = [visibleIndexPaths firstObject]; + NSIndexPath *lastIndexPath = [visibleIndexPaths lastObject]; + NSUInteger diffFirst = [self differenceFromIndexPath:indexPath againstAnother:firstIndexPath]; + NSUInteger diffLast = [self differenceFromIndexPath:indexPath againstAnother:lastIndexPath]; + return MIN(diffFirst, diffLast); +} + +- (NSArray *)findFurthestIndexPathsFromScreen { + NSUInteger visibleItemsCount = [[self.tableView visibleCells] count]; + NSUInteger maxCachedItemCount = [self maxCachedItemCount] == NSUIntegerMax ? visibleItemsCount * 2 : [self maxCachedItemCount]; + NSUInteger cachedCount = [_cachedItems count]; + NSInteger cachedCountToRemove = cachedCount > maxCachedItemCount ? cachedCount - maxCachedItemCount : 0; + if (0 != cachedCountToRemove) { + NSArray *visibleIndexPaths = [_tableView indexPathsForVisibleRows]; + NSArray *sortedCachedItemKey = [[_cachedItems allKeys] sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { + NSIndexPath *ip1 = obj1; + NSIndexPath *ip2 = obj2; + NSUInteger ip1Diff = [self differenceFromIndexPath:ip1 againstVisibleIndexPaths:visibleIndexPaths]; + NSUInteger ip2Diff = [self differenceFromIndexPath:ip2 againstVisibleIndexPaths:visibleIndexPaths]; + if (ip1Diff > ip2Diff) { + return NSOrderedAscending; + } + else if (ip1Diff < ip2Diff) { + return NSOrderedDescending; + } + else { + return NSOrderedSame; + } + }]; + NSArray *result = [sortedCachedItemKey subarrayWithRange:NSMakeRange(0, cachedCountToRemove)]; + return result; + } + return nil; +} + +- (void)purgeFurthestIndexPathsFromScreen { + NSArray *furthestIndexPaths = [self findFurthestIndexPathsFromScreen]; + if (furthestIndexPaths) { + //purge view + NSArray *objects = [_cachedItems objectsForKeys:furthestIndexPaths notFoundMarker:@(-1)]; + [_bridge.uiManager removeNativeViewFromTags:objects]; + //purge cache + [_cachedItems removeObjectsForKeys:furthestIndexPaths]; + } +} + + +- (void)didReceiveMemoryWarning { + [self cleanUpCachedItems]; +} + +- (void)cleanUpCachedItems { + //purge view + NSArray *objects = [_cachedItems allValues]; + [_bridge.uiManager removeNativeViewFromTags:objects]; + [_cachedItems removeAllObjects]; +} + - (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; [_headerRefreshView unsetFromScrollView]; [_footerRefreshView unsetFromScrollView]; } diff --git a/ios/sdk/component/waterfalllist/HippyReusableNodeCache.h b/ios/sdk/component/waterfalllist/HippyReusableNodeCache.h deleted file mode 100644 index ba38d73c464..00000000000 --- a/ios/sdk/component/waterfalllist/HippyReusableNodeCache.h +++ /dev/null @@ -1,39 +0,0 @@ -/*! - * 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 - -NS_ASSUME_NONNULL_BEGIN - -@class UIView; -@class HippyVirtualNode; - -@interface HippyReusableNodeCache : NSObject - -- (void)enqueueItemNode:(HippyVirtualNode *)node forIdentifier:(NSString *)identifier; -- (HippyVirtualNode *)dequeueItemNodeForIdentifier:(NSString *)identifier; -- (BOOL)queueContainsNode:(HippyVirtualNode *)node forIdentifier:(NSString *)identifier; -- (BOOL)removeNode:(HippyVirtualNode *)node forIdentifier:(NSString *)identifier; - -@end - -NS_ASSUME_NONNULL_END diff --git a/ios/sdk/component/waterfalllist/HippyReusableNodeCache.m b/ios/sdk/component/waterfalllist/HippyReusableNodeCache.m deleted file mode 100644 index f6201a59246..00000000000 --- a/ios/sdk/component/waterfalllist/HippyReusableNodeCache.m +++ /dev/null @@ -1,77 +0,0 @@ -/*! - * 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 "HippyReusableNodeCache.h" -#import "HippyVirtualNode.h" - -@interface HippyReusableNodeCache () { - NSMutableDictionary *> *_cache; -} - -@end - -@implementation HippyReusableNodeCache - -- (instancetype)init { - self = [super init]; - if (self) { - _cache = [[NSMutableDictionary alloc] initWithCapacity:8]; - } - return self; -} - -- (void)enqueueItemNode:(HippyVirtualNode *)node forIdentifier:(NSString *)identifier { - if (!node || !identifier) { - return; - } - NSMutableSet *set = _cache[identifier]; - if (!set) { - set = [NSMutableSet set]; - _cache[identifier] = set; - } - [set addObject:node]; -} - -- (HippyVirtualNode *)dequeueItemNodeForIdentifier:(NSString *)identifier { - NSMutableSet *set = _cache[identifier]; - HippyVirtualNode *cell = [set anyObject]; - if (cell) { - [set removeObject:cell]; - } - return cell; -} - -- (BOOL)queueContainsNode:(HippyVirtualNode *)node forIdentifier:(NSString *)identifier { - NSSet *set = _cache[identifier]; - return [set containsObject:node]; -} - -- (BOOL)removeNode:(HippyVirtualNode *)node forIdentifier:(NSString *)identifier { - NSMutableSet *set = _cache[identifier]; - if ([set containsObject:node]) { - [set removeObject:node]; - return YES; - } - return NO; -} - -@end diff --git a/ios/sdk/component/waterfalllist/HippyWaterfallView.m b/ios/sdk/component/waterfalllist/HippyWaterfallView.m index c687882e3fd..26bf99be76b 100644 --- a/ios/sdk/component/waterfalllist/HippyWaterfallView.m +++ b/ios/sdk/component/waterfalllist/HippyWaterfallView.m @@ -24,7 +24,6 @@ #import "HippyCollectionViewWaterfallLayout.h" #import "HippyHeaderRefresh.h" #import "HippyFooterRefresh.h" -#import "HippyReusableNodeCache.h" #import "HippyWaterfallItemView.h" #import "HippyVirtualList.h" @@ -61,7 +60,7 @@ @interface HippyWaterfallView () *_cachedItems; } @property (nonatomic, strong) HippyCollectionViewWaterfallLayout *layout; @@ -94,9 +93,12 @@ - (instancetype)initWithBridge:(HippyBridge *)bridge { self.bridge = bridge; _scrollListeners = [NSMutableArray array]; _scrollEventThrottle = 100.f; - + _cachedItems = [NSMutableDictionary dictionaryWithCapacity:64]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didReceiveMemoryWarning) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; [self initCollectionView]; - _reusableNodeCache = [[HippyReusableNodeCache alloc] init]; if (@available(iOS 11.0, *)) { self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } @@ -126,6 +128,10 @@ - (void)setScrollEventThrottle:(double)scrollEventThrottle { } - (void)removeHippySubview:(UIView *)subview { + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(purgeFurthestIndexPathsFromScreen) + object:nil]; + [self purgeFurthestIndexPathsFromScreen]; } - (void)hippySetFrame:(CGRect)frame { @@ -343,30 +349,22 @@ - (void)collectionView:(UICollectionView *)collectionView - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { HippyCollectionViewCell *hpCell = (HippyCollectionViewCell *)cell; + [_cachedItems setObject:[hpCell.cellView hippyTag] forKey:indexPath]; [hpCell.cellView removeFromSuperview]; HippyVirtualNode *cellNode = hpCell.node; NSString *reuseIdentifier = [self reuseIdentifierForIndexPath:indexPath]; if (cellNode && reuseIdentifier) { - [_reusableNodeCache enqueueItemNode:cellNode forIdentifier:reuseIdentifier]; hpCell.node = nil; } } - (void)itemViewForCollectionViewCell:(UICollectionViewCell *)cell indexPath:(NSIndexPath *)indexPath { - NSString *reuseIdentifier = [self reuseIdentifierForIndexPath:indexPath]; HippyVirtualNode *cellNode = [self nodeAtIndexPath:indexPath]; - [_reusableNodeCache removeNode:cellNode forIdentifier:reuseIdentifier]; - HippyVirtualNode *reusedNode = [_reusableNodeCache dequeueItemNodeForIdentifier:reuseIdentifier]; HippyCollectionViewCell *hpCell = (HippyCollectionViewCell *)cell; - HippyWaterfallItemView *cellView = nil; - if (reusedNode) { - cellView = (HippyWaterfallItemView *)[self.bridge.uiManager updateNode:reusedNode withNode:cellNode]; - } - if (!cellView) { - cellView = (HippyWaterfallItemView *)[self.bridge.uiManager createViewFromNode:cellNode]; - } + HippyWaterfallItemView *cellView = (HippyWaterfallItemView *)[self.bridge.uiManager createViewFromNode:cellNode]; hpCell.cellView = cellView; hpCell.node = cellNode; + [_cachedItems removeObjectForKey:indexPath]; } #pragma mark - HippyCollectionViewDelegateWaterfallLayout @@ -441,7 +439,8 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { _allowNextScrollNoMatterWhat = NO; } } - + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(purgeFurthestIndexPathsFromScreen) object:nil]; + [self performSelector:@selector(purgeFurthestIndexPathsFromScreen) withObject:nil afterDelay:.5f]; [_headerRefreshView scrollViewDidScroll]; [_footerRefreshView scrollViewDidScroll]; } @@ -574,6 +573,81 @@ - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollViewxt { - (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView { } +- (NSUInteger)maxCachedItemCount { + return NSUIntegerMax; +} + +- (NSUInteger)differenceFromIndexPath:(NSIndexPath *)indexPath1 againstAnother:(NSIndexPath *)indexPath2 { + NSAssert([NSThread mainThread], @"must be in main thread"); + long diffCount = 0; + for (NSUInteger index = MIN([indexPath1 section], [indexPath2 section]); index < MAX([indexPath1 section], [indexPath2 section]); index++) { + diffCount += [_collectionView numberOfItemsInSection:index]; + } + diffCount = diffCount + [indexPath1 row] - [indexPath2 row]; + return labs(diffCount); +} + +- (NSInteger)differenceFromIndexPath:(NSIndexPath *)indexPath + againstVisibleIndexPaths:(NSArray *)visibleIndexPaths { + NSIndexPath *firstIndexPath = [visibleIndexPaths firstObject]; + NSIndexPath *lastIndexPath = [visibleIndexPaths lastObject]; + NSUInteger diffFirst = [self differenceFromIndexPath:indexPath againstAnother:firstIndexPath]; + NSUInteger diffLast = [self differenceFromIndexPath:indexPath againstAnother:lastIndexPath]; + return MIN(diffFirst, diffLast); +} + +- (NSArray *)findFurthestIndexPathsFromScreen { + NSUInteger visibleItemsCount = [[self.collectionView visibleCells] count]; + NSUInteger maxCachedItemCount = [self maxCachedItemCount] == NSUIntegerMax ? visibleItemsCount * 2 : [self maxCachedItemCount]; + NSUInteger cachedCount = [_cachedItems count]; + NSInteger cachedCountToRemove = cachedCount > maxCachedItemCount ? cachedCount - maxCachedItemCount : 0; + if (0 != cachedCountToRemove) { + NSArray *visibleIndexPaths = [_collectionView indexPathsForVisibleItems]; + NSArray *sortedCachedItemKey = [[_cachedItems allKeys] sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { + NSIndexPath *ip1 = obj1; + NSIndexPath *ip2 = obj2; + NSUInteger ip1Diff = [self differenceFromIndexPath:ip1 againstVisibleIndexPaths:visibleIndexPaths]; + NSUInteger ip2Diff = [self differenceFromIndexPath:ip2 againstVisibleIndexPaths:visibleIndexPaths]; + if (ip1Diff > ip2Diff) { + return NSOrderedAscending; + } + else if (ip1Diff < ip2Diff) { + return NSOrderedDescending; + } + else { + return NSOrderedSame; + } + }]; + NSArray *result = [sortedCachedItemKey subarrayWithRange:NSMakeRange(0, cachedCountToRemove)]; + return result; + } + return nil; +} + +- (void)purgeFurthestIndexPathsFromScreen { + NSArray *furthestIndexPaths = [self findFurthestIndexPathsFromScreen]; + if (furthestIndexPaths) { + //purge view + NSArray *objects = [_cachedItems objectsForKeys:furthestIndexPaths notFoundMarker:@(-1)]; + [self.bridge.uiManager removeNativeViewFromTags:objects]; + //purge cache + [_cachedItems removeObjectsForKeys:furthestIndexPaths]; + } +} + + +- (void)didReceiveMemoryWarning { + [self cleanUpCachedItems]; +} + +- (void)cleanUpCachedItems { + //purge view + NSArray *objects = [_cachedItems allValues]; + [self.bridge.uiManager removeNativeViewFromTags:objects]; + [_cachedItems removeAllObjects]; +} + + #pragma mark - #pragma mark JS CALL Native - (void)refreshCompleted:(NSInteger)status text:(NSString *)text { @@ -646,4 +720,15 @@ - (void)didMoveToSuperview { _rootView = nil; } +- (void)didMoveToWindow { + if (!self.window) { + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(purgeFurthestIndexPathsFromScreen) object:nil]; + [self purgeFurthestIndexPathsFromScreen]; + } +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + @end diff --git a/ios/sdk/module/uimanager/HippyUIManager.h b/ios/sdk/module/uimanager/HippyUIManager.h index 10a07751689..628fa7bced8 100644 --- a/ios/sdk/module/uimanager/HippyUIManager.h +++ b/ios/sdk/module/uimanager/HippyUIManager.h @@ -162,6 +162,7 @@ HIPPY_EXTERN NSString *const HippyUIManagerDidEndBatchNotification; - (void)removeNativeNode:(HippyVirtualNode *)node; - (void)removeNativeNodeView:(UIView *)nodeView; +- (void)removeNativeViewFromTags:(NSArray *)hippyTags; - (void)updateViewsFromParams:(NSArray *)params completion:(HippyViewUpdateCompletedBlock)block; - (void)updateViewWithHippyTag:(NSNumber *)hippyTag props:(NSDictionary *)pros; @end diff --git a/ios/sdk/module/uimanager/HippyUIManager.mm b/ios/sdk/module/uimanager/HippyUIManager.mm index ea1555421d7..dd20d7ad6c5 100644 --- a/ios/sdk/module/uimanager/HippyUIManager.mm +++ b/ios/sdk/module/uimanager/HippyUIManager.mm @@ -1608,6 +1608,14 @@ - (void)removeNativeNodeView:(UIView *)nodeView { }]; } +- (void)removeNativeViewFromTags:(NSArray *)hippyTags { + NSArray *views = [_viewRegistry objectsForKeys:hippyTags + notFoundMarker:[[UIView alloc] initWithFrame:CGRectZero]]; + for (UIView *view in views) { + [self removeNativeNodeView:view]; + } +} + - (NSDictionary *)mergeProps:(NSDictionary *)newProps oldProps:(NSDictionary *)oldProps { NSMutableDictionary *tmpProps = [NSMutableDictionary dictionaryWithDictionary:newProps]; [oldProps enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, __unused id _Nonnull obj, __unused BOOL *stop) {