Skip to content

Commit 9124877

Browse files
committed
Fix and optimize the logic for maintaining the internal priority queue NSCache.
Currently when multiple objects are set into an NSCache object, the internal priority queue is not maintained correctly. The logic for maintaining the queue is correct now.
1 parent d893611 commit 9124877

File tree

2 files changed

+121
-76
lines changed

2 files changed

+121
-76
lines changed

Foundation/NSCache.swift

Lines changed: 79 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ open class NSCache<KeyType : AnyObject, ObjectType : AnyObject> : NSObject {
5858
private var _entries = Dictionary<NSCacheKey, NSCacheEntry<KeyType, ObjectType>>()
5959
private let _lock = NSLock()
6060
private var _totalCost = 0
61-
private var _byCost: NSCacheEntry<KeyType, ObjectType>?
61+
private var _head: NSCacheEntry<KeyType, ObjectType>?
6262

6363
open var name: String = ""
6464
open var totalCostLimit: Int = 0 // limits are imprecise/not strict
@@ -90,103 +90,107 @@ open class NSCache<KeyType : AnyObject, ObjectType : AnyObject> : NSObject {
9090
private func remove(_ entry: NSCacheEntry<KeyType, ObjectType>) {
9191
let oldPrev = entry.prevByCost
9292
let oldNext = entry.nextByCost
93+
9394
oldPrev?.nextByCost = oldNext
9495
oldNext?.prevByCost = oldPrev
95-
if entry === _byCost {
96-
_byCost = entry.nextByCost
96+
97+
if entry === _head {
98+
_head = oldNext
9799
}
98100
}
99101

100102
private func insert(_ entry: NSCacheEntry<KeyType, ObjectType>) {
101-
if _byCost == nil {
102-
_byCost = entry
103-
} else {
104-
var element = _byCost
105-
while let e = element {
106-
if e.cost > entry.cost {
107-
let newPrev = e.prevByCost
108-
entry.prevByCost = newPrev
109-
entry.nextByCost = e
110-
break
111-
}
112-
element = e.nextByCost
113-
}
103+
guard var currentElement = _head else {
104+
// The cache is empty
105+
entry.prevByCost = nil
106+
entry.nextByCost = nil
107+
108+
_head = entry
109+
return
110+
}
111+
112+
guard entry.cost > currentElement.cost else {
113+
// Insert entry at the head
114+
entry.prevByCost = nil
115+
entry.nextByCost = currentElement
116+
currentElement.prevByCost = entry
117+
118+
_head = entry
119+
return
114120
}
121+
122+
while currentElement.nextByCost != nil && currentElement.nextByCost!.cost < entry.cost {
123+
currentElement = currentElement.nextByCost!
124+
}
125+
126+
// Insert entry between currentElement and nextElement
127+
let nextElement = currentElement.nextByCost
128+
129+
currentElement.nextByCost = entry
130+
entry.prevByCost = currentElement
131+
132+
entry.nextByCost = nextElement
133+
nextElement?.prevByCost = entry
115134
}
116135

117136
open func setObject(_ obj: ObjectType, forKey key: KeyType, cost g: Int) {
137+
let g = max(g, 0)
118138
let keyRef = NSCacheKey(key)
119139

120140
_lock.lock()
121-
_totalCost += g
122-
123-
var purgeAmount = 0
124-
if totalCostLimit > 0 {
125-
purgeAmount = (_totalCost + g) - totalCostLimit
126-
}
127141

128-
var purgeCount = 0
129-
if countLimit > 0 {
130-
purgeCount = (_entries.count + 1) - countLimit
131-
}
142+
let costDiff: Int
132143

133144
if let entry = _entries[keyRef] {
145+
costDiff = g - entry.cost
146+
entry.cost = g
147+
134148
entry.value = obj
135-
if entry.cost != g {
136-
entry.cost = g
149+
150+
if costDiff != 0 {
137151
remove(entry)
138152
insert(entry)
139153
}
140154
} else {
141155
let entry = NSCacheEntry(key: key, value: obj, cost: g)
142156
_entries[keyRef] = entry
143157
insert(entry)
158+
159+
costDiff = g
144160
}
145-
_lock.unlock()
146161

147-
var toRemove = [NSCacheEntry<KeyType, ObjectType>]()
148-
149-
if purgeAmount > 0 {
150-
_lock.lock()
151-
while _totalCost - totalCostLimit > 0 {
152-
if let entry = _byCost {
153-
_totalCost -= entry.cost
154-
toRemove.append(entry)
155-
remove(entry)
156-
} else {
157-
break
158-
}
159-
}
160-
if countLimit > 0 {
161-
purgeCount = (_entries.count - toRemove.count) - countLimit
162-
}
163-
_lock.unlock()
164-
}
162+
_totalCost += costDiff
165163

166-
if purgeCount > 0 {
167-
_lock.lock()
168-
while (_entries.count - toRemove.count) - countLimit > 0 {
169-
if let entry = _byCost {
170-
_totalCost -= entry.cost
171-
toRemove.append(entry)
172-
remove(entry)
173-
} else {
174-
break
175-
}
164+
var purgeAmount = (totalCostLimit > 0) ? (_totalCost - totalCostLimit) : 0
165+
while purgeAmount > 0 {
166+
if let entry = _head {
167+
delegate?.cache(unsafeDowncast(self, to:NSCache<AnyObject, AnyObject>.self), willEvictObject: entry.value)
168+
169+
_totalCost -= entry.cost
170+
purgeAmount -= entry.cost
171+
172+
remove(entry) // _head will be changed to next entry in remove(_:)
173+
_entries[NSCacheKey(entry.key)] = nil
174+
} else {
175+
break
176176
}
177-
_lock.unlock()
178177
}
179178

180-
if let del = delegate {
181-
for entry in toRemove {
182-
del.cache(unsafeDowncast(self, to:NSCache<AnyObject, AnyObject>.self), willEvictObject: entry.value)
179+
var purgeCount = (countLimit > 0) ? (_entries.count - countLimit) : 0
180+
while purgeCount > 0 {
181+
if let entry = _head {
182+
delegate?.cache(unsafeDowncast(self, to:NSCache<AnyObject, AnyObject>.self), willEvictObject: entry.value)
183+
184+
_totalCost -= entry.cost
185+
purgeCount -= 1
186+
187+
remove(entry) // _head will be changed to next entry in remove(_:)
188+
_entries[NSCacheKey(entry.key)] = nil
189+
} else {
190+
break
183191
}
184192
}
185193

186-
_lock.lock()
187-
for entry in toRemove {
188-
_entries.removeValue(forKey: NSCacheKey(entry.key)) // the cost list is already fixed up in the purge routines
189-
}
190194
_lock.unlock()
191195
}
192196

@@ -204,7 +208,16 @@ open class NSCache<KeyType : AnyObject, ObjectType : AnyObject> : NSObject {
204208
open func removeAllObjects() {
205209
_lock.lock()
206210
_entries.removeAll()
207-
_byCost = nil
211+
212+
while let currentElement = _head {
213+
let nextElement = currentElement.nextByCost
214+
215+
currentElement.prevByCost = nil
216+
currentElement.nextByCost = nil
217+
218+
_head = nextElement
219+
}
220+
208221
_totalCost = 0
209222
_lock.unlock()
210223
}

TestFoundation/TestNSCache.swift

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class TestNSCache : XCTestCase {
2424
("test_costLimit", test_costLimit),
2525
("test_countLimit", test_countLimit),
2626
("test_hashableKey", test_hashableKey),
27-
("test_nonHashableKey", test_nonHashableKey)
27+
("test_nonHashableKey", test_nonHashableKey),
28+
("test_objectCorrectlyReleased", test_objectCorrectlyReleased)
2829
]
2930
}
3031

@@ -99,9 +100,9 @@ class TestNSCache : XCTestCase {
99100
let key3 = NSString(string: "key3")
100101
let value = NSString(string: "value")
101102

102-
cache.setObject(value, forKey: key1)
103-
cache.setObject(value, forKey: key2)
104-
cache.setObject(value, forKey: key3)
103+
cache.setObject(value, forKey: key1, cost: 1)
104+
cache.setObject(value, forKey: key2, cost: 2)
105+
cache.setObject(value, forKey: key3, cost: 3)
105106

106107
XCTAssertEqual(cache.object(forKey: key2), value, "should be equal to \(value)")
107108
XCTAssertEqual(cache.object(forKey: key3), value, "should be equal to \(value)")
@@ -134,9 +135,9 @@ class TestNSCache : XCTestCase {
134135
let key3 = TestHashableCacheKey(string: "key3")
135136
let value = NSString(string: "value")
136137

137-
cache.setObject(value, forKey: key1)
138-
cache.setObject(value, forKey: key2)
139-
cache.setObject(value, forKey: key3)
138+
cache.setObject(value, forKey: key1, cost: 1)
139+
cache.setObject(value, forKey: key2, cost: 2)
140+
cache.setObject(value, forKey: key3, cost: 3)
140141

141142
XCTAssertEqual(cache.object(forKey: key2), value, "should be equal to \(value)")
142143
XCTAssertEqual(cache.object(forKey: key3), value, "should be equal to \(value)")
@@ -162,12 +163,43 @@ class TestNSCache : XCTestCase {
162163
let key3 = TestCacheKey(string: "key3")
163164
let value = NSString(string: "value")
164165

165-
cache.setObject(value, forKey: key1)
166-
cache.setObject(value, forKey: key2)
167-
cache.setObject(value, forKey: key3)
166+
cache.setObject(value, forKey: key1, cost: 1)
167+
cache.setObject(value, forKey: key2, cost: 2)
168+
cache.setObject(value, forKey: key3, cost: 3)
168169

169170
XCTAssertEqual(cache.object(forKey: key2), value, "should be equal to \(value)")
170171
XCTAssertEqual(cache.object(forKey: key3), value, "should be equal to \(value)")
171172
XCTAssertNil(cache.object(forKey: key1), "should be nil")
172173
}
174+
175+
func test_objectCorrectlyReleased() {
176+
let cache = NSCache<NSString, AnyObject>()
177+
cache.totalCostLimit = 10
178+
179+
var object1 = NSObject()
180+
weak var weakObject1: NSObject? = object1
181+
182+
var object2 = NSObject()
183+
weak var weakObject2: NSObject? = object2
184+
185+
var object3 = NSObject()
186+
weak var weakObject3: NSObject? = object3
187+
188+
let object4 = NSObject()
189+
let object5 = NSObject()
190+
191+
cache.setObject(object1, forKey: "key1", cost: 1)
192+
cache.setObject(object2, forKey: "key2", cost: 2)
193+
cache.setObject(object3, forKey: "key3", cost: 3)
194+
cache.setObject(object4, forKey: "key4", cost: 4)
195+
cache.setObject(object5, forKey: "key5", cost: 5)
196+
197+
object1 = NSObject()
198+
object2 = NSObject()
199+
object3 = NSObject()
200+
201+
XCTAssertNil(weakObject1, "removed cached object not released")
202+
XCTAssertNil(weakObject2, "removed cached object not released")
203+
XCTAssertNil(weakObject3, "removed cached object not released")
204+
}
173205
}

0 commit comments

Comments
 (0)