1 | // Copyright (C) 2016 The Qt Company Ltd. |
---|---|
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qpixmapcache.h" |
5 | #include "qobject.h" |
6 | #include "qdebug.h" |
7 | #include "qpixmapcache_p.h" |
8 | #include "qthread.h" |
9 | #include "qcoreapplication.h" |
10 | |
11 | using namespace std::chrono_literals; |
12 | |
13 | QT_BEGIN_NAMESPACE |
14 | |
15 | /*! |
16 | \class QPixmapCache |
17 | \inmodule QtGui |
18 | |
19 | \brief The QPixmapCache class provides an application-wide cache for pixmaps. |
20 | |
21 | This class is a tool for optimized drawing with QPixmap. You can |
22 | use it to store temporary pixmaps that are expensive to generate |
23 | without using more storage space than cacheLimit(). Use insert() |
24 | to insert pixmaps, find() to find them, and clear() to empty the |
25 | cache. |
26 | |
27 | QPixmapCache contains no member data, only static functions to |
28 | access the global pixmap cache. It creates an internal QCache |
29 | object for caching the pixmaps. |
30 | |
31 | The cache associates a pixmap with a user-provided string as a key, |
32 | or with a QPixmapCache::Key that the cache generates. |
33 | Using QPixmapCache::Key for keys is faster than using strings. The string API is |
34 | very convenient for complex keys but the QPixmapCache::Key API will be very |
35 | efficient and convenient for a one-to-one object-to-pixmap mapping - in |
36 | this case, you can store the keys as members of an object. |
37 | |
38 | If two pixmaps are inserted into the cache using equal keys then the |
39 | last pixmap will replace the first pixmap in the cache. This follows the |
40 | behavior of the QHash and QCache classes. |
41 | |
42 | The cache becomes full when the total size of all pixmaps in the |
43 | cache exceeds cacheLimit(). The initial cache limit is 10240 KB (10 MB); |
44 | you can change this by calling setCacheLimit() with the required value. |
45 | A pixmap takes roughly (\e{width} * \e{height} * \e{depth})/8 bytes of |
46 | memory. |
47 | |
48 | The \e{Qt Quarterly} article |
49 | \l{http://doc.qt.io/archives/qq/qq12-qpixmapcache.html}{Optimizing |
50 | with QPixmapCache} explains how to use QPixmapCache to speed up |
51 | applications by caching the results of painting. |
52 | |
53 | \note QPixmapCache is only usable from the application's main thread. |
54 | Access from other threads will be ignored and return failure. |
55 | |
56 | \sa QCache, QPixmap |
57 | */ |
58 | |
59 | static const int cache_limit_default = 10240; // 10 MB cache limit |
60 | |
61 | static inline qsizetype cost(const QPixmap &pixmap) |
62 | { |
63 | // make sure to do a 64bit calculation; qsizetype might be smaller |
64 | const qint64 costKb = static_cast<qint64>(pixmap.width()) |
65 | * pixmap.height() * pixmap.depth() / (8 * 1024); |
66 | const qint64 costMax = std::numeric_limits<qsizetype>::max(); |
67 | // a small pixmap should have at least a cost of 1(kb) |
68 | return static_cast<qsizetype>(qBound(min: 1LL, val: costKb, max: costMax)); |
69 | } |
70 | |
71 | static inline bool qt_pixmapcache_thread_test() |
72 | { |
73 | if (Q_LIKELY(QThread::isMainThread())) |
74 | return true; |
75 | |
76 | return false; |
77 | } |
78 | |
79 | /*! |
80 | \class QPixmapCache::Key |
81 | \brief The QPixmapCache::Key class can be used for efficient access |
82 | to the QPixmapCache. |
83 | \inmodule QtGui |
84 | |
85 | Use QPixmapCache::insert() to receive an instance of Key generated |
86 | by the pixmap cache. You can store the key in your own objects for |
87 | a very efficient one-to-one object-to-pixmap mapping. |
88 | */ |
89 | |
90 | /*! |
91 | Constructs an empty Key object. |
92 | */ |
93 | QPixmapCache::Key::Key() : d(nullptr) |
94 | { |
95 | } |
96 | |
97 | /*! |
98 | \internal |
99 | Constructs a copy of \a other. |
100 | */ |
101 | QPixmapCache::Key::Key(const Key &other) |
102 | { |
103 | if (other.d) |
104 | ++(other.d->ref); |
105 | d = other.d; |
106 | } |
107 | |
108 | /*! |
109 | Destroys the key. |
110 | */ |
111 | QPixmapCache::Key::~Key() |
112 | { |
113 | if (d && --(d->ref) == 0) |
114 | delete d; |
115 | } |
116 | |
117 | /*! |
118 | \internal |
119 | |
120 | Returns \c true if this key is the same as the given \a key; otherwise returns |
121 | false. |
122 | */ |
123 | bool QPixmapCache::Key::operator ==(const Key &key) const |
124 | { |
125 | return (d == key.d); |
126 | } |
127 | |
128 | /*! |
129 | \fn bool QPixmapCache::Key::operator !=(const Key &key) const |
130 | \internal |
131 | */ |
132 | |
133 | /*! |
134 | \fn QPixmapCache::Key::Key(Key &&) |
135 | \internal |
136 | \since 5.6 |
137 | */ |
138 | |
139 | /*! |
140 | \fn QPixmapCache::Key &QPixmapCache::Key::operator=(Key &&) |
141 | \internal |
142 | \since 5.6 |
143 | */ |
144 | |
145 | /*! |
146 | \fn void QPixmapCache::Key::swap(Key &) |
147 | \since 5.6 |
148 | \memberswap{key} |
149 | */ |
150 | |
151 | /*! |
152 | Returns \c true if there is a cached pixmap associated with this key. |
153 | Otherwise, if pixmap was flushed, the key is no longer valid. |
154 | \since 5.7 |
155 | */ |
156 | bool QPixmapCache::Key::isValid() const noexcept |
157 | { |
158 | return d && d->isValid; |
159 | } |
160 | |
161 | /*! |
162 | \internal |
163 | */ |
164 | QPixmapCache::Key &QPixmapCache::Key::operator =(const Key &other) |
165 | { |
166 | if (d != other.d) { |
167 | if (other.d) |
168 | ++(other.d->ref); |
169 | if (d && --(d->ref) == 0) |
170 | delete d; |
171 | d = other.d; |
172 | } |
173 | return *this; |
174 | } |
175 | |
176 | class QPMCache : public QObject, public QCache<QPixmapCache::Key, QPixmapCacheEntry> |
177 | { |
178 | Q_OBJECT |
179 | public: |
180 | QPMCache(); |
181 | ~QPMCache(); |
182 | |
183 | void timerEvent(QTimerEvent *) override; |
184 | bool insert(const QString& key, const QPixmap &pixmap, int cost); |
185 | QPixmapCache::Key insert(const QPixmap &pixmap, int cost); |
186 | bool remove(const QString &key); |
187 | bool remove(const QPixmapCache::Key &key); |
188 | |
189 | void resizeKeyArray(int size); |
190 | QPixmapCache::Key createKey(); |
191 | void releaseKey(const QPixmapCache::Key &key); |
192 | void clear(); |
193 | |
194 | QPixmap *object(const QString &key) const; |
195 | QPixmap *object(const QPixmapCache::Key &key) const; |
196 | |
197 | static inline QPixmapCache::KeyData *get(const QPixmapCache::Key &key) |
198 | {return key.d;} |
199 | |
200 | static QPixmapCache::KeyData* getKeyData(QPixmapCache::Key *key); |
201 | |
202 | bool flushDetachedPixmaps(bool nt); |
203 | |
204 | private: |
205 | static constexpr auto soon_time = 10s; |
206 | static constexpr auto flush_time = 30s; |
207 | int *keyArray; |
208 | int theid; |
209 | int ps; |
210 | int keyArraySize; |
211 | int freeKey; |
212 | QHash<QString, QPixmapCache::Key> cacheKeys; |
213 | bool t; |
214 | }; |
215 | |
216 | QT_BEGIN_INCLUDE_NAMESPACE |
217 | #include "qpixmapcache.moc" |
218 | QT_END_INCLUDE_NAMESPACE |
219 | |
220 | /*! |
221 | \fn size_t QPixmapCache::Key::qHash(const Key &key, size_t seed) |
222 | \since 6.6 |
223 | \qhash{QPixmapCache::Key} |
224 | */ |
225 | size_t QPixmapCache::Key::hash(size_t seed) const noexcept |
226 | { |
227 | return qHash(key: this->d ? this->d->key : 0, seed); |
228 | } |
229 | |
230 | QPMCache::QPMCache() |
231 | : QObject(nullptr), |
232 | QCache<QPixmapCache::Key, QPixmapCacheEntry>(cache_limit_default), |
233 | keyArray(nullptr), theid(0), ps(0), keyArraySize(0), freeKey(0), t(false) |
234 | { |
235 | } |
236 | QPMCache::~QPMCache() |
237 | { |
238 | clear(); |
239 | free(ptr: keyArray); |
240 | } |
241 | |
242 | /* |
243 | This is supposed to cut the cache size down by about 25% in a |
244 | minute once the application becomes idle, to let any inserted pixmap |
245 | remain in the cache for some time before it becomes a candidate for |
246 | cleaning-up, and to not cut down the size of the cache while the |
247 | cache is in active use. |
248 | |
249 | When the last detached pixmap has been deleted from the cache, kill the |
250 | timer so Qt won't keep the CPU from going into sleep mode. Currently |
251 | the timer is not restarted when the pixmap becomes unused, but it does |
252 | restart once something else is added (i.e. the cache space is actually needed). |
253 | |
254 | Returns \c true if any were removed. |
255 | */ |
256 | bool QPMCache::flushDetachedPixmaps(bool nt) |
257 | { |
258 | auto mc = maxCost(); |
259 | const qsizetype currentTotal = totalCost(); |
260 | const qsizetype oldSize = size(); |
261 | if (currentTotal) |
262 | setMaxCost(nt ? currentTotal * 3 / 4 : currentTotal - 1); |
263 | setMaxCost(mc); |
264 | ps = totalCost(); |
265 | return size() != oldSize; |
266 | } |
267 | |
268 | void QPMCache::timerEvent(QTimerEvent *) |
269 | { |
270 | bool nt = totalCost() == ps; |
271 | if (!flushDetachedPixmaps(nt)) { |
272 | killTimer(id: theid); |
273 | theid = 0; |
274 | } else if (nt != t) { |
275 | killTimer(id: theid); |
276 | theid = startTimer(time: nt ? soon_time : flush_time); |
277 | t = nt; |
278 | } |
279 | } |
280 | |
281 | |
282 | QPixmap *QPMCache::object(const QString &key) const |
283 | { |
284 | if (const auto it = cacheKeys.find(key); it != cacheKeys.cend()) |
285 | return object(key: it.value()); |
286 | return nullptr; |
287 | } |
288 | |
289 | QPixmap *QPMCache::object(const QPixmapCache::Key &key) const |
290 | { |
291 | Q_ASSERT(key.isValid()); |
292 | QPixmap *ptr = QCache<QPixmapCache::Key, QPixmapCacheEntry>::object(key); |
293 | //We didn't find the pixmap in the cache, the key is not valid anymore |
294 | if (!ptr) |
295 | const_cast<QPMCache *>(this)->releaseKey(key); |
296 | return ptr; |
297 | } |
298 | |
299 | bool QPMCache::insert(const QString& key, const QPixmap &pixmap, int cost) |
300 | { |
301 | //If for the same key we add already a pixmap we should delete it |
302 | remove(key); |
303 | |
304 | // this will create a new key; the old one has been removed |
305 | auto k = insert(pixmap, cost); |
306 | if (k.isValid()) { |
307 | k.d->stringKey = key; |
308 | cacheKeys[key] = std::move(k); |
309 | return true; |
310 | } |
311 | return false; |
312 | } |
313 | |
314 | QPixmapCache::Key QPMCache::insert(const QPixmap &pixmap, int cost) |
315 | { |
316 | QPixmapCache::Key cacheKey = createKey(); // invalidated by ~QPixmapCacheEntry on failed insert |
317 | bool success = QCache<QPixmapCache::Key, QPixmapCacheEntry>::insert(key: cacheKey, object: new QPixmapCacheEntry(cacheKey, pixmap), cost); |
318 | Q_ASSERT(success || !cacheKey.isValid()); |
319 | if (success) { |
320 | if (!theid) { |
321 | theid = startTimer(time: flush_time); |
322 | t = false; |
323 | } |
324 | } |
325 | return cacheKey; |
326 | } |
327 | |
328 | bool QPMCache::remove(const QString &key) |
329 | { |
330 | const auto cacheKey = cacheKeys.take(key); |
331 | return cacheKey.isValid() && remove(key: cacheKey); |
332 | } |
333 | |
334 | bool QPMCache::remove(const QPixmapCache::Key &key) |
335 | { |
336 | return QCache<QPixmapCache::Key, QPixmapCacheEntry>::remove(key); |
337 | } |
338 | |
339 | void QPMCache::resizeKeyArray(int size) |
340 | { |
341 | if (size <= keyArraySize || size == 0) |
342 | return; |
343 | keyArray = q_check_ptr(p: static_cast<int *>(realloc(ptr: keyArray, |
344 | size: size * sizeof(int)))); |
345 | for (int i = keyArraySize; i != size; ++i) |
346 | keyArray[i] = i + 1; |
347 | keyArraySize = size; |
348 | } |
349 | |
350 | QPixmapCache::Key QPMCache::createKey() |
351 | { |
352 | if (freeKey == keyArraySize) |
353 | resizeKeyArray(size: keyArraySize ? keyArraySize << 1 : 2); |
354 | int id = freeKey; |
355 | freeKey = keyArray[id]; |
356 | QPixmapCache::Key key; |
357 | QPixmapCache::KeyData *d = QPMCache::getKeyData(key: &key); |
358 | d->key = ++id; |
359 | return key; |
360 | } |
361 | |
362 | void QPMCache::releaseKey(const QPixmapCache::Key &key) |
363 | { |
364 | QPixmapCache::KeyData *keyData = key.d; |
365 | if (!keyData) |
366 | return; |
367 | if (!keyData->stringKey.isNull()) |
368 | cacheKeys.remove(key: keyData->stringKey); |
369 | if (keyData->key > keyArraySize || keyData->key <= 0) |
370 | return; |
371 | keyData->key--; |
372 | keyArray[keyData->key] = freeKey; |
373 | freeKey = keyData->key; |
374 | keyData->isValid = false; |
375 | keyData->key = 0; |
376 | } |
377 | |
378 | void QPMCache::clear() |
379 | { |
380 | free(ptr: keyArray); |
381 | keyArray = nullptr; |
382 | freeKey = 0; |
383 | keyArraySize = 0; |
384 | //Mark all keys as invalid |
385 | const QList<QPixmapCache::Key> keys = QCache<QPixmapCache::Key, QPixmapCacheEntry>::keys(); |
386 | for (const auto &key : keys) { |
387 | if (key.d) |
388 | key.d->isValid = false; |
389 | } |
390 | QCache<QPixmapCache::Key, QPixmapCacheEntry>::clear(); |
391 | // Nothing left to flush; stop the timer |
392 | if (theid) { |
393 | killTimer(id: theid); |
394 | theid = 0; |
395 | } |
396 | } |
397 | |
398 | QPixmapCache::KeyData* QPMCache::getKeyData(QPixmapCache::Key *key) |
399 | { |
400 | if (!key->d) |
401 | key->d = new QPixmapCache::KeyData; |
402 | return key->d; |
403 | } |
404 | |
405 | Q_GLOBAL_STATIC(QPMCache, pm_cache) |
406 | |
407 | int Q_AUTOTEST_EXPORT q_QPixmapCache_keyHashSize() |
408 | { |
409 | return pm_cache()->size(); |
410 | } |
411 | |
412 | QPixmapCacheEntry::~QPixmapCacheEntry() |
413 | { |
414 | pm_cache()->releaseKey(key); |
415 | } |
416 | |
417 | /*! |
418 | Looks for a cached pixmap associated with the given \a key in the cache. |
419 | If the pixmap is found, the function sets \a pixmap to that pixmap and |
420 | returns \c true; otherwise it leaves \a pixmap alone and returns \c false. |
421 | |
422 | Example: |
423 | \snippet code/src_gui_image_qpixmapcache.cpp 1 |
424 | */ |
425 | |
426 | bool QPixmapCache::find(const QString &key, QPixmap *pixmap) |
427 | { |
428 | if (key.isEmpty() || !qt_pixmapcache_thread_test()) |
429 | return false; |
430 | QPixmap *ptr = pm_cache()->object(key); |
431 | if (ptr && pixmap) |
432 | *pixmap = *ptr; |
433 | return ptr != nullptr; |
434 | } |
435 | |
436 | /*! |
437 | Looks for a cached pixmap associated with the given \a key in the cache. |
438 | If the pixmap is found, the function sets \a pixmap to that pixmap and |
439 | returns \c true; otherwise it leaves \a pixmap alone and returns \c false. If |
440 | the pixmap is not found, it means that the \a key is no longer valid, |
441 | so it will be released for the next insertion. |
442 | */ |
443 | bool QPixmapCache::find(const Key &key, QPixmap *pixmap) |
444 | { |
445 | if (!qt_pixmapcache_thread_test()) |
446 | return false; |
447 | //The key is not valid anymore, a flush happened before probably |
448 | if (!key.d || !key.d->isValid) |
449 | return false; |
450 | QPixmap *ptr = pm_cache()->object(key); |
451 | if (ptr && pixmap) |
452 | *pixmap = *ptr; |
453 | return ptr != nullptr; |
454 | } |
455 | |
456 | /*! |
457 | Inserts a copy of the pixmap \a pixmap associated with the \a key into |
458 | the cache. |
459 | |
460 | All pixmaps inserted by the Qt library have a key starting with |
461 | "$qt", so your own pixmap keys should never begin "$qt". |
462 | |
463 | When a pixmap is inserted and the cache is about to exceed its |
464 | limit, it removes pixmaps until there is enough room for the |
465 | pixmap to be inserted. |
466 | |
467 | The oldest pixmaps (least recently accessed in the cache) are |
468 | deleted when more space is needed. |
469 | |
470 | The function returns \c true if the object was inserted into the |
471 | cache; otherwise it returns \c false. |
472 | |
473 | \sa setCacheLimit() |
474 | */ |
475 | |
476 | bool QPixmapCache::insert(const QString &key, const QPixmap &pixmap) |
477 | { |
478 | if (key.isEmpty() || !qt_pixmapcache_thread_test()) |
479 | return false; |
480 | return pm_cache()->insert(key, pixmap, cost: cost(pixmap)); |
481 | } |
482 | |
483 | /*! |
484 | Inserts a copy of the given \a pixmap into the cache and returns a key |
485 | that can be used to retrieve it. |
486 | |
487 | When a pixmap is inserted and the cache is about to exceed its |
488 | limit, it removes pixmaps until there is enough room for the |
489 | pixmap to be inserted. |
490 | |
491 | The oldest pixmaps (least recently accessed in the cache) are |
492 | deleted when more space is needed. |
493 | |
494 | \sa setCacheLimit(), replace() |
495 | */ |
496 | QPixmapCache::Key QPixmapCache::insert(const QPixmap &pixmap) |
497 | { |
498 | if (!qt_pixmapcache_thread_test()) |
499 | return QPixmapCache::Key(); |
500 | return pm_cache()->insert(pixmap, cost: cost(pixmap)); |
501 | } |
502 | |
503 | #if QT_DEPRECATED_SINCE(6, 6) |
504 | /*! |
505 | \fn bool QPixmapCache::replace(const Key &key, const QPixmap &pixmap) |
506 | |
507 | \deprecated [6.6] Use \c{remove(key); key = insert(pixmap);} instead. |
508 | |
509 | Replaces the pixmap associated with the given \a key with the \a pixmap |
510 | specified. Returns \c true if the \a pixmap has been correctly inserted into |
511 | the cache; otherwise returns \c false. |
512 | |
513 | The passed \a key is updated to reference \a pixmap now. Other copies of \a |
514 | key, if any, still refer to the old pixmap, which is, however, removed from |
515 | the cache by this function. |
516 | |
517 | \sa setCacheLimit(), insert() |
518 | */ |
519 | #endif // QT_DEPRECATED_SINCE(6, 6) |
520 | |
521 | /*! |
522 | Returns the cache limit (in kilobytes). |
523 | |
524 | The default cache limit is 10240 KB. |
525 | |
526 | \sa setCacheLimit() |
527 | */ |
528 | |
529 | int QPixmapCache::cacheLimit() |
530 | { |
531 | if (!qt_pixmapcache_thread_test()) |
532 | return 0; |
533 | return pm_cache()->maxCost(); |
534 | } |
535 | |
536 | /*! |
537 | Sets the cache limit to \a n kilobytes. |
538 | |
539 | The default setting is 10240 KB. |
540 | |
541 | \sa cacheLimit() |
542 | */ |
543 | |
544 | void QPixmapCache::setCacheLimit(int n) |
545 | { |
546 | if (!qt_pixmapcache_thread_test()) |
547 | return; |
548 | pm_cache()->setMaxCost(n); |
549 | } |
550 | |
551 | /*! |
552 | Removes the pixmap associated with \a key from the cache. |
553 | */ |
554 | void QPixmapCache::remove(const QString &key) |
555 | { |
556 | if (key.isEmpty() || !qt_pixmapcache_thread_test()) |
557 | return; |
558 | pm_cache()->remove(key); |
559 | } |
560 | |
561 | /*! |
562 | Removes the pixmap associated with \a key from the cache and releases |
563 | the key for a future insertion. |
564 | */ |
565 | void QPixmapCache::remove(const Key &key) |
566 | { |
567 | if (!qt_pixmapcache_thread_test()) |
568 | return; |
569 | //The key is not valid anymore, a flush happened before probably |
570 | if (!key.d || !key.d->isValid) |
571 | return; |
572 | pm_cache()->remove(key); |
573 | } |
574 | |
575 | /*! |
576 | Removes all pixmaps from the cache. |
577 | */ |
578 | |
579 | void QPixmapCache::clear() |
580 | { |
581 | if (!QCoreApplication::closingDown() && !qt_pixmapcache_thread_test()) |
582 | return; |
583 | QT_TRY { |
584 | if (pm_cache.exists()) |
585 | pm_cache->clear(); |
586 | } QT_CATCH(const std::bad_alloc &) { |
587 | // if we ran out of memory during pm_cache(), it's no leak, |
588 | // so just ignore it. |
589 | } |
590 | } |
591 | |
592 | Q_AUTOTEST_EXPORT void qt_qpixmapcache_flush_detached_pixmaps() // for tst_qpixmapcache |
593 | { |
594 | if (!qt_pixmapcache_thread_test()) |
595 | return; |
596 | pm_cache()->flushDetachedPixmaps(nt: true); |
597 | } |
598 | |
599 | Q_AUTOTEST_EXPORT int qt_qpixmapcache_qpixmapcache_total_used() // for tst_qpixmapcache |
600 | { |
601 | if (!qt_pixmapcache_thread_test()) |
602 | return 0; |
603 | return (pm_cache()->totalCost()+1023) / 1024; |
604 | } |
605 | |
606 | /*! |
607 | \fn QPixmapCache::KeyData::KeyData() |
608 | |
609 | \internal |
610 | */ |
611 | /*! |
612 | \fn QPixmapCache::KeyData::KeyData(const KeyData &other) |
613 | \internal |
614 | */ |
615 | /*! |
616 | \fn QPixmapCache::KeyData::~KeyData() |
617 | |
618 | \internal |
619 | */ |
620 | QT_END_NAMESPACE |
621 |
Definitions
- cache_limit_default
- cost
- qt_pixmapcache_thread_test
- Key
- Key
- ~Key
- operator ==
- isValid
- operator =
- QPMCache
- get
- soon_time
- flush_time
- hash
- QPMCache
- ~QPMCache
- flushDetachedPixmaps
- timerEvent
- object
- object
- insert
- insert
- remove
- remove
- resizeKeyArray
- createKey
- releaseKey
- clear
- getKeyData
- pm_cache
- q_QPixmapCache_keyHashSize
- ~QPixmapCacheEntry
- find
- find
- insert
- insert
- cacheLimit
- setCacheLimit
- remove
- remove
- clear
- qt_qpixmapcache_flush_detached_pixmaps
Start learning QML with our Intro Training
Find out more