properly clear all cached avatar images

// FREEBIE
This commit is contained in:
Michael Kirk 2017-08-24 18:47:11 -04:00
parent b579ea5915
commit f6720f9afa
6 changed files with 138 additions and 13 deletions

View File

@ -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 = "<group>"; };
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 = "<group>"; };
455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCacheTest.swift; sourceTree = "<group>"; };
45638BDB1F3DD0D400128435 /* DebugUICalling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugUICalling.swift; sourceTree = "<group>"; };
45638BDE1F3DDB2200128435 /* MessageSender+Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Promise.swift"; sourceTree = "<group>"; };
4563ADF01F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWS106EnsureProfileComplete.swift; sourceTree = "<group>"; };
@ -1439,6 +1444,7 @@
76EB04FB18170B33006006FC /* Util.h */,
45F170D51E315310003FC1F2 /* Weak.swift */,
45F170CB1E310E22003FC1F2 /* WeakTimer.swift */,
455AC69A1F4F79E500134004 /* ImageCache.swift */,
);
path = util;
sourceTree = "<group>";
@ -1639,6 +1645,7 @@
B660F6B31C29868000687D6E /* UtilTest.h */,
B660F6B41C29868000687D6E /* UtilTest.m */,
45666F571D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m */,
455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */,
);
path = util;
sourceTree = "<group>";
@ -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;

View File

@ -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;
}

View File

@ -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<NSString *, UIImage *> *avatarCache;
@property (nonnull, readonly) ImageCache *avatarCache;
@property (atomic, readonly) NSArray<Contact *> *allContacts;

View File

@ -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<Contact *> *)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), ^{

View File

@ -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<AnyObject, ImageCacheRecord>
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)
}
}

View File

@ -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))
}
}