diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index ce497d109..e288fa0b4 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -148,6 +148,9 @@ 454EBAB41F2BE14C00ACE0BB /* OWSAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */; }; 455A16DD1F1FEA0000F86704 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 455A16DB1F1FEA0000F86704 /* Metal.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 455A16DE1F1FEA0000F86704 /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 455A16DC1F1FEA0000F86704 /* MetalKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 455AC69B1F4F79E500134004 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455AC69A1F4F79E500134004 /* ImageCache.swift */; }; + 455AC69C1F4F79E500134004 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455AC69A1F4F79E500134004 /* ImageCache.swift */; }; + 455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */; }; 45638BDC1F3DD0D400128435 /* DebugUICalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45638BDB1F3DD0D400128435 /* DebugUICalling.swift */; }; 45638BDF1F3DDB2200128435 /* MessageSender+Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45638BDE1F3DDB2200128435 /* MessageSender+Promise.swift */; }; 4563ADF11F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4563ADF01F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift */; }; @@ -602,6 +605,8 @@ 454B35071D08EED80026D658 /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = translations/mk.lproj/Localizable.strings; sourceTree = ""; }; 455A16DB1F1FEA0000F86704 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; 455A16DC1F1FEA0000F86704 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; }; + 455AC69A1F4F79E500134004 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; + 455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCacheTest.swift; sourceTree = ""; }; 45638BDB1F3DD0D400128435 /* DebugUICalling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugUICalling.swift; sourceTree = ""; }; 45638BDE1F3DDB2200128435 /* MessageSender+Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Promise.swift"; sourceTree = ""; }; 4563ADF01F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWS106EnsureProfileComplete.swift; sourceTree = ""; }; @@ -1439,6 +1444,7 @@ 76EB04FB18170B33006006FC /* Util.h */, 45F170D51E315310003FC1F2 /* Weak.swift */, 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */, + 455AC69A1F4F79E500134004 /* ImageCache.swift */, ); path = util; sourceTree = ""; @@ -1639,6 +1645,7 @@ B660F6B31C29868000687D6E /* UtilTest.h */, B660F6B41C29868000687D6E /* UtilTest.m */, 45666F571D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m */, + 455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */, ); path = util; sourceTree = ""; @@ -2194,6 +2201,7 @@ 34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */, 344F2F671E57A932000D9322 /* UIViewController+OWS.m in Sources */, B6DA6B071B8A2F9A00CA6F98 /* AppStoreRating.m in Sources */, + 455AC69B1F4F79E500134004 /* ImageCache.swift in Sources */, 451A13B11E13DED2000A50FD /* CallNotificationsAdapter.swift in Sources */, 3456710A1E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m in Sources */, 450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */, @@ -2434,6 +2442,7 @@ B660F7771C29988E00687D6E /* UIImage+normalizeImage.m in Sources */, 954AEE6A1DF33E01002E5410 /* ContactsPickerTest.swift in Sources */, B660F77B1C29988E00687D6E /* Queue.m in Sources */, + 455AC69C1F4F79E500134004 /* ImageCache.swift in Sources */, 45666F581D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m in Sources */, B660F77F1C29988E00687D6E /* DateUtil.m in Sources */, B660F7811C29988E00687D6E /* FunctionalUtil.m in Sources */, @@ -2462,6 +2471,7 @@ B660F6D21C29868000687D6E /* PushManagerTest.m in Sources */, 45C0DC1F1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */, 4505C2C01E648EA300CEBF41 /* ExperienceUpgrade.swift in Sources */, + 455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */, 450873C81D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2554,7 +2564,11 @@ "DEBUG=1", "$(inherited)", ); - "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = "DEBUG=1 $(inherited) SSK_BUILDING_FOR_TESTS=1"; + "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = ( + "DEBUG=1", + "$(inherited)", + "SSK_BUILDING_FOR_TESTS=1", + ); GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Signal/src/Models/OWSContactAvatarBuilder.m b/Signal/src/Models/OWSContactAvatarBuilder.m index 2211ee870..910b9aace 100644 --- a/Signal/src/Models/OWSContactAvatarBuilder.m +++ b/Signal/src/Models/OWSContactAvatarBuilder.m @@ -4,6 +4,7 @@ #import "OWSContactAvatarBuilder.h" #import "OWSContactsManager.h" +#import "Signal-Swift.h" #import "TSContactThread.h" #import "TSGroupThread.h" #import "TSThread.h" @@ -76,8 +77,8 @@ NS_ASSUME_NONNULL_BEGIN - (UIImage *)buildDefaultImage { - NSString *cacheKey = [NSString stringWithFormat:@"signalId:%@:diamater:%lu", self.signalId, (unsigned long)self.diameter]; - UIImage *cachedAvatar = [self.contactsManager.avatarCache objectForKey:cacheKey]; + UIImage *cachedAvatar = + [self.contactsManager.avatarCache imageForKey:self.signalId diameter:(CGFloat)self.diameter]; if (cachedAvatar) { return cachedAvatar; } @@ -115,7 +116,8 @@ NS_ASSUME_NONNULL_BEGIN textColor:[UIColor whiteColor] font:[UIFont ows_boldFontWithSize:fontSize] diameter:self.diameter] avatarImage]; - [self.contactsManager.avatarCache setObject:image forKey:cacheKey]; + + [self.contactsManager.avatarCache setImage:image forKey:self.signalId diameter:self.diameter]; return image; } diff --git a/Signal/src/contact/OWSContactsManager.h b/Signal/src/contact/OWSContactsManager.h index 4cbc6fcc4..8d8e09a7c 100644 --- a/Signal/src/contact/OWSContactsManager.h +++ b/Signal/src/contact/OWSContactsManager.h @@ -11,6 +11,7 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification; @class UIFont; @class SignalAccount; +@class ImageCache; /** * Get latest Signal contacts, and be notified when they change. @@ -23,7 +24,7 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification; #pragma mark - Accessors -@property (nonnull, readonly) NSCache *avatarCache; +@property (nonnull, readonly) ImageCache *avatarCache; @property (atomic, readonly) NSArray *allContacts; diff --git a/Signal/src/contact/OWSContactsManager.m b/Signal/src/contact/OWSContactsManager.m index acfc16157..6c05bc0ce 100644 --- a/Signal/src/contact/OWSContactsManager.m +++ b/Signal/src/contact/OWSContactsManager.m @@ -50,7 +50,7 @@ NSString *const kTSStorageManager_AccountLastNames = @"kTSStorageManager_Account } // TODO: We need to configure the limits of this cache. - _avatarCache = [NSCache new]; + _avatarCache = [ImageCache new]; _allContacts = @[]; _signalAccountMap = @{}; _signalAccounts = @[]; @@ -151,7 +151,7 @@ NSString *const kTSStorageManager_AccountLastNames = @"kTSStorageManager_Account NSString *recipientId = notification.userInfo[kNSNotificationKey_ProfileRecipientId]; OWSAssert(recipientId.length > 0); - [self clearAvatarCacheForRecipientId:recipientId]; + [self.avatarCache removeAllImagesForKey:recipientId]; } - (void)updateWithContacts:(NSArray *)contacts @@ -172,7 +172,7 @@ NSString *const kTSStorageManager_AccountLastNames = @"kTSStorageManager_Account self.allContacts = contacts; self.allContactsMap = [allContactsMap copy]; - [self.avatarCache removeAllObjects]; + [self.avatarCache removeAllImages]; [self intersectContacts]; @@ -183,11 +183,6 @@ NSString *const kTSStorageManager_AccountLastNames = @"kTSStorageManager_Account }); } -- (void)clearAvatarCacheForRecipientId:(NSString *)recipientId -{ - [self.avatarCache removeObjectForKey:recipientId]; -} - - (void)updateSignalAccounts { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ diff --git a/Signal/src/util/ImageCache.swift b/Signal/src/util/ImageCache.swift new file mode 100644 index 000000000..005ee82bb --- /dev/null +++ b/Signal/src/util/ImageCache.swift @@ -0,0 +1,53 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation +import UIKit + +class ImageCacheRecord: NSObject { + var variations: [CGFloat: UIImage] + init(variations: [CGFloat: UIImage]) { + self.variations = variations + } +} + +/** + * A two dimensional hash, allowing you to store variations under a single key. + * This is useful because we generate multiple diameters of an image, but when we + * want to clear out the images for a key we want to clear out *all* variations. + */ +@objc +class ImageCache: NSObject { + + let backingCache: NSCache + + override init() { + self.backingCache = NSCache() + } + + func image(forKey key: AnyObject, diameter: CGFloat) -> UIImage? { + guard let record = backingCache.object(forKey: key) else { + return nil + } + return record.variations[diameter] + } + + func setImage(_ image: UIImage, forKey key: AnyObject, diameter: CGFloat) { + if let existingRecord = backingCache.object(forKey: key) { + existingRecord.variations[diameter] = image + backingCache.setObject(existingRecord, forKey: key) + } else { + let newRecord = ImageCacheRecord(variations: [diameter: image]) + backingCache.setObject(newRecord, forKey: key) + } + } + + func removeAllImages() { + backingCache.removeAllObjects() + } + + func removeAllImages(forKey key: AnyObject) { + backingCache.removeObject(forKey: key) + } +} diff --git a/Signal/test/util/ImageCacheTest.swift b/Signal/test/util/ImageCacheTest.swift new file mode 100644 index 000000000..5837420ff --- /dev/null +++ b/Signal/test/util/ImageCacheTest.swift @@ -0,0 +1,60 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import XCTest + +class ImageCacheTest: XCTestCase { + + var imageCache: ImageCache! + + let firstVariation = UIImage() + let secondVariation = UIImage() + let otherImage = UIImage() + + let cacheKey1 = "cache-key-1" as NSString + let cacheKey2 = "cache-key-2" as NSString + + override func setUp() { + super.setUp() + self.imageCache = ImageCache() + imageCache.setImage(firstVariation, forKey:cacheKey1, diameter:100) + imageCache.setImage(secondVariation, forKey:cacheKey1, diameter:200) + imageCache.setImage(otherImage, forKey:cacheKey2, diameter:100) + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testSetGet() { + XCTAssertEqual(firstVariation, imageCache.image(forKey:cacheKey1, diameter: 100)) + XCTAssertEqual(secondVariation, imageCache.image(forKey:cacheKey1, diameter: 200)) + XCTAssertNotEqual(secondVariation, imageCache.image(forKey:cacheKey1, diameter: 100)) + XCTAssertEqual(otherImage, imageCache.image(forKey:cacheKey2, diameter: 100)) + XCTAssertNil(imageCache.image(forKey:cacheKey2, diameter: 200)) + } + + func testRemoveAllForKey() { + // sanity check + XCTAssertEqual(firstVariation, imageCache.image(forKey:cacheKey1, diameter: 100)) + XCTAssertEqual(otherImage, imageCache.image(forKey:cacheKey2, diameter: 100)) + + imageCache.removeAllImages(forKey:cacheKey1) + + XCTAssertNil(imageCache.image(forKey:cacheKey1, diameter: 100)) + XCTAssertNil(imageCache.image(forKey:cacheKey1, diameter: 200)) + XCTAssertEqual(otherImage, imageCache.image(forKey:cacheKey2, diameter: 100)) + } + + func testRemoveAll() { + XCTAssertEqual(firstVariation, imageCache.image(forKey:cacheKey1, diameter: 100)) + + imageCache.removeAllImages() + + XCTAssertNil(imageCache.image(forKey:cacheKey1, diameter: 100)) + XCTAssertNil(imageCache.image(forKey:cacheKey1, diameter: 200)) + XCTAssertNil(imageCache.image(forKey:cacheKey2, diameter: 100)) + } +}