Lazy-load contact avatar data and images. Use NSCache for avatar images.

pull/1/head
Matthew Chen 7 years ago
parent af977ca409
commit 08ca4fdb50

@ -5134,7 +5134,7 @@ interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransiti
}
BOOL isProfileAvatar = NO;
NSData *_Nullable avatarImageData = contact.imageData;
NSData *_Nullable avatarImageData = [self.contactsManager avatarDataForCNContactId:cnContact.identifier];
for (NSString *recipientId in contact.textSecureIdentifiers) {
if (avatarImageData) {
break;

@ -78,7 +78,7 @@ class ContactCell: UITableViewCell {
titleLabel.attributedText = cnContact?.formattedFullName(font: titleLabel.font)
updateSubtitle(subtitleType: subtitleType, contact: contact)
if let contactImage = contact.image {
if let contactImage = contactsManager.avatarImage(forCNContactId: contact.cnContactId) {
contactImageView.image = contactImage
} else {
let contactIdForDeterminingBackgroundColor: String

@ -187,7 +187,7 @@ typedef void (^SendMessageBlock)(SendCompletionBlock completion);
}
BOOL isProfileAvatar = NO;
NSData *_Nullable avatarImageData = contact.imageData;
NSData *_Nullable avatarImageData = [self.contactsManager avatarDataForCNContactId:contact.cnContactId];
for (NSString *recipientId in contact.textSecureIdentifiers) {
if (avatarImageData) {
break;

@ -59,8 +59,6 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
// contacts haven't changed, and will clear out any stale cached SignalAccounts
- (void)userRequestedSystemContactsRefreshWithCompletion:(void (^)(NSError *_Nullable error))completionHandler;
- (nullable CNContact *)cnContactWithId:(nullable NSString *)contactId;
#pragma mark - Util
- (BOOL)isSystemContact:(NSString *)recipientId;

@ -37,6 +37,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
@property (nonatomic, readonly) YapDatabaseConnection *dbReadConnection;
@property (nonatomic, readonly) YapDatabaseConnection *dbWriteConnection;
@property (nonatomic, readonly) AnyLRUCache *cnContactCache;
@property (nonatomic, readonly) AnyLRUCache *cnContactAvatarCache;
@end
@ -62,6 +63,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
_systemContactsFetcher = [SystemContactsFetcher new];
_systemContactsFetcher.delegate = self;
_cnContactCache = [[AnyLRUCache alloc] initWithMaxSize:50];
_cnContactAvatarCache = [[AnyLRUCache alloc] initWithMaxSize:25];
OWSSingletonAssert();
@ -135,6 +137,8 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
return self.systemContactsFetcher.supportsContactEditing;
}
#pragma mark - CNContacts
- (nullable CNContact *)cnContactWithId:(nullable NSString *)contactId
{
OWSAssert(contactId.length > 0);
@ -158,6 +162,38 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
return cnContact;
}
- (nullable NSData *)avatarDataForCNContactId:(nullable NSString *)contactId
{
// Don't bother to cache avatar data.
CNContact *_Nullable cnContact = [self cnContactWithId:contactId];
return [Contact avatarDataForCNContact:cnContact];
}
- (nullable UIImage *)avatarImageForCNContactId:(nullable NSString *)contactId
{
OWSAssert(self.cnContactAvatarCache);
if (!contactId) {
return nil;
}
UIImage *_Nullable avatarImage;
@synchronized(self.cnContactAvatarCache) {
avatarImage = (UIImage * _Nullable)[self.cnContactAvatarCache getWithKey:contactId];
if (!avatarImage) {
NSData *_Nullable avatarData = [self avatarDataForCNContactId:contactId];
if (avatarData) {
avatarImage = [UIImage imageWithData:avatarData];
}
if (avatarImage) {
[self.cnContactAvatarCache setWithKey:contactId value:avatarImage];
}
}
}
return avatarImage;
}
#pragma mark - SystemContactsFetcherDelegate
- (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher
@ -257,6 +293,8 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
dispatch_async(dispatch_get_main_queue(), ^{
self.allContacts = contacts;
self.allContactsMap = [allContactsMap copy];
[self.cnContactCache clear];
[self.cnContactAvatarCache clear];
[self.avatarCache removeAllImages];
@ -791,7 +829,7 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
contact = [self signalAccountForRecipientId:identifier].contact;
}
return contact.image;
return [self avatarImageForCNContactId:contact.cnContactId];
}
- (nullable UIImage *)profileImageForPhoneIdentifier:(nullable NSString *)identifier

@ -5,7 +5,7 @@
@objc
public class AnyLRUCache: NSObject {
let backingCache: LRUCache<NSObject, NSObject>
private let backingCache: LRUCache<NSObject, NSObject>
@objc
public init(maxSize: Int) {
@ -21,6 +21,11 @@ public class AnyLRUCache: NSObject {
public func set(key: NSObject, value: NSObject) {
self.backingCache.set(key: key, value: value)
}
@objc
public func clear() {
self.backingCache.clear()
}
}
// A simple LRU cache bounded by the number of entries.
@ -47,8 +52,7 @@ public class LRUCache<KeyType: Hashable & Equatable, ValueType> {
@objc func didReceiveMemoryWarning() {
SwiftAssertIsOnMainThread(#function)
cacheMap.removeAll()
cacheOrder.removeAll()
clear()
}
private func updateCacheOrder(key: KeyType) {
@ -82,4 +86,10 @@ public class LRUCache<KeyType: Hashable & Equatable, ValueType> {
cacheMap.removeValue(forKey: staleKey)
}
}
@objc
public func clear() {
cacheMap.removeAll()
cacheOrder.removeAll()
}
}

@ -28,10 +28,6 @@ NS_ASSUME_NONNULL_BEGIN
@property (readonly, nonatomic) NSArray<NSString *> *emails;
@property (nonatomic, readonly) BOOL isSignalContact;
@property (nonatomic, readonly) NSString *cnContactId;
#if TARGET_OS_IOS
@property (nullable, readonly, nonatomic) UIImage *image;
@property (nullable, readonly, nonatomic) NSData *imageData;
#endif // TARGET_OS_IOS
- (NSArray<SignalRecipient *> *)signalRecipientsWithTransaction:(YapDatabaseReadTransaction *)transaction;
// TODO: Remove this method.
@ -39,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN
#if TARGET_OS_IOS
- (instancetype)initWithSystemContact:(CNContact *)contact NS_AVAILABLE_IOS(9_0);
- (instancetype)initWithSystemContact:(CNContact *)cnContact NS_AVAILABLE_IOS(9_0);
+ (nullable Contact *)contactWithVCardData:(NSData *)data;
+ (nullable CNContact *)cnContactWithVCardData:(NSData *)data;
@ -54,6 +50,8 @@ NS_ASSUME_NONNULL_BEGIN
+ (CNContact *)mergeCNContact:(CNContact *)oldCNContact
newCNContact:(CNContact *)newCNContact NS_SWIFT_NAME(merge(cnContact:newCNContact:));
+ (nullable NSData *)avatarDataForCNContact:(nullable CNContact *)cnContact;
@end
NS_ASSUME_NONNULL_END

@ -9,6 +9,7 @@
#import "PhoneNumber.h"
#import "SignalRecipient.h"
#import "TSAccountManager.h"
#import "TextSecureKitEnv.h"
@import Contacts;
@ -17,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface Contact ()
@property (nonatomic, readonly) NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap;
@property (nonatomic, readonly) NSUInteger imageHash;
@end
@ -26,25 +28,24 @@ NS_ASSUME_NONNULL_BEGIN
@synthesize comparableNameFirstLast = _comparableNameFirstLast;
@synthesize comparableNameLastFirst = _comparableNameLastFirst;
@synthesize image = _image;
#if TARGET_OS_IOS
- (instancetype)initWithSystemContact:(CNContact *)contact
- (instancetype)initWithSystemContact:(CNContact *)cnContact
{
self = [super init];
if (!self) {
return self;
}
_cnContactId = contact.identifier;
_firstName = contact.givenName.ows_stripped;
_lastName = contact.familyName.ows_stripped;
_fullName = [Contact formattedFullNameWithCNContact:contact];
_cnContactId = cnContact.identifier;
_firstName = cnContact.givenName.ows_stripped;
_lastName = cnContact.familyName.ows_stripped;
_fullName = [Contact formattedFullNameWithCNContact:cnContact];
NSMutableArray<NSString *> *phoneNumbers = [NSMutableArray new];
NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap = [NSMutableDictionary new];
for (CNLabeledValue *phoneNumberField in contact.phoneNumbers) {
for (CNLabeledValue *phoneNumberField in cnContact.phoneNumbers) {
if ([phoneNumberField.value isKindOfClass:[CNPhoneNumber class]]) {
CNPhoneNumber *phoneNumber = (CNPhoneNumber *)phoneNumberField.value;
[phoneNumbers addObject:phoneNumber.stringValue];
@ -96,18 +97,21 @@ NS_ASSUME_NONNULL_BEGIN
[self parsedPhoneNumbersFromUserTextPhoneNumbers:phoneNumbers phoneNumberNameMap:phoneNumberNameMap];
NSMutableArray<NSString *> *emailAddresses = [NSMutableArray new];
for (CNLabeledValue *emailField in contact.emailAddresses) {
for (CNLabeledValue *emailField in cnContact.emailAddresses) {
if ([emailField.value isKindOfClass:[NSString class]]) {
[emailAddresses addObject:(NSString *)emailField.value];
}
}
_emails = [emailAddresses copy];
if (contact.thumbnailImageData) {
_imageData = [contact.thumbnailImageData copy];
} else if (contact.imageData) {
// This only occurs when sharing a contact via the share extension
_imageData = [contact.imageData copy];
NSData *_Nullable avatarData = [Contact avatarDataForCNContact:cnContact];
if (avatarData) {
NSUInteger hashValue = 0;
NSData *hashData = [Cryptography computeSHA256Digest:avatarData truncatedToBytes:sizeof(hashValue)];
[hashData getBytes:&hashValue length:sizeof(hashValue)];
_imageHash = hashValue;
} else {
_imageHash = 0;
}
return self;
@ -124,29 +128,6 @@ NS_ASSUME_NONNULL_BEGIN
return [[self alloc] initWithSystemContact:cnContact];
}
- (nullable UIImage *)image
{
if (_image) {
return _image;
}
if (!self.imageData) {
return nil;
}
_image = [UIImage imageWithData:self.imageData];
return _image;
}
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey
{
if ([propertyKey isEqualToString:@"cnContact"] || [propertyKey isEqualToString:@"image"]) {
return MTLPropertyStorageTransitory;
} else {
return [super storageBehaviorForPropertyWithKey:propertyKey];
}
}
#endif // TARGET_OS_IOS
- (NSArray<PhoneNumber *> *)parsedPhoneNumbersFromUserTextPhoneNumbers:(NSArray<NSString *> *)userTextPhoneNumbers
@ -277,6 +258,20 @@ NS_ASSUME_NONNULL_BEGIN
return value;
}
+ (nullable NSData *)avatarDataForCNContact:(nullable CNContact *)cnContact
{
if (cnContact.thumbnailImageData) {
return cnContact.thumbnailImageData.copy;
} else if (cnContact.imageData) {
// This only occurs when sharing a contact via the share extension
return cnContact.imageData.copy;
} else {
return nil;
}
}
// This method is used to de-bounce system contact fetch notifications
// by checking for changes in the contact data.
- (NSUInteger)hash
{
// base hash is some arbitrary number
@ -284,13 +279,7 @@ NS_ASSUME_NONNULL_BEGIN
hash = hash ^ self.fullName.hash;
if (self.imageData) {
NSUInteger thumbnailHash = 0;
NSData *thumbnailHashData =
[Cryptography computeSHA256Digest:self.imageData truncatedToBytes:sizeof(thumbnailHash)];
[thumbnailHashData getBytes:&thumbnailHash length:sizeof(thumbnailHash)];
hash = hash ^ thumbnailHash;
}
hash = hash ^ self.imageHash;
for (PhoneNumber *phoneNumber in self.parsedPhoneNumbers) {
hash = hash ^ phoneNumber.toE164.hash;

@ -1,19 +1,22 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSChunkedOutputStream.h"
NS_ASSUME_NONNULL_BEGIN
@class SignalAccount;
@class OWSRecipientIdentity;
@class SignalAccount;
@protocol ContactsManagerProtocol;
@interface OWSContactsOutputStream : OWSChunkedOutputStream
- (void)writeSignalAccount:(SignalAccount *)signalAccount
recipientIdentity:(nullable OWSRecipientIdentity *)recipientIdentity
profileKeyData:(nullable NSData *)profileKeyData;
profileKeyData:(nullable NSData *)profileKeyData
contactsManager:(id<ContactsManagerProtocol>)contactsManager;
@end

@ -4,6 +4,7 @@
#import "OWSContactsOutputStream.h"
#import "Contact.h"
#import "ContactsManagerProtocol.h"
#import "Cryptography.h"
#import "MIMETypeUtil.h"
#import "NSData+keyVersionByte.h"
@ -22,9 +23,11 @@ NS_ASSUME_NONNULL_BEGIN
- (void)writeSignalAccount:(SignalAccount *)signalAccount
recipientIdentity:(nullable OWSRecipientIdentity *)recipientIdentity
profileKeyData:(nullable NSData *)profileKeyData
contactsManager:(id<ContactsManagerProtocol>)contactsManager
{
OWSAssert(signalAccount);
OWSAssert(signalAccount.contact);
OWSAssert(contactsManager);
OWSSignalServiceProtosContactDetailsBuilder *contactBuilder = [OWSSignalServiceProtosContactDetailsBuilder new];
[contactBuilder setName:signalAccount.contact.fullName];
@ -38,15 +41,18 @@ NS_ASSUME_NONNULL_BEGIN
contactBuilder.verifiedBuilder = verifiedBuilder;
}
NSData *avatarPng;
if (signalAccount.contact.image) {
OWSSignalServiceProtosContactDetailsAvatarBuilder *avatarBuilder =
[OWSSignalServiceProtosContactDetailsAvatarBuilder new];
[avatarBuilder setContentType:OWSMimeTypeImagePng];
avatarPng = UIImagePNGRepresentation(signalAccount.contact.image);
[avatarBuilder setLength:(uint32_t)avatarPng.length];
[contactBuilder setAvatarBuilder:avatarBuilder];
UIImage *_Nullable rawAvatar = [contactsManager avatarImageForCNContactId:signalAccount.contact.cnContactId];
NSData *_Nullable avatarPng;
if (rawAvatar) {
avatarPng = UIImagePNGRepresentation(rawAvatar);
if (avatarPng) {
OWSSignalServiceProtosContactDetailsAvatarBuilder *avatarBuilder =
[OWSSignalServiceProtosContactDetailsAvatarBuilder new];
[avatarBuilder setContentType:OWSMimeTypeImagePng];
[avatarBuilder setLength:(uint32_t)avatarPng.length];
[contactBuilder setAvatarBuilder:avatarBuilder];
}
}
if (profileKeyData) {
@ -79,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN
[self.delegateStream writeRawVarint32:contactDataLength];
[self.delegateStream writeRawData:contactData];
if (signalAccount.contact.image) {
if (avatarPng) {
[self.delegateStream writeRawData:avatarPng];
}
}

@ -13,6 +13,7 @@
#import "SignalAccount.h"
#import "TSAttachment.h"
#import "TSAttachmentStream.h"
#import "TextSecureKitEnv.h"
NS_ASSUME_NONNULL_BEGIN
@ -71,6 +72,8 @@ NS_ASSUME_NONNULL_BEGIN
- (NSData *)buildPlainTextAttachmentData
{
id<ContactsManagerProtocol> contactsManager = TextSecureKitEnv.sharedEnv.contactsManager;
// TODO use temp file stream to avoid loading everything into memory at once
// First though, we need to re-engineer our attachment process to accept streams (encrypting with stream,
// and uploading with streams).
@ -82,10 +85,11 @@ NS_ASSUME_NONNULL_BEGIN
OWSRecipientIdentity *_Nullable recipientIdentity =
[self.identityManager recipientIdentityForRecipientId:signalAccount.recipientId];
NSData *_Nullable profileKeyData = [self.profileManager profileKeyDataForRecipientId:signalAccount.recipientId];
[contactsOutputStream writeSignalAccount:signalAccount
recipientIdentity:recipientIdentity
profileKeyData:profileKeyData];
profileKeyData:profileKeyData
contactsManager:contactsManager];
}
[contactsOutputStream flush];

@ -4,6 +4,7 @@
NS_ASSUME_NONNULL_BEGIN
@class CNContact;
@class Contact;
@class PhoneNumber;
@class SignalAccount;
@ -20,6 +21,12 @@ NS_ASSUME_NONNULL_BEGIN
- (NSComparisonResult)compareSignalAccount:(SignalAccount *)left
withSignalAccount:(SignalAccount *)right NS_SWIFT_NAME(compare(signalAccount:with:));
#pragma mark - CNContacts
- (nullable CNContact *)cnContactWithId:(nullable NSString *)contactId;
- (nullable NSData *)avatarDataForCNContactId:(nullable NSString *)contactId;
- (nullable UIImage *)avatarImageForCNContactId:(nullable NSString *)contactId;
@end
NS_ASSUME_NONNULL_END

Loading…
Cancel
Save