Skip to content

Commit f7dc0f9

Browse files
Merge pull request #812 from stigger/chatview-performance
Chatview performance
2 parents 6bb23ab + aa32c0d commit f7dc0f9

File tree

3 files changed

+214
-22
lines changed

3 files changed

+214
-22
lines changed

ChatSecure.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
63FA130C1C8A4EB700AE33EF /* OTRMessagesCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63FA130B1C8A4EB700AE33EF /* OTRMessagesCollectionViewFlowLayout.swift */; };
114114
63FCB1371DA3224A00A801F2 /* OTRSignalEncryptionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63FCB1361DA3224A00A801F2 /* OTRSignalEncryptionHelper.swift */; };
115115
7A6540ECCA04445E88F06962 /* Pods_ChatSecureCorePods_ChatSecureTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 281981F599E0F5C8397E6A3F /* Pods_ChatSecureCorePods_ChatSecureTests.framework */; };
116+
8F56C3272EBE7F45BC8F925A /* OTRMessagesLoadingView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8F56C50436DA64774EBB16E3 /* OTRMessagesLoadingView.xib */; };
116117
924F67C51EA5541C00528FB6 /* MigrationInfoHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 924F67C41EA5541C00528FB6 /* MigrationInfoHeaderView.xib */; };
117118
924F67C71EA55C6F00528FB6 /* MigrationInfoHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924F67C61EA55C6F00528FB6 /* MigrationInfoHeaderView.swift */; };
118119
924F68571EA7A2FD00528FB6 /* MigratedBuddyHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924F68561EA7A2FD00528FB6 /* MigratedBuddyHeaderView.swift */; };
@@ -1041,6 +1042,7 @@
10411042
63FCB1361DA3224A00A801F2 /* OTRSignalEncryptionHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTRSignalEncryptionHelper.swift; sourceTree = "<group>"; };
10421043
702F03DE10003A33635A366F /* Pods-ChatSecureCorePods-ChatSecureTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatSecureCorePods-ChatSecureTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ChatSecureCorePods-ChatSecureTests/Pods-ChatSecureCorePods-ChatSecureTests.release.xcconfig"; sourceTree = "<group>"; };
10431044
7A62FCE8FEC1E7C9644F8C38 /* Pods-ChatSecureCorePods-ChatSecure.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatSecureCorePods-ChatSecure.release.xcconfig"; path = "Pods/Target Support Files/Pods-ChatSecureCorePods-ChatSecure/Pods-ChatSecureCorePods-ChatSecure.release.xcconfig"; sourceTree = "<group>"; };
1045+
8F56C50436DA64774EBB16E3 /* OTRMessagesLoadingView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OTRMessagesLoadingView.xib; sourceTree = "<group>"; };
10441046
9118152C0A7B287ABD07FF70 /* Pods-ChatSecureCorePods-ChatSecureCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatSecureCorePods-ChatSecureCore.release.xcconfig"; path = "Pods/Target Support Files/Pods-ChatSecureCorePods-ChatSecureCore/Pods-ChatSecureCorePods-ChatSecureCore.release.xcconfig"; sourceTree = "<group>"; };
10451047
924F67C41EA5541C00528FB6 /* MigrationInfoHeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = MigrationInfoHeaderView.xib; path = Interface/MigrationInfoHeaderView.xib; sourceTree = "<group>"; };
10461048
924F67C61EA55C6F00528FB6 /* MigrationInfoHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationInfoHeaderView.swift; sourceTree = "<group>"; };
@@ -2063,6 +2065,7 @@
20632065
D9315CAD1BB600450077D2EE /* AddFriendsView.xib */,
20642066
924F67C41EA5541C00528FB6 /* MigrationInfoHeaderView.xib */,
20652067
924F68581EA7A31A00528FB6 /* MigratedBuddyHeaderView.xib */,
2068+
8F56C50436DA64774EBB16E3 /* OTRMessagesLoadingView.xib */,
20662069
);
20672070
path = OTRResources;
20682071
sourceTree = "<group>";
@@ -2655,6 +2658,7 @@
26552658
D9315CAE1BB600450077D2EE /* AddFriendsView.xib in Resources */,
26562659
D97070921EEF382D004FEBDE /* MediaDownloadView.xib in Resources */,
26572660
924F68591EA7A31A00528FB6 /* MigratedBuddyHeaderView.xib in Resources */,
2661+
8F56C3272EBE7F45BC8F925A /* OTRMessagesLoadingView.xib in Resources */,
26582662
);
26592663
runOnlyForDeploymentPostprocessing = 0;
26602664
};

ChatSecure/Classes/View Controllers/OTRMessagesViewController.m

Lines changed: 184 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,19 @@
6161
@import MediaPlayer;
6262

6363
static NSTimeInterval const kOTRMessageSentDateShowTimeInterval = 5 * 60;
64+
static NSUInteger const kOTRMessagePageSize = 50;
6465

6566
typedef NS_ENUM(int, OTRDropDownType) {
6667
OTRDropDownTypeNone = 0,
6768
OTRDropDownTypeEncryption = 1,
6869
OTRDropDownTypePush = 2
6970
};
7071

71-
@interface OTRMessagesViewController () <UITextViewDelegate, OTRAttachmentPickerDelegate, OTRYapViewHandlerDelegateProtocol, OTRMessagesCollectionViewFlowLayoutSizeProtocol>
72+
@interface OTRMessagesViewController () <UITextViewDelegate, OTRAttachmentPickerDelegate, OTRYapViewHandlerDelegateProtocol, OTRMessagesCollectionViewFlowLayoutSizeProtocol> {
73+
JSQMessagesAvatarImage *_warningAvatarImage;
74+
JSQMessagesAvatarImage *_accountAvatarImage;
75+
JSQMessagesAvatarImage *_buddyAvatarImage;
76+
}
7277

7378
@property (nonatomic, strong) OTRYapViewHandler *viewHandler;
7479

@@ -91,6 +96,11 @@ @interface OTRMessagesViewController () <UITextViewDelegate, OTRAttachmentPicker
9196
@property (nonatomic, strong) NSTimer *lastSeenRefreshTimer;
9297
@property (nonatomic, strong) UIView *jidForwardingHeaderView;
9398

99+
@property (nonatomic) BOOL loadingMessages;
100+
@property (nonatomic, strong) NSIndexPath *currentIndexPath;
101+
@property (nonatomic, strong) id currentMessage;
102+
@property (nonatomic, strong) NSCache *messageSizeCache;
103+
94104
@end
95105

96106
@implementation OTRMessagesViewController
@@ -101,6 +111,8 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB
101111
self.senderId = @"";
102112
self.senderDisplayName = @"";
103113
_state = [[MessagesViewControllerState alloc] init];
114+
self.messageSizeCache = [NSCache new];
115+
self.messageSizeCache.countLimit = kOTRMessagePageSize;
104116
}
105117
return self;
106118
}
@@ -166,7 +178,12 @@ - (void)viewDidLoad
166178
OTRMessagesCollectionViewFlowLayout *layout = [[OTRMessagesCollectionViewFlowLayout alloc] init];
167179
layout.sizeDelegate = self;
168180
self.collectionView.collectionViewLayout = layout;
169-
181+
182+
///"Loading Earlier" header view
183+
[self.collectionView registerNib:[UINib nibWithNibName:@"OTRMessagesLoadingView" bundle:OTRAssets.resourcesBundle]
184+
forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
185+
withReuseIdentifier:[JSQMessagesLoadEarlierHeaderView headerReuseIdentifier]];
186+
170187
//Subscribe to changes in encryption state
171188
__weak typeof(self)weakSelf = self;
172189
[self.KVOController observe:self.state keyPath:NSStringFromSelector(@selector(messageSecurity)) options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
@@ -207,6 +224,7 @@ - (void)viewDidAppear:(BOOL)animated
207224
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
208225
[self scrollToBottomAnimated:animated];
209226
});
227+
self.loadingMessages = NO;
210228
}
211229

212230
- (void)viewWillAppear:(BOOL)animated
@@ -262,8 +280,8 @@ - (void)viewWillAppear:(BOOL)animated
262280
[self moveLastComposingTextForThreadKey:self.threadKey colleciton:self.threadCollection toTextView:self.inputToolbar.contentView.textView];
263281
}
264282
}
265-
266-
283+
284+
self.loadingMessages = YES;
267285
[self.collectionView reloadData];
268286
}
269287

@@ -282,6 +300,15 @@ - (void)viewWillDisappear:(BOOL)animated
282300
// [self.inputToolbar.contentView.textView resignFirstResponder];
283301
}
284302

303+
- (void)viewDidDisappear:(BOOL)animated
304+
{
305+
[super viewDidDisappear:animated];
306+
307+
_warningAvatarImage = nil;
308+
_accountAvatarImage = nil;
309+
_buddyAvatarImage = nil;
310+
}
311+
285312
#pragma - mark Setters & getters
286313

287314
- (OTRAttachmentPicker *)attachmentPicker
@@ -1026,7 +1053,82 @@ - (void)moveLastComposingTextForThreadKey:(NSString *)key colleciton:(NSString *
10261053

10271054
- (id <OTRMessageProtocol,JSQMessageData>)messageAtIndexPath:(NSIndexPath *)indexPath
10281055
{
1029-
return [self.viewHandler object:indexPath];
1056+
// Multiple invocations with the same indexPath tend to come in groups, no need to hit the DB each time.
1057+
// Even though the object is cached, the row ID calculation still takes time
1058+
if (![indexPath isEqual:self.currentIndexPath]) {
1059+
self.currentIndexPath = indexPath;
1060+
self.currentMessage = [self.viewHandler object:indexPath];
1061+
}
1062+
return self.currentMessage;
1063+
}
1064+
1065+
/**
1066+
* Updates the flexible range of the DB connection.
1067+
* @param reset When NO, adds kOTRMessagePageSize to the range length, when YES resets the length to the kOTRMessagePageSize
1068+
*/
1069+
- (void)updateRangeOptions:(BOOL)reset
1070+
{
1071+
YapDatabaseViewRangeOptions *options = [self.viewHandler.mappings rangeOptionsForGroup:self.threadKey];
1072+
if (reset) {
1073+
if (options != nil && options.length <= kOTRMessagePageSize) {
1074+
return;
1075+
}
1076+
options = [YapDatabaseViewRangeOptions flexibleRangeWithLength:kOTRMessagePageSize
1077+
offset:0
1078+
from:YapDatabaseViewEnd];
1079+
self.messageSizeCache.countLimit = kOTRMessagePageSize;
1080+
} else {
1081+
options = [YapDatabaseViewRangeOptions flexibleRangeWithLength:options.length + kOTRMessagePageSize
1082+
offset:0
1083+
from:YapDatabaseViewEnd];
1084+
self.messageSizeCache.countLimit += kOTRMessagePageSize;
1085+
}
1086+
[self.viewHandler.mappings setRangeOptions:options forGroup:self.threadKey];
1087+
1088+
self.loadingMessages = YES;
1089+
1090+
CGFloat distanceToBottom = self.collectionView.contentSize.height - self.collectionView.contentOffset.y;
1091+
1092+
void (^doReload)() = ^{
1093+
[self.collectionView reloadData];
1094+
1095+
[self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
1096+
NSUInteger shownCount = [self.viewHandler.mappings numberOfItemsInGroup:self.threadKey];
1097+
NSUInteger totalCount = [[transaction ext:OTRFilteredChatDatabaseViewExtensionName] numberOfItemsInGroup:self.threadKey];
1098+
[self setShowLoadEarlierMessagesHeader:shownCount < totalCount];
1099+
}];
1100+
1101+
if (!reset) {
1102+
[self.collectionView layoutSubviews];
1103+
self.collectionView.contentOffset = CGPointMake(0, self.collectionView.contentSize.height - distanceToBottom);
1104+
}
1105+
1106+
self.loadingMessages = NO;
1107+
};
1108+
1109+
if (reset) {
1110+
doReload();
1111+
}
1112+
else {
1113+
JSQMessagesCollectionViewFlowLayout *layout = self.collectionView.collectionViewLayout;
1114+
1115+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
1116+
NSMutableArray *objects = [NSMutableArray arrayWithCapacity:kOTRMessagePageSize];
1117+
for (NSUInteger i = 0; i < kOTRMessagePageSize; i++) {
1118+
// Populating connection's cache in background, so when we call "reloadData" in the UI thread, the objects are returned much faster
1119+
[self.viewHandler object:[NSIndexPath indexPathForRow:i inSection:0]];
1120+
}
1121+
[objects enumerateObjectsWithOptions:NSEnumerationConcurrent
1122+
usingBlock:^(id <JSQMessageData> obj, NSUInteger idx, BOOL *stop) {
1123+
// The result of the heaviest calculation will remain in the calculator's internal cache, so the
1124+
// collectionView:layout:sizeForItemAtIndexPath: will work faster on the UI thread
1125+
[layout.bubbleSizeCalculator messageBubbleSizeForMessageData:obj
1126+
atIndexPath:nil
1127+
withLayout:layout];
1128+
}];
1129+
dispatch_async(dispatch_get_main_queue(), doReload);
1130+
});
1131+
}
10301132
}
10311133

10321134
- (BOOL)showDateAtIndexPath:(NSIndexPath *)indexPath
@@ -1323,6 +1425,22 @@ - (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)act
13231425
}
13241426
}
13251427

1428+
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
1429+
{
1430+
id <OTRMessageProtocol, JSQMessageData> message = [self messageAtIndexPath:indexPath];
1431+
1432+
NSNumber *key = @(message.messageHash);
1433+
NSValue *sizeValue = [self.messageSizeCache objectForKey:key];
1434+
if (sizeValue != nil) {
1435+
return [sizeValue CGSizeValue];
1436+
}
1437+
1438+
// Although JSQMessagesBubblesSizeCalculator has its own cache, its size is fixed and quite small, so it quickly chokes on scrolling into the past
1439+
CGSize size = [super collectionView:collectionView layout:collectionViewLayout sizeForItemAtIndexPath:indexPath];
1440+
[self.messageSizeCache setObject:[NSValue valueWithCGSize:size] forKey:key];
1441+
return size;
1442+
}
1443+
13261444
#pragma - mark UIPopoverPresentationControllerDelegate Methods
13271445

13281446
- (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController {
@@ -1404,6 +1522,22 @@ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
14041522
[self hideDropdownAnimated:YES completion:nil];
14051523
}
14061524

1525+
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
1526+
{
1527+
if (!self.loadingMessages) {
1528+
UIEdgeInsets insets = scrollView.contentInset;
1529+
CGFloat highestOffset = -insets.top;
1530+
CGFloat lowestOffset = scrollView.contentSize.height - scrollView.frame.size.height + insets.bottom;
1531+
CGFloat pos = scrollView.contentOffset.y;
1532+
1533+
if (self.showLoadEarlierMessagesHeader && (pos == highestOffset || pos < 0 && (scrollView.isDecelerating || scrollView.isDragging))) {
1534+
[self updateRangeOptions:NO];
1535+
} else if (pos == lowestOffset) {
1536+
[self updateRangeOptions:YES];
1537+
}
1538+
}
1539+
}
1540+
14071541
#pragma mark - UICollectionView DataSource
14081542
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
14091543
{
@@ -1457,32 +1591,59 @@ - (NSString *)senderDisplayName
14571591
return nil;
14581592
}
14591593

1460-
UIImage *avatarImage = nil;
14611594
NSError *messageError = [message messageError];
14621595
if ((messageError && !messageError.isAutomaticDownloadError) ||
14631596
![self isMessageTrusted:message]) {
1464-
avatarImage = [OTRImages circleWarningWithColor:[OTRColors warnColor]];
1597+
return [self warningAvatarImage];
14651598
}
1466-
else if ([message isMessageIncoming]) {
1467-
__block OTRBuddy *buddy = nil;
1468-
[self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1469-
buddy = [self buddyWithTransaction:transaction];
1599+
if ([message isMessageIncoming]) {
1600+
return [self buddyAvatarImage];
1601+
}
1602+
1603+
return [self accountAvatarImage];
1604+
}
1605+
1606+
- (JSQMessagesAvatarImage *)createAvatarImage:(UIImage *(^)(YapDatabaseReadTransaction *))getImage
1607+
{
1608+
__block UIImage *avatarImage;
1609+
[self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
1610+
avatarImage = getImage(transaction);
1611+
}];
1612+
if (avatarImage != nil) {
1613+
NSUInteger diameter = (NSUInteger) MIN(avatarImage.size.width, avatarImage.size.height);
1614+
return [JSQMessagesAvatarImageFactory avatarImageWithImage:avatarImage diameter:diameter];
1615+
}
1616+
return nil;
1617+
}
1618+
1619+
- (JSQMessagesAvatarImage *)warningAvatarImage
1620+
{
1621+
if (_warningAvatarImage == nil) {
1622+
_warningAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1623+
return [OTRImages circleWarningWithColor:[OTRColors warnColor]];
14701624
}];
1471-
avatarImage = [buddy avatarImage];
14721625
}
1473-
else {
1474-
__block OTRAccount *account = nil;
1475-
[self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
1476-
account = [self accountWithTransaction:transaction];
1626+
return _warningAvatarImage;
1627+
}
1628+
1629+
- (JSQMessagesAvatarImage *)accountAvatarImage
1630+
{
1631+
if (_accountAvatarImage == nil) {
1632+
_accountAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1633+
return [[self accountWithTransaction:transaction] avatarImage];
14771634
}];
1478-
avatarImage = [account avatarImage];
14791635
}
1480-
1481-
if (avatarImage) {
1482-
NSUInteger diameter = MIN(avatarImage.size.width, avatarImage.size.height);
1483-
return [JSQMessagesAvatarImageFactory avatarImageWithImage:avatarImage diameter:diameter];
1636+
return _accountAvatarImage;
1637+
}
1638+
1639+
- (JSQMessagesAvatarImage *)buddyAvatarImage
1640+
{
1641+
if (_buddyAvatarImage == nil) {
1642+
_buddyAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
1643+
return [[self buddyWithTransaction:transaction] avatarImage];
1644+
}];
14841645
}
1485-
return nil;
1646+
return _buddyAvatarImage;
14861647
}
14871648

14881649
////// Optional //////
@@ -1745,6 +1906,7 @@ - (void)didSetupMappings:(OTRYapViewHandler *)handler
17451906
{
17461907
// The databse view is setup now so refresh from there
17471908
[self updateViewWithKey:self.threadKey collection:self.threadCollection];
1909+
[self updateRangeOptions:YES];
17481910
[self.collectionView reloadData];
17491911
}
17501912

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
3+
<device id="retina4_7" orientation="portrait">
4+
<adaptation id="fullscreen"/>
5+
</device>
6+
<dependencies>
7+
<deployment identifier="iOS"/>
8+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
9+
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
10+
</dependencies>
11+
<objects>
12+
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
13+
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
14+
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="JSQMessagesLoadEarlierHeaderView" id="HXb-xw-ujM" customClass="JSQMessagesLoadEarlierHeaderView">
15+
<rect key="frame" x="0.0" y="0.0" width="320" height="32"/>
16+
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
17+
<subviews>
18+
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" animating="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="OgU-mr-PZv">
19+
<rect key="frame" x="150" y="6" width="20" height="20"/>
20+
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
21+
</activityIndicatorView>
22+
</subviews>
23+
<point key="canvasLocation" x="-182" y="30"/>
24+
</collectionReusableView>
25+
</objects>
26+
</document>

0 commit comments

Comments
 (0)