Forráskód Böngészése

Merge branch 'master' of https://gogs.yehaoji.cn/yongxu/yongxu-app-ios-v1

wanwenkai 1 éve
szülő
commit
4847fe16f3
100 módosított fájl, 4655 hozzáadás és 2076 törlés
  1. BIN
      .DS_Store
  2. 12 12
      Podfile.lock
  3. 0 0
      Pods/AFNetworking/AFNetworking/AFURLResponseSerialization.m
  4. 0 0
      Pods/FMDB/src/fmdb/FMDatabasePool.h
  5. 0 0
      Pods/FMDB/src/fmdb/FMDatabasePool.m
  6. 0 0
      Pods/FMDB/src/fmdb/FMDatabaseQueue.h
  7. 0 0
      Pods/FMDB/src/fmdb/FMDatabaseQueue.m
  8. 0 0
      Pods/MJExtension/MJExtension/MJPropertyType.h
  9. 0 0
      Pods/MJExtension/MJExtension/MJPropertyType.m
  10. 0 0
      Pods/MJExtension/MJExtension/NSObject+MJCoding.h
  11. 0 0
      Pods/MJExtension/MJExtension/NSObject+MJCoding.m
  12. 0 0
      Pods/MJExtension/MJExtension/NSObject+MJKeyValue.h
  13. 0 0
      Pods/MJExtension/MJExtension/NSObject+MJKeyValue.m
  14. 0 0
      Pods/MJRefresh/MJRefresh/MJRefresh.bundle/arrow@2x.png
  15. 287 288
      Pods/MMKV/README.md
  16. 46 1
      Pods/MMKV/iOS/MMKV/MMKV/MMKV.h
  17. 116 34
      Pods/MMKV/iOS/MMKV/MMKV/libMMKV.mm
  18. 0 0
      Pods/MMKVCore/Core/InterProcessLock_Win32.cpp
  19. 18 0
      Pods/MMKVCore/Core/MMBuffer.cpp
  20. 1 0
      Pods/MMKVCore/Core/MMBuffer.h
  21. 205 39
      Pods/MMKVCore/Core/MMKV.cpp
  22. 53 6
      Pods/MMKVCore/Core/MMKV.h
  23. 1 1
      Pods/MMKVCore/Core/MMKVLog.cpp
  24. 18 0
      Pods/MMKVCore/Core/MMKVMetaInfo.hpp
  25. 2 2
      Pods/MMKVCore/Core/MMKVPredef.h
  26. 496 72
      Pods/MMKVCore/Core/MMKV_IO.cpp
  27. 71 15
      Pods/MMKVCore/Core/MMKV_OSX.cpp
  28. 8 4
      Pods/MMKVCore/Core/MemoryFile.cpp
  29. 1 1
      Pods/MMKVCore/Core/MemoryFile_OSX.cpp
  30. 39 35
      Pods/MMKVCore/Core/MemoryFile_Win32.cpp
  31. 3 7
      Pods/MMKVCore/Core/MiniPBCoder.cpp
  32. 4 4
      Pods/MMKVCore/Core/MiniPBCoder_OSX.cpp
  33. 1 1
      Pods/MMKVCore/Core/PBUtility.cpp
  34. 7 0
      Pods/MMKVCore/Core/PBUtility.h
  35. 1 1
      Pods/MMKVCore/Core/aes/AESCrypt.h
  36. 12 0
      Pods/MMKVCore/Core/aes/openssl/openssl_aes-armv4.S
  37. 0 0
      Pods/MMKVCore/Core/aes/openssl/openssl_aes_core.cpp
  38. 0 0
      Pods/MMKVCore/Core/aes/openssl/openssl_md32_common.h
  39. 0 0
      Pods/MMKVCore/Core/aes/openssl/openssl_md5.h
  40. 0 0
      Pods/MMKVCore/Core/aes/openssl/openssl_md5_dgst.cpp
  41. 0 0
      Pods/MMKVCore/Core/aes/openssl/openssl_md5_locl.h
  42. 0 0
      Pods/MMKVCore/Core/aes/openssl/openssl_md5_one.cpp
  43. 287 288
      Pods/MMKVCore/README.md
  44. 12 12
      Pods/Manifest.lock
  45. 348 337
      Pods/Pods.xcodeproj/project.pbxproj
  46. 4 1
      Pods/SDWebImage/README.md
  47. 1 1
      Pods/SDWebImage/SDWebImage/Core/NSButton+WebCache.m
  48. 2 0
      Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.h
  49. 12 0
      Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.m
  50. 18 0
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImage.h
  51. 0 1
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImage.m
  52. 1 0
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.h
  53. 55 106
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.m
  54. 5 0
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageRep.m
  55. 7 3
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView.m
  56. 22 14
      Pods/SDWebImage/SDWebImage/Core/SDDiskCache.m
  57. 6 0
      Pods/SDWebImage/SDWebImage/Core/SDGraphicsImageRenderer.h
  58. 14 2
      Pods/SDWebImage/SDWebImage/Core/SDGraphicsImageRenderer.m
  59. 63 11
      Pods/SDWebImage/SDWebImage/Core/SDImageCache.h
  60. 183 89
      Pods/SDWebImage/SDWebImage/Core/SDImageCache.m
  61. 9 0
      Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.h
  62. 3 0
      Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.m
  63. 42 6
      Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.h
  64. 82 23
      Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.m
  65. 14 10
      Pods/SDWebImage/SDWebImage/Core/SDImageCachesManager.m
  66. 63 2
      Pods/SDWebImage/SDWebImage/Core/SDImageCoder.h
  67. 4 0
      Pods/SDWebImage/SDWebImage/Core/SDImageCoder.m
  68. 101 3
      Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.h
  69. 401 118
      Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.m
  70. 15 0
      Pods/SDWebImage/SDWebImage/Core/SDImageCodersManager.m
  71. 9 1
      Pods/SDWebImage/SDWebImage/Core/SDImageFrame.h
  72. 10 4
      Pods/SDWebImage/SDWebImage/Core/SDImageFrame.m
  73. 18 7
      Pods/SDWebImage/SDWebImage/Core/SDImageGraphics.m
  74. 0 1
      Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.h
  75. 291 80
      Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.m
  76. 140 12
      Pods/SDWebImage/SDWebImage/Core/SDImageIOCoder.m
  77. 2 2
      Pods/SDWebImage/SDWebImage/Core/SDImageLoader.h
  78. 35 67
      Pods/SDWebImage/SDWebImage/Core/SDImageLoader.m
  79. 1 1
      Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheSerializer.h
  80. 74 6
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.h
  81. 36 4
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.m
  82. 7 1
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.h
  83. 67 18
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.m
  84. 8 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderConfig.m
  85. 19 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.h
  86. 178 67
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.m
  87. 1 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageError.h
  88. 1 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageIndicator.m
  89. 3 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.h
  90. 229 162
      Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.m
  91. 6 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageOperation.h
  92. 29 4
      Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.h
  93. 79 43
      Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.m
  94. 6 0
      Pods/SDWebImage/SDWebImage/Core/UIImage+ForceDecode.h
  95. 2 1
      Pods/SDWebImage/SDWebImage/Core/UIImage+ForceDecode.m
  96. 21 0
      Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.h
  97. 36 4
      Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.m
  98. 4 0
      Pods/SDWebImage/SDWebImage/Core/UIImage+Transform.h
  99. 159 34
      Pods/SDWebImage/SDWebImage/Core/UIImage+Transform.m
  100. 8 7
      Pods/SDWebImage/SDWebImage/Core/UIView+WebCache.h

BIN
.DS_Store


+ 12 - 12
Podfile.lock

@@ -21,12 +21,12 @@ PODS:
   - Masonry (1.1.0)
   - MJExtension (3.4.1)
   - MJRefresh (3.7.5)
-  - MMKV (1.2.14):
-    - MMKVCore (~> 1.2.14)
-  - MMKVCore (1.2.14)
-  - SDWebImage (5.12.5):
-    - SDWebImage/Core (= 5.12.5)
-  - SDWebImage/Core (5.12.5)
+  - MMKV (1.3.1):
+    - MMKVCore (~> 1.3.1)
+  - MMKVCore (1.3.1)
+  - SDWebImage (5.17.0):
+    - SDWebImage/Core (= 5.17.0)
+  - SDWebImage/Core (5.17.0)
   - SVProgressHUD (2.2.5)
 
 DEPENDENCIES:
@@ -54,17 +54,17 @@ SPEC REPOS:
     - SVProgressHUD
 
 SPEC CHECKSUMS:
-  AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce
+  AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58
   FDFullscreenPopGesture: a8a620179e3d9c40e8e00256dcee1c1a27c6d0f0
   FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
   Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
   MJExtension: 21c5f6f8c4d5d8844b7ae8fbae08fed0b501f961
   MJRefresh: fdf5e979eb406a0341468932d1dfc8b7f9fce961
-  MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
-  MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
-  SDWebImage: 0905f1b7760fc8ac4198cae0036600d67478751e
+  MMKV: 5a07930c70c70b86cd87761a42c8f3836fb681d7
+  MMKVCore: e50135dbd33235b6ab390635991bab437ab873c0
+  SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9
   SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6
 
-PODFILE CHECKSUM: 07c37e5e20e787e9c692ab63148fe787a0690751
+PODFILE CHECKSUM: f431c30b6a399a5b12b65d3398c428de7bdc256e
 
-COCOAPODS: 1.11.3
+COCOAPODS: 1.12.1

+ 0 - 0
Pods/AFNetworking/AFNetworking/AFURLResponseSerialization.m


+ 0 - 0
Pods/FMDB/src/fmdb/FMDatabasePool.h


+ 0 - 0
Pods/FMDB/src/fmdb/FMDatabasePool.m


+ 0 - 0
Pods/FMDB/src/fmdb/FMDatabaseQueue.h


+ 0 - 0
Pods/FMDB/src/fmdb/FMDatabaseQueue.m


+ 0 - 0
Pods/MJExtension/MJExtension/MJPropertyType.h


+ 0 - 0
Pods/MJExtension/MJExtension/MJPropertyType.m


+ 0 - 0
Pods/MJExtension/MJExtension/NSObject+MJCoding.h


+ 0 - 0
Pods/MJExtension/MJExtension/NSObject+MJCoding.m


+ 0 - 0
Pods/MJExtension/MJExtension/NSObject+MJKeyValue.h


+ 0 - 0
Pods/MJExtension/MJExtension/NSObject+MJKeyValue.m


+ 0 - 0
Pods/MJRefresh/MJRefresh/MJRefresh.bundle/arrow@2x.png


+ 287 - 288
Pods/MMKV/README.md

@@ -1,291 +1,290 @@
-[![license](https://img.shields.io/badge/license-BSD_3-brightgreen.svg?style=flat)](https://github.com/Tencent/MMKV/blob/master/LICENSE.TXT)
-[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Tencent/MMKV/pulls)
-[![Release Version](https://img.shields.io/badge/release-1.2.14-brightgreen.svg)](https://github.com/Tencent/MMKV/releases)
-[![Platform](https://img.shields.io/badge/Platform-%20Android%20%7C%20iOS%2FmacOS%20%7C%20Win32%20%7C%20POSIX-brightgreen.svg)](https://github.com/Tencent/MMKV/wiki/home)
-
-中文版本请参看[这里](./README_CN.md)
-
-MMKV is an **efficient**, **small**, **easy-to-use** mobile key-value storage framework used in the WeChat application. It's currently available on **Android**, **iOS/macOS**, **Win32** and **POSIX**.
-
-# MMKV for Android
-
-## Features
-
-* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of Android to achieve the best performance.
-  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
-
-* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `sync`, no `apply` calls needed.
-
-* **Small**.
-  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
-  * **About 50K in binary size**: MMKV adds about 50K per architecture on App size, and much less when zipped (APK).
-
-
-## Getting Started
-
-### Installation Via Maven
-Add the following lines to `build.gradle` on your app module:
-
-```gradle
-dependencies {
-    implementation 'com.tencent:mmkv:1.2.14'
-    // replace "1.2.14" with any available version
-}
-```
-
-Starting from v1.2.8, MMKV has been **migrated to Maven Central**.  
-For other installation options, see [Android Setup](https://github.com/Tencent/MMKV/wiki/android_setup).
-
-### Quick Tutorial
-You can use MMKV as you go. All changes are saved immediately, no `sync`, no `apply` calls needed.  
-Setup MMKV on App startup, say your `Application` class, add these lines:
-
-```Java
-public void onCreate() {
-    super.onCreate();
-
-    String rootDir = MMKV.initialize(this);
-    System.out.println("mmkv root: " + rootDir);
-    //……
-}
-```
-
-MMKV has a global instance, that can be used directly:
-
-```Java
-import com.tencent.mmkv.MMKV;
-    
-MMKV kv = MMKV.defaultMMKV();
-
-kv.encode("bool", true);
-boolean bValue = kv.decodeBool("bool");
-
-kv.encode("int", Integer.MIN_VALUE);
-int iValue = kv.decodeInt("int");
-
-kv.encode("string", "Hello from mmkv");
-String str = kv.decodeString("string");
-```
-
-MMKV also supports **Multi-Process Access**. Full tutorials can be found here [Android Tutorial](https://github.com/Tencent/MMKV/wiki/android_tutorial).
-
-## Performance
-Writing random `int` for 1000 times, we get this chart:  
-![](https://github.com/Tencent/MMKV/wiki/assets/profile_android_mini.png)  
-For more benchmark data, please refer to [our benchmark](https://github.com/Tencent/MMKV/wiki/android_benchmark).
-
-# MMKV for iOS/macOS
-
-## Features
-
-* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of iOS/macOS to achieve the best performance.
- 
-* **Easy-to-use**. You can use MMKV as you go, no configurations are needed. All changes are saved immediately, no `synchronize` calls are needed.
-
-* **Small**.
-  * **A handful of files**: MMKV contains encode/decode helpers and mmap logics and nothing more. It's really tidy.
-  * **Less than 30K in binary size**: MMKV adds less than 30K per architecture on App size, and much less when zipped (IPA).
-
-## Getting Started
-
-### Installation Via CocoaPods:
-  1. Install [CocoaPods](https://guides.CocoaPods.org/using/getting-started.html);
-  2. Open the terminal, `cd` to your project directory, run `pod repo update` to make CocoaPods aware of the latest available MMKV versions;
-  3. Edit your Podfile, add `pod 'MMKV'` to your app target;
-  4. Run `pod install`;
-  5. Open the `.xcworkspace` file generated by CocoaPods;
-  6. Add `#import <MMKV/MMKV.h>` to your source file and we are done.
-
-For other installation options, see [iOS/macOS Setup](https://github.com/Tencent/MMKV/wiki/iOS_setup).
-
-### Quick Tutorial
-You can use MMKV as you go, no configurations are needed. All changes are saved immediately, no `synchronize` calls are needed.
-Setup MMKV on App startup, in your `-[MyApp application: didFinishLaunchingWithOptions:]`, add these lines:
-
-```objective-c
-- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
-    // init MMKV in the main thread
-    [MMKV initializeMMKV:nil];
-
-    //...
-    return YES;
-}
-```
-
-MMKV has a global instance, that can be used directly:
-
-```objective-c
-MMKV *mmkv = [MMKV defaultMMKV];
-    
-[mmkv setBool:YES forKey:@"bool"];
-BOOL bValue = [mmkv getBoolForKey:@"bool"];
-    
-[mmkv setInt32:-1024 forKey:@"int32"];
-int32_t iValue = [mmkv getInt32ForKey:@"int32"];
-    
-[mmkv setString:@"hello, mmkv" forKey:@"string"];
-NSString *str = [mmkv getStringForKey:@"string"];
-```
-
-MMKV also supports **Multi-Process Access**. Full tutorials can be found [here](https://github.com/Tencent/MMKV/wiki/iOS_tutorial).
-
-## Performance
-Writing random `int` for 10000 times, we get this chart:  
-![](https://github.com/Tencent/MMKV/wiki/assets/profile_mini.png)  
-For more benchmark data, please refer to [our benchmark](https://github.com/Tencent/MMKV/wiki/iOS_benchmark).
-
-
-# MMKV for Win32
-
-## Features
-
-* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of Windows to achieve the best performance.
-  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
-
-* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `save`, no `sync` calls are needed.
-
-* **Small**.
-  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
-  * **About 10K in binary size**: MMKV adds about 10K on application size, and much less when zipped.
-
-
-## Getting Started
-
-### Installation Via Source
-1. Getting source code from git repository:
-  
-   ```
-   git clone https://github.com/Tencent/MMKV.git
-   ```
-  
-2. Add `Win32/MMKV/MMKV.vcxproj` to your solution;
-3. Add `MMKV` project to your project's dependencies;
-4. Add `$(OutDir)include` to your project's `C/C++` -> `General` -> `Additional Include Directories`;
-5. Add `$(OutDir)` to your project's `Linker` -> `General` -> `Additional Library Directories`;
-6. Add `MMKV.lib` to your project's `Linker` -> `Input` -> `Additional Dependencies`;
-7. Add `#include <MMKV/MMKV.h>` to your source file and we are done.
-
-
-note:  
-
-1. MMKV is compiled with `MT/MTd` runtime by default. If your project uses `MD/MDd`, you should change MMKV's setting to match your project's (`C/C++` -> `Code Generation` -> `Runtime Library`), or vice versa.
-2. MMKV is developed with Visual Studio 2017, change the `Platform Toolset` if you use a different version of Visual Studio.
-
-For other installation options, see [Win32 Setup](https://github.com/Tencent/MMKV/wiki/windows_setup).
-
-### Quick Tutorial
-You can use MMKV as you go. All changes are saved immediately, no `sync`, no `save` calls needed.  
-Setup MMKV on App startup, say in your `main()`, add these lines:
-
-```C++
-#include <MMKV/MMKV.h>
-
-int main() {
-    std::wstring rootDir = getYourAppDocumentDir();
-    MMKV::initializeMMKV(rootDir);
-    //...
-}
-```
-
-MMKV has a global instance, that can be used directly:
-
-```C++
-auto mmkv = MMKV::defaultMMKV();
-
-mmkv->set(true, "bool");
-std::cout << "bool = " << mmkv->getBool("bool") << std::endl;
-
-mmkv->set(1024, "int32");
-std::cout << "int32 = " << mmkv->getInt32("int32") << std::endl;
-
-mmkv->set("Hello, MMKV for Win32", "string");
-std::string result;
-mmkv->getString("string", result);
-std::cout << "string = " << result << std::endl;
-```
-
-MMKV also supports **Multi-Process Access**. Full tutorials can be found here [Win32 Tutorial](https://github.com/Tencent/MMKV/wiki/windows_tutorial).
-
-# MMKV for POSIX
-
-## Features
-
-* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of POSIX to achieve the best performance.
-  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
-
-* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `save`, no `sync` calls are needed.
-
-* **Small**.
-  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
-  * **About 7K in binary size**: MMKV adds about 7K on application size, and much less when zipped.
-
-
-## Getting Started
-
-### Installation Via CMake
-1. Getting source code from the git repository:
-  
-   ```
-   git clone https://github.com/Tencent/MMKV.git
-   ```
-2. Edit your `CMakeLists.txt`, add those lines:
-
-    ```cmake
-    add_subdirectory(mmkv/POSIX/src mmkv)
-    target_link_libraries(MyApp
-        mmkv)
-    ```
-3. Add `#include "MMKV.h"` to your source file and we are done.
-
-For other installation options, see [POSIX Setup](https://github.com/Tencent/MMKV/wiki/posix_setup).
-
-### Quick Tutorial
-You can use MMKV as you go. All changes are saved immediately, no `sync`, no `save` calls needed.  
-Setup MMKV on App startup, say in your `main()`, add these lines:
-
-```C++
-#include "MMKV.h"
-
-int main() {
-    std::string rootDir = getYourAppDocumentDir();
-    MMKV::initializeMMKV(rootDir);
-    //...
-}
-```
-
-MMKV has a global instance, that can be used directly:
-
-```C++
-auto mmkv = MMKV::defaultMMKV();
-
-mmkv->set(true, "bool");
-std::cout << "bool = " << mmkv->getBool("bool") << std::endl;
-
-mmkv->set(1024, "int32");
-std::cout << "int32 = " << mmkv->getInt32("int32") << std::endl;
-
-mmkv->set("Hello, MMKV for Win32", "string");
-std::string result;
-mmkv->getString("string", result);
-std::cout << "string = " << result << std::endl;
-```
-
-MMKV also supports **Multi-Process Access**. Full tutorials can be found here [POSIX Tutorial](https://github.com/Tencent/MMKV/wiki/posix_tutorial).
-
-## License
-MMKV is published under the BSD 3-Clause license. For details check out the [LICENSE.TXT](./LICENSE.TXT).
-
-## Change Log
-Check out the [CHANGELOG.md](./CHANGELOG.md) for details of change history.
-
-## Contributing
-
-If you are interested in contributing, check out the [CONTRIBUTING.md](./CONTRIBUTING.md), also join our [Tencent OpenSource Plan](https://opensource.tencent.com/contribution).
-
-To give clarity of what is expected of our members, MMKV has adopted the code of conduct defined by the Contributor Covenant, which is widely used. And we think it articulates our values well. For more, check out the [Code of Conduct](./CODE_OF_CONDUCT.md).
-
-## FAQ & Feedback
-Check out the [FAQ](https://github.com/Tencent/MMKV/wiki/FAQ) first. Should there be any questions, don't hesitate to create [issues](https://github.com/Tencent/MMKV/issues).
+[![license](https://img.shields.io/badge/license-BSD_3-brightgreen.svg?style=flat)](https://github.com/Tencent/MMKV/blob/master/LICENSE.TXT)
+[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Tencent/MMKV/pulls)
+[![Release Version](https://img.shields.io/badge/release-1.3.1-brightgreen.svg)](https://github.com/Tencent/MMKV/releases)
+[![Platform](https://img.shields.io/badge/Platform-%20Android%20%7C%20iOS%2FmacOS%20%7C%20Win32%20%7C%20POSIX-brightgreen.svg)](https://github.com/Tencent/MMKV/wiki/home)
+
+中文版本请参看[这里](./README_CN.md)
+
+MMKV is an **efficient**, **small**, **easy-to-use** mobile key-value storage framework used in the WeChat application. It's currently available on **Android**, **iOS/macOS**, **Win32** and **POSIX**.
+
+# MMKV for Android
+
+## Features
+
+* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of Android to achieve the best performance.
+  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
+
+* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `sync`, no `apply` calls needed.
+
+* **Small**.
+  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
+  * **About 50K in binary size**: MMKV adds about 50K per architecture on App size, and much less when zipped (APK).
+
+
+## Getting Started
+
+### Installation Via Maven
+Add the following lines to `build.gradle` on your app module:
+
+```gradle
+dependencies {
+    implementation 'com.tencent:mmkv:1.3.1'
+    // replace "1.3.1" with any available version
+}
+```
+
+For other installation options, see [Android Setup](https://github.com/Tencent/MMKV/wiki/android_setup).
+
+### Quick Tutorial
+You can use MMKV as you go. All changes are saved immediately, no `sync`, no `apply` calls needed.  
+Setup MMKV on App startup, say your `Application` class, add these lines:
+
+```Java
+public void onCreate() {
+    super.onCreate();
+
+    String rootDir = MMKV.initialize(this);
+    System.out.println("mmkv root: " + rootDir);
+    //……
+}
+```
+
+MMKV has a global instance, that can be used directly:
+
+```Java
+import com.tencent.mmkv.MMKV;
+    
+MMKV kv = MMKV.defaultMMKV();
+
+kv.encode("bool", true);
+boolean bValue = kv.decodeBool("bool");
+
+kv.encode("int", Integer.MIN_VALUE);
+int iValue = kv.decodeInt("int");
+
+kv.encode("string", "Hello from mmkv");
+String str = kv.decodeString("string");
+```
+
+MMKV also supports **Multi-Process Access**. Full tutorials can be found here [Android Tutorial](https://github.com/Tencent/MMKV/wiki/android_tutorial).
+
+## Performance
+Writing random `int` for 1000 times, we get this chart:  
+![](https://github.com/Tencent/MMKV/wiki/assets/profile_android_mini.png)  
+For more benchmark data, please refer to [our benchmark](https://github.com/Tencent/MMKV/wiki/android_benchmark).
+
+# MMKV for iOS/macOS
+
+## Features
+
+* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of iOS/macOS to achieve the best performance.
+ 
+* **Easy-to-use**. You can use MMKV as you go, no configurations are needed. All changes are saved immediately, no `synchronize` calls are needed.
+
+* **Small**.
+  * **A handful of files**: MMKV contains encode/decode helpers and mmap logics and nothing more. It's really tidy.
+  * **Less than 30K in binary size**: MMKV adds less than 30K per architecture on App size, and much less when zipped (IPA).
+
+## Getting Started
+
+### Installation Via CocoaPods:
+  1. Install [CocoaPods](https://guides.CocoaPods.org/using/getting-started.html);
+  2. Open the terminal, `cd` to your project directory, run `pod repo update` to make CocoaPods aware of the latest available MMKV versions;
+  3. Edit your Podfile, add `pod 'MMKV'` to your app target;
+  4. Run `pod install`;
+  5. Open the `.xcworkspace` file generated by CocoaPods;
+  6. Add `#import <MMKV/MMKV.h>` to your source file and we are done.
+
+For other installation options, see [iOS/macOS Setup](https://github.com/Tencent/MMKV/wiki/iOS_setup).
+
+### Quick Tutorial
+You can use MMKV as you go, no configurations are needed. All changes are saved immediately, no `synchronize` calls are needed.
+Setup MMKV on App startup, in your `-[MyApp application: didFinishLaunchingWithOptions:]`, add these lines:
+
+```objective-c
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+    // init MMKV in the main thread
+    [MMKV initializeMMKV:nil];
+
+    //...
+    return YES;
+}
+```
+
+MMKV has a global instance, that can be used directly:
+
+```objective-c
+MMKV *mmkv = [MMKV defaultMMKV];
+    
+[mmkv setBool:YES forKey:@"bool"];
+BOOL bValue = [mmkv getBoolForKey:@"bool"];
+    
+[mmkv setInt32:-1024 forKey:@"int32"];
+int32_t iValue = [mmkv getInt32ForKey:@"int32"];
+    
+[mmkv setString:@"hello, mmkv" forKey:@"string"];
+NSString *str = [mmkv getStringForKey:@"string"];
+```
+
+MMKV also supports **Multi-Process Access**. Full tutorials can be found [here](https://github.com/Tencent/MMKV/wiki/iOS_tutorial).
+
+## Performance
+Writing random `int` for 10000 times, we get this chart:  
+![](https://github.com/Tencent/MMKV/wiki/assets/profile_mini.png)  
+For more benchmark data, please refer to [our benchmark](https://github.com/Tencent/MMKV/wiki/iOS_benchmark).
+
+
+# MMKV for Win32
+
+## Features
+
+* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of Windows to achieve the best performance.
+  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
+
+* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `save`, no `sync` calls are needed.
+
+* **Small**.
+  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
+  * **About 10K in binary size**: MMKV adds about 10K on application size, and much less when zipped.
+
+
+## Getting Started
+
+### Installation Via Source
+1. Getting source code from git repository:
+  
+   ```
+   git clone https://github.com/Tencent/MMKV.git
+   ```
+  
+2. Add `Win32/MMKV/MMKV.vcxproj` to your solution;
+3. Add `MMKV` project to your project's dependencies;
+4. Add `$(OutDir)include` to your project's `C/C++` -> `General` -> `Additional Include Directories`;
+5. Add `$(OutDir)` to your project's `Linker` -> `General` -> `Additional Library Directories`;
+6. Add `MMKV.lib` to your project's `Linker` -> `Input` -> `Additional Dependencies`;
+7. Add `#include <MMKV/MMKV.h>` to your source file and we are done.
+
+
+note:  
+
+1. MMKV is compiled with `MT/MTd` runtime by default. If your project uses `MD/MDd`, you should change MMKV's setting to match your project's (`C/C++` -> `Code Generation` -> `Runtime Library`), or vice versa.
+2. MMKV is developed with Visual Studio 2017, change the `Platform Toolset` if you use a different version of Visual Studio.
+
+For other installation options, see [Win32 Setup](https://github.com/Tencent/MMKV/wiki/windows_setup).
+
+### Quick Tutorial
+You can use MMKV as you go. All changes are saved immediately, no `sync`, no `save` calls needed.  
+Setup MMKV on App startup, say in your `main()`, add these lines:
+
+```C++
+#include <MMKV/MMKV.h>
+
+int main() {
+    std::wstring rootDir = getYourAppDocumentDir();
+    MMKV::initializeMMKV(rootDir);
+    //...
+}
+```
+
+MMKV has a global instance, that can be used directly:
+
+```C++
+auto mmkv = MMKV::defaultMMKV();
+
+mmkv->set(true, "bool");
+std::cout << "bool = " << mmkv->getBool("bool") << std::endl;
+
+mmkv->set(1024, "int32");
+std::cout << "int32 = " << mmkv->getInt32("int32") << std::endl;
+
+mmkv->set("Hello, MMKV for Win32", "string");
+std::string result;
+mmkv->getString("string", result);
+std::cout << "string = " << result << std::endl;
+```
+
+MMKV also supports **Multi-Process Access**. Full tutorials can be found here [Win32 Tutorial](https://github.com/Tencent/MMKV/wiki/windows_tutorial).
+
+# MMKV for POSIX
+
+## Features
+
+* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of POSIX to achieve the best performance.
+  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
+
+* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `save`, no `sync` calls are needed.
+
+* **Small**.
+  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
+  * **About 7K in binary size**: MMKV adds about 7K on application size, and much less when zipped.
+
+
+## Getting Started
+
+### Installation Via CMake
+1. Getting source code from the git repository:
+  
+   ```
+   git clone https://github.com/Tencent/MMKV.git
+   ```
+2. Edit your `CMakeLists.txt`, add those lines:
+
+    ```cmake
+    add_subdirectory(mmkv/POSIX/src mmkv)
+    target_link_libraries(MyApp
+        mmkv)
+    ```
+3. Add `#include "MMKV.h"` to your source file and we are done.
+
+For other installation options, see [POSIX Setup](https://github.com/Tencent/MMKV/wiki/posix_setup).
+
+### Quick Tutorial
+You can use MMKV as you go. All changes are saved immediately, no `sync`, no `save` calls needed.  
+Setup MMKV on App startup, say in your `main()`, add these lines:
+
+```C++
+#include "MMKV.h"
+
+int main() {
+    std::string rootDir = getYourAppDocumentDir();
+    MMKV::initializeMMKV(rootDir);
+    //...
+}
+```
+
+MMKV has a global instance, that can be used directly:
+
+```C++
+auto mmkv = MMKV::defaultMMKV();
+
+mmkv->set(true, "bool");
+std::cout << "bool = " << mmkv->getBool("bool") << std::endl;
+
+mmkv->set(1024, "int32");
+std::cout << "int32 = " << mmkv->getInt32("int32") << std::endl;
+
+mmkv->set("Hello, MMKV for Win32", "string");
+std::string result;
+mmkv->getString("string", result);
+std::cout << "string = " << result << std::endl;
+```
+
+MMKV also supports **Multi-Process Access**. Full tutorials can be found here [POSIX Tutorial](https://github.com/Tencent/MMKV/wiki/posix_tutorial).
+
+## License
+MMKV is published under the BSD 3-Clause license. For details check out the [LICENSE.TXT](./LICENSE.TXT).
+
+## Change Log
+Check out the [CHANGELOG.md](./CHANGELOG.md) for details of change history.
+
+## Contributing
+
+If you are interested in contributing, check out the [CONTRIBUTING.md](./CONTRIBUTING.md), also join our [Tencent OpenSource Plan](https://opensource.tencent.com/contribution).
+
+To give clarity of what is expected of our members, MMKV has adopted the code of conduct defined by the Contributor Covenant, which is widely used. And we think it articulates our values well. For more, check out the [Code of Conduct](./CODE_OF_CONDUCT.md).
+
+## FAQ & Feedback
+Check out the [FAQ](https://github.com/Tencent/MMKV/wiki/FAQ) first. Should there be any questions, don't hesitate to create [issues](https://github.com/Tencent/MMKV/issues).
 
 ## Personal Information Protection Rules
 User privacy is taken very seriously: MMKV does not obtain, collect or upload any personal information. Please refer to the [MMKV SDK Personal Information Protection Rules](https://support.weixin.qq.com/cgi-bin/mmsupportacctnodeweb-bin/pages/aY5BAtRiO1BpoHxo) for details.

+ 46 - 1
Pods/MMKV/iOS/MMKV/MMKV/MMKV.h

@@ -27,6 +27,15 @@ typedef NS_ENUM(NSUInteger, MMKVMode) {
     MMKVMultiProcess = 0x2,
 };
 
+typedef NS_ENUM(UInt32, MMKVExpireDuration) {
+    MMKVExpireNever = 0,
+    MMKVExpireInMinute = 60,
+    MMKVExpireInHour = 60 * 60,
+    MMKVExpireInDay = 24 * 60 * 60,
+    MMKVExpireInMonth = 30 * 24 * 60 * 60,
+    MMKVExpireInYear = 365 * 30 * 24 * 60 * 60,
+};
+
 @interface MMKV : NSObject
 
 NS_ASSUME_NONNULL_BEGIN
@@ -42,6 +51,12 @@ NS_ASSUME_NONNULL_BEGIN
 /// @return root dir of MMKV
 + (NSString *)initializeMMKV:(nullable NSString *)rootDir logLevel:(MMKVLogLevel)logLevel NS_SWIFT_NAME(initialize(rootDir:logLevel:));
 
+/// call this in main thread, before calling any other MMKV methods
+/// @param rootDir the root dir of MMKV, passing nil defaults to {NSDocumentDirectory}/mmkv
+/// @param logLevel MMKVLogInfo by default, MMKVLogNone to disable all logging
+/// @return root dir of MMKV
++ (NSString *)initializeMMKV:(nullable NSString *)rootDir logLevel:(MMKVLogLevel)logLevel handler:(nullable id<MMKVHandler>)handler NS_SWIFT_NAME(initialize(rootDir:logLevel:handler:));
+
 /// call this in main thread, before calling any other MMKV methods
 /// @param rootDir the root dir of MMKV, passing nil defaults to {NSDocumentDirectory}/mmkv
 /// @param groupDir the root dir of multi-process MMKV, MMKV with MMKVMultiProcess mode will be stored in groupDir/mmkv
@@ -49,6 +64,13 @@ NS_ASSUME_NONNULL_BEGIN
 /// @return root dir of MMKV
 + (NSString *)initializeMMKV:(nullable NSString *)rootDir groupDir:(NSString *)groupDir logLevel:(MMKVLogLevel)logLevel NS_SWIFT_NAME(initialize(rootDir:groupDir:logLevel:));
 
+/// call this in main thread, before calling any other MMKV methods
+/// @param rootDir the root dir of MMKV, passing nil defaults to {NSDocumentDirectory}/mmkv
+/// @param groupDir the root dir of multi-process MMKV, MMKV with MMKVMultiProcess mode will be stored in groupDir/mmkv
+/// @param logLevel MMKVLogInfo by default, MMKVLogNone to disable all logging
+/// @return root dir of MMKV
++ (NSString *)initializeMMKV:(nullable NSString *)rootDir groupDir:(NSString *)groupDir logLevel:(MMKVLogLevel)logLevel handler:(nullable id<MMKVHandler>)handler NS_SWIFT_NAME(initialize(rootDir:groupDir:logLevel:handler:));
+
 /// a generic purpose instance (in MMKVSingleProcess mode)
 + (nullable instancetype)defaultMMKV;
 
@@ -114,26 +136,37 @@ NS_ASSUME_NONNULL_BEGIN
 - (void)checkReSetCryptKey:(nullable NSData *)cryptKey NS_SWIFT_NAME(checkReSet(cryptKey:));
 
 - (BOOL)setObject:(nullable NSObject<NSCoding> *)object forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
+- (BOOL)setObject:(nullable NSObject<NSCoding> *)object forKey:(NSString *)key expireDuration:(uint32_t)seconds NS_SWIFT_NAME(set(_:forKey:expireDuration:));
 
 - (BOOL)setBool:(BOOL)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
+- (BOOL)setBool:(BOOL)value forKey:(NSString *)key expireDuration:(uint32_t)seconds NS_SWIFT_NAME(set(_:forKey:expireDuration:));
 
 - (BOOL)setInt32:(int32_t)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
+- (BOOL)setInt32:(int32_t)value forKey:(NSString *)key expireDuration:(uint32_t)seconds NS_SWIFT_NAME(set(_:forKey:expireDuration:));
 
 - (BOOL)setUInt32:(uint32_t)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
+- (BOOL)setUInt32:(uint32_t)value forKey:(NSString *)key expireDuration:(uint32_t)seconds NS_SWIFT_NAME(set(_:forKey:expireDuration:));
 
 - (BOOL)setInt64:(int64_t)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
+- (BOOL)setInt64:(int64_t)value forKey:(NSString *)key expireDuration:(uint32_t)seconds NS_SWIFT_NAME(set(_:forKey:expireDuration:));
 
 - (BOOL)setUInt64:(uint64_t)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
+- (BOOL)setUInt64:(uint64_t)value forKey:(NSString *)key expireDuration:(uint32_t)seconds NS_SWIFT_NAME(set(_:forKey:expireDuration:));
 
 - (BOOL)setFloat:(float)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
+- (BOOL)setFloat:(float)value forKey:(NSString *)key expireDuration:(uint32_t)seconds NS_SWIFT_NAME(set(_:forKey:expireDuration:));
 
 - (BOOL)setDouble:(double)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
+- (BOOL)setDouble:(double)value forKey:(NSString *)key expireDuration:(uint32_t)seconds NS_SWIFT_NAME(set(_:forKey:expireDuration:));
 
 - (BOOL)setString:(NSString *)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
+- (BOOL)setString:(NSString *)value forKey:(NSString *)key expireDuration:(uint32_t)seconds NS_SWIFT_NAME(set(_:forKey:expireDuration:));
 
 - (BOOL)setDate:(NSDate *)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
+- (BOOL)setDate:(NSDate *)value forKey:(NSString *)key expireDuration:(uint32_t)seconds NS_SWIFT_NAME(set(_:forKey:expireDuration:));
 
 - (BOOL)setData:(NSData *)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:));
+- (BOOL)setData:(NSData *)value forKey:(NSString *)key expireDuration:(uint32_t)seconds NS_SWIFT_NAME(set(_:forKey:expireDuration:));
 
 - (nullable id)getObjectOfClass:(Class)cls forKey:(NSString *)key NS_SWIFT_NAME(object(of:forKey:));
 
@@ -200,6 +233,18 @@ NS_ASSUME_NONNULL_BEGIN
 - (void)enumerateKeys:(void (^)(NSString *key, BOOL *stop))block;
 - (NSArray *)allKeys;
 
+/// return count of non-expired keys, keep in mind that it comes with cost
+- (size_t)countNonExpiredKeys;
+
+/// return all non-expired keys, keep in mind that it comes with cost
+- (NSArray *)allNonExpiredKeys;
+
+/// all keys created (or last modified) longger than expiredInSeconds will be deleted on next full-write-back
+/// @param expiredInSeconds = MMKVExpireNever (0) means no common expiration duration for all keys, aka each key will have it's own expiration duration
+- (BOOL)enableAutoKeyExpire:(uint32_t) expiredInSeconds NS_SWIFT_NAME(enableAutoKeyExpire(expiredInSeconds:));
+
+- (BOOL)disableAutoKeyExpire;
+
 - (void)removeValueForKey:(NSString *)key NS_SWIFT_NAME(removeValue(forKey:));
 
 - (void)removeValuesForKeys:(NSArray<NSString *> *)arrKeys NS_SWIFT_NAME(removeValues(forKeys:));
@@ -278,7 +323,7 @@ NS_ASSUME_NONNULL_BEGIN
 /// check if content changed by other process
 - (void)checkContentChanged;
 
-+ (void)registerHandler:(id<MMKVHandler>)handler;
++ (void)registerHandler:(id<MMKVHandler>)handler __attribute__((deprecated("use +initializeMMKV:logLevel:handler: instead")));
 + (void)unregiserHandler;
 
 /// MMKVLogInfo by default

+ 116 - 34
Pods/MMKV/iOS/MMKV/MMKV/libMMKV.mm

@@ -59,57 +59,81 @@ static void ContentChangeHandler(const string &mmapID);
 
 #pragma mark - init
 
-// protect from some old code that don't call +initializeMMKV:
-+ (void)initialize {
-    if (self == MMKV.class) {
-        g_instanceDic = [[NSMutableDictionary alloc] init];
-        g_lock = new mmkv::ThreadLock();
-        g_lock->initialize();
-
-        mmkv::MMKV::minimalInit([self mmkvBasePath].UTF8String);
-
-#if defined(MMKV_IOS) && !defined(MMKV_IOS_EXTENSION)
-        // just in case someone forget to set the MMKV_IOS_EXTENSION macro
-        if ([[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]) {
-            g_isRunningInAppExtension = YES;
-        }
-        if (!g_isRunningInAppExtension) {
-            auto appState = [UIApplication sharedApplication].applicationState;
-            auto isInBackground = (appState == UIApplicationStateBackground);
-            mmkv::MMKV::setIsInBackground(isInBackground);
-            MMKVInfo("appState:%ld", (long) appState);
-
-            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
-            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
-        }
-#endif
-    }
++ (NSString *)initializeMMKV:(nullable NSString *)rootDir {
+    return [MMKV initializeMMKV:rootDir logLevel:MMKVLogInfo handler:nil];
 }
 
-+ (NSString *)initializeMMKV:(nullable NSString *)rootDir {
-    return [MMKV initializeMMKV:rootDir logLevel:MMKVLogInfo];
++ (NSString *)initializeMMKV:(nullable NSString *)rootDir logLevel:(MMKVLogLevel)logLevel {
+    return [MMKV initializeMMKV:rootDir logLevel:logLevel handler:nil];
 }
 
 static BOOL g_hasCalledInitializeMMKV = NO;
 
-+ (NSString *)initializeMMKV:(nullable NSString *)rootDir logLevel:(MMKVLogLevel)logLevel {
++ (NSString *)initializeMMKV:(nullable NSString *)rootDir logLevel:(MMKVLogLevel)logLevel handler:(id<MMKVHandler>)handler {
     if (g_hasCalledInitializeMMKV) {
         MMKVWarning("already called +initializeMMKV before, ignore this request");
         return [self mmkvBasePath];
     }
-    g_hasCalledInitializeMMKV = YES;
+    g_instanceDic = [[NSMutableDictionary alloc] init];
+    g_lock = new mmkv::ThreadLock();
+    g_lock->initialize();
+
+    g_callbackHandler = handler;
+    mmkv::LogHandler logHandler = nullptr;
+    if (g_callbackHandler && [g_callbackHandler respondsToSelector:@selector(mmkvLogWithLevel:file:line:func:message:)]) {
+        g_isLogRedirecting = true;
+        logHandler = LogHandler;
+    }
 
     if (rootDir != nil) {
         [g_basePath release];
         g_basePath = [rootDir retain];
+    } else {
+        [self mmkvBasePath];
     }
-    mmkv::MMKV::initializeMMKV(g_basePath.UTF8String, (mmkv::MMKVLogLevel) logLevel);
+    NSAssert(g_basePath.length > 0, @"MMKV not initialized properly, must not call +initializeMMKV: before -application:didFinishLaunchingWithOptions:");
+    mmkv::MMKV::initializeMMKV(g_basePath.UTF8String, (mmkv::MMKVLogLevel) logLevel, logHandler);
+
+    if ([g_callbackHandler respondsToSelector:@selector(onMMKVCRCCheckFail:)] ||
+        [g_callbackHandler respondsToSelector:@selector(onMMKVFileLengthError:)]) {
+        mmkv::MMKV::registerErrorHandler(ErrorHandler);
+    }
+    if ([g_callbackHandler respondsToSelector:@selector(onMMKVContentChange:)]) {
+        mmkv::MMKV::registerContentChangeHandler(ContentChangeHandler);
+    }
+
+#if defined(MMKV_IOS) && !defined(MMKV_IOS_EXTENSION)
+    // just in case someone forget to set the MMKV_IOS_EXTENSION macro
+    if ([[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"]) {
+        g_isRunningInAppExtension = YES;
+    }
+    if (!g_isRunningInAppExtension) {
+        auto appState = [UIApplication sharedApplication].applicationState;
+        auto isInBackground = (appState == UIApplicationStateBackground);
+        mmkv::MMKV::setIsInBackground(isInBackground);
+        MMKVInfo("appState:%ld", (long) appState);
+
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
+    }
+#endif
+
+    g_hasCalledInitializeMMKV = YES;
 
     return [self mmkvBasePath];
 }
 
 + (NSString *)initializeMMKV:(nullable NSString *)rootDir groupDir:(NSString *)groupDir logLevel:(MMKVLogLevel)logLevel {
-    auto ret = [MMKV initializeMMKV:rootDir logLevel:logLevel];
+    auto ret = [MMKV initializeMMKV:rootDir logLevel:logLevel handler:nil];
+
+    g_groupPath = [[groupDir stringByAppendingPathComponent:@"mmkv"] retain];
+    MMKVInfo("groupDir: %@", g_groupPath);
+
+    return ret;
+}
+
++ (NSString *)initializeMMKV:(nullable NSString *)rootDir groupDir:(NSString *)groupDir logLevel:(MMKVLogLevel)logLevel handler:(nullable id<MMKVHandler>)handler {
+    auto ret = [MMKV initializeMMKV:rootDir logLevel:logLevel handler:handler];
 
     g_groupPath = [[groupDir stringByAppendingPathComponent:@"mmkv"] retain];
     MMKVInfo("groupDir: %@", g_groupPath);
@@ -163,9 +187,7 @@ static BOOL g_hasCalledInitializeMMKV = NO;
 
 // relatePath and MMKVMultiProcess mode can't be set at the same time, so we hide this method from public
 + (instancetype)mmkvWithID:(NSString *)mmapID cryptKey:(NSData *)cryptKey rootPath:(nullable NSString *)rootPath mode:(MMKVMode)mode {
-    if (!g_hasCalledInitializeMMKV) {
-        MMKVError("MMKV not initialized properly, must call +initializeMMKV: in main thread before calling any other MMKV methods");
-    }
+    NSAssert(g_hasCalledInitializeMMKV, @"MMKV not initialized properly, must call +initializeMMKV: in main thread before calling any other MMKV methods");
     if (mmapID.length <= 0) {
         return nil;
     }
@@ -338,46 +360,90 @@ static BOOL g_hasCalledInitializeMMKV = NO;
     return m_mmkv->set(object, key);
 }
 
+- (BOOL)setObject:(nullable NSObject<NSCoding> *)object forKey:(NSString *)key expireDuration:(uint32_t)seconds {
+    return m_mmkv->set(object, key, seconds);
+}
+
 - (BOOL)setBool:(BOOL)value forKey:(NSString *)key {
     return m_mmkv->set((bool) value, key);
 }
 
+- (BOOL)setBool:(BOOL)value forKey:(NSString *)key expireDuration:(uint32_t)seconds {
+    return m_mmkv->set((bool) value, key, seconds);
+}
+
 - (BOOL)setInt32:(int32_t)value forKey:(NSString *)key {
     return m_mmkv->set(value, key);
 }
 
+- (BOOL)setInt32:(int32_t)value forKey:(NSString *)key expireDuration:(uint32_t)seconds {
+    return m_mmkv->set(value, key, seconds);
+}
+
 - (BOOL)setUInt32:(uint32_t)value forKey:(NSString *)key {
     return m_mmkv->set(value, key);
 }
 
+- (BOOL)setUInt32:(uint32_t)value forKey:(NSString *)key expireDuration:(uint32_t)seconds {
+    return m_mmkv->set(value, key, seconds);
+}
+
 - (BOOL)setInt64:(int64_t)value forKey:(NSString *)key {
     return m_mmkv->set(value, key);
 }
 
+- (BOOL)setInt64:(int64_t)value forKey:(NSString *)key expireDuration:(uint32_t)seconds {
+    return m_mmkv->set(value, key, seconds);
+}
+
 - (BOOL)setUInt64:(uint64_t)value forKey:(NSString *)key {
     return m_mmkv->set(value, key);
 }
 
+- (BOOL)setUInt64:(uint64_t)value forKey:(NSString *)key expireDuration:(uint32_t)seconds {
+    return m_mmkv->set(value, key, seconds);
+}
+
 - (BOOL)setFloat:(float)value forKey:(NSString *)key {
     return m_mmkv->set(value, key);
 }
 
+- (BOOL)setFloat:(float)value forKey:(NSString *)key expireDuration:(uint32_t)seconds {
+    return m_mmkv->set(value, key, seconds);
+}
+
 - (BOOL)setDouble:(double)value forKey:(NSString *)key {
     return m_mmkv->set(value, key);
 }
 
+- (BOOL)setDouble:(double)value forKey:(NSString *)key expireDuration:(uint32_t)seconds {
+    return m_mmkv->set(value, key, seconds);
+}
+
 - (BOOL)setString:(NSString *)value forKey:(NSString *)key {
     return [self setObject:value forKey:key];
 }
 
+- (BOOL)setString:(NSString *)value forKey:(NSString *)key expireDuration:(uint32_t)seconds {
+    return [self setObject:value forKey:key expireDuration:seconds];
+}
+
 - (BOOL)setDate:(NSDate *)value forKey:(NSString *)key {
     return [self setObject:value forKey:key];
 }
 
+- (BOOL)setDate:(NSDate *)value forKey:(NSString *)key expireDuration:(uint32_t)seconds {
+    return [self setObject:value forKey:key expireDuration:seconds];
+}
+
 - (BOOL)setData:(NSData *)value forKey:(NSString *)key {
     return [self setObject:value forKey:key];
 }
 
+- (BOOL)setData:(NSData *)value forKey:(NSString *)key expireDuration:(uint32_t)seconds {
+    return [self setObject:value forKey:key expireDuration:seconds];
+}
+
 - (id)getObjectOfClass:(Class)cls forKey:(NSString *)key {
     return m_mmkv->getObject(key, cls);
 }
@@ -554,6 +620,22 @@ static BOOL g_hasCalledInitializeMMKV = NO;
     return m_mmkv->allKeys();
 }
 
+- (size_t)countNonExpiredKeys {
+    return m_mmkv->count(true);
+}
+
+- (NSArray *)allNonExpiredKeys {
+    return m_mmkv->allKeys(true);
+}
+
+- (BOOL)enableAutoKeyExpire:(uint32_t) expiredInSeconds {
+    return m_mmkv->enableAutoKeyExpire(expiredInSeconds);
+}
+
+- (BOOL)disableAutoKeyExpire {
+    return m_mmkv->disableAutoKeyExpire();
+}
+
 - (void)removeValueForKey:(NSString *)key {
     m_mmkv->removeValueForKey(key);
 }

+ 0 - 0
Pods/MMKVCore/Core/InterProcessLock_Win32.cpp


+ 18 - 0
Pods/MMKVCore/Core/MMBuffer.cpp

@@ -18,12 +18,15 @@
  * limitations under the License.
  */
 
+#define NOMINMAX // undefine max/min
+
 #include "MMBuffer.h"
 #include <cerrno>
 #include <cstdlib>
 #include <cstring>
 #include <utility>
 #include <stdexcept>
+#include <algorithm>
 
 #ifdef MMKV_APPLE
 #    if __has_feature(objc_arc)
@@ -107,6 +110,21 @@ MMBuffer::MMBuffer(MMBuffer &&other) noexcept : type(other.type) {
     }
 }
 
+MMBuffer::MMBuffer(MMBuffer &&other, size_t length) noexcept : type(other.type) {
+    if (type == MMBufferType_Normal) {
+        size = std::min(other.size, length);
+        ptr = other.ptr;
+        isNoCopy = other.isNoCopy;
+#ifdef MMKV_APPLE
+        m_data = other.m_data;
+#endif
+        other.detach();
+    } else {
+        paddedSize = std::min(other.paddedSize, static_cast<uint8_t>(length));
+        memcpy(paddedBuffer, other.paddedBuffer, paddedSize);
+    }
+}
+
 MMBuffer &MMBuffer::operator=(MMBuffer &&other) noexcept {
     if (type == MMBufferType_Normal) {
         if (other.type == MMBufferType_Normal) {

+ 1 - 0
Pods/MMKVCore/Core/MMBuffer.h

@@ -77,6 +77,7 @@ public:
 #endif
 
     MMBuffer(MMBuffer &&other) noexcept;
+    MMBuffer(MMBuffer &&other, size_t length) noexcept;
     MMBuffer &operator=(MMBuffer &&other) noexcept;
 
     ~MMBuffer();

+ 205 - 39
Pods/MMKVCore/Core/MMKV.cpp

@@ -40,6 +40,7 @@
 #include <cstring>
 #include <unordered_set>
 //#include <unistd.h>
+#include <cassert>
 
 #if defined(__aarch64__) && defined(__linux)
 #    include <asm/hwcap.h>
@@ -69,8 +70,6 @@ constexpr auto SPECIAL_CHARACTER_DIRECTORY_NAME = L"specialCharacter";
 constexpr auto CRC_SUFFIX = L".crc";
 #endif
 
-constexpr uint32_t Fixed32Size = pbFixed32Size();
-
 MMKV_NAMESPACE_BEGIN
 
 static MMKVPath_t encodeFilePath(const string &mmapID, const MMKVPath_t &rootDir);
@@ -189,15 +188,16 @@ void initialize() {
 #endif     // __aarch64__ && defined(__linux__)
 
 #if defined(MMKV_DEBUG) && !defined(MMKV_DISABLE_CRYPT)
-    AESCrypt::testAESCrypt();
-    KeyValueHolderCrypt::testAESToMMBuffer();
+    // AESCrypt::testAESCrypt();
+    // KeyValueHolderCrypt::testAESToMMBuffer();
 #endif
 }
 
 ThreadOnceToken_t once_control = ThreadOnceUninitialized;
 
-void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
+void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel, mmkv::LogHandler handler) {
     g_currentLogLevel = logLevel;
+    g_logHandler = handler;
 
     ThreadLock::ThreadOnce(&once_control, initialize);
 
@@ -306,6 +306,8 @@ void MMKV::clearMemoryCache() {
     m_output = nullptr;
 
     m_file->clearMemoryCache();
+    // inter-process lock rely on MetaFile's fd, never close it
+    // m_metaFile->clearMemoryCache();
     m_actualSize = 0;
     m_metaInfo->m_crcDigest = 0;
 }
@@ -418,121 +420,257 @@ void MMKV::updateCRCDigest(const uint8_t *ptr, size_t length) {
 // set & get
 
 bool MMKV::set(bool value, MMKVKey_t key) {
+    return set(value, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(bool value, MMKVKey_t key, uint32_t expireDuration) {
     if (isKeyEmpty(key)) {
         return false;
     }
-    size_t size = pbBoolSize();
+    size_t size = unlikely(m_enableKeyExpire) ? Fixed32Size + pbBoolSize() : pbBoolSize();
     MMBuffer data(size);
     CodedOutputData output(data.getPtr(), size);
     output.writeBool(value);
+    if (unlikely(m_enableKeyExpire)) {
+        auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
+        output.writeRawLittleEndian32(UInt32ToInt32(time));
+    } else {
+        assert(expireDuration == ExpireNever && "setting expire duration without calling enableAutoKeyExpire() first");
+    }
 
-    return setDataForKey(move(data), key);
+    return setDataForKey(std::move(data), key);
 }
 
 bool MMKV::set(int32_t value, MMKVKey_t key) {
+    return set(value, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(int32_t value, MMKVKey_t key, uint32_t expireDuration) {
     if (isKeyEmpty(key)) {
         return false;
     }
-    size_t size = pbInt32Size(value);
+    size_t size = unlikely(m_enableKeyExpire) ? Fixed32Size + pbInt32Size(value) : pbInt32Size(value);
     MMBuffer data(size);
     CodedOutputData output(data.getPtr(), size);
     output.writeInt32(value);
+    if (unlikely(m_enableKeyExpire)) {
+        auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
+        output.writeRawLittleEndian32(UInt32ToInt32(time));
+    } else {
+        assert(expireDuration == ExpireNever && "setting expire duration without calling enableAutoKeyExpire() first");
+    }
 
-    return setDataForKey(move(data), key);
+    return setDataForKey(std::move(data), key);
 }
 
 bool MMKV::set(uint32_t value, MMKVKey_t key) {
+    return set(value, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(uint32_t value, MMKVKey_t key, uint32_t expireDuration) {
     if (isKeyEmpty(key)) {
         return false;
     }
-    size_t size = pbUInt32Size(value);
+    size_t size = unlikely(m_enableKeyExpire) ? Fixed32Size + pbUInt32Size(value) : pbUInt32Size(value);
     MMBuffer data(size);
     CodedOutputData output(data.getPtr(), size);
     output.writeUInt32(value);
+    if (unlikely(m_enableKeyExpire)) {
+        auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
+        output.writeRawLittleEndian32(UInt32ToInt32(time));
+    } else {
+        assert(expireDuration == ExpireNever && "setting expire duration without calling enableAutoKeyExpire() first");
+    }
 
-    return setDataForKey(move(data), key);
+    return setDataForKey(std::move(data), key);
 }
 
 bool MMKV::set(int64_t value, MMKVKey_t key) {
+    return set(value, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(int64_t value, MMKVKey_t key, uint32_t expireDuration) {
     if (isKeyEmpty(key)) {
         return false;
     }
-    size_t size = pbInt64Size(value);
+    size_t size = unlikely(m_enableKeyExpire) ? Fixed32Size + pbInt64Size(value) : pbInt64Size(value);
     MMBuffer data(size);
     CodedOutputData output(data.getPtr(), size);
     output.writeInt64(value);
+    if (unlikely(m_enableKeyExpire)) {
+        auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
+        output.writeRawLittleEndian32(UInt32ToInt32(time));
+    } else {
+        assert(expireDuration == ExpireNever && "setting expire duration without calling enableAutoKeyExpire() first");
+    }
 
-    return setDataForKey(move(data), key);
+    return setDataForKey(std::move(data), key);
 }
 
 bool MMKV::set(uint64_t value, MMKVKey_t key) {
+    return set(value, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(uint64_t value, MMKVKey_t key, uint32_t expireDuration) {
     if (isKeyEmpty(key)) {
         return false;
     }
-    size_t size = pbUInt64Size(value);
+    size_t size = unlikely(m_enableKeyExpire) ? Fixed32Size + pbUInt64Size(value) : pbUInt64Size(value);
     MMBuffer data(size);
     CodedOutputData output(data.getPtr(), size);
     output.writeUInt64(value);
+    if (unlikely(m_enableKeyExpire)) {
+        auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
+        output.writeRawLittleEndian32(UInt32ToInt32(time));
+    } else {
+        assert(expireDuration == ExpireNever && "setting expire duration without calling enableAutoKeyExpire() first");
+    }
 
-    return setDataForKey(move(data), key);
+    return setDataForKey(std::move(data), key);
 }
 
 bool MMKV::set(float value, MMKVKey_t key) {
+    return set(value, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(float value, MMKVKey_t key, uint32_t expireDuration) {
     if (isKeyEmpty(key)) {
         return false;
     }
-    size_t size = pbFloatSize();
+    size_t size = unlikely(m_enableKeyExpire) ? Fixed32Size + pbFloatSize() : pbFloatSize();
     MMBuffer data(size);
     CodedOutputData output(data.getPtr(), size);
     output.writeFloat(value);
+    if (unlikely(m_enableKeyExpire)) {
+        auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
+        output.writeRawLittleEndian32(UInt32ToInt32(time));
+    } else {
+        assert(expireDuration == ExpireNever && "setting expire duration without calling enableAutoKeyExpire() first");
+    }
 
-    return setDataForKey(move(data), key);
+    return setDataForKey(std::move(data), key);
 }
 
 bool MMKV::set(double value, MMKVKey_t key) {
+    return set(value, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(double value, MMKVKey_t key, uint32_t expireDuration) {
     if (isKeyEmpty(key)) {
         return false;
     }
-    size_t size = pbDoubleSize();
+    size_t size = unlikely(m_enableKeyExpire) ? Fixed32Size + pbDoubleSize() : pbDoubleSize();
     MMBuffer data(size);
     CodedOutputData output(data.getPtr(), size);
     output.writeDouble(value);
+    if (unlikely(m_enableKeyExpire)) {
+        auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
+        output.writeRawLittleEndian32(UInt32ToInt32(time));
+    } else {
+        assert(expireDuration == ExpireNever && "setting expire duration without calling enableAutoKeyExpire() first");
+    }
 
-    return setDataForKey(move(data), key);
+    return setDataForKey(std::move(data), key);
 }
 
 #ifndef MMKV_APPLE
 
 bool MMKV::set(const char *value, MMKVKey_t key) {
+    return set(value, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(const char *value, MMKVKey_t key, uint32_t expireDuration) {
     if (!value) {
         removeValueForKey(key);
         return true;
     }
-    return setDataForKey(MMBuffer((void *) value, strlen(value), MMBufferNoCopy), key, true);
+    if (likely(!m_enableKeyExpire)) {
+        assert(expireDuration == ExpireNever && "setting expire duration without calling enableAutoKeyExpire() first");
+        return setDataForKey(MMBuffer((void *) value, strlen(value), MMBufferNoCopy), key, true);
+    } else {
+        MMBuffer data((void *) value, strlen(value), MMBufferNoCopy);
+        if (data.length() > 0) {
+            auto tmp = MMBuffer(pbMMBufferSize(data) + Fixed32Size);
+            CodedOutputData output(tmp.getPtr(), tmp.length());
+            output.writeData(data);
+            auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
+            output.writeRawLittleEndian32(UInt32ToInt32(time));
+            data = std::move(tmp);
+        }
+        return setDataForKey(std::move(data), key);
+    }
 }
 
 bool MMKV::set(const string &value, MMKVKey_t key) {
+    return set(value, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(const string &value, MMKVKey_t key, uint32_t expireDuration) {
     if (isKeyEmpty(key)) {
         return false;
     }
-    return setDataForKey(MMBuffer((void *) value.data(), value.length(), MMBufferNoCopy), key, true);
+    if (likely(!m_enableKeyExpire)) {
+        assert(expireDuration == ExpireNever && "setting expire duration without calling enableAutoKeyExpire() first");
+        return setDataForKey(MMBuffer((void *) value.data(), value.length(), MMBufferNoCopy), key, true);
+    } else {
+        MMBuffer data((void *) value.data(), value.length(), MMBufferNoCopy);
+        if (data.length() > 0) {
+            auto tmp = MMBuffer(pbMMBufferSize(data) + Fixed32Size);
+            CodedOutputData output(tmp.getPtr(), tmp.length());
+            output.writeData(data);
+            auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
+            output.writeRawLittleEndian32(UInt32ToInt32(time));
+            data = std::move(tmp);
+        }
+        return setDataForKey(std::move(data), key);
+    }
 }
 
 bool MMKV::set(const MMBuffer &value, MMKVKey_t key) {
+    return set(value, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(const MMBuffer &value, MMKVKey_t key, uint32_t expireDuration) {
     if (isKeyEmpty(key)) {
         return false;
     }
     // delay write the size needed for encoding value
     // avoid memory copying
-    return setDataForKey(MMBuffer(value.getPtr(), value.length(), MMBufferNoCopy), key, true);
+    if (likely(!m_enableKeyExpire)) {
+        assert(expireDuration == ExpireNever && "setting expire duration without calling enableAutoKeyExpire() first");
+        return setDataForKey(MMBuffer(value.getPtr(), value.length(), MMBufferNoCopy), key, true);
+    } else {
+        MMBuffer data(value.getPtr(), value.length(), MMBufferNoCopy);
+        if (data.length() > 0) {
+            auto tmp = MMBuffer(pbMMBufferSize(data) + Fixed32Size);
+            CodedOutputData output(tmp.getPtr(), tmp.length());
+            output.writeData(data);
+            auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
+            output.writeRawLittleEndian32(UInt32ToInt32(time));
+            data = std::move(tmp);
+        }
+        return setDataForKey(std::move(data), key);
+    }
 }
 
-bool MMKV::set(const vector<string> &v, MMKVKey_t key) {
+bool MMKV::set(const vector<string> &value, MMKVKey_t key) {
+    return set(value, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(const vector<string> &v, MMKVKey_t key, uint32_t expireDuration) {
     if (isKeyEmpty(key)) {
         return false;
     }
     auto data = MiniPBCoder::encodeDataWithObject(v);
-    return setDataForKey(move(data), key);
+    if (unlikely(m_enableKeyExpire) && data.length() > 0) {
+        auto tmp = MMBuffer(data.length() + Fixed32Size);
+        auto ptr = (uint8_t *)tmp.getPtr();
+        memcpy(ptr, data.getPtr(), data.length());
+        auto time = (expireDuration != ExpireNever) ? getCurrentTimeInSecond() + expireDuration : ExpireNever;
+        memcpy(ptr + data.length(), &time, Fixed32Size);
+        data = std::move(tmp);
+    }
+    return setDataForKey(std::move(data), key);
 }
 
 bool MMKV::getString(MMKVKey_t key, string &result) {
@@ -564,7 +702,7 @@ bool MMKV::getBytes(MMKVKey_t key, mmkv::MMBuffer &result) {
     if (data.length() > 0) {
         try {
             CodedInputData input(data.getPtr(), data.length());
-            result = move(input.readData());
+            result = std::move(input.readData());
             return true;
         } catch (std::exception &exception) {
             MMKVError("%s", exception.what());
@@ -863,16 +1001,26 @@ bool MMKV::containsKey(MMKVKey_t key) {
     SCOPED_LOCK(m_lock);
     checkLoadData();
 
-    if (m_crypter) {
-        return m_dicCrypt->find(key) != m_dicCrypt->end();
-    } else {
-        return m_dic->find(key) != m_dic->end();
+    if (likely(!m_enableKeyExpire)) {
+        if (m_crypter) {
+            return m_dicCrypt->find(key) != m_dicCrypt->end();
+        } else {
+            return m_dic->find(key) != m_dic->end();
+        }
     }
+    auto raw = getDataWithoutMTimeForKey(key);
+    return raw.length() != 0;
 }
 
-size_t MMKV::count() {
+size_t MMKV::count(bool filterExpire) {
     SCOPED_LOCK(m_lock);
     checkLoadData();
+
+    if (unlikely(filterExpire && m_enableKeyExpire)) {
+        SCOPED_LOCK(m_exclusiveProcessLock);
+        fullWriteback(nullptr, true);
+    }
+
     if (m_crypter) {
         return m_dicCrypt->size();
     } else {
@@ -905,10 +1053,15 @@ void MMKV::removeValueForKey(MMKVKey_t key) {
 
 #ifndef MMKV_APPLE
 
-vector<string> MMKV::allKeys() {
+vector<string> MMKV::allKeys(bool filterExpire) {
     SCOPED_LOCK(m_lock);
     checkLoadData();
 
+    if (unlikely(filterExpire && m_enableKeyExpire)) {
+        SCOPED_LOCK(m_exclusiveProcessLock);
+        fullWriteback(nullptr, true);
+    }
+
     vector<string> keys;
     if (m_crypter) {
         for (const auto &itr : *m_dicCrypt) {
@@ -975,12 +1128,15 @@ void MMKV::sync(SyncFlag flag) {
 }
 
 void MMKV::lock() {
+    SCOPED_LOCK(m_lock);
     m_exclusiveProcessLock->lock();
 }
 void MMKV::unlock() {
+    SCOPED_LOCK(m_lock);
     m_exclusiveProcessLock->unlock();
 }
 bool MMKV::try_lock() {
+    SCOPED_LOCK(m_lock);
     return m_exclusiveProcessLock->try_lock();
 }
 
@@ -995,7 +1151,7 @@ static bool backupOneToDirectoryByFilePath(const string &mmapKey, const MMKVPath
     bool ret = false;
     {
 #ifdef MMKV_WIN32
-        MMKVInfo("backup one mmkv[%s] from [%ws] to [%ws]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
+        MMKVInfo("backup one mmkv[%s] from [%ls] to [%ls]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
 #else
         MMKVInfo("backup one mmkv[%s] from [%s] to [%s]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
 #endif
@@ -1035,7 +1191,7 @@ bool MMKV::backupOneToDirectory(const string &mmapKey, const MMKVPath_t &dstPath
     // get one in cache, do it the easy way
     if (kv) {
 #ifdef MMKV_WIN32
-        MMKVInfo("backup one cached mmkv[%s] from [%ws] to [%ws]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
+        MMKVInfo("backup one cached mmkv[%s] from [%ls] to [%ls]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
 #else
         MMKVInfo("backup one cached mmkv[%s] from [%s] to [%s]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
 #endif
@@ -1109,7 +1265,7 @@ size_t MMKV::backupAllToDirectory(const MMKVPath_t &dstDir, const MMKVPath_t &sr
             auto srcCRCPath = srcPath + CRC_SUFFIX;
             if (mmapIDCRCSet.find(srcCRCPath) == mmapIDCRCSet.end()) {
 #ifdef MMKV_WIN32
-                MMKVWarning("crc not exist [%ws]", srcCRCPath.c_str());
+                MMKVWarning("crc not exist [%ls]", srcCRCPath.c_str());
 #else
                 MMKVWarning("crc not exist [%s]", srcCRCPath.c_str());
 #endif
@@ -1146,7 +1302,7 @@ size_t MMKV::backupAllToDirectory(const MMKVPath_t &dstDir, const MMKVPath_t *sr
 
 static bool restoreOneFromDirectoryByFilePath(const string &mmapKey, const MMKVPath_t &srcPath, const MMKVPath_t &dstPath) {
     auto dstCRCPath = dstPath + CRC_SUFFIX;
-    File dstCRCFile(move(dstCRCPath), OpenFlag::ReadWrite | OpenFlag::Create);
+    File dstCRCFile(std::move(dstCRCPath), OpenFlag::ReadWrite | OpenFlag::Create);
     if (!dstCRCFile.isFileValid()) {
         return false;
     }
@@ -1154,7 +1310,7 @@ static bool restoreOneFromDirectoryByFilePath(const string &mmapKey, const MMKVP
     bool ret = false;
     {
 #ifdef MMKV_WIN32
-        MMKVInfo("restore one mmkv[%s] from [%ws] to [%ws]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
+        MMKVInfo("restore one mmkv[%s] from [%ls] to [%ls]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
 #else
         MMKVInfo("restore one mmkv[%s] from [%s] to [%s]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
 #endif
@@ -1196,7 +1352,7 @@ bool MMKV::restoreOneFromDirectory(const string &mmapKey, const MMKVPath_t &srcP
     // get one in cache, do it the easy way
     if (kv) {
 #ifdef MMKV_WIN32
-        MMKVInfo("restore one cached mmkv[%s] from [%ws] to [%ws]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
+        MMKVInfo("restore one cached mmkv[%s] from [%ls] to [%ls]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
 #else
         MMKVInfo("restore one cached mmkv[%s] from [%s] to [%s]", mmapKey.c_str(), srcPath.c_str(), dstPath.c_str());
 #endif
@@ -1207,7 +1363,17 @@ bool MMKV::restoreOneFromDirectory(const string &mmapKey, const MMKVPath_t &srcP
         auto ret = copyFileContent(srcPath, kv->m_file->getFd());
         if (ret) {
             auto srcCRCPath = srcPath + CRC_SUFFIX;
-            ret = copyFileContent(srcCRCPath, kv->m_metaFile->getFd());
+            // ret = copyFileContent(srcCRCPath, kv->m_metaFile->getFd());
+#ifndef MMKV_ANDROID
+            MemoryFile srcCRCFile(srcCRCPath);
+#else
+            MemoryFile srcCRCFile(srcCRCPath, DEFAULT_MMAP_SIZE, MMFILE_TYPE_FILE);
+#endif
+            if (srcCRCFile.isFileValid()) {
+                memcpy(kv->m_metaFile->getMemory(), srcCRCFile.getMemory(), sizeof(MMKVMetaInfo));
+            } else {
+                ret = false;
+            }
         }
 
         // reload data after restore
@@ -1263,7 +1429,7 @@ size_t MMKV::restoreAllFromDirectory(const MMKVPath_t &srcDir, const MMKVPath_t
             auto srcCRCPath = srcPath + CRC_SUFFIX;
             if (mmapIDCRCSet.find(srcCRCPath) == mmapIDCRCSet.end()) {
 #ifdef MMKV_WIN32
-                MMKVWarning("crc not exist [%ws]", srcCRCPath.c_str());
+                MMKVWarning("crc not exist [%ls]", srcCRCPath.c_str());
 #else
                 MMKVWarning("crc not exist [%s]", srcCRCPath.c_str());
 #endif

+ 53 - 6
Pods/MMKVCore/Core/MMKV.h

@@ -89,18 +89,29 @@ class MMKV {
     mmkv::InterProcessLock *m_sharedProcessLock;
     mmkv::InterProcessLock *m_exclusiveProcessLock;
 
+    bool m_enableKeyExpire = false;
+    uint32_t m_expiredInSeconds = ExpireNever;
+
 #ifdef MMKV_APPLE
     using MMKVKey_t = NSString *__unsafe_unretained;
     static bool isKeyEmpty(MMKVKey_t key) { return key.length <= 0; }
+#  define key_length(key) key.length
+#  define retain_key(key) [key retain]
+#  define release_key(key) [key release]
 #else
     using MMKVKey_t = const std::string &;
     static bool isKeyEmpty(MMKVKey_t key) { return key.empty(); }
+#  define key_length(key) key.length()
+#  define retain_key(key) ((void)0)
+#  define release_key(key) ((void)0)
 #endif
 
     void loadFromFile();
 
     void partialLoadFromFile();
 
+    void loadMetaInfoAndCheck();
+
     void checkDataValid(bool &loadFromFile, bool &needFullWriteback);
 
     void checkLoadData();
@@ -121,9 +132,15 @@ class MMKV {
 
     bool ensureMemorySize(size_t newSize);
 
-    bool fullWriteback(mmkv::AESCrypt *newCrypter = nullptr);
+    bool expandAndWriteBack(size_t newSize, std::pair<mmkv::MMBuffer, size_t> preparedData, bool needSync = true);
+
+    bool fullWriteback(mmkv::AESCrypt *newCrypter = nullptr, bool onlyWhileExpire = false);
+
+    bool doFullWriteBack(std::pair<mmkv::MMBuffer, size_t> preparedData, mmkv::AESCrypt *newCrypter, bool needSync = true);
+
+    bool doFullWriteBack(mmkv::MMKVVector &&vec);
 
-    bool doFullWriteBack(std::pair<mmkv::MMBuffer, size_t> preparedData, mmkv::AESCrypt *newCrypter);
+    mmkv::MMBuffer getRawDataForKey(MMKVKey_t key);
 
     mmkv::MMBuffer getDataForKey(MMKVKey_t key);
 
@@ -154,9 +171,14 @@ class MMKV {
     static bool restoreOneFromDirectory(const std::string &mmapKey, const MMKVPath_t &srcPath, const MMKVPath_t &dstPath, bool compareFullPath);
     static size_t restoreAllFromDirectory(const MMKVPath_t &srcDir, const MMKVPath_t &dstDir, bool isInSpecialDir);
 
+    static uint32_t getCurrentTimeInSecond();
+    uint32_t getExpireTimeForKey(MMKVKey_t key);
+    mmkv::MMBuffer getDataWithoutMTimeForKey(MMKVKey_t key);
+    size_t filterExpiredKeys();
+
 public:
     // call this before getting any MMKV instance
-    static void initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel = MMKVLogInfo);
+    static void initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel = MMKVLogInfo, mmkv::LogHandler handler = nullptr);
 
 #ifdef MMKV_APPLE
     // protect from some old code that don't call initializeMMKV()
@@ -216,35 +238,49 @@ public:
 #endif
 
     bool set(bool value, MMKVKey_t key);
+    bool set(bool value, MMKVKey_t key, uint32_t expireDuration);
 
     bool set(int32_t value, MMKVKey_t key);
+    bool set(int32_t value, MMKVKey_t key, uint32_t expireDuration);
 
     bool set(uint32_t value, MMKVKey_t key);
+    bool set(uint32_t value, MMKVKey_t key, uint32_t expireDuration);
 
     bool set(int64_t value, MMKVKey_t key);
+    bool set(int64_t value, MMKVKey_t key, uint32_t expireDuration);
 
     bool set(uint64_t value, MMKVKey_t key);
+    bool set(uint64_t value, MMKVKey_t key, uint32_t expireDuration);
 
     bool set(float value, MMKVKey_t key);
+    bool set(float value, MMKVKey_t key, uint32_t expireDuration);
 
     bool set(double value, MMKVKey_t key);
+    bool set(double value, MMKVKey_t key, uint32_t expireDuration);
 
     // avoid unexpected type conversion (pointer to bool, etc)
     template <typename T>
     bool set(T value, MMKVKey_t key) = delete;
+    template <typename T>
+    bool set(T value, MMKVKey_t key, uint32_t expireDuration) = delete;
 
 #ifdef MMKV_APPLE
     bool set(NSObject<NSCoding> *__unsafe_unretained obj, MMKVKey_t key);
+    bool set(NSObject<NSCoding> *__unsafe_unretained obj, MMKVKey_t key, uint32_t expireDuration);
 
     NSObject *getObject(MMKVKey_t key, Class cls);
 #else  // !defined(MMKV_APPLE)
     bool set(const char *value, MMKVKey_t key);
+    bool set(const char *value, MMKVKey_t key, uint32_t expireDuration);
 
     bool set(const std::string &value, MMKVKey_t key);
+    bool set(const std::string &value, MMKVKey_t key, uint32_t expireDuration);
 
     bool set(const mmkv::MMBuffer &value, MMKVKey_t key);
+    bool set(const mmkv::MMBuffer &value, MMKVKey_t key, uint32_t expireDuration);
 
     bool set(const std::vector<std::string> &vector, MMKVKey_t key);
+    bool set(const std::vector<std::string> &vector, MMKVKey_t key, uint32_t expireDuration);
 
     bool getString(MMKVKey_t key, std::string &result);
 
@@ -279,14 +315,24 @@ public:
 
     bool containsKey(MMKVKey_t key);
 
-    size_t count();
+    // filterExpire: return count of all non-expired keys, keep in mind it comes with cost
+    size_t count(bool filterExpire = false);
 
     size_t totalSize();
 
     size_t actualSize();
 
+    static constexpr uint32_t ExpireNever = 0;
+
+    // all keys created (or last modified) longer than expiredInSeconds will be deleted on next full-write-back
+    // expiredInSeconds = MMKV::ExpireNever (0) means no common expiration duration for all keys, aka each key will have it's own expiration duration
+    bool enableAutoKeyExpire(uint32_t expiredInSeconds = 0);
+
+    bool disableAutoKeyExpire();
+
 #ifdef MMKV_APPLE
-    NSArray *allKeys();
+    // filterExpire: return all non-expired keys, keep in mind it comes with cost
+    NSArray *allKeys(bool filterExpire = false);
 
     void removeValuesForKeys(NSArray *arrKeys);
 
@@ -298,7 +344,8 @@ public:
     static bool isInBackground();
 #    endif
 #else  // !defined(MMKV_APPLE)
-    std::vector<std::string> allKeys();
+    // filterExpire: return all non-expired keys, keep in mind it comes with cost
+    std::vector<std::string> allKeys(bool filterExpire = false);
 
     void removeValuesForKeys(const std::vector<std::string> &arrKeys);
 #endif // MMKV_APPLE

+ 1 - 1
Pods/MMKVCore/Core/MMKVLog.cpp

@@ -28,7 +28,7 @@ MMKVLogLevel g_currentLogLevel = MMKVLogDebug;
 MMKVLogLevel g_currentLogLevel = MMKVLogInfo;
 #endif
 
-mmkv::LogHandler g_logHandler;
+mmkv::LogHandler g_logHandler = nullptr;
 
 #ifndef __FILE_NAME__
 const char *_getFileName(const char *path) {

+ 18 - 0
Pods/MMKVCore/Core/MMKVMetaInfo.hpp

@@ -39,6 +39,15 @@ enum MMKVVersion : uint32_t {
 
     // store actual size together with crc checksum, try to reduce file corruption
     MMKVVersionActualSize = 3,
+
+    // store extra flags
+    MMKVVersionFlag = 4,
+
+    // preserved for next use
+    MMKVVersionNext = 5,
+
+    // always large than next, a placeholder for error check
+    MMKVVersionHolder = MMKVVersionNext + 1,
 };
 
 struct MMKVMetaInfo {
@@ -55,6 +64,15 @@ struct MMKVMetaInfo {
         uint32_t _reserved[16] = {};
     } m_lastConfirmedMetaInfo;
 
+    uint64_t m_flags = 0;
+
+    enum MMKVMetaInfoFlag : uint64_t {
+        EnableKeyExipre = 1 << 0,
+    };
+    bool hasFlag(MMKVMetaInfoFlag flag) { return (m_flags & flag) != 0; }
+    void setFlag(MMKVMetaInfoFlag flag) { m_flags |= flag; }
+    void unsetFlag(MMKVMetaInfoFlag flag) { m_flags &= ~flag; }
+
     void write(void *ptr) const {
         MMKV_ASSERT(ptr);
         memcpy(ptr, this, sizeof(MMKVMetaInfo));

+ 2 - 2
Pods/MMKVCore/Core/MMKVPredef.h

@@ -34,7 +34,7 @@
 #include <vector>
 #include <unordered_map>
 
-constexpr auto MMKV_VERSION = "v1.2.14";
+constexpr auto MMKV_VERSION = "v1.3.1";
 
 #ifdef DEBUG
 #    define MMKV_DEBUG
@@ -84,7 +84,7 @@ constexpr auto MMKV_VERSION = "v1.2.14";
 #    include <windows.h>
 
 constexpr auto MMKV_PATH_SLASH = L"\\";
-#    define MMKV_PATH_FORMAT "%ws"
+#    define MMKV_PATH_FORMAT "%ls"
 using MMKVFileHandle_t = HANDLE;
 using MMKVPath_t = std::wstring;
 extern MMKVPath_t string2MMKVPath_t(const std::string &str);

+ 496 - 72
Pods/MMKVCore/Core/MMKV_IO.cpp

@@ -37,6 +37,7 @@
 #include <algorithm>
 #include <cassert>
 #include <cstring>
+#include <ctime>
 
 #ifdef MMKV_IOS
 #    include "MMKV_OSX.h"
@@ -52,14 +53,10 @@ using namespace std;
 using namespace mmkv;
 using KVHolderRet_t = std::pair<bool, KeyValueHolder>;
 
-constexpr uint32_t Fixed32Size = pbFixed32Size();
-
 MMKV_NAMESPACE_BEGIN
 
 void MMKV::loadFromFile() {
-    if (m_metaFile->isFileValid()) {
-        m_metaInfo->read(m_metaFile->getMemory());
-    }
+    loadMetaInfoAndCheck();
 #ifndef MMKV_DISABLE_CRYPT
     if (m_crypter) {
         if (m_metaInfo->m_version >= MMKVVersionRandomIV) {
@@ -164,7 +161,7 @@ void MMKV::partialLoadFromFile() {
                     m_output->seek(addedSize);
                     m_hasFullWriteback = false;
 
-                    auto count = m_crypter ? m_dicCrypt->size() : m_dic->size();
+                    [[maybe_unused]] auto count = m_crypter ? m_dicCrypt->size() : m_dic->size();
                     MMKVDebug("partial loaded [%s] with %zu values", m_mmapID.c_str(), count);
                     return;
                 } else {
@@ -178,6 +175,37 @@ void MMKV::partialLoadFromFile() {
     loadFromFile();
 }
 
+void MMKV::loadMetaInfoAndCheck() {
+    if (!m_metaFile->isFileValid()) {
+        m_metaFile->reloadFromFile();
+    }
+    if (!m_metaFile->isFileValid()) {
+        MMKVError("file [%s] not valid", m_metaFile->getPath().c_str());
+        return;
+    }
+
+    m_metaInfo->read(m_metaFile->getMemory());
+    // the meta file is in specious status
+    if (m_metaInfo->m_version >= MMKVVersionHolder) {
+        MMKVWarning("meta file [%s] in specious state, version %u, flags 0x%llx", m_mmapID.c_str(), m_metaInfo->m_version, m_metaInfo->m_flags);
+
+        // MMKVVersionActualSize is the last version we don't check meta file
+        m_metaInfo->m_version = MMKVVersionActualSize;
+        m_metaInfo->m_flags = 0;
+        m_metaInfo->write(m_metaFile->getMemory());
+    }
+
+    if (m_metaInfo->m_version >= MMKVVersionFlag) {
+        m_enableKeyExpire = m_metaInfo->hasFlag(MMKVMetaInfo::EnableKeyExipre);
+        MMKVInfo("meta file [%s] has flag [%llu]", m_mmapID.c_str(), m_metaInfo->m_flags);
+    } else {
+        if (m_metaInfo->m_flags != 0) {
+            m_metaInfo->m_flags = 0;
+            m_metaInfo->write(m_metaFile->getMemory());
+        }
+    }
+}
+
 void MMKV::checkDataValid(bool &loadFromFile, bool &needFullWriteback) {
     // try auto recover from last confirmed location
     auto fileSize = m_file->getFileSize();
@@ -335,10 +363,20 @@ static pair<MMBuffer, size_t> prepareEncode(const MMKVMapCrypt &dic) {
     // skip the pb size of buffer
     auto sizeOfMap = CodedInputData(buffer.getPtr(), buffer.length()).readUInt32();
     totalSize += sizeOfMap;
-    return make_pair(move(buffer), totalSize);
+    return make_pair(std::move(buffer), totalSize);
 }
 #endif
 
+static pair<MMBuffer, size_t> prepareEncode(MMKVVector &&vec) {
+    // make some room for placeholder
+    size_t totalSize = ItemSizeHolderSize;
+    auto buffer = MiniPBCoder::encodeDataWithObject(vec);
+    // skip the pb size of buffer
+    auto sizeOfMap = CodedInputData(buffer.getPtr(), buffer.length()).readUInt32();
+    totalSize += sizeOfMap;
+    return make_pair(std::move(buffer), totalSize);
+}
+
 // since we use append mode, when -[setData: forKey:] many times, space may not be enough
 // try a full rewrite to make space
 bool MMKV::ensureMemorySize(size_t newSize) {
@@ -348,38 +386,50 @@ bool MMKV::ensureMemorySize(size_t newSize) {
     }
 
     if (newSize >= m_output->spaceLeft() || (m_crypter ? m_dicCrypt->empty() : m_dic->empty())) {
+        // remove expired keys
+        if (m_enableKeyExpire) {
+            filterExpiredKeys();
+        }
         // try a full rewrite to make space
-        auto fileSize = m_file->getFileSize();
         auto preparedData = m_crypter ? prepareEncode(*m_dicCrypt) : prepareEncode(*m_dic);
-        auto sizeOfDic = preparedData.second;
-        size_t lenNeeded = sizeOfDic + Fixed32Size + newSize;
-        size_t dicCount = m_crypter ? m_dicCrypt->size() : m_dic->size();
-        size_t avgItemSize = lenNeeded / std::max<size_t>(1, dicCount);
-        size_t futureUsage = avgItemSize * std::max<size_t>(8, (dicCount + 1) / 2);
-        // 1. no space for a full rewrite, double it
-        // 2. or space is not large enough for future usage, double it to avoid frequently full rewrite
-        if (lenNeeded >= fileSize || (lenNeeded + futureUsage) >= fileSize) {
-            size_t oldSize = fileSize;
-            do {
-                fileSize *= 2;
-            } while (lenNeeded + futureUsage >= fileSize);
-            MMKVInfo("extending [%s] file size from %zu to %zu, incoming size:%zu, future usage:%zu", m_mmapID.c_str(),
-                     oldSize, fileSize, newSize, futureUsage);
-
-            // if we can't extend size, rollback to old state
-            if (!m_file->truncate(fileSize)) {
-                return false;
-            }
+        // m_actualSize == 0 means inserting key-vakue for the first time, no need to call msync()
+        return expandAndWriteBack(newSize, std::move(preparedData), m_actualSize > 0);
+    }
+    return true;
+}
 
-            // check if we fail to make more space
-            if (!isFileValid()) {
-                MMKVWarning("[%s] file not valid", m_mmapID.c_str());
-                return false;
-            }
+// try a full rewrite to make space
+bool MMKV::expandAndWriteBack(size_t newSize, std::pair<mmkv::MMBuffer, size_t> preparedData, bool needSync) {
+    auto fileSize = m_file->getFileSize();
+    auto sizeOfDic = preparedData.second;
+    size_t lenNeeded = sizeOfDic + Fixed32Size + newSize;
+    size_t nowDicCount = m_crypter ? m_dicCrypt->size() : m_dic->size();
+    size_t laterDicCount = std::max<size_t>(1, nowDicCount + 1);
+    // or use <cmath> ceil()
+    size_t avgItemSize = (lenNeeded + laterDicCount - 1) / laterDicCount;
+    size_t futureUsage = avgItemSize * std::max<size_t>(8, laterDicCount / 2);
+    // 1. no space for a full rewrite, double it
+    // 2. or space is not large enough for future usage, double it to avoid frequently full rewrite
+    if (lenNeeded >= fileSize || (needSync && (lenNeeded + futureUsage) >= fileSize)) {
+        size_t oldSize = fileSize;
+        do {
+            fileSize *= 2;
+        } while (lenNeeded + futureUsage >= fileSize);
+        MMKVInfo("extending [%s] file size from %zu to %zu, incoming size:%zu, future usage:%zu", m_mmapID.c_str(),
+                 oldSize, fileSize, newSize, futureUsage);
+
+        // if we can't extend size, rollback to old state
+        if (!m_file->truncate(fileSize)) {
+            return false;
+        }
+
+        // check if we fail to make more space
+        if (!isFileValid()) {
+            MMKVWarning("[%s] file not valid", m_mmapID.c_str());
+            return false;
         }
-        return doFullWriteBack(move(preparedData), nullptr);
     }
-    return true;
+    return doFullWriteBack(std::move(preparedData), nullptr, needSync);
 }
 
 size_t MMKV::readActualSize() {
@@ -450,6 +500,11 @@ bool MMKV::writeActualSize(size_t size, uint32_t crcDigest, const void *iv, bool
         MMKVInfo("[%s] increase sequence to %u, crc %u, actualSize %u", m_mmapID.c_str(), m_metaInfo->m_sequence,
                  m_metaInfo->m_crcDigest, m_metaInfo->m_actualSize);
     }
+    if (m_metaInfo->m_version < MMKVVersionFlag) {
+        m_metaInfo->m_flags = 0;
+        m_metaInfo->m_version = MMKVVersionFlag;
+        needsFullWrite = true;
+    }
 #ifdef MMKV_IOS
     auto ret = guardForBackgroundWriting(m_metaFile->getMemory(), sizeof(MMKVMetaInfo));
     if (!ret.first) {
@@ -464,7 +519,7 @@ bool MMKV::writeActualSize(size_t size, uint32_t crcDigest, const void *iv, bool
     return true;
 }
 
-MMBuffer MMKV::getDataForKey(MMKVKey_t key) {
+MMBuffer MMKV::getRawDataForKey(MMKVKey_t key) {
     checkLoadData();
 #ifndef MMKV_DISABLE_CRYPT
     if (m_crypter) {
@@ -486,6 +541,13 @@ MMBuffer MMKV::getDataForKey(MMKVKey_t key) {
     return nan;
 }
 
+mmkv::MMBuffer MMKV::getDataForKey(MMKVKey_t key) {
+    if (unlikely(m_enableKeyExpire)) {
+        return getDataWithoutMTimeForKey(key);
+    }
+    return getRawDataForKey(key);
+}
+
 #ifndef MMKV_DISABLE_CRYPT
 // for Apple watch simulator
 #    if defined(TARGET_OS_SIMULATOR) && defined(TARGET_CPU_X86)
@@ -522,12 +584,24 @@ bool MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key, bool isDataHolder) {
             if (!ret.first) {
                 return false;
             }
+            KeyValueHolderCrypt kvHolder;
             if (KeyValueHolderCrypt::isValueStoredAsOffset(ret.second.valueSize)) {
-                KeyValueHolderCrypt kvHolder(ret.second.keySize, ret.second.valueSize, ret.second.offset);
+                kvHolder = KeyValueHolderCrypt(ret.second.keySize, ret.second.valueSize, ret.second.offset);
                 memcpy(&kvHolder.cryptStatus, &t_status, sizeof(t_status));
-                itr->second = move(kvHolder);
             } else {
-                itr->second = KeyValueHolderCrypt(move(data));
+                kvHolder = KeyValueHolderCrypt(std::move(data));
+            }
+            if (likely(!m_enableKeyExpire)) {
+                itr->second = std::move(kvHolder);
+            } else {
+                itr = m_dicCrypt->find(key);
+                if (itr != m_dicCrypt->end()) {
+                    itr->second = std::move(kvHolder);
+                } else {
+                    // in case filterExpiredKeys() is triggered
+                    m_dicCrypt->emplace(key, std::move(kvHolder));
+                    retain_key(key);
+                }
             }
         } else {
             auto ret = appendDataWithKey(data, key, isDataHolder);
@@ -541,31 +615,45 @@ bool MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key, bool isDataHolder) {
                     memcpy(&(r.first->second.cryptStatus), &t_status, sizeof(t_status));
                 }
             } else {
-                m_dicCrypt->emplace(key, KeyValueHolderCrypt(move(data)));
+                m_dicCrypt->emplace(key, KeyValueHolderCrypt(std::move(data)));
             }
+            retain_key(key);
         }
     } else
 #endif // MMKV_DISABLE_CRYPT
     {
         auto itr = m_dic->find(key);
         if (itr != m_dic->end()) {
-            auto ret = appendDataWithKey(data, itr->second, isDataHolder);
-            if (!ret.first) {
-                return false;
+            if (likely(!m_enableKeyExpire)) {
+                auto ret = appendDataWithKey(data, itr->second, isDataHolder);
+                if (!ret.first) {
+                    return false;
+                }
+                itr->second = std::move(ret.second);
+            } else {
+                auto ret = appendDataWithKey(data, key, isDataHolder);
+                if (!ret.first) {
+                    return false;
+                }
+                itr = m_dic->find(key);
+                if (itr != m_dic->end()) {
+                    itr->second = std::move(ret.second);
+                } else {
+                    // in case filterExpiredKeys() is triggered
+                    m_dic->emplace(key, std::move(ret.second));
+                    retain_key(key);
+                }
             }
-            itr->second = std::move(ret.second);
         } else {
             auto ret = appendDataWithKey(data, key, isDataHolder);
             if (!ret.first) {
                 return false;
             }
             m_dic->emplace(key, std::move(ret.second));
+            retain_key(key);
         }
     }
     m_hasFullWriteback = false;
-#ifdef MMKV_APPLE
-    [key retain];
-#endif
     return true;
 }
 
@@ -582,6 +670,13 @@ bool MMKV::removeDataForKey(MMKVKey_t key) {
 #    ifdef MMKV_APPLE
             auto ret = appendDataWithKey(nan, key, itr->second);
             if (ret.first) {
+                if (unlikely(m_enableKeyExpire)) {
+                    // filterExpiredKeys() may invalid itr
+                    itr = m_dicCrypt->find(key);
+                    if (itr == m_dicCrypt->end()) {
+                        return true;
+                    }
+                }
                 auto oldKey = itr->first;
                 m_dicCrypt->erase(itr);
                 [oldKey release];
@@ -589,7 +684,11 @@ bool MMKV::removeDataForKey(MMKVKey_t key) {
 #    else
             auto ret = appendDataWithKey(nan, key);
             if (ret.first) {
-                m_dicCrypt->erase(itr);
+                if (unlikely(m_enableKeyExpire)) {
+                    m_dicCrypt->erase(key);
+                } else {
+                    m_dicCrypt->erase(itr);
+                }
             }
 #    endif
             return ret.first;
@@ -601,14 +700,26 @@ bool MMKV::removeDataForKey(MMKVKey_t key) {
         if (itr != m_dic->end()) {
             m_hasFullWriteback = false;
             static MMBuffer nan;
-            auto ret = appendDataWithKey(nan, itr->second);
+            auto ret = likely(!m_enableKeyExpire) ? appendDataWithKey(nan, itr->second) : appendDataWithKey(nan, key);
             if (ret.first) {
 #ifdef MMKV_APPLE
+                if (unlikely(m_enableKeyExpire)) {
+                    // filterExpiredKeys() may invalid itr
+                    itr = m_dic->find(key);
+                    if (itr == m_dic->end()) {
+                        return true;
+                    }
+                }
                 auto oldKey = itr->first;
                 m_dic->erase(itr);
                 [oldKey release];
 #else
-                m_dic->erase(itr);
+                if (unlikely(m_enableKeyExpire)) {
+                    // filterExpiredKeys() may invalid itr
+                    m_dic->erase(key);
+                } else {
+                    m_dic->erase(itr);
+                }
 #endif
             }
             return ret.first;
@@ -714,7 +825,7 @@ KVHolderRet_t MMKV::appendDataWithKey(const MMBuffer &data, const KeyValueHolder
     return doAppendDataWithKey(data, keyData, isDataHolder, keyLength);
 }
 
-bool MMKV::fullWriteback(AESCrypt *newCrypter) {
+bool MMKV::fullWriteback(AESCrypt *newCrypter, bool onlyWhileExpire) {
     if (m_hasFullWriteback) {
         return true;
     }
@@ -726,23 +837,32 @@ bool MMKV::fullWriteback(AESCrypt *newCrypter) {
         return false;
     }
 
-    if (m_crypter ? m_dicCrypt->empty() : m_dic->empty()) {
+    if (unlikely(m_enableKeyExpire)) {
+        auto expiredCount = filterExpiredKeys();
+        if (onlyWhileExpire && expiredCount == 0) {
+            return true;
+        }
+    }
+
+    auto isEmpty = m_crypter ? m_dicCrypt->empty() : m_dic->empty();
+    if (isEmpty) {
         clearAll();
         return true;
     }
 
+    SCOPED_LOCK(m_exclusiveProcessLock);
     auto preparedData = m_crypter ? prepareEncode(*m_dicCrypt) : prepareEncode(*m_dic);
     auto sizeOfDic = preparedData.second;
-    SCOPED_LOCK(m_exclusiveProcessLock);
     if (sizeOfDic > 0) {
         auto fileSize = m_file->getFileSize();
         if (sizeOfDic + Fixed32Size <= fileSize) {
-            return doFullWriteBack(move(preparedData), newCrypter);
+            return doFullWriteBack(std::move(preparedData), newCrypter);
         } else {
             assert(0);
             assert(newCrypter == nullptr);
-            // ensureMemorySize will extend file & full rewrite, no need to write back again
-            return ensureMemorySize(sizeOfDic + Fixed32Size - fileSize);
+            // expandAndWriteBack() will extend file & full rewrite, no need to write back again
+            auto newSize = sizeOfDic + Fixed32Size - fileSize;
+            return expandAndWriteBack(newSize, std::move(preparedData));
         }
     }
     return false;
@@ -893,9 +1013,25 @@ static void memmoveDictionary(MMKVMapCrypt &dic,
 
 #endif // MMKV_DISABLE_CRYPT
 
-bool MMKV::doFullWriteBack(pair<MMBuffer, size_t> preparedData, AESCrypt *newCrypter) {
+static void fullWriteBackWholeData(MMBuffer allData, size_t totalSize, CodedOutputData *output) {
+    auto originOutputPtr = output->curWritePointer();
+    output->writeRawVarint32(ItemSizeHolder);
+    if (allData.length() > 0) {
+        auto dataSize = CodedInputData(allData.getPtr(), allData.length()).readUInt32();
+        if (dataSize > 0) {
+            auto dataPtr = (uint8_t *)allData.getPtr() + pbRawVarint32Size(dataSize);
+            memcpy(output->curWritePointer(), dataPtr, dataSize);
+            output->seek(dataSize);
+        }
+    }
+    [[maybe_unused]] auto writtenSize = (size_t) (output->curWritePointer() - originOutputPtr);
+    assert(writtenSize == totalSize);
+}
+
+#ifndef MMKV_DISABLE_CRYPT
+bool MMKV::doFullWriteBack(pair<MMBuffer, size_t> prepared, AESCrypt *newCrypter, bool needSync) {
     auto ptr = (uint8_t *) m_file->getMemory();
-    auto totalSize = preparedData.second;
+    auto totalSize = prepared.second;
 #ifdef MMKV_IOS
     auto ret = guardForBackgroundWriting(ptr + Fixed32Size, totalSize);
     if (!ret.first) {
@@ -903,47 +1039,79 @@ bool MMKV::doFullWriteBack(pair<MMBuffer, size_t> preparedData, AESCrypt *newCry
     }
 #endif
 
-#ifndef MMKV_DISABLE_CRYPT
     uint8_t newIV[AES_KEY_LEN];
-    auto decrypter = m_crypter;
     auto encrypter = (newCrypter == InvalidCryptPtr) ? nullptr : (newCrypter ? newCrypter : m_crypter);
     if (encrypter) {
         AESCrypt::fillRandomIV(newIV);
         encrypter->resetIV(newIV, sizeof(newIV));
     }
-#endif
 
     delete m_output;
     m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
-#ifndef MMKV_DISABLE_CRYPT
     if (m_crypter) {
-        memmoveDictionary(*m_dicCrypt, m_output, ptr, decrypter, encrypter, preparedData);
+        auto decrypter = m_crypter;
+        memmoveDictionary(*m_dicCrypt, m_output, ptr, decrypter, encrypter, prepared);
+    } else if (prepared.first.length() != 0) {
+        auto &preparedData = prepared.first;
+        fullWriteBackWholeData(std::move(preparedData), totalSize, m_output);
+        if (encrypter) {
+            encrypter->encrypt(ptr + Fixed32Size, ptr + Fixed32Size, totalSize);
+        }
     } else {
-#else
-    {
-        auto encrypter = m_crypter;
-#endif
         memmoveDictionary(*m_dic, m_output, ptr, encrypter, totalSize);
     }
 
     m_actualSize = totalSize;
-#ifndef MMKV_DISABLE_CRYPT
     if (encrypter) {
         recaculateCRCDigestWithIV(newIV);
-    } else
-#endif
-    {
+    } else {
         recaculateCRCDigestWithIV(nullptr);
     }
     m_hasFullWriteback = true;
-    // make sure lastConfirmedMetaInfo is saved
-    sync(MMKV_SYNC);
+    // make sure lastConfirmedMetaInfo is saved if needed
+    if (needSync) {
+        sync(MMKV_SYNC);
+    }
+    return true;
+}
+
+#else // MMKV_DISABLE_CRYPT
+
+bool MMKV::doFullWriteBack(pair<MMBuffer, size_t> prepared, AESCrypt *, bool needSync) {
+    auto ptr = (uint8_t *) m_file->getMemory();
+    auto totalSize = prepared.second;
+#ifdef MMKV_IOS
+    auto ret = guardForBackgroundWriting(ptr + Fixed32Size, totalSize);
+    if (!ret.first) {
+        return false;
+    }
+#endif
+
+    delete m_output;
+    m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
+    if (prepared.first.length() != 0) {
+        auto &preparedData = prepared.first;
+        fullWriteBackWholeData(std::move(preparedData), totalSize, m_output);
+    } else {
+        constexpr AESCrypt *encrypter = nullptr;
+        memmoveDictionary(*m_dic, m_output, ptr, encrypter, totalSize);
+    }
+
+    m_actualSize = totalSize;
+    recaculateCRCDigestWithIV(nullptr);
+    m_hasFullWriteback = true;
+    // make sure lastConfirmedMetaInfo is saved if needed
+    if (needSync) {
+        sync(MMKV_SYNC);
+    }
     return true;
 }
+#endif // MMKV_DISABLE_CRYPT
 
 #ifndef MMKV_DISABLE_CRYPT
 bool MMKV::reKey(const string &cryptKey) {
     SCOPED_LOCK(m_lock);
+    SCOPED_LOCK(m_exclusiveProcessLock);
     checkLoadData();
 
     bool ret = false;
@@ -1118,4 +1286,260 @@ bool MMKV::isFileValid(const string &mmapID, MMKVPath_t *relatePath) {
     }
 }
 
+// ---- auto expire ----
+
+uint32_t MMKV::getCurrentTimeInSecond() {
+    auto time = ::time(nullptr);
+    return static_cast<uint32_t>(time);
+}
+
+bool MMKV::doFullWriteBack(MMKVVector &&vec) {
+    auto preparedData = prepareEncode(std::move(vec));
+
+    // must clean before write-back and after prepareEncode()
+    if (m_crypter) {
+        clearDictionary(m_dicCrypt);
+    } else {
+        clearDictionary(m_dic);
+    }
+
+    bool ret = false;
+    auto sizeOfDic = preparedData.second;
+    auto fileSize = m_file->getFileSize();
+    if (sizeOfDic + Fixed32Size <= fileSize) {
+        ret = doFullWriteBack(std::move(preparedData), nullptr);
+    } else {
+        // expandAndWriteBack() will extend file & full rewrite, no need to write back again
+        auto newSize = sizeOfDic + Fixed32Size - fileSize;
+        ret = expandAndWriteBack(newSize, std::move(preparedData));
+    }
+
+    clearMemoryCache();
+    return ret;
+}
+
+bool MMKV::enableAutoKeyExpire(uint32_t expiredInSeconds) {
+    SCOPED_LOCK(m_lock);
+    SCOPED_LOCK(m_exclusiveProcessLock);
+    checkLoadData();
+
+    if (m_expiredInSeconds != expiredInSeconds) {
+        MMKVInfo("expiredInSeconds: %u", expiredInSeconds);
+        m_expiredInSeconds = expiredInSeconds;
+    }
+    m_enableKeyExpire = true;
+    if (m_metaInfo->hasFlag(MMKVMetaInfo::EnableKeyExipre)) {
+        return true;
+    }
+
+    auto autoRecordExpireTime = (m_expiredInSeconds != 0);
+    auto time = autoRecordExpireTime ? getCurrentTimeInSecond() + m_expiredInSeconds : 0;
+    MMKVInfo("turn on recording expire date for all keys inside [%s] from now %u", m_mmapID.c_str(), time);
+    m_metaInfo->setFlag(MMKVMetaInfo::EnableKeyExipre);
+    m_metaInfo->m_version = MMKVVersionFlag;
+
+    if (m_file->getFileSize() == DEFAULT_MMAP_SIZE && m_actualSize == 0) {
+        MMKVInfo("file is new, don't need a full writeback [%s], just update meta file", m_mmapID.c_str());
+        writeActualSize(0, 0, nullptr, IncreaseSequence);
+        m_metaFile->msync(MMKV_SYNC);
+        return true;
+    }
+
+    MMKVVector vec;
+    auto packKeyValue = [&](const MMKVKey_t &key, const MMBuffer &value) {
+        MMBuffer data(value.length() + Fixed32Size);
+        auto ptr = (uint8_t *)data.getPtr();
+        memcpy(ptr, value.getPtr(), value.length());
+        memcpy(ptr + value.length(), &time, Fixed32Size);
+        vec.emplace_back(key, std::move(data));
+    };
+
+    auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
+#ifndef MMKV_DISABLE_CRYPT
+    if (m_crypter) {
+        for (auto &pair : *m_dicCrypt) {
+            auto &key = pair.first;
+            auto &value = pair.second;
+            auto buffer = value.toMMBuffer(basePtr, m_crypter);
+            packKeyValue(key, buffer);
+        }
+    } else
+#endif
+    {
+        for (auto &pair : *m_dic) {
+            auto &key = pair.first;
+            auto &value = pair.second;
+            auto buffer = value.toMMBuffer(basePtr);
+            packKeyValue(key, buffer);
+        }
+    }
+
+    return doFullWriteBack(std::move(vec));
+}
+
+bool MMKV::disableAutoKeyExpire() {
+    SCOPED_LOCK(m_lock);
+    SCOPED_LOCK(m_exclusiveProcessLock);
+    checkLoadData();
+
+    m_expiredInSeconds = 0;
+    m_enableKeyExpire = false;
+    if (!m_metaInfo->hasFlag(MMKVMetaInfo::EnableKeyExipre)) {
+        return true;
+    }
+
+    MMKVInfo("erase previous recorded expire date for all keys inside [%s]", m_mmapID.c_str());
+    m_metaInfo->unsetFlag(MMKVMetaInfo::EnableKeyExipre);
+    m_metaInfo->m_version = MMKVVersionFlag;
+
+    if (m_file->getFileSize() == DEFAULT_MMAP_SIZE && m_actualSize == 0) {
+        MMKVInfo("file is new, don't need a full write-back [%s], just update meta file", m_mmapID.c_str());
+        writeActualSize(0, 0, nullptr, IncreaseSequence);
+        m_metaFile->msync(MMKV_SYNC);
+        return true;
+    }
+
+    MMKVVector vec;
+    auto packKeyValue = [&](const MMKVKey_t &key, const MMBuffer &value) {
+        assert(value.length() >= Fixed32Size);
+        MMBuffer data(value.length() - Fixed32Size);
+        auto ptr = (uint8_t *)data.getPtr();
+        memcpy(ptr, value.getPtr(), value.length() - Fixed32Size);
+        vec.emplace_back(key, std::move(data));
+    };
+
+    auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
+#ifndef MMKV_DISABLE_CRYPT
+    if (m_crypter) {
+        for (auto &pair : *m_dicCrypt) {
+            auto &key = pair.first;
+            auto &value = pair.second;
+            auto buffer = value.toMMBuffer(basePtr, m_crypter);
+            packKeyValue(key, buffer);
+        }
+    } else
+#endif
+    {
+        for (auto &pair : *m_dic) {
+            auto &key = pair.first;
+            auto &value = pair.second;
+            auto buffer = value.toMMBuffer(basePtr);
+            packKeyValue(key, buffer);
+        }
+    }
+
+    return doFullWriteBack(std::move(vec));
+}
+
+uint32_t MMKV::getExpireTimeForKey(MMKVKey_t key) {
+    SCOPED_LOCK(m_lock);
+    SCOPED_LOCK(m_sharedProcessLock);
+    checkLoadData();
+
+    if (!m_enableKeyExpire || key_length(key) == 0) {
+        return 0;
+    }
+    auto raw = getRawDataForKey(key);
+    assert(raw.length() == 0 || raw.length() >= Fixed32Size);
+    if (raw.length() < Fixed32Size) {
+        return 0;
+    }
+    auto ptr = (const uint8_t *)raw.getPtr() + raw.length() - Fixed32Size;
+    auto time = *(const uint32_t *)ptr;
+    return time;
+}
+
+mmkv::MMBuffer MMKV::getDataWithoutMTimeForKey(MMKVKey_t key) {
+    SCOPED_LOCK(m_lock);
+    SCOPED_LOCK(m_sharedProcessLock);
+    checkLoadData();
+
+    auto raw = getRawDataForKey(key);
+    assert(raw.length() == 0 || raw.length() >= Fixed32Size);
+    if (raw.length() < Fixed32Size) {
+        return raw;
+    }
+    auto newLength = raw.length() - Fixed32Size;
+    if (m_enableKeyExpire) {
+        auto ptr = (const uint8_t *)raw.getPtr() + newLength;
+        auto time = *(const uint32_t *)ptr;
+        if (time != ExpireNever && time <= getCurrentTimeInSecond()) {
+#ifdef MMKV_APPLE
+            MMKVInfo("deleting expired key [%@] in mmkv [%s], due date %u", key, m_mmapID.c_str(), time);
+#else
+            MMKVInfo("deleting expired key [%s] in mmkv [%s], due date %u", key.c_str(), m_mmapID.c_str(), time);
+#endif
+            removeValueForKey(key);
+            return MMBuffer();
+        }
+    }
+    return MMBuffer(std::move(raw), newLength);
+}
+
+#define NOOP ((void)0)
+
+size_t MMKV::filterExpiredKeys() {
+    if (!m_enableKeyExpire || (m_crypter ? m_dicCrypt->empty() : m_dic->empty())) {
+        return 0;
+    }
+    SCOPED_LOCK(m_sharedProcessLock);
+
+    auto now = getCurrentTimeInSecond();
+    MMKVInfo("filtering expired keys inside [%s] now: %u, m_expiredInSeconds: %u", m_mmapID.c_str(), now, m_expiredInSeconds);
+
+    size_t count = 0;
+    auto basePtr = (uint8_t *)(m_file->getMemory()) + Fixed32Size;
+#ifndef MMKV_DISABLE_CRYPT
+    if (m_crypter) {
+        for (auto itr = m_dicCrypt->begin(); itr != m_dicCrypt->end(); NOOP) {
+            auto &kvHolder = itr->second;
+            assert(kvHolder.realValueSize() >= Fixed32Size);
+            auto buffer = kvHolder.toMMBuffer(basePtr, m_crypter);
+            auto ptr = (uint8_t*) buffer.getPtr();
+            ptr += buffer.length() - Fixed32Size;
+            auto time = *(const uint32_t *)ptr;
+            if (time != ExpireNever && time <= now) {
+                auto oldKey = itr->first;
+                itr = m_dicCrypt->erase(itr);
+#ifdef MMKV_APPLE
+                MMKVInfo("deleting expired key [%@], due date %u", oldKey, time);
+                [oldKey release];
+#else
+                MMKVInfo("deleting expired key [%s], due date %u", oldKey.c_str(), time);
+#endif
+                count++;
+            } else {
+                itr++;
+            }
+        }
+    } else
+#endif // !MMKV_DISABLE_CRYPT
+    {
+        for (auto itr = m_dic->begin(); itr != m_dic->end(); NOOP) {
+            auto &kvHolder = itr->second;
+            assert(kvHolder.valueSize >= Fixed32Size);
+            auto ptr = basePtr + kvHolder.offset + kvHolder.computedKVSize;
+            ptr += kvHolder.valueSize - Fixed32Size;
+            auto time = *(const uint32_t *)ptr;
+            if (time != ExpireNever && time <= now) {
+                auto oldKey = itr->first;
+                itr = m_dic->erase(itr);
+#ifdef MMKV_APPLE
+                MMKVInfo("deleting expired key [%@], due date %u", oldKey, time);
+                [oldKey release];
+#else
+                MMKVInfo("deleting expired key [%s], due date %u", oldKey.c_str(), time);
+#endif
+                count++;
+            } else {
+                itr++;
+            }
+        }
+    }
+    if (count != 0) {
+        MMKVInfo("deleted %zu expired keys inside [%s]", count, m_mmapID.c_str());
+    }
+    return count;
+}
+
 MMKV_NAMESPACE_END

+ 71 - 15
Pods/MMKVCore/Core/MMKV_OSX.cpp

@@ -125,7 +125,7 @@ bool MMKV::isInBackground() {
 pair<bool, MLockPtr> guardForBackgroundWriting(void *ptr, size_t size) {
     if (g_isInBackground) {
         MLockPtr mlockPtr(ptr, size);
-        return make_pair(mlockPtr.isLocked(), move(mlockPtr));
+        return make_pair(mlockPtr.isLocked(), std::move(mlockPtr));
     } else {
         return make_pair(true, MLockPtr(nullptr, 0));
     }
@@ -134,6 +134,10 @@ pair<bool, MLockPtr> guardForBackgroundWriting(void *ptr, size_t size) {
 #    endif // MMKV_IOS
 
 bool MMKV::set(NSObject<NSCoding> *__unsafe_unretained obj, MMKVKey_t key) {
+    return set(obj, key, m_expiredInSeconds);
+}
+
+bool MMKV::set(NSObject<NSCoding> *__unsafe_unretained obj, MMKVKey_t key, uint32_t expireDuration) {
     if (isKeyEmpty(key)) {
         return false;
     }
@@ -152,22 +156,76 @@ bool MMKV::set(NSObject<NSCoding> *__unsafe_unretained obj, MMKVKey_t key) {
     if (tmpData) {
         // delay write the size needed for encoding tmpData
         // avoid memory copying
-        return setDataForKey(MMBuffer(tmpData, MMBufferNoCopy), key, true);
+        if (likely(!m_enableKeyExpire)) {
+            return setDataForKey(MMBuffer(tmpData, MMBufferNoCopy), key, true);
+        } else {
+            MMBuffer data(tmpData, MMBufferNoCopy);
+            if (data.length() > 0) {
+                auto tmp = MMBuffer(pbMMBufferSize(data) + Fixed32Size);
+                CodedOutputData output(tmp.getPtr(), tmp.length());
+                output.writeData(data);
+                auto time = (expireDuration != 0) ? getCurrentTimeInSecond() + expireDuration : 0;
+                output.writeRawLittleEndian32(UInt32ToInt32(time));
+                data = std::move(tmp);
+            }
+            return setDataForKey(std::move(data), key);
+        }
     } else if ([obj isKindOfClass:NSDate.class]) {
         NSDate *oDate = (NSDate *) obj;
         double time = oDate.timeIntervalSince1970;
-        return set(time, key);
+        return set(time, key, expireDuration);
     } else {
         /*if ([object conformsToProtocol:@protocol(NSCoding)])*/ {
-            auto tmp = [NSKeyedArchiver archivedDataWithRootObject:obj];
-            if (tmp.length > 0) {
-                return setDataForKey(MMBuffer(tmp, MMBufferNoCopy), key);
+            @try {
+                NSError *error = nil;
+                auto archived = [NSKeyedArchiver archivedDataWithRootObject:obj requiringSecureCoding:NO error:&error];
+                if (error) {
+                    MMKVError("fail to archive: %@", error);
+                    return false;
+                }
+                if (archived.length > 0) {
+                    if (likely(!m_enableKeyExpire)) {
+                        return setDataForKey(MMBuffer(archived, MMBufferNoCopy), key);
+                    } else {
+                        MMBuffer data(archived, MMBufferNoCopy);
+                        if (data.length() > 0) {
+                            auto tmp = MMBuffer(data.length() + Fixed32Size);
+                            CodedOutputData output(tmp.getPtr(), tmp.length());
+                            output.writeRawData(data); // NSKeyedArchiver has its own size management
+                            auto time = (expireDuration != 0) ? getCurrentTimeInSecond() + expireDuration : 0;
+                            output.writeRawLittleEndian32(UInt32ToInt32(time));
+                            data = std::move(tmp);
+                        }
+                        return setDataForKey(std::move(data), key);
+                    }
+                }
+            } @catch (NSException *exception) {
+                MMKVError("exception: %@", exception.reason);
             }
         }
     }
     return false;
 }
 
+static id unSecureUnArchiveObjectWithData(NSData *data) {
+    @try {
+        NSError *error = nil;
+        auto unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:&error];
+        if (error) {
+            MMKVError("fail to init unarchiver %@", error);
+            return nil;
+        }
+
+        unarchiver.requiresSecureCoding = NO;
+        id result = [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];
+        [unarchiver release];
+        return result;
+    } @catch (NSException *exception) {
+        MMKVError("exception: %@", exception.reason);
+    }
+    return nil;
+}
+
 NSObject *MMKV::getObject(MMKVKey_t key, Class cls) {
     if (isKeyEmpty(key) || !cls) {
         return nil;
@@ -186,12 +244,7 @@ NSObject *MMKV::getObject(MMKVKey_t key, Class cls) {
         } else {
             if ([cls conformsToProtocol:@protocol(NSCoding)]) {
                 auto tmp = [NSData dataWithBytesNoCopy:data.getPtr() length:data.length() freeWhenDone:NO];
-                try {
-                    id result = [NSKeyedUnarchiver unarchiveObjectWithData:tmp];
-                    return result;
-                } catch (NSException *exception) {
-                    MMKVError("%s", exception.reason);
-                }
+                return unSecureUnArchiveObjectWithData(tmp);
             }
         }
     }
@@ -200,8 +253,6 @@ NSObject *MMKV::getObject(MMKVKey_t key, Class cls) {
 
 #    ifndef MMKV_DISABLE_CRYPT
 
-constexpr uint32_t Fixed32Size = pbFixed32Size();
-
 pair<bool, KeyValueHolder>
 MMKV::appendDataWithKey(const MMBuffer &data, MMKVKey_t key, const KeyValueHolderCrypt &kvHolder, bool isDataHolder) {
     if (kvHolder.type != KeyValueHolderType_Offset) {
@@ -222,10 +273,15 @@ MMKV::appendDataWithKey(const MMBuffer &data, MMKVKey_t key, const KeyValueHolde
 }
 #    endif
 
-NSArray *MMKV::allKeys() {
+NSArray *MMKV::allKeys(bool filterExpire) {
     SCOPED_LOCK(m_lock);
     checkLoadData();
 
+    if (unlikely(filterExpire && m_enableKeyExpire)) {
+        SCOPED_LOCK(m_exclusiveProcessLock);
+        fullWriteback(nullptr, true);
+    }
+
     NSMutableArray *keys = [NSMutableArray array];
     if (m_crypter) {
         for (const auto &pair : *m_dicCrypt) {

+ 8 - 4
Pods/MMKVCore/Core/MemoryFile.cpp

@@ -201,7 +201,7 @@ void MemoryFile::reloadFromFile() {
     if (isFileValid()) {
         MMKVWarning("calling reloadFromFile while the cache [%s] is still valid", m_diskFile.m_path.c_str());
         MMKV_ASSERT(0);
-        clearMemoryCache();
+        doCleanMemoryCache(false);
     }
 
     if (!m_diskFile.open()) {
@@ -271,15 +271,19 @@ extern bool mkPath(const MMKVPath_t &str) {
         if (stat(path, &sb) != 0) {
             if (errno != ENOENT || mkdir(path, 0777) != 0) {
                 MMKVWarning("%s : %s", path, strerror(errno));
-                free(path);
-                return false;
+                // there's report that some Android devices might not have access permission on parent dir
+                if (done) {
+                    free(path);
+                    return false;
+                }
+                goto LContinue;
             }
         } else if (!S_ISDIR(sb.st_mode)) {
             MMKVWarning("%s: %s", path, strerror(ENOTDIR));
             free(path);
             return false;
         }
-
+LContinue:
         *slash = '/';
     }
     free(path);

+ 1 - 1
Pods/MMKVCore/Core/MemoryFile_OSX.cpp

@@ -61,7 +61,7 @@ bool tryAtomicRename(const char *src, const char *dst) {
     bool renamed = false;
 
     // try atomic swap first
-    if (@available(iOS 10.0, watchOS 3.0, *)) {
+    if (@available(iOS 10.0, watchOS 3.0, macOS 10.12, *)) {
         // renameat2() equivalent
         if (renamex_np(src, dst, RENAME_SWAP) == 0) {
             renamed = true;

+ 39 - 35
Pods/MMKVCore/Core/MemoryFile_Win32.cpp

@@ -70,20 +70,20 @@ bool File::open() {
     m_fd = CreateFile(m_path.c_str(), pair.first, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
                       pair.second, FILE_ATTRIBUTE_NORMAL, nullptr);
     if (!isFileValid()) {
-        MMKVError("fail to open:[%ws], %d", m_path.c_str(), GetLastError());
+        MMKVError("fail to open:[%ls], %d", m_path.c_str(), GetLastError());
         return false;
     }
-    MMKVInfo("open fd[%p], %ws", m_fd, m_path.c_str());
+    MMKVInfo("open fd[%p], %ls", m_fd, m_path.c_str());
     return true;
 }
 
 void File::close() {
     if (isFileValid()) {
-        MMKVInfo("closing fd[%p], %ws", m_fd, m_path.c_str());
+        MMKVInfo("closing fd[%p], %ls", m_fd, m_path.c_str());
         if (CloseHandle(m_fd)) {
             m_fd = INVALID_HANDLE_VALUE;
         } else {
-            MMKVError("fail to close [%ws], %d", m_path.c_str(), GetLastError());
+            MMKVError("fail to close [%ls], %d", m_path.c_str(), GetLastError());
         }
     }
 }
@@ -117,29 +117,32 @@ bool MemoryFile::truncate(size_t size) {
         m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
     }
 
+    if (m_ptr) {
+        if (!UnmapViewOfFile(m_ptr)) {
+            MMKVError("fail to munmap [%ls], %d", m_diskFile.m_path.c_str(), GetLastError());
+        }
+        m_ptr = nullptr;
+    }
+    if (m_fileMapping) {
+        CloseHandle(m_fileMapping);
+        m_fileMapping = nullptr;
+    }
+
     if (!ftruncate(m_diskFile.getFd(), m_size)) {
-        MMKVError("fail to truncate [%ws] to size %zu", m_diskFile.m_path.c_str(), m_size);
+        MMKVError("fail to truncate [%ls] to size %zu", m_diskFile.m_path.c_str(), m_size);
         m_size = oldSize;
+        mmap();
         return false;
     }
     if (m_size > oldSize) {
         if (!zeroFillFile(m_diskFile.getFd(), oldSize, m_size - oldSize)) {
-            MMKVError("fail to zeroFile [%ws] to size %zu", m_diskFile.m_path.c_str(), m_size);
+            MMKVError("fail to zeroFile [%ls] to size %zu", m_diskFile.m_path.c_str(), m_size);
             m_size = oldSize;
+            mmap();
             return false;
         }
     }
 
-    if (m_ptr) {
-        if (!UnmapViewOfFile(m_ptr)) {
-            MMKVError("fail to munmap [%ws], %d", m_diskFile.m_path.c_str(), GetLastError());
-        }
-        m_ptr = nullptr;
-    }
-    if (m_fileMapping) {
-        CloseHandle(m_fileMapping);
-        m_fileMapping = nullptr;
-    }
     auto ret = mmap();
     if (!ret) {
         doCleanMemoryCache(true);
@@ -152,13 +155,13 @@ bool MemoryFile::msync(SyncFlag syncFlag) {
         if (FlushViewOfFile(m_ptr, m_size)) {
             if (syncFlag == MMKV_SYNC) {
                 if (!FlushFileBuffers(m_diskFile.getFd())) {
-                    MMKVError("fail to FlushFileBuffers [%ws]:%d", m_diskFile.m_path.c_str(), GetLastError());
+                    MMKVError("fail to FlushFileBuffers [%ls]:%d", m_diskFile.m_path.c_str(), GetLastError());
                     return false;
                 }
             }
             return true;
         }
-        MMKVError("fail to FlushViewOfFile [%ws]:%d", m_diskFile.m_path.c_str(), GetLastError());
+        MMKVError("fail to FlushViewOfFile [%ls]:%d", m_diskFile.m_path.c_str(), GetLastError());
         return false;
     }
     return false;
@@ -167,12 +170,12 @@ bool MemoryFile::msync(SyncFlag syncFlag) {
 bool MemoryFile::mmap() {
     m_fileMapping = CreateFileMapping(m_diskFile.getFd(), nullptr, PAGE_READWRITE, 0, 0, nullptr);
     if (!m_fileMapping) {
-        MMKVError("fail to CreateFileMapping [%ws], %d", m_diskFile.m_path.c_str(), GetLastError());
+        MMKVError("fail to CreateFileMapping [%ls], %d", m_diskFile.m_path.c_str(), GetLastError());
         return false;
     } else {
         m_ptr = (char *) MapViewOfFile(m_fileMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);
         if (!m_ptr) {
-            MMKVError("fail to mmap [%ws], %d", m_diskFile.m_path.c_str(), GetLastError());
+            MMKVError("fail to mmap [%ls], %d", m_diskFile.m_path.c_str(), GetLastError());
             return false;
         }
     }
@@ -182,7 +185,7 @@ bool MemoryFile::mmap() {
 
 void MemoryFile::reloadFromFile() {
     if (isFileValid()) {
-        MMKVWarning("calling reloadFromFile while the cache [%ws] is still valid", m_diskFile.m_path.c_str());
+        MMKVWarning("calling reloadFromFile while the cache [%ls] is still valid", m_diskFile.m_path.c_str());
         assert(0);
         clearMemoryCache();
     }
@@ -248,12 +251,12 @@ bool mkPath(const MMKVPath_t &str) {
         auto attribute = GetFileAttributes(path);
         if (attribute == INVALID_FILE_ATTRIBUTES) {
             if (!CreateDirectory(path, nullptr)) {
-                MMKVError("fail to create dir:%ws, %d", str.c_str(), GetLastError());
+                MMKVError("fail to create dir:%ls, %d", str.c_str(), GetLastError());
                 free(path);
                 return false;
             }
         } else if (!(attribute & FILE_ATTRIBUTE_DIRECTORY)) {
-            MMKVError("%ws attribute:%d not a directry", str.c_str(), attribute);
+            MMKVError("%ls attribute:%d not a directry", str.c_str(), attribute);
             free(path);
             return false;
         }
@@ -279,14 +282,14 @@ MMBuffer *readWholeFile(const MMKVPath_t &nsFilePath) {
             if (ReadFile(fd, buffer->getPtr(), fileLength, &readSize, nullptr)) {
                 //fileSize = readSize;
             } else {
-                MMKVWarning("fail to read %ws: %d", nsFilePath.c_str(), GetLastError());
+                MMKVWarning("fail to read %ls: %d", nsFilePath.c_str(), GetLastError());
                 delete buffer;
                 buffer = nullptr;
             }
         }
         CloseHandle(fd);
     } else {
-        MMKVWarning("fail to open %ws: %d", nsFilePath.c_str(), GetLastError());
+        MMKVWarning("fail to open %ls: %d", nsFilePath.c_str(), GetLastError());
     }
     return buffer;
 }
@@ -363,18 +366,19 @@ static pair<MMKVPath_t, MMKVFileHandle_t> createUniqueTempFile(const wchar_t *pr
         MMKVError("GetTempFileName failed %d", GetLastError());
         return {L"", INVALID_HANDLE_VALUE};
     }
-    auto hTempFile = CreateFile(szTempFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
+    auto hTempFile =
+        CreateFile(szTempFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
     if (hTempFile == INVALID_HANDLE_VALUE) {
-        MMKVError("fail to create unique temp file [%ws], %d", szTempFileName, GetLastError());
+        MMKVError("fail to create unique temp file [%ls], %d", szTempFileName, GetLastError());
         return {L"", INVALID_HANDLE_VALUE};
     }
-    MMKVDebug("create unique temp file [%ws] with fd[%p]", szTempFileName, hTempFile);
+    MMKVDebug("create unique temp file [%ls] with fd[%p]", szTempFileName, hTempFile);
     return {MMKVPath_t(szTempFileName), hTempFile};
 }
 
 bool tryAtomicRename(const MMKVPath_t &srcPath, const MMKVPath_t &dstPath) {
     if (MoveFileEx(srcPath.c_str(), dstPath.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED) == 0) {
-        MMKVError("MoveFileEx [%ws] to [%ws] failed %d", srcPath.c_str(), dstPath.c_str(), GetLastError());
+        MMKVError("MoveFileEx [%ls] to [%ls] failed %d", srcPath.c_str(), dstPath.c_str(), GetLastError());
         return false;
     }
     return true;
@@ -401,7 +405,7 @@ bool copyFileContent(const MMKVPath_t &srcPath, MMKVFileHandle_t dstFD, bool nee
     while (true) {
         DWORD sizeRead = 0;
         if (!ReadFile(srcFile.getFd(), buffer, bufferSize, &sizeRead, nullptr)) {
-            MMKVError("fail to read %ws: %d", srcPath.c_str(), GetLastError());
+            MMKVError("fail to read %ls: %d", srcPath.c_str(), GetLastError());
             goto errorOut;
         }
 
@@ -426,7 +430,7 @@ bool copyFileContent(const MMKVPath_t &srcPath, MMKVFileHandle_t dstFD, bool nee
     }
 
     ret = true;
-    MMKVInfo("copy content from %ws to fd[%d] finish", srcPath.c_str(), dstFD);
+    MMKVInfo("copy content from %ls to fd[%d] finish", srcPath.c_str(), dstFD);
 
 errorOut:
     free(buffer);
@@ -445,11 +449,11 @@ bool copyFile(const MMKVPath_t &srcPath, const MMKVPath_t &dstPath) {
 
     bool renamed = false;
     if (copyFileContent(srcPath, tmpFD, false)) {
-        MMKVInfo("copyed file [%ws] to [%ws]", srcPath.c_str(), tmpPath.c_str());
+        MMKVInfo("copyed file [%ls] to [%ls]", srcPath.c_str(), tmpPath.c_str());
         CloseHandle(tmpFD);
         renamed = tryAtomicRename(tmpPath.c_str(), dstPath.c_str());
         if (renamed) {
-            MMKVInfo("copyfile [%ws] to [%ws] finish.", srcPath.c_str(), dstPath.c_str());
+            MMKVInfo("copyfile [%ls] to [%ls] finish.", srcPath.c_str(), dstPath.c_str());
         }
     } else {
         CloseHandle(tmpFD);
@@ -468,9 +472,9 @@ bool copyFileContent(const MMKVPath_t &srcPath, const MMKVPath_t &dstPath) {
     }
     auto ret = copyFileContent(srcPath, dstFile.getFd(), false);
     if (!ret) {
-        MMKVError("fail to copyfile(): target file %ws", dstPath.c_str());
+        MMKVError("fail to copyfile(): target file %ls", dstPath.c_str());
     } else {
-        MMKVInfo("copy content from %ws to [%ws] finish", srcPath.c_str(), dstPath.c_str());
+        MMKVInfo("copy content from %ls to [%ls] finish", srcPath.c_str(), dstPath.c_str());
     }
     return ret;
 }

+ 3 - 7
Pods/MMKVCore/Core/MiniPBCoder.cpp

@@ -127,8 +127,6 @@ size_t MiniPBCoder::prepareObjectForEncode(const MMBuffer &buffer) {
     return index;
 }
 
-#ifndef MMKV_DISABLE_CRYPT
-
 size_t MiniPBCoder::prepareObjectForEncode(const MMKVVector &vec) {
     m_encodeItems->push_back(PBEncodeItem());
     PBEncodeItem *encodeItem = &(m_encodeItems->back());
@@ -167,8 +165,6 @@ size_t MiniPBCoder::prepareObjectForEncode(const MMKVVector &vec) {
     return index;
 }
 
-#endif // MMKV_DISABLE_CRYPT
-
 MMBuffer MiniPBCoder::writePreparedItems(size_t index) {
     PBEncodeItem *oItem = (index < m_encodeItems->size()) ? &(*m_encodeItems)[index] : nullptr;
     if (oItem && oItem->compiledSize > 0) {
@@ -240,7 +236,7 @@ vector<string> MiniPBCoder::decodeOneVector() {
 
     while (!m_inputData->isAtEnd()) {
         auto value = m_inputData->readString();
-        v.push_back(move(value));
+        v.push_back(std::move(value));
     }
 
     return v;
@@ -259,7 +255,7 @@ void MiniPBCoder::decodeOneMap(MMKVMap &dic, size_t position, bool greedy) {
             if (key.length() > 0) {
                 m_inputData->readData(kvHolder);
                 if (kvHolder.valueSize > 0) {
-                    dictionary[key] = move(kvHolder);
+                    dictionary[key] = std::move(kvHolder);
                 } else {
                     auto itr = dictionary.find(key);
                     if (itr != dictionary.end()) {
@@ -302,7 +298,7 @@ void MiniPBCoder::decodeOneMap(MMKVMapCrypt &dic, size_t position, bool greedy)
             if (key.length() > 0) {
                 m_inputDataDecrpt->readData(kvHolder);
                 if (kvHolder.realValueSize() > 0) {
-                    dictionary[key] = move(kvHolder);
+                    dictionary[key] = std::move(kvHolder);
                 } else {
                     auto itr = dictionary.find(key);
                     if (itr != dictionary.end()) {

+ 4 - 4
Pods/MMKVCore/Core/MiniPBCoder_OSX.cpp

@@ -90,7 +90,7 @@ void MiniPBCoder::decodeOneMap(MMKVMap &dic, size_t position, bool greedy) {
                 auto itr = dictionary.find(key);
                 if (itr != dictionary.end()) {
                     if (kvHolder.valueSize > 0) {
-                        itr->second = move(kvHolder);
+                        itr->second = std::move(kvHolder);
                     } else {
                         auto oldKey = itr->first;
                         dictionary.erase(itr);
@@ -98,7 +98,7 @@ void MiniPBCoder::decodeOneMap(MMKVMap &dic, size_t position, bool greedy) {
                     }
                 } else {
                     if (kvHolder.valueSize > 0) {
-                        dictionary.emplace(key, move(kvHolder));
+                        dictionary.emplace(key, std::move(kvHolder));
                         [key retain];
                     }
                 }
@@ -143,7 +143,7 @@ void MiniPBCoder::decodeOneMap(MMKVMapCrypt &dic, size_t position, bool greedy)
                 auto itr = dictionary.find(key);
                 if (itr != dictionary.end()) {
                     if (kvHolder.realValueSize() > 0) {
-                        itr->second = move(kvHolder);
+                        itr->second = std::move(kvHolder);
                     } else {
                         auto oldKey = itr->first;
                         dictionary.erase(itr);
@@ -151,7 +151,7 @@ void MiniPBCoder::decodeOneMap(MMKVMapCrypt &dic, size_t position, bool greedy)
                     }
                 } else {
                     if (kvHolder.realValueSize() > 0) {
-                        dictionary.emplace(key, move(kvHolder));
+                        dictionary.emplace(key, std::move(kvHolder));
                         [key retain];
                     }
                 }

+ 1 - 1
Pods/MMKVCore/Core/PBUtility.cpp

@@ -17,7 +17,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
+#include "MMBuffer.h"
 #include "PBUtility.h"
 
 namespace mmkv {

+ 7 - 0
Pods/MMKVCore/Core/PBUtility.h

@@ -147,6 +147,13 @@ static inline uint32_t pbUInt32Size(uint32_t value) {
     return pbRawVarint32Size(value);
 }
 
+static inline uint32_t pbMMBufferSize(const MMBuffer &data) {
+    auto valueLength = static_cast<uint32_t>(data.length());
+    return valueLength + pbUInt32Size(valueLength);
+}
+
+constexpr uint32_t Fixed32Size = pbFixed32Size();
+
 } // namespace mmkv
 
 #endif

+ 1 - 1
Pods/MMKVCore/Core/aes/AESCrypt.h

@@ -22,7 +22,7 @@
 #define AES_CRYPT_H_
 #ifdef __cplusplus
 
-#include "MMKVPredef.h"
+#include "../MMKVPredef.h"
 #include <cstddef>
 
 #ifdef MMKV_DISABLE_CRYPT

+ 12 - 0
Pods/MMKVCore/Core/aes/openssl/openssl_aes-armv4.S

@@ -161,6 +161,9 @@ AES_Te:
 
 /* void openssl_aes_arm_encrypt(const uint8_t *in, uint8_t *out, const AES_KEY *key) {
 */
+#ifndef __APPLE__
+.type openssl_aes_arm_encrypt, %function
+#endif
 #ifndef __linux__
 .globl	_openssl_aes_arm_encrypt
 #ifdef __thumb2__
@@ -422,6 +425,9 @@ Lenc_loop:
 	ldr	pc,[sp],#4		// pop and return
 
 
+#ifndef __APPLE__
+.type openssl_aes_arm_set_encrypt_key, %function
+#endif
 #ifndef __linux__
 .globl	_openssl_aes_arm_set_encrypt_key
 #ifdef __thumb2__
@@ -735,6 +741,9 @@ Labrt:
 .word	0xe12fff1e			// interoperable with Thumb ISA:-)
 #endif
 
+#ifndef __APPLE__
+.type     openssl_aes_arm_set_decrypt_key, %function
+#endif
 #ifndef __linux__
 .globl    _openssl_aes_arm_set_decrypt_key
 #ifdef __thumb2__
@@ -954,6 +963,9 @@ AES_Td:
 .byte    0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d
 
 
+#ifndef __APPLE__
+.type openssl_aes_arm_decrypt, %function
+#endif
 // void AES_decrypt(const uint8_t *in, uint8_t *out, const AES_KEY *key) {
 #ifndef __linux__
 .globl    _openssl_aes_arm_decrypt

+ 0 - 0
Pods/MMKVCore/Core/aes/openssl/openssl_aes_core.cpp


+ 0 - 0
Pods/MMKVCore/Core/aes/openssl/openssl_md32_common.h


+ 0 - 0
Pods/MMKVCore/Core/aes/openssl/openssl_md5.h


+ 0 - 0
Pods/MMKVCore/Core/aes/openssl/openssl_md5_dgst.cpp


+ 0 - 0
Pods/MMKVCore/Core/aes/openssl/openssl_md5_locl.h


+ 0 - 0
Pods/MMKVCore/Core/aes/openssl/openssl_md5_one.cpp


+ 287 - 288
Pods/MMKVCore/README.md

@@ -1,291 +1,290 @@
-[![license](https://img.shields.io/badge/license-BSD_3-brightgreen.svg?style=flat)](https://github.com/Tencent/MMKV/blob/master/LICENSE.TXT)
-[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Tencent/MMKV/pulls)
-[![Release Version](https://img.shields.io/badge/release-1.2.14-brightgreen.svg)](https://github.com/Tencent/MMKV/releases)
-[![Platform](https://img.shields.io/badge/Platform-%20Android%20%7C%20iOS%2FmacOS%20%7C%20Win32%20%7C%20POSIX-brightgreen.svg)](https://github.com/Tencent/MMKV/wiki/home)
-
-中文版本请参看[这里](./README_CN.md)
-
-MMKV is an **efficient**, **small**, **easy-to-use** mobile key-value storage framework used in the WeChat application. It's currently available on **Android**, **iOS/macOS**, **Win32** and **POSIX**.
-
-# MMKV for Android
-
-## Features
-
-* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of Android to achieve the best performance.
-  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
-
-* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `sync`, no `apply` calls needed.
-
-* **Small**.
-  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
-  * **About 50K in binary size**: MMKV adds about 50K per architecture on App size, and much less when zipped (APK).
-
-
-## Getting Started
-
-### Installation Via Maven
-Add the following lines to `build.gradle` on your app module:
-
-```gradle
-dependencies {
-    implementation 'com.tencent:mmkv:1.2.14'
-    // replace "1.2.14" with any available version
-}
-```
-
-Starting from v1.2.8, MMKV has been **migrated to Maven Central**.  
-For other installation options, see [Android Setup](https://github.com/Tencent/MMKV/wiki/android_setup).
-
-### Quick Tutorial
-You can use MMKV as you go. All changes are saved immediately, no `sync`, no `apply` calls needed.  
-Setup MMKV on App startup, say your `Application` class, add these lines:
-
-```Java
-public void onCreate() {
-    super.onCreate();
-
-    String rootDir = MMKV.initialize(this);
-    System.out.println("mmkv root: " + rootDir);
-    //……
-}
-```
-
-MMKV has a global instance, that can be used directly:
-
-```Java
-import com.tencent.mmkv.MMKV;
-    
-MMKV kv = MMKV.defaultMMKV();
-
-kv.encode("bool", true);
-boolean bValue = kv.decodeBool("bool");
-
-kv.encode("int", Integer.MIN_VALUE);
-int iValue = kv.decodeInt("int");
-
-kv.encode("string", "Hello from mmkv");
-String str = kv.decodeString("string");
-```
-
-MMKV also supports **Multi-Process Access**. Full tutorials can be found here [Android Tutorial](https://github.com/Tencent/MMKV/wiki/android_tutorial).
-
-## Performance
-Writing random `int` for 1000 times, we get this chart:  
-![](https://github.com/Tencent/MMKV/wiki/assets/profile_android_mini.png)  
-For more benchmark data, please refer to [our benchmark](https://github.com/Tencent/MMKV/wiki/android_benchmark).
-
-# MMKV for iOS/macOS
-
-## Features
-
-* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of iOS/macOS to achieve the best performance.
- 
-* **Easy-to-use**. You can use MMKV as you go, no configurations are needed. All changes are saved immediately, no `synchronize` calls are needed.
-
-* **Small**.
-  * **A handful of files**: MMKV contains encode/decode helpers and mmap logics and nothing more. It's really tidy.
-  * **Less than 30K in binary size**: MMKV adds less than 30K per architecture on App size, and much less when zipped (IPA).
-
-## Getting Started
-
-### Installation Via CocoaPods:
-  1. Install [CocoaPods](https://guides.CocoaPods.org/using/getting-started.html);
-  2. Open the terminal, `cd` to your project directory, run `pod repo update` to make CocoaPods aware of the latest available MMKV versions;
-  3. Edit your Podfile, add `pod 'MMKV'` to your app target;
-  4. Run `pod install`;
-  5. Open the `.xcworkspace` file generated by CocoaPods;
-  6. Add `#import <MMKV/MMKV.h>` to your source file and we are done.
-
-For other installation options, see [iOS/macOS Setup](https://github.com/Tencent/MMKV/wiki/iOS_setup).
-
-### Quick Tutorial
-You can use MMKV as you go, no configurations are needed. All changes are saved immediately, no `synchronize` calls are needed.
-Setup MMKV on App startup, in your `-[MyApp application: didFinishLaunchingWithOptions:]`, add these lines:
-
-```objective-c
-- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
-    // init MMKV in the main thread
-    [MMKV initializeMMKV:nil];
-
-    //...
-    return YES;
-}
-```
-
-MMKV has a global instance, that can be used directly:
-
-```objective-c
-MMKV *mmkv = [MMKV defaultMMKV];
-    
-[mmkv setBool:YES forKey:@"bool"];
-BOOL bValue = [mmkv getBoolForKey:@"bool"];
-    
-[mmkv setInt32:-1024 forKey:@"int32"];
-int32_t iValue = [mmkv getInt32ForKey:@"int32"];
-    
-[mmkv setString:@"hello, mmkv" forKey:@"string"];
-NSString *str = [mmkv getStringForKey:@"string"];
-```
-
-MMKV also supports **Multi-Process Access**. Full tutorials can be found [here](https://github.com/Tencent/MMKV/wiki/iOS_tutorial).
-
-## Performance
-Writing random `int` for 10000 times, we get this chart:  
-![](https://github.com/Tencent/MMKV/wiki/assets/profile_mini.png)  
-For more benchmark data, please refer to [our benchmark](https://github.com/Tencent/MMKV/wiki/iOS_benchmark).
-
-
-# MMKV for Win32
-
-## Features
-
-* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of Windows to achieve the best performance.
-  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
-
-* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `save`, no `sync` calls are needed.
-
-* **Small**.
-  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
-  * **About 10K in binary size**: MMKV adds about 10K on application size, and much less when zipped.
-
-
-## Getting Started
-
-### Installation Via Source
-1. Getting source code from git repository:
-  
-   ```
-   git clone https://github.com/Tencent/MMKV.git
-   ```
-  
-2. Add `Win32/MMKV/MMKV.vcxproj` to your solution;
-3. Add `MMKV` project to your project's dependencies;
-4. Add `$(OutDir)include` to your project's `C/C++` -> `General` -> `Additional Include Directories`;
-5. Add `$(OutDir)` to your project's `Linker` -> `General` -> `Additional Library Directories`;
-6. Add `MMKV.lib` to your project's `Linker` -> `Input` -> `Additional Dependencies`;
-7. Add `#include <MMKV/MMKV.h>` to your source file and we are done.
-
-
-note:  
-
-1. MMKV is compiled with `MT/MTd` runtime by default. If your project uses `MD/MDd`, you should change MMKV's setting to match your project's (`C/C++` -> `Code Generation` -> `Runtime Library`), or vice versa.
-2. MMKV is developed with Visual Studio 2017, change the `Platform Toolset` if you use a different version of Visual Studio.
-
-For other installation options, see [Win32 Setup](https://github.com/Tencent/MMKV/wiki/windows_setup).
-
-### Quick Tutorial
-You can use MMKV as you go. All changes are saved immediately, no `sync`, no `save` calls needed.  
-Setup MMKV on App startup, say in your `main()`, add these lines:
-
-```C++
-#include <MMKV/MMKV.h>
-
-int main() {
-    std::wstring rootDir = getYourAppDocumentDir();
-    MMKV::initializeMMKV(rootDir);
-    //...
-}
-```
-
-MMKV has a global instance, that can be used directly:
-
-```C++
-auto mmkv = MMKV::defaultMMKV();
-
-mmkv->set(true, "bool");
-std::cout << "bool = " << mmkv->getBool("bool") << std::endl;
-
-mmkv->set(1024, "int32");
-std::cout << "int32 = " << mmkv->getInt32("int32") << std::endl;
-
-mmkv->set("Hello, MMKV for Win32", "string");
-std::string result;
-mmkv->getString("string", result);
-std::cout << "string = " << result << std::endl;
-```
-
-MMKV also supports **Multi-Process Access**. Full tutorials can be found here [Win32 Tutorial](https://github.com/Tencent/MMKV/wiki/windows_tutorial).
-
-# MMKV for POSIX
-
-## Features
-
-* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of POSIX to achieve the best performance.
-  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
-
-* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `save`, no `sync` calls are needed.
-
-* **Small**.
-  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
-  * **About 7K in binary size**: MMKV adds about 7K on application size, and much less when zipped.
-
-
-## Getting Started
-
-### Installation Via CMake
-1. Getting source code from the git repository:
-  
-   ```
-   git clone https://github.com/Tencent/MMKV.git
-   ```
-2. Edit your `CMakeLists.txt`, add those lines:
-
-    ```cmake
-    add_subdirectory(mmkv/POSIX/src mmkv)
-    target_link_libraries(MyApp
-        mmkv)
-    ```
-3. Add `#include "MMKV.h"` to your source file and we are done.
-
-For other installation options, see [POSIX Setup](https://github.com/Tencent/MMKV/wiki/posix_setup).
-
-### Quick Tutorial
-You can use MMKV as you go. All changes are saved immediately, no `sync`, no `save` calls needed.  
-Setup MMKV on App startup, say in your `main()`, add these lines:
-
-```C++
-#include "MMKV.h"
-
-int main() {
-    std::string rootDir = getYourAppDocumentDir();
-    MMKV::initializeMMKV(rootDir);
-    //...
-}
-```
-
-MMKV has a global instance, that can be used directly:
-
-```C++
-auto mmkv = MMKV::defaultMMKV();
-
-mmkv->set(true, "bool");
-std::cout << "bool = " << mmkv->getBool("bool") << std::endl;
-
-mmkv->set(1024, "int32");
-std::cout << "int32 = " << mmkv->getInt32("int32") << std::endl;
-
-mmkv->set("Hello, MMKV for Win32", "string");
-std::string result;
-mmkv->getString("string", result);
-std::cout << "string = " << result << std::endl;
-```
-
-MMKV also supports **Multi-Process Access**. Full tutorials can be found here [POSIX Tutorial](https://github.com/Tencent/MMKV/wiki/posix_tutorial).
-
-## License
-MMKV is published under the BSD 3-Clause license. For details check out the [LICENSE.TXT](./LICENSE.TXT).
-
-## Change Log
-Check out the [CHANGELOG.md](./CHANGELOG.md) for details of change history.
-
-## Contributing
-
-If you are interested in contributing, check out the [CONTRIBUTING.md](./CONTRIBUTING.md), also join our [Tencent OpenSource Plan](https://opensource.tencent.com/contribution).
-
-To give clarity of what is expected of our members, MMKV has adopted the code of conduct defined by the Contributor Covenant, which is widely used. And we think it articulates our values well. For more, check out the [Code of Conduct](./CODE_OF_CONDUCT.md).
-
-## FAQ & Feedback
-Check out the [FAQ](https://github.com/Tencent/MMKV/wiki/FAQ) first. Should there be any questions, don't hesitate to create [issues](https://github.com/Tencent/MMKV/issues).
+[![license](https://img.shields.io/badge/license-BSD_3-brightgreen.svg?style=flat)](https://github.com/Tencent/MMKV/blob/master/LICENSE.TXT)
+[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/Tencent/MMKV/pulls)
+[![Release Version](https://img.shields.io/badge/release-1.3.1-brightgreen.svg)](https://github.com/Tencent/MMKV/releases)
+[![Platform](https://img.shields.io/badge/Platform-%20Android%20%7C%20iOS%2FmacOS%20%7C%20Win32%20%7C%20POSIX-brightgreen.svg)](https://github.com/Tencent/MMKV/wiki/home)
+
+中文版本请参看[这里](./README_CN.md)
+
+MMKV is an **efficient**, **small**, **easy-to-use** mobile key-value storage framework used in the WeChat application. It's currently available on **Android**, **iOS/macOS**, **Win32** and **POSIX**.
+
+# MMKV for Android
+
+## Features
+
+* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of Android to achieve the best performance.
+  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
+
+* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `sync`, no `apply` calls needed.
+
+* **Small**.
+  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
+  * **About 50K in binary size**: MMKV adds about 50K per architecture on App size, and much less when zipped (APK).
+
+
+## Getting Started
+
+### Installation Via Maven
+Add the following lines to `build.gradle` on your app module:
+
+```gradle
+dependencies {
+    implementation 'com.tencent:mmkv:1.3.1'
+    // replace "1.3.1" with any available version
+}
+```
+
+For other installation options, see [Android Setup](https://github.com/Tencent/MMKV/wiki/android_setup).
+
+### Quick Tutorial
+You can use MMKV as you go. All changes are saved immediately, no `sync`, no `apply` calls needed.  
+Setup MMKV on App startup, say your `Application` class, add these lines:
+
+```Java
+public void onCreate() {
+    super.onCreate();
+
+    String rootDir = MMKV.initialize(this);
+    System.out.println("mmkv root: " + rootDir);
+    //……
+}
+```
+
+MMKV has a global instance, that can be used directly:
+
+```Java
+import com.tencent.mmkv.MMKV;
+    
+MMKV kv = MMKV.defaultMMKV();
+
+kv.encode("bool", true);
+boolean bValue = kv.decodeBool("bool");
+
+kv.encode("int", Integer.MIN_VALUE);
+int iValue = kv.decodeInt("int");
+
+kv.encode("string", "Hello from mmkv");
+String str = kv.decodeString("string");
+```
+
+MMKV also supports **Multi-Process Access**. Full tutorials can be found here [Android Tutorial](https://github.com/Tencent/MMKV/wiki/android_tutorial).
+
+## Performance
+Writing random `int` for 1000 times, we get this chart:  
+![](https://github.com/Tencent/MMKV/wiki/assets/profile_android_mini.png)  
+For more benchmark data, please refer to [our benchmark](https://github.com/Tencent/MMKV/wiki/android_benchmark).
+
+# MMKV for iOS/macOS
+
+## Features
+
+* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of iOS/macOS to achieve the best performance.
+ 
+* **Easy-to-use**. You can use MMKV as you go, no configurations are needed. All changes are saved immediately, no `synchronize` calls are needed.
+
+* **Small**.
+  * **A handful of files**: MMKV contains encode/decode helpers and mmap logics and nothing more. It's really tidy.
+  * **Less than 30K in binary size**: MMKV adds less than 30K per architecture on App size, and much less when zipped (IPA).
+
+## Getting Started
+
+### Installation Via CocoaPods:
+  1. Install [CocoaPods](https://guides.CocoaPods.org/using/getting-started.html);
+  2. Open the terminal, `cd` to your project directory, run `pod repo update` to make CocoaPods aware of the latest available MMKV versions;
+  3. Edit your Podfile, add `pod 'MMKV'` to your app target;
+  4. Run `pod install`;
+  5. Open the `.xcworkspace` file generated by CocoaPods;
+  6. Add `#import <MMKV/MMKV.h>` to your source file and we are done.
+
+For other installation options, see [iOS/macOS Setup](https://github.com/Tencent/MMKV/wiki/iOS_setup).
+
+### Quick Tutorial
+You can use MMKV as you go, no configurations are needed. All changes are saved immediately, no `synchronize` calls are needed.
+Setup MMKV on App startup, in your `-[MyApp application: didFinishLaunchingWithOptions:]`, add these lines:
+
+```objective-c
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+    // init MMKV in the main thread
+    [MMKV initializeMMKV:nil];
+
+    //...
+    return YES;
+}
+```
+
+MMKV has a global instance, that can be used directly:
+
+```objective-c
+MMKV *mmkv = [MMKV defaultMMKV];
+    
+[mmkv setBool:YES forKey:@"bool"];
+BOOL bValue = [mmkv getBoolForKey:@"bool"];
+    
+[mmkv setInt32:-1024 forKey:@"int32"];
+int32_t iValue = [mmkv getInt32ForKey:@"int32"];
+    
+[mmkv setString:@"hello, mmkv" forKey:@"string"];
+NSString *str = [mmkv getStringForKey:@"string"];
+```
+
+MMKV also supports **Multi-Process Access**. Full tutorials can be found [here](https://github.com/Tencent/MMKV/wiki/iOS_tutorial).
+
+## Performance
+Writing random `int` for 10000 times, we get this chart:  
+![](https://github.com/Tencent/MMKV/wiki/assets/profile_mini.png)  
+For more benchmark data, please refer to [our benchmark](https://github.com/Tencent/MMKV/wiki/iOS_benchmark).
+
+
+# MMKV for Win32
+
+## Features
+
+* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of Windows to achieve the best performance.
+  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
+
+* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `save`, no `sync` calls are needed.
+
+* **Small**.
+  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
+  * **About 10K in binary size**: MMKV adds about 10K on application size, and much less when zipped.
+
+
+## Getting Started
+
+### Installation Via Source
+1. Getting source code from git repository:
+  
+   ```
+   git clone https://github.com/Tencent/MMKV.git
+   ```
+  
+2. Add `Win32/MMKV/MMKV.vcxproj` to your solution;
+3. Add `MMKV` project to your project's dependencies;
+4. Add `$(OutDir)include` to your project's `C/C++` -> `General` -> `Additional Include Directories`;
+5. Add `$(OutDir)` to your project's `Linker` -> `General` -> `Additional Library Directories`;
+6. Add `MMKV.lib` to your project's `Linker` -> `Input` -> `Additional Dependencies`;
+7. Add `#include <MMKV/MMKV.h>` to your source file and we are done.
+
+
+note:  
+
+1. MMKV is compiled with `MT/MTd` runtime by default. If your project uses `MD/MDd`, you should change MMKV's setting to match your project's (`C/C++` -> `Code Generation` -> `Runtime Library`), or vice versa.
+2. MMKV is developed with Visual Studio 2017, change the `Platform Toolset` if you use a different version of Visual Studio.
+
+For other installation options, see [Win32 Setup](https://github.com/Tencent/MMKV/wiki/windows_setup).
+
+### Quick Tutorial
+You can use MMKV as you go. All changes are saved immediately, no `sync`, no `save` calls needed.  
+Setup MMKV on App startup, say in your `main()`, add these lines:
+
+```C++
+#include <MMKV/MMKV.h>
+
+int main() {
+    std::wstring rootDir = getYourAppDocumentDir();
+    MMKV::initializeMMKV(rootDir);
+    //...
+}
+```
+
+MMKV has a global instance, that can be used directly:
+
+```C++
+auto mmkv = MMKV::defaultMMKV();
+
+mmkv->set(true, "bool");
+std::cout << "bool = " << mmkv->getBool("bool") << std::endl;
+
+mmkv->set(1024, "int32");
+std::cout << "int32 = " << mmkv->getInt32("int32") << std::endl;
+
+mmkv->set("Hello, MMKV for Win32", "string");
+std::string result;
+mmkv->getString("string", result);
+std::cout << "string = " << result << std::endl;
+```
+
+MMKV also supports **Multi-Process Access**. Full tutorials can be found here [Win32 Tutorial](https://github.com/Tencent/MMKV/wiki/windows_tutorial).
+
+# MMKV for POSIX
+
+## Features
+
+* **Efficient**. MMKV uses mmap to keep memory synced with files, and protobuf to encode/decode values, making the most of POSIX to achieve the best performance.
+  * **Multi-Process concurrency**: MMKV supports concurrent read-read and read-write access between processes.
+
+* **Easy-to-use**. You can use MMKV as you go. All changes are saved immediately, no `save`, no `sync` calls are needed.
+
+* **Small**.
+  * **A handful of files**: MMKV contains process locks, encode/decode helpers and mmap logics, and nothing more. It's really tidy.
+  * **About 7K in binary size**: MMKV adds about 7K on application size, and much less when zipped.
+
+
+## Getting Started
+
+### Installation Via CMake
+1. Getting source code from the git repository:
+  
+   ```
+   git clone https://github.com/Tencent/MMKV.git
+   ```
+2. Edit your `CMakeLists.txt`, add those lines:
+
+    ```cmake
+    add_subdirectory(mmkv/POSIX/src mmkv)
+    target_link_libraries(MyApp
+        mmkv)
+    ```
+3. Add `#include "MMKV.h"` to your source file and we are done.
+
+For other installation options, see [POSIX Setup](https://github.com/Tencent/MMKV/wiki/posix_setup).
+
+### Quick Tutorial
+You can use MMKV as you go. All changes are saved immediately, no `sync`, no `save` calls needed.  
+Setup MMKV on App startup, say in your `main()`, add these lines:
+
+```C++
+#include "MMKV.h"
+
+int main() {
+    std::string rootDir = getYourAppDocumentDir();
+    MMKV::initializeMMKV(rootDir);
+    //...
+}
+```
+
+MMKV has a global instance, that can be used directly:
+
+```C++
+auto mmkv = MMKV::defaultMMKV();
+
+mmkv->set(true, "bool");
+std::cout << "bool = " << mmkv->getBool("bool") << std::endl;
+
+mmkv->set(1024, "int32");
+std::cout << "int32 = " << mmkv->getInt32("int32") << std::endl;
+
+mmkv->set("Hello, MMKV for Win32", "string");
+std::string result;
+mmkv->getString("string", result);
+std::cout << "string = " << result << std::endl;
+```
+
+MMKV also supports **Multi-Process Access**. Full tutorials can be found here [POSIX Tutorial](https://github.com/Tencent/MMKV/wiki/posix_tutorial).
+
+## License
+MMKV is published under the BSD 3-Clause license. For details check out the [LICENSE.TXT](./LICENSE.TXT).
+
+## Change Log
+Check out the [CHANGELOG.md](./CHANGELOG.md) for details of change history.
+
+## Contributing
+
+If you are interested in contributing, check out the [CONTRIBUTING.md](./CONTRIBUTING.md), also join our [Tencent OpenSource Plan](https://opensource.tencent.com/contribution).
+
+To give clarity of what is expected of our members, MMKV has adopted the code of conduct defined by the Contributor Covenant, which is widely used. And we think it articulates our values well. For more, check out the [Code of Conduct](./CODE_OF_CONDUCT.md).
+
+## FAQ & Feedback
+Check out the [FAQ](https://github.com/Tencent/MMKV/wiki/FAQ) first. Should there be any questions, don't hesitate to create [issues](https://github.com/Tencent/MMKV/issues).
 
 ## Personal Information Protection Rules
 User privacy is taken very seriously: MMKV does not obtain, collect or upload any personal information. Please refer to the [MMKV SDK Personal Information Protection Rules](https://support.weixin.qq.com/cgi-bin/mmsupportacctnodeweb-bin/pages/aY5BAtRiO1BpoHxo) for details.

+ 12 - 12
Pods/Manifest.lock

@@ -21,12 +21,12 @@ PODS:
   - Masonry (1.1.0)
   - MJExtension (3.4.1)
   - MJRefresh (3.7.5)
-  - MMKV (1.2.14):
-    - MMKVCore (~> 1.2.14)
-  - MMKVCore (1.2.14)
-  - SDWebImage (5.12.5):
-    - SDWebImage/Core (= 5.12.5)
-  - SDWebImage/Core (5.12.5)
+  - MMKV (1.3.1):
+    - MMKVCore (~> 1.3.1)
+  - MMKVCore (1.3.1)
+  - SDWebImage (5.17.0):
+    - SDWebImage/Core (= 5.17.0)
+  - SDWebImage/Core (5.17.0)
   - SVProgressHUD (2.2.5)
 
 DEPENDENCIES:
@@ -54,17 +54,17 @@ SPEC REPOS:
     - SVProgressHUD
 
 SPEC CHECKSUMS:
-  AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce
+  AFNetworking: 3bd23d814e976cd148d7d44c3ab78017b744cd58
   FDFullscreenPopGesture: a8a620179e3d9c40e8e00256dcee1c1a27c6d0f0
   FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
   Masonry: 678fab65091a9290e40e2832a55e7ab731aad201
   MJExtension: 21c5f6f8c4d5d8844b7ae8fbae08fed0b501f961
   MJRefresh: fdf5e979eb406a0341468932d1dfc8b7f9fce961
-  MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd
-  MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb
-  SDWebImage: 0905f1b7760fc8ac4198cae0036600d67478751e
+  MMKV: 5a07930c70c70b86cd87761a42c8f3836fb681d7
+  MMKVCore: e50135dbd33235b6ab390635991bab437ab873c0
+  SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9
   SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6
 
-PODFILE CHECKSUM: 07c37e5e20e787e9c692ab63148fe787a0690751
+PODFILE CHECKSUM: f431c30b6a399a5b12b65d3398c428de7bdc256e
 
-COCOAPODS: 1.11.3
+COCOAPODS: 1.12.1

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 348 - 337
Pods/Pods.xcodeproj/project.pbxproj


+ 4 - 1
Pods/SDWebImage/README.md

@@ -3,7 +3,7 @@
 </p>
 
 
-[![Build Status](http://img.shields.io/travis/SDWebImage/SDWebImage/master.svg?style=flat)](https://travis-ci.org/SDWebImage/SDWebImage)
+[![Build Status](https://github.com/SDWebImage/SDWebImage/actions/workflows/CI.yml/badge.svg)](https://github.com/SDWebImage/SDWebImage/actions/workflows/CI.yml)
 [![Pod Version](http://img.shields.io/cocoapods/v/SDWebImage.svg?style=flat)](http://cocoadocs.org/docsets/SDWebImage/)
 [![Pod Platform](http://img.shields.io/cocoapods/p/SDWebImage.svg?style=flat)](http://cocoadocs.org/docsets/SDWebImage/)
 [![Pod License](http://img.shields.io/cocoapods/l/SDWebImage.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0.html)
@@ -14,6 +14,8 @@
 
 This library provides an async image downloader with cache support. For convenience, we added categories for UI elements like `UIImageView`, `UIButton`, `MKAnnotationView`.
 
+Note: `SD` is the prefix for **Simple Design** (which is the team name in Daily Motion company from the author Olivier Poitrey)
+
 ## Features
 
 - [x] Categories for `UIImageView`, `UIButton`, `MKAnnotationView` adding web image and cache management
@@ -62,6 +64,7 @@ The new framework introduce two View structs `WebImage` and `AnimatedImage` for
 - [SDWebImageAVIFCoder](https://github.com/SDWebImage/SDWebImageAVIFCoder) - coder for AVIF (AV1-based) format. Based on [libavif](https://github.com/AOMediaCodec/libavif)
 - [SDWebImagePDFCoder](https://github.com/SDWebImage/SDWebImagePDFCoder) - coder for PDF vector format. Using built-in frameworks
 - [SDWebImageSVGCoder](https://github.com/SDWebImage/SDWebImageSVGCoder) - coder for SVG vector format. Using built-in frameworks
+- [SDWebImageSVGNativeCoder](https://github.com/SDWebImage/SDWebImageSVGNativeCoder) - coder for SVG-Native vector format. Based on [svg-native](https://github.com/adobe/svg-native-viewer)
 - [SDWebImageLottieCoder](https://github.com/SDWebImage/SDWebImageLottieCoder) - coder for Lottie animation format. Based on [rlottie](https://github.com/Samsung/rlottie)
 - and more from community!
 

+ 1 - 1
Pods/SDWebImage/SDWebImage/Core/NSButton+WebCache.m

@@ -149,7 +149,7 @@ static NSString * const SDAlternateImageOperationKey = @"NSButtonAlternateImageO
     [self sd_cancelImageLoadOperationWithKey:SDAlternateImageOperationKey];
 }
 
-#pragma mar - Private
+#pragma mark - Private
 
 - (NSURL *)sd_currentImageURL {
     return objc_getAssociatedObject(self, @selector(sd_currentImageURL));

+ 2 - 0
Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.h

@@ -25,6 +25,8 @@ static const SDImageFormat SDImageFormatHEIC      = 5;
 static const SDImageFormat SDImageFormatHEIF      = 6;
 static const SDImageFormat SDImageFormatPDF       = 7;
 static const SDImageFormat SDImageFormatSVG       = 8;
+static const SDImageFormat SDImageFormatBMP       = 9;
+static const SDImageFormat SDImageFormatRAW       = 10;
 
 /**
  NSData category about the image content type and UTI.

+ 12 - 0
Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.m

@@ -37,6 +37,8 @@
         case 0x49:
         case 0x4D:
             return SDImageFormatTIFF;
+        case 0x42:
+            return SDImageFormatBMP;
         case 0x52: {
             if (data.length >= 12) {
                 //RIFF....WEBP
@@ -113,6 +115,12 @@
         case SDImageFormatSVG:
             UTType = kSDUTTypeSVG;
             break;
+        case SDImageFormatBMP:
+            UTType = kSDUTTypeBMP;
+            break;
+        case SDImageFormatRAW:
+            UTType = kSDUTTypeRAW;
+            break;
         default:
             // default is kUTTypeImage abstract type
             UTType = kSDUTTypeImage;
@@ -144,6 +152,10 @@
         imageFormat = SDImageFormatPDF;
     } else if (CFStringCompare(uttype, kSDUTTypeSVG, 0) == kCFCompareEqualTo) {
         imageFormat = SDImageFormatSVG;
+    } else if (CFStringCompare(uttype, kSDUTTypeBMP, 0) == kCFCompareEqualTo) {
+        imageFormat = SDImageFormatBMP;
+    } else if (UTTypeConformsTo(uttype, kSDUTTypeRAW)) {
+        imageFormat = SDImageFormatRAW;
     } else {
         imageFormat = SDImageFormatUndefined;
     }

+ 18 - 0
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImage.h

@@ -107,8 +107,26 @@
 
 // By default, animated image frames are returned by decoding just in time without keeping into memory. But you can choose to preload them into memory as well, See the description in `SDAnimatedImage` protocol.
 // After preloaded, there is no huge difference on performance between this and UIImage's `animatedImageWithImages:duration:`. But UIImage's animation have some issues such like blanking and pausing during segue when using in `UIImageView`. It's recommend to use only if need.
+/**
+ Pre-load all animated image frame into memory. Then later frame image request can directly return the frame for index without decoding.
+ This method may be called on background thread.
+ 
+ @note If one image instance is shared by lots of imageViews, the CPU performance for large animated image will drop down because the request frame index will be random (not in order) and the decoder should take extra effort to keep it re-entrant. You can use this to reduce CPU usage if need. Attention this will consume more memory usage.
+ */
 - (void)preloadAllFrames;
+
+/**
+ Unload all animated image frame from memory if are already pre-loaded. Then later frame image request need decoding. You can use this to free up the memory usage if need.
+ */
 - (void)unloadAllFrames;
+/**
+ Returns a Boolean value indicating whether all animated image frames are already pre-loaded into memory.
+ */
 @property (nonatomic, assign, readonly, getter=isAllFramesLoaded) BOOL allFramesLoaded;
+/**
+ Return the animated image coder if the image is created with `initWithAnimatedCoder:scale:` method.
+ @note We use this with animated coder which conforms to `SDProgressiveImageCoder` for progressive animation decoding.
+ */
+@property (nonatomic, strong, readonly, nullable) id<SDAnimatedImageCoder> animatedCoder;
 
 @end

+ 0 - 1
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImage.m

@@ -124,7 +124,6 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
     if (!data || data.length == 0) {
         return nil;
     }
-    data = [data copy]; // avoid mutable data
     id<SDAnimatedImageCoder> animatedCoder = nil;
     for (id<SDImageCoder>coder in [SDImageCodersManager sharedManager].coders.reverseObjectEnumerator) {
         if ([coder conformsToProtocol:@protocol(SDAnimatedImageCoder)]) {

+ 1 - 0
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.h

@@ -10,6 +10,7 @@
 #import "SDWebImageCompat.h"
 #import "SDImageCoder.h"
 
+/// Animated image playback mode
 typedef NS_ENUM(NSUInteger, SDAnimatedImagePlaybackMode) {
     /**
      * From first to last frame and stop or next loop.

+ 55 - 106
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.m

@@ -10,24 +10,24 @@
 #import "NSImage+Compatibility.h"
 #import "SDDisplayLink.h"
 #import "SDDeviceHelper.h"
+#import "SDImageFramePool.h"
 #import "SDInternalMacros.h"
 
 @interface SDAnimatedImagePlayer () {
-    SD_LOCK_DECLARE(_lock);
     NSRunLoopMode _runLoopMode;
 }
 
+@property (nonatomic, strong) SDImageFramePool *framePool;
+
 @property (nonatomic, strong, readwrite) UIImage *currentFrame;
 @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
 @property (nonatomic, assign, readwrite) NSUInteger currentLoopCount;
 @property (nonatomic, strong) id<SDAnimatedImageProvider> animatedProvider;
-@property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIImage *> *frameBuffer;
+@property (nonatomic, assign) NSUInteger currentFrameBytes;
 @property (nonatomic, assign) NSTimeInterval currentTime;
 @property (nonatomic, assign) BOOL bufferMiss;
 @property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
 @property (nonatomic, assign) BOOL shouldReverse;
-@property (nonatomic, assign) NSUInteger maxBufferCount;
-@property (nonatomic, strong) NSOperationQueue *fetchQueue;
 @property (nonatomic, strong) SDDisplayLink *displayLink;
 
 @end
@@ -47,10 +47,7 @@
         self.totalLoopCount = provider.animatedImageLoopCount;
         self.animatedProvider = provider;
         self.playbackRate = 1.0;
-        SD_LOCK_INIT(_lock);
-#if SD_UIKIT
-        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
-#endif
+        self.framePool = [SDImageFramePool registerProvider:provider];
     }
     return self;
 }
@@ -60,45 +57,12 @@
     return player;
 }
 
-#pragma mark - Life Cycle
-
 - (void)dealloc {
-#if SD_UIKIT
-    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
-#endif
-}
-
-- (void)didReceiveMemoryWarning:(NSNotification *)notification {
-    [_fetchQueue cancelAllOperations];
-    [_fetchQueue addOperationWithBlock:^{
-        NSNumber *currentFrameIndex = @(self.currentFrameIndex);
-        SD_LOCK(self->_lock);
-        NSArray *keys = self.frameBuffer.allKeys;
-        // only keep the next frame for later rendering
-        for (NSNumber * key in keys) {
-            if (![key isEqualToNumber:currentFrameIndex]) {
-                [self.frameBuffer removeObjectForKey:key];
-            }
-        }
-        SD_UNLOCK(self->_lock);
-    }];
+    // Dereference the frame pool, when zero the frame pool for provider will dealloc
+    [SDImageFramePool unregisterProvider:self.animatedProvider];
 }
 
 #pragma mark - Private
-- (NSOperationQueue *)fetchQueue {
-    if (!_fetchQueue) {
-        _fetchQueue = [[NSOperationQueue alloc] init];
-        _fetchQueue.maxConcurrentOperationCount = 1;
-    }
-    return _fetchQueue;
-}
-
-- (NSMutableDictionary<NSNumber *,UIImage *> *)frameBuffer {
-    if (!_frameBuffer) {
-        _frameBuffer = [NSMutableDictionary dictionary];
-    }
-    return _frameBuffer;
-}
 
 - (SDDisplayLink *)displayLink {
     if (!_displayLink) {
@@ -144,18 +108,18 @@
     
     if (!self.currentFrame && [self.animatedProvider isKindOfClass:[UIImage class]]) {
         UIImage *image = (UIImage *)self.animatedProvider;
-        // Use the poster image if available
+        // Cache the poster image if available, but should not callback to avoid caller thread issues
         #if SD_MAC
         UIImage *posterFrame = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
         #else
         UIImage *posterFrame = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
         #endif
         if (posterFrame) {
-            self.currentFrame = posterFrame;
-            SD_LOCK(self->_lock);
-            self.frameBuffer[@(self.currentFrameIndex)] = self.currentFrame;
-            SD_UNLOCK(self->_lock);
-            [self handleFrameChange];
+            // Calculate max buffer size
+            [self calculateMaxBufferCountWithFrame:posterFrame];
+            // HACK: The first frame should not check duration and immediately display
+            self.needsDisplayWhenImageBecomesAvailable = YES;
+            [self.framePool setFrame:posterFrame atIndex:self.currentFrameIndex];
         }
     }
     
@@ -172,9 +136,7 @@
 }
 
 - (void)clearFrameBuffer {
-    SD_LOCK(_lock);
-    [_frameBuffer removeAllObjects];
-    SD_UNLOCK(_lock);
+    [self.framePool removeAllFrames];
 }
 
 #pragma mark - Animation Control
@@ -182,12 +144,9 @@
     [self.displayLink start];
     // Setup frame
     [self setupCurrentFrame];
-    // Calculate max buffer size
-    [self calculateMaxBufferCount];
 }
 
 - (void)stopPlaying {
-    [_fetchQueue cancelAllOperations];
     // Using `_displayLink` here because when UIImageView dealloc, it may trigger `[self stopAnimating]`, we already release the display link in SDAnimatedImageView's dealloc method.
     [_displayLink stop];
     // We need to reset the frame status, but not trigger any handle. This can ensure next time's playing status correct.
@@ -195,7 +154,6 @@
 }
 
 - (void)pausePlaying {
-    [_fetchQueue cancelAllOperations];
     [_displayLink stop];
 }
 
@@ -247,9 +205,9 @@
     } else if (self.playbackMode == SDAnimatedImagePlaybackModeBounce ||
                self.playbackMode == SDAnimatedImagePlaybackModeReversedBounce) {
         if (currentFrameIndex == 0) {
-            self.shouldReverse = false;
+            self.shouldReverse = NO;
         } else if (currentFrameIndex == totalFrameCount - 1) {
-            self.shouldReverse = true;
+            self.shouldReverse = YES;
         }
         nextFrameIndex = self.shouldReverse ? (currentFrameIndex - 1) : (currentFrameIndex + 1);
         nextFrameIndex %= totalFrameCount;
@@ -257,26 +215,11 @@
     
     
     // Check if we need to display new frame firstly
-    BOOL bufferFull = NO;
     if (self.needsDisplayWhenImageBecomesAvailable) {
-        UIImage *currentFrame;
-        SD_LOCK(_lock);
-        currentFrame = self.frameBuffer[@(currentFrameIndex)];
-        SD_UNLOCK(_lock);
+        UIImage *currentFrame = [self.framePool frameAtIndex:currentFrameIndex];
         
         // Update the current frame
         if (currentFrame) {
-            SD_LOCK(_lock);
-            // Remove the frame buffer if need
-            if (self.frameBuffer.count > self.maxBufferCount) {
-                self.frameBuffer[@(currentFrameIndex)] = nil;
-            }
-            // Check whether we can stop fetch
-            if (self.frameBuffer.count == totalFrameCount) {
-                bufferFull = YES;
-            }
-            SD_UNLOCK(_lock);
-            
             // Update the current frame immediately
             self.currentFrame = currentFrame;
             [self handleFrameChange];
@@ -296,7 +239,9 @@
         NSTimeInterval currentDuration = [self.animatedProvider animatedImageDurationAtIndex:currentFrameIndex];
         currentDuration = currentDuration / playbackRate;
         if (self.currentTime < currentDuration) {
-            // Current frame timestamp not reached, return
+            // Current frame timestamp not reached, prefetch frame in advance.
+            [self prefetchFrameAtIndex:currentFrameIndex
+                             nextIndex:nextFrameIndex];
             return;
         }
         
@@ -331,34 +276,30 @@
         return;
     }
     
-    // Check if we should prefetch next frame or current frame
-    // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame
-    // Or, most cases, the decode speed is faster than render speed, we fetch next frame
-    NSUInteger fetchFrameIndex = self.bufferMiss? currentFrameIndex : nextFrameIndex;
-    UIImage *fetchFrame;
-    SD_LOCK(_lock);
-    fetchFrame = self.bufferMiss? nil : self.frameBuffer[@(nextFrameIndex)];
-    SD_UNLOCK(_lock);
-    
-    if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) {
-        // Prefetch next frame in background queue
-        id<SDAnimatedImageProvider> animatedProvider = self.animatedProvider;
-        @weakify(self);
-        NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
-            @strongify(self);
-            if (!self) {
-                return;
-            }
-            UIImage *frame = [animatedProvider animatedImageFrameAtIndex:fetchFrameIndex];
+    [self prefetchFrameAtIndex:currentFrameIndex
+                     nextIndex:nextFrameIndex];
+}
 
-            BOOL isAnimating = self.displayLink.isRunning;
-            if (isAnimating) {
-                SD_LOCK(self->_lock);
-                self.frameBuffer[@(fetchFrameIndex)] = frame;
-                SD_UNLOCK(self->_lock);
-            }
-        }];
-        [self.fetchQueue addOperation:operation];
+// Check if we should prefetch next frame or current frame
+// When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame
+// Or, most cases, the decode speed is faster than render speed, we fetch next frame
+- (void)prefetchFrameAtIndex:(NSUInteger)currentIndex
+                   nextIndex:(NSUInteger)nextIndex {
+    NSUInteger fetchFrameIndex = currentIndex;
+    UIImage *fetchFrame = nil;
+    if (!self.bufferMiss) {
+        fetchFrameIndex = nextIndex;
+        fetchFrame = [self.framePool frameAtIndex:nextIndex];
+    }
+    BOOL bufferFull = NO;
+    if (self.framePool.currentFrameCount == self.totalFrameCount) {
+        bufferFull = YES;
+    }
+    if (!fetchFrame && !bufferFull) {
+        // Calculate max buffer size
+        [self calculateMaxBufferCountWithFrame:self.currentFrame];
+        // Prefetch next frame
+        [self.framePool prefetchFrameAtIndex:fetchFrameIndex];
     }
 }
 
@@ -375,9 +316,17 @@
 }
 
 #pragma mark - Util
-- (void)calculateMaxBufferCount {
-    NSUInteger bytes = CGImageGetBytesPerRow(self.currentFrame.CGImage) * CGImageGetHeight(self.currentFrame.CGImage);
-    if (bytes == 0) bytes = 1024;
+- (void)calculateMaxBufferCountWithFrame:(nonnull UIImage *)frame {
+    NSUInteger bytes = self.currentFrameBytes;
+    if (bytes == 0) {
+        bytes = CGImageGetBytesPerRow(frame.CGImage) * CGImageGetHeight(frame.CGImage);
+        if (bytes == 0) {
+            bytes = 1024;
+        } else {
+            // Cache since most animated image each frame bytes is the same
+            self.currentFrameBytes = bytes;
+        }
+    }
     
     NSUInteger max = 0;
     if (self.maxBufferSize > 0) {
@@ -395,7 +344,7 @@
         maxBufferCount = 1;
     }
     
-    self.maxBufferCount = maxBufferCount;
+    self.framePool.maxBufferCount = maxBufferCount;
 }
 
 + (NSString *)defaultRunLoopMode {

+ 5 - 0
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageRep.m

@@ -16,6 +16,11 @@
 #import "SDImageHEICCoder.h"
 #import "SDImageAWebPCoder.h"
 
+@interface SDAnimatedImageRep ()
+/// This wrap the animated image frames for legacy animated image coder API (`encodedDataWithImage:`).
+@property (nonatomic, readwrite, weak) NSArray<SDImageFrame *> *frames;
+@end
+
 @implementation SDAnimatedImageRep {
     CGImageSourceRef _imageSource;
 }

+ 7 - 3
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView.m

@@ -192,9 +192,8 @@
         
         [self stopAnimating];
         [self checkPlay];
-
-        [self.imageViewLayer setNeedsDisplay];
     }
+    [self.imageViewLayer setNeedsDisplay];
 }
 
 #pragma mark - Configuration
@@ -472,7 +471,7 @@
 {
     if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)] && image.sd_isIncremental && [image respondsToSelector:@selector(animatedCoder)]) {
         id<SDAnimatedImageCoder> animatedCoder = [(id<SDAnimatedImage>)image animatedCoder];
-        if ([animatedCoder conformsToProtocol:@protocol(SDProgressiveImageCoder)]) {
+        if ([animatedCoder respondsToSelector:@selector(initIncrementalWithOptions:)]) {
             return (id<SDAnimatedImageCoder, SDProgressiveImageCoder>)animatedCoder;
         }
     }
@@ -493,6 +492,11 @@
         // If we have no animation frames, call super implementation. iOS 14+ UIImageView use this delegate method for rendering.
         if ([UIImageView instancesRespondToSelector:@selector(displayLayer:)]) {
             [super displayLayer:layer];
+        } else {
+            // Fallback to implements the static image rendering by ourselves (like macOS or before iOS 14)
+            currentFrame = super.image;
+            layer.contentsScale = currentFrame.scale;
+            layer.contents = (__bridge id)currentFrame.CGImage;
         }
     }
 }

+ 22 - 14
Pods/SDWebImage/SDWebImage/Core/SDDiskCache.m

@@ -43,6 +43,8 @@ static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDis
     } else {
         self.fileManager = [NSFileManager new];
     }
+  
+    [self createDirectory];
 }
 
 - (BOOL)containsDataForKey:(NSString *)key {
@@ -80,22 +82,13 @@ static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDis
 - (void)setData:(NSData *)data forKey:(NSString *)key {
     NSParameterAssert(data);
     NSParameterAssert(key);
-    if (![self.fileManager fileExistsAtPath:self.diskCachePath]) {
-        [self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
-    }
     
     // get cache Path for image key
     NSString *cachePathForKey = [self cachePathForKey:key];
     // transform to NSURL
-    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
+    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey isDirectory:NO];
     
     [data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
-    
-    // disable iCloud backup
-    if (self.config.shouldDisableiCloud) {
-        // ignore iCloud backup resource value error
-        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
-    }
 }
 
 - (NSData *)extendedDataForKey:(NSString *)key {
@@ -131,10 +124,20 @@ static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDis
 
 - (void)removeAllData {
     [self.fileManager removeItemAtPath:self.diskCachePath error:nil];
-    [self.fileManager createDirectoryAtPath:self.diskCachePath
-            withIntermediateDirectories:YES
-                             attributes:nil
-                                  error:NULL];
+    [self createDirectory];
+}
+
+- (void)createDirectory {
+  [self.fileManager createDirectoryAtPath:self.diskCachePath
+          withIntermediateDirectories:YES
+                           attributes:nil
+                                error:NULL];
+  
+  // disable iCloud backup
+  if (self.config.shouldDisableiCloud) {
+      // ignore iCloud backup resource value error
+      [[NSURL fileURLWithPath:self.diskCachePath isDirectory:YES] setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
+  }
 }
 
 - (void)removeExpiredData {
@@ -285,6 +288,11 @@ static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDis
         }
         // New directory does not exist, rename directory
         [self.fileManager moveItemAtPath:srcPath toPath:dstPath error:nil];
+        // disable iCloud backup
+        if (self.config.shouldDisableiCloud) {
+            // ignore iCloud backup resource value error
+            [[NSURL fileURLWithPath:dstPath isDirectory:YES] setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
+        }
     } else {
         // New directory exist, merge the files
         NSDirectoryEnumerator *dirEnumerator = [self.fileManager enumeratorAtPath:srcPath];

+ 6 - 0
Pods/SDWebImage/SDWebImage/Core/SDGraphicsImageRenderer.h

@@ -17,11 +17,17 @@
  For others (macOS/watchOS or iOS/tvOS 10-), these method use the `SDImageGraphics.h` to implements the same behavior (but without dynamic bitmap support)
 */
 
+/// A closure for drawing an image.
 typedef void (^SDGraphicsImageDrawingActions)(CGContextRef _Nonnull context);
+/// Constants that specify the color range of the image renderer context.
 typedef NS_ENUM(NSInteger, SDGraphicsImageRendererFormatRange) {
+    /// The image renderer context doesn’t specify a color range.
     SDGraphicsImageRendererFormatRangeUnspecified = -1,
+    /// The system automatically chooses the image renderer context’s pixel format according to the color range of its content.
     SDGraphicsImageRendererFormatRangeAutomatic = 0,
+    /// The image renderer context supports wide color.
     SDGraphicsImageRendererFormatRangeExtended,
+    /// The image renderer context doesn’t support extended colors.
     SDGraphicsImageRendererFormatRangeStandard
 };
 

+ 14 - 2
Pods/SDWebImage/SDWebImage/Core/SDGraphicsImageRenderer.m

@@ -132,7 +132,13 @@
 #elif SD_UIKIT
             CGFloat screenScale = [UIScreen mainScreen].scale;
 #elif SD_MAC
-            CGFloat screenScale = [NSScreen mainScreen].backingScaleFactor;
+            NSScreen *mainScreen = nil;
+            if (@available(macOS 10.12, *)) {
+                mainScreen = [NSScreen mainScreen];
+            } else {
+                mainScreen = [NSScreen screens].firstObject;
+            }
+            CGFloat screenScale = mainScreen.backingScaleFactor ?: 1.0f;
 #endif
             self.scale = screenScale;
             self.opaque = NO;
@@ -166,7 +172,13 @@
 #elif SD_UIKIT
             CGFloat screenScale = [UIScreen mainScreen].scale;
 #elif SD_MAC
-            CGFloat screenScale = [NSScreen mainScreen].backingScaleFactor;
+            NSScreen *mainScreen = nil;
+            if (@available(macOS 10.12, *)) {
+                mainScreen = [NSScreen mainScreen];
+            } else {
+                mainScreen = [NSScreen screens].firstObject;
+            }
+            CGFloat screenScale = mainScreen.backingScaleFactor ?: 1.0f;
 #endif
             self.scale = screenScale;
             self.opaque = NO;

+ 63 - 11
Pods/SDWebImage/SDWebImage/Core/SDImageCache.h

@@ -37,8 +37,10 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
     /**
      * By default, we will decode the image in the background during cache query and download from the network. This can help to improve performance because when rendering image on the screen, it need to be firstly decoded. But this happen on the main queue by Core Animation.
      * However, this process may increase the memory usage as well. If you are experiencing a issue due to excessive memory consumption, This flag can prevent decode the image.
+     * @note 5.14.0 introduce `SDImageCoderDecodeUseLazyDecoding`, use that for better control from codec, instead of post-processing. Which acts the similar like this option but works for SDAnimatedImage as well (this one does not)
+     * @deprecated Deprecated in v5.17.0, if you don't want force-decode, pass [.imageForceDecodePolicy] = [SDImageForceDecodePolicy.never] in context option
      */
-    SDImageCacheAvoidDecodeImage = 1 << 4,
+    SDImageCacheAvoidDecodeImage API_DEPRECATED("Use SDWebImageContextImageForceDecodePolicy instead", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0)) = 1 << 4,
     /**
      * By default, we decode the animated image. This flag can force decode the first frame only and produce the static image.
      */
@@ -55,6 +57,23 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
     SDImageCacheMatchAnimatedImageClass = 1 << 7,
 };
 
+/**
+ *  A token associated with each cache query. Can be used to cancel a cache query
+ */
+@interface SDImageCacheToken : NSObject <SDWebImageOperation>
+
+/**
+ Cancel the current cache query.
+ */
+- (void)cancel;
+
+/**
+ The query's cache key.
+ */
+@property (nonatomic, strong, nullable, readonly) NSString *key;
+
+@end
+
 /**
  * SDImageCache maintains a memory cache and a disk cache. Disk cache write operations are performed
  * asynchronous so it doesn’t add unnecessary latency to the UI.
@@ -179,6 +198,17 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
             toDisk:(BOOL)toDisk
         completion:(nullable SDWebImageNoParamsBlock)completionBlock;
 
+/**
+ * Asynchronously store an image data into disk cache at the given key.
+ *
+ * @param imageData           The image data to store
+ * @param key             The unique image cache key, usually it's image absolute URL
+ * @param completionBlock A block executed after the operation is finished
+ */
+- (void)storeImageData:(nullable NSData *)imageData
+                forKey:(nullable NSString *)key
+            completion:(nullable SDWebImageNoParamsBlock)completionBlock;
+
 /**
  * Asynchronously store an image into memory and disk cache at the given key.
  *
@@ -198,7 +228,29 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
         completion:(nullable SDWebImageNoParamsBlock)completionBlock;
 
 /**
- * Synchronously store image into memory cache at the given key.
+ * Asynchronously store an image into memory and disk cache at the given key.
+ *
+ * @param image           The image to store
+ * @param imageData       The image data as returned by the server, this representation will be used for disk storage
+ *                        instead of converting the given image object into a storable/compressed image format in order
+ *                        to save quality and CPU
+ * @param key             The unique image cache key, usually it's image absolute URL
+ * @param options A mask to specify options to use for this store
+ * @param context The context options to use. Pass `.callbackQueue` to control callback queue
+ * @param cacheType The image store op cache type
+ * @param completionBlock A block executed after the operation is finished
+ * @note If no image data is provided and encode to disk, we will try to detect the image format (using either `sd_imageFormat` or `SDAnimatedImage` protocol method) and animation status, to choose the best matched format, including GIF, JPEG or PNG.
+ */
+- (void)storeImage:(nullable UIImage *)image
+         imageData:(nullable NSData *)imageData
+            forKey:(nullable NSString *)key
+           options:(SDWebImageOptions)options
+           context:(nullable SDWebImageContext *)context
+         cacheType:(SDImageCacheType)cacheType
+        completion:(nullable SDWebImageNoParamsBlock)completionBlock;
+
+/**
+ * Synchronously store an image into memory cache at the given key.
  *
  * @param image  The image to store
  * @param key    The unique image cache key, usually it's image absolute URL
@@ -207,7 +259,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
                     forKey:(nullable NSString *)key;
 
 /**
- * Synchronously store image data into disk cache at the given key.
+ * Synchronously store an image data into disk cache at the given key.
  *
  * @param imageData  The image data to store
  * @param key        The unique image cache key, usually it's image absolute URL
@@ -259,9 +311,9 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
  * @param key       The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`.
  * @param doneBlock The completion block. Will not get called if the operation is cancelled
  *
- * @return a NSOperation instance containing the cache op
+ * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancelled
  */
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
 
 /**
  * Asynchronously queries the cache with operation and call the completion when done.
@@ -270,9 +322,9 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
  * @param options   A mask to specify options to use for this cache query
  * @param doneBlock The completion block. Will not get called if the operation is cancelled
  *
- * @return a NSOperation instance containing the cache op
+ * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancelled
  */
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
 
 /**
  * Asynchronously queries the cache with operation and call the completion when done.
@@ -282,9 +334,9 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
  * @param context   A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
  * @param doneBlock The completion block. Will not get called if the operation is cancelled
  *
- * @return a NSOperation instance containing the cache op
+ * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancellederation, will callback immediately when cancelled
  */
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
 
 /**
  * Asynchronously queries the cache with operation and call the completion when done.
@@ -295,9 +347,9 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
  * @param queryCacheType Specify where to query the cache from. By default we use `.all`, which means both memory cache and disk cache. You can choose to query memory only or disk only as well. Pass `.none` is invalid and callback with nil immediately.
  * @param doneBlock The completion block. Will not get called if the operation is cancelled
  *
- * @return a NSOperation instance containing the cache op
+ * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancelled
  */
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
 
 /**
  * Synchronously query the memory cache.

+ 183 - 89
Pods/SDWebImage/SDWebImage/Core/SDImageCache.m

@@ -14,6 +14,45 @@
 #import "UIImage+MemoryCacheCost.h"
 #import "UIImage+Metadata.h"
 #import "UIImage+ExtendedCacheData.h"
+#import "SDCallbackQueue.h"
+
+@interface SDImageCacheToken ()
+
+@property (nonatomic, strong, nullable, readwrite) NSString *key;
+@property (nonatomic, assign, getter=isCancelled) BOOL cancelled;
+@property (nonatomic, copy, nullable) SDImageCacheQueryCompletionBlock doneBlock;
+@property (nonatomic, strong, nullable) SDCallbackQueue *callbackQueue;
+
+@end
+
+@implementation SDImageCacheToken
+
+-(instancetype)initWithDoneBlock:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
+    self = [super init];
+    if (self) {
+        self.doneBlock = doneBlock;
+    }
+    return self;
+}
+
+- (void)cancel {
+    @synchronized (self) {
+        if (self.isCancelled) {
+            return;
+        }
+        self.cancelled = YES;
+        
+        SDImageCacheQueryCompletionBlock doneBlock = self.doneBlock;
+        self.doneBlock = nil;
+        if (doneBlock) {
+            [(self.callbackQueue ?: SDCallbackQueue.mainQueue) async:^{
+                doneBlock(nil, nil, SDImageCacheTypeNone);
+            }];
+        }
+    }
+}
+
+@end
 
 static NSString * _defaultDiskCacheDirectory;
 
@@ -24,7 +63,7 @@ static NSString * _defaultDiskCacheDirectory;
 @property (nonatomic, strong, readwrite, nonnull) id<SDDiskCache> diskCache;
 @property (nonatomic, copy, readwrite, nonnull) SDImageCacheConfig *config;
 @property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath;
-@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue;
+@property (nonatomic, strong, nonnull) dispatch_queue_t ioQueue;
 
 @end
 
@@ -72,14 +111,16 @@ static NSString * _defaultDiskCacheDirectory;
     if ((self = [super init])) {
         NSAssert(ns, @"Cache namespace should not be nil");
         
-        // Create IO serial queue
-        _ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL);
-        
         if (!config) {
             config = SDImageCacheConfig.defaultCacheConfig;
         }
         _config = [config copy];
         
+        // Create IO queue
+        dispatch_queue_attr_t ioQueueAttributes = _config.ioQueueAttributes;
+        _ioQueue = dispatch_queue_create("com.hackemist.SDImageCache.ioQueue", ioQueueAttributes);
+        NSAssert(_ioQueue, @"The IO queue should not be nil. Your configured `ioQueueAttributes` may be wrong");
+        
         // Init the memory cache
         NSAssert([config.memoryCacheClass conformsToProtocol:@protocol(SDMemoryCache)], @"Custom memory cache class must conform to `SDMemoryCache` protocol");
         _memoryCache = [[config.memoryCacheClass alloc] initWithConfig:_config];
@@ -158,14 +199,20 @@ static NSString * _defaultDiskCacheDirectory;
 - (void)storeImage:(nullable UIImage *)image
             forKey:(nullable NSString *)key
         completion:(nullable SDWebImageNoParamsBlock)completionBlock {
-    [self storeImage:image imageData:nil forKey:key toDisk:YES completion:completionBlock];
+    [self storeImage:image imageData:nil forKey:key options:0 context:nil cacheType:SDImageCacheTypeAll completion:completionBlock];
 }
 
 - (void)storeImage:(nullable UIImage *)image
             forKey:(nullable NSString *)key
             toDisk:(BOOL)toDisk
         completion:(nullable SDWebImageNoParamsBlock)completionBlock {
-    [self storeImage:image imageData:nil forKey:key toDisk:toDisk completion:completionBlock];
+    [self storeImage:image imageData:nil forKey:key options:0 context:nil cacheType:(toDisk ? SDImageCacheTypeAll : SDImageCacheTypeMemory) completion:completionBlock];
+}
+
+- (void)storeImageData:(nullable NSData *)imageData
+                forKey:(nullable NSString *)key
+            completion:(nullable SDWebImageNoParamsBlock)completionBlock {
+    [self storeImage:nil imageData:imageData forKey:key options:0 context:nil cacheType:SDImageCacheTypeAll completion:completionBlock];
 }
 
 - (void)storeImage:(nullable UIImage *)image
@@ -173,23 +220,26 @@ static NSString * _defaultDiskCacheDirectory;
             forKey:(nullable NSString *)key
             toDisk:(BOOL)toDisk
         completion:(nullable SDWebImageNoParamsBlock)completionBlock {
-    return [self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:toDisk completion:completionBlock];
+    [self storeImage:image imageData:imageData forKey:key options:0 context:nil cacheType:(toDisk ? SDImageCacheTypeAll : SDImageCacheTypeMemory) completion:completionBlock];
 }
 
 - (void)storeImage:(nullable UIImage *)image
          imageData:(nullable NSData *)imageData
             forKey:(nullable NSString *)key
-          toMemory:(BOOL)toMemory
-            toDisk:(BOOL)toDisk
+           options:(SDWebImageOptions)options
+           context:(nullable SDWebImageContext *)context
+         cacheType:(SDImageCacheType)cacheType
         completion:(nullable SDWebImageNoParamsBlock)completionBlock {
-    if (!image || !key) {
+    if ((!image && !imageData) || !key) {
         if (completionBlock) {
             completionBlock();
         }
         return;
     }
+    BOOL toMemory = cacheType == SDImageCacheTypeMemory || cacheType == SDImageCacheTypeAll;
+    BOOL toDisk = cacheType == SDImageCacheTypeDisk || cacheType == SDImageCacheTypeAll;
     // if memory cache is enabled
-    if (toMemory && self.config.shouldCacheImagesInMemory) {
+    if (image && toMemory && self.config.shouldCacheImagesInMemory) {
         NSUInteger cost = image.sd_memoryCost;
         [self.memoryCache setObject:image forKey:key cost:cost];
     }
@@ -200,41 +250,51 @@ static NSString * _defaultDiskCacheDirectory;
         }
         return;
     }
-    dispatch_async(self.ioQueue, ^{
-        @autoreleasepool {
-            NSData *data = imageData;
-            if (!data && [image conformsToProtocol:@protocol(SDAnimatedImage)]) {
-                // If image is custom animated image class, prefer its original animated data
-                data = [((id<SDAnimatedImage>)image) animatedImageData];
-            }
-            if (!data && image) {
-                // Check image's associated image format, may return .undefined
-                SDImageFormat format = image.sd_imageFormat;
-                if (format == SDImageFormatUndefined) {
-                    // If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14)
-                    if (image.sd_isAnimated) {
-                        format = SDImageFormatGIF;
-                    } else {
-                        // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
-                        format = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage] ? SDImageFormatPNG : SDImageFormatJPEG;
-                    }
+    NSData *data = imageData;
+    if (!data && [image respondsToSelector:@selector(animatedImageData)]) {
+        // If image is custom animated image class, prefer its original animated data
+        data = [((id<SDAnimatedImage>)image) animatedImageData];
+    }
+    SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];
+    if (!data && image) {
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+            // Check image's associated image format, may return .undefined
+            SDImageFormat format = image.sd_imageFormat;
+            if (format == SDImageFormatUndefined) {
+                // If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14)
+                if (image.sd_imageFrameCount > 1) {
+                    format = SDImageFormatGIF;
+                } else {
+                    // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
+                    format = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage] ? SDImageFormatPNG : SDImageFormatJPEG;
                 }
-                data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
             }
+            NSData *data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:context[SDWebImageContextImageEncodeOptions]];
+            dispatch_async(self.ioQueue, ^{
+                [self _storeImageDataToDisk:data forKey:key];
+                [self _archivedDataWithImage:image forKey:key];
+                if (completionBlock) {
+                    [(queue ?: SDCallbackQueue.mainQueue) async:^{
+                        completionBlock();
+                    }];
+                }
+            });
+        });
+    } else {
+        dispatch_async(self.ioQueue, ^{
             [self _storeImageDataToDisk:data forKey:key];
             [self _archivedDataWithImage:image forKey:key];
-        }
-        
-        if (completionBlock) {
-            dispatch_async(dispatch_get_main_queue(), ^{
-                completionBlock();
-            });
-        }
-    });
+            if (completionBlock) {
+                [(queue ?: SDCallbackQueue.mainQueue) async:^{
+                    completionBlock();
+                }];
+            }
+        });
+    }
 }
 
 - (void)_archivedDataWithImage:(UIImage *)image forKey:(NSString *)key {
-    if (!image) {
+    if (!image || !key) {
         return;
     }
     // Check extended data
@@ -359,6 +419,9 @@ static NSString * _defaultDiskCacheDirectory;
 }
 
 - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context {
+    if (!key) {
+        return nil;
+    }
     NSData *data = [self diskImageDataForKey:key];
     UIImage *diskImage = [self diskImageForKey:key data:data options:options context:context];
     
@@ -367,7 +430,20 @@ static NSString * _defaultDiskCacheDirectory;
         SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
         shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
     }
-    if (diskImage && self.config.shouldCacheImagesInMemory && shouldCacheToMomery) {
+    CGSize thumbnailSize = CGSizeZero;
+    NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
+    if (thumbnailSizeValue != nil) {
+#if SD_MAC
+        thumbnailSize = thumbnailSizeValue.sizeValue;
+#else
+        thumbnailSize = thumbnailSizeValue.CGSizeValue;
+#endif
+    }
+    if (thumbnailSize.width > 0 && thumbnailSize.height > 0) {
+        // Query full size cache key which generate a thumbnail, should not write back to full size memory cache
+        shouldCacheToMomery = NO;
+    }
+    if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
         NSUInteger cost = diskImage.sd_memoryCost;
         [self.memoryCache setObject:diskImage forKey:key cost:cost];
     }
@@ -385,8 +461,7 @@ static NSString * _defaultDiskCacheDirectory;
     if (image) {
         if (options & SDImageCacheDecodeFirstFrameOnly) {
             // Ensure static image
-            Class animatedImageClass = image.class;
-            if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
+            if (image.sd_imageFrameCount > 1) {
 #if SD_MAC
                 image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
 #else
@@ -435,11 +510,10 @@ static NSString * _defaultDiskCacheDirectory;
 }
 
 - (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
+    if (!key) {
+        return nil;
+    }
     NSData *data = [self diskImageDataForKey:key];
-    return [self diskImageForKey:key data:data];
-}
-
-- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data {
     return [self diskImageForKey:key data:data options:0 context:nil];
 }
 
@@ -453,7 +527,7 @@ static NSString * _defaultDiskCacheDirectory;
 }
 
 - (void)_unarchiveObjectWithImage:(UIImage *)image forKey:(NSString *)key {
-    if (!image) {
+    if (!image || !key) {
         return;
     }
     // Check extended data
@@ -483,19 +557,19 @@ static NSString * _defaultDiskCacheDirectory;
     image.sd_extendedObject = extendedObject;
 }
 
-- (nullable NSOperation *)queryCacheOperationForKey:(NSString *)key done:(SDImageCacheQueryCompletionBlock)doneBlock {
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(NSString *)key done:(SDImageCacheQueryCompletionBlock)doneBlock {
     return [self queryCacheOperationForKey:key options:0 done:doneBlock];
 }
 
-- (nullable NSOperation *)queryCacheOperationForKey:(NSString *)key options:(SDImageCacheOptions)options done:(SDImageCacheQueryCompletionBlock)doneBlock {
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(NSString *)key options:(SDImageCacheOptions)options done:(SDImageCacheQueryCompletionBlock)doneBlock {
     return [self queryCacheOperationForKey:key options:options context:nil done:doneBlock];
 }
 
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
     return [self queryCacheOperationForKey:key options:options context:context cacheType:SDImageCacheTypeAll done:doneBlock];
 }
 
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
     if (!key) {
         if (doneBlock) {
             doneBlock(nil, nil, SDImageCacheTypeNone);
@@ -519,8 +593,7 @@ static NSString * _defaultDiskCacheDirectory;
     if (image) {
         if (options & SDImageCacheDecodeFirstFrameOnly) {
             // Ensure static image
-            Class animatedImageClass = image.class;
-            if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
+            if (image.sd_imageFrameCount > 1) {
 #if SD_MAC
                 image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
 #else
@@ -546,23 +619,30 @@ static NSString * _defaultDiskCacheDirectory;
     }
     
     // Second check the disk cache...
-    NSOperation *operation = [NSOperation new];
+    SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];
+    SDImageCacheToken *operation = [[SDImageCacheToken alloc] initWithDoneBlock:doneBlock];
+    operation.key = key;
+    operation.callbackQueue = queue;
     // Check whether we need to synchronously query disk
     // 1. in-memory cache hit & memoryDataSync
     // 2. in-memory cache miss & diskDataSync
     BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                                 (!image && options & SDImageCacheQueryDiskDataSync));
     NSData* (^queryDiskDataBlock)(void) = ^NSData* {
-        if (operation.isCancelled) {
-            return nil;
+        @synchronized (operation) {
+            if (operation.isCancelled) {
+                return nil;
+            }
         }
         
         return [self diskImageDataBySearchingAllPathsForKey:key];
     };
     
     UIImage* (^queryDiskImageBlock)(NSData*) = ^UIImage*(NSData* diskData) {
-        if (operation.isCancelled) {
-            return nil;
+        @synchronized (operation) {
+            if (operation.isCancelled) {
+                return nil;
+            }
         }
         
         UIImage *diskImage;
@@ -575,11 +655,30 @@ static NSString * _defaultDiskCacheDirectory;
                 SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
                 shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
             }
+            CGSize thumbnailSize = CGSizeZero;
+            NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
+            if (thumbnailSizeValue != nil) {
+        #if SD_MAC
+                thumbnailSize = thumbnailSizeValue.sizeValue;
+        #else
+                thumbnailSize = thumbnailSizeValue.CGSizeValue;
+        #endif
+            }
+            if (thumbnailSize.width > 0 && thumbnailSize.height > 0) {
+                // Query full size cache key which generate a thumbnail, should not write back to full size memory cache
+                shouldCacheToMomery = NO;
+            }
+            // Special case: If user query image in list for the same URL, to avoid decode and write **same** image object into disk cache multiple times, we query and check memory cache here again.
+            if (shouldCacheToMomery && self.config.shouldCacheImagesInMemory) {
+                diskImage = [self.memoryCache objectForKey:key];
+            }
             // decode image data only if in-memory cache missed
-            diskImage = [self diskImageForKey:key data:diskData options:options context:context];
-            if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
-                NSUInteger cost = diskImage.sd_memoryCost;
-                [self.memoryCache setObject:diskImage forKey:key cost:cost];
+            if (!diskImage) {
+                diskImage = [self diskImageForKey:key data:diskData options:options context:context];
+                if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
+                    NSUInteger cost = diskImage.sd_memoryCost;
+                    [self.memoryCache setObject:diskImage forKey:key cost:cost];
+                }
             }
         }
         return diskImage;
@@ -600,10 +699,22 @@ static NSString * _defaultDiskCacheDirectory;
         dispatch_async(self.ioQueue, ^{
             NSData* diskData = queryDiskDataBlock();
             UIImage* diskImage = queryDiskImageBlock(diskData);
+            @synchronized (operation) {
+                if (operation.isCancelled) {
+                    return;
+                }
+            }
             if (doneBlock) {
-                dispatch_async(dispatch_get_main_queue(), ^{
+                [(queue ?: SDCallbackQueue.mainQueue) async:^{
+                    // Dispatch from IO queue to main queue need time, user may call cancel during the dispatch timing
+                    // This check is here to avoid double callback (one is from `SDImageCacheToken` in sync)
+                    @synchronized (operation) {
+                        if (operation.isCancelled) {
+                            return;
+                        }
+                    }
                     doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
-                });
+                }];
             }
         });
     }
@@ -622,7 +733,7 @@ static NSString * _defaultDiskCacheDirectory;
 }
 
 - (void)removeImageForKey:(nullable NSString *)key fromMemory:(BOOL)fromMemory fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
-    if (key == nil) {
+    if (!key) {
         return;
     }
 
@@ -772,6 +883,8 @@ static NSString * _defaultDiskCacheDirectory;
 }
 
 #pragma mark - Helper
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
 + (SDWebImageOptions)imageOptionsFromCacheOptions:(SDImageCacheOptions)cacheOptions {
     SDWebImageOptions options = 0;
     if (cacheOptions & SDImageCacheScaleDownLargeImages) options |= SDWebImageScaleDownLargeImages;
@@ -782,6 +895,7 @@ static NSString * _defaultDiskCacheDirectory;
     
     return options;
 }
+#pragma clang diagnostic pop
 
 @end
 
@@ -793,6 +907,8 @@ static NSString * _defaultDiskCacheDirectory;
     return [self queryImageForKey:key options:options context:context cacheType:SDImageCacheTypeAll completion:completionBlock];
 }
 
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
 - (id<SDWebImageOperation>)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock {
     SDImageCacheOptions cacheOptions = 0;
     if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData;
@@ -806,32 +922,10 @@ static NSString * _defaultDiskCacheDirectory;
     
     return [self queryCacheOperationForKey:key options:cacheOptions context:context cacheType:cacheType done:completionBlock];
 }
+#pragma clang diagnostic pop
 
 - (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock {
-    switch (cacheType) {
-        case SDImageCacheTypeNone: {
-            [self storeImage:image imageData:imageData forKey:key toMemory:NO toDisk:NO completion:completionBlock];
-        }
-            break;
-        case SDImageCacheTypeMemory: {
-            [self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:NO completion:completionBlock];
-        }
-            break;
-        case SDImageCacheTypeDisk: {
-            [self storeImage:image imageData:imageData forKey:key toMemory:NO toDisk:YES completion:completionBlock];
-        }
-            break;
-        case SDImageCacheTypeAll: {
-            [self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:YES completion:completionBlock];
-        }
-            break;
-        default: {
-            if (completionBlock) {
-                completionBlock();
-            }
-        }
-            break;
-    }
+    [self storeImage:image imageData:imageData forKey:key options:0 context:nil cacheType:cacheType completion:completionBlock];
 }
 
 - (void)removeImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock {

+ 9 - 0
Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.h

@@ -127,6 +127,15 @@ typedef NS_ENUM(NSUInteger, SDImageCacheConfigExpireType) {
  */
 @property (strong, nonatomic, nullable) NSFileManager *fileManager;
 
+/**
+ * The dispatch queue attr for ioQueue. You can config the QoS and concurrent/serial to internal IO queue. The ioQueue is used by SDImageCache to access read/write for disk data.
+ * Defaults we use `DISPATCH_QUEUE_SERIAL`(NULL), to use serial dispatch queue to ensure single access for disk data. It's safe but may be slow.
+ * @note You can override this to use `DISPATCH_QUEUE_CONCURRENT`, use concurrent queue.
+ * @warning **MAKE SURE** to keep `diskCacheWritingOptions` to use `NSDataWritingAtomic`, or concurrent queue may cause corrupted disk data (because multiple threads read/write same file without atomic is not IO-safe).
+ * @note This value does not support dynamic changes. Which means further modification on this value after cache initialized has no effect.
+ */
+@property (strong, nonatomic, nullable) dispatch_queue_attr_t ioQueueAttributes;
+
 /**
  * The custom memory cache class. Provided class instance must conform to `SDMemoryCache` protocol to allow usage.
  * Defaults to built-in `SDMemoryCache` class.

+ 3 - 0
Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.m

@@ -35,6 +35,8 @@ static const NSInteger kDefaultCacheMaxDiskAge = 60 * 60 * 24 * 7; // 1 week
         _maxDiskAge = kDefaultCacheMaxDiskAge;
         _maxDiskSize = 0;
         _diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate;
+        _fileManager = nil;
+        _ioQueueAttributes = DISPATCH_QUEUE_SERIAL; // NULL
         _memoryCacheClass = [SDMemoryCache class];
         _diskCacheClass = [SDDiskCache class];
     }
@@ -56,6 +58,7 @@ static const NSInteger kDefaultCacheMaxDiskAge = 60 * 60 * 24 * 7; // 1 week
     config.maxMemoryCount = self.maxMemoryCount;
     config.diskCacheExpireType = self.diskCacheExpireType;
     config.fileManager = self.fileManager; // NSFileManager does not conform to NSCopying, just pass the reference
+    config.ioQueueAttributes = self.ioQueueAttributes; // Pass the reference
     config.memoryCacheClass = self.memoryCacheClass;
     config.diskCacheClass = self.diskCacheClass;
     

+ 42 - 6
Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.h

@@ -10,6 +10,7 @@
 #import "SDWebImageCompat.h"
 #import "SDWebImageOperation.h"
 #import "SDWebImageDefine.h"
+#import "SDImageCoder.h"
 
 /// Image Cache Type
 typedef NS_ENUM(NSInteger, SDImageCacheType) {
@@ -54,6 +55,18 @@ typedef void(^SDImageCacheContainsCompletionBlock)(SDImageCacheType containsCach
  */
 FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context);
 
+/// Get the decode options from the loading context options and cache key. This is the built-in translate between the web loading part to the decoding part (which does not depends on).
+/// @param context The context arg from the input
+/// @param options The options arg from the input
+/// @param cacheKey The image cache key from the input. Should not be nil
+FOUNDATION_EXPORT SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext * _Nullable context, SDWebImageOptions options, NSString * _Nonnull cacheKey);
+
+/// Set the decode options to the loading context options. This is the built-in translate between the web loading part from the decoding part (which does not depends on).
+/// @param mutableContext The context arg to override
+/// @param mutableOptions The options arg to override
+/// @param decodeOptions The image decoding options
+FOUNDATION_EXPORT void SDSetDecodeOptionsToContext(SDWebImageMutableContext * _Nonnull mutableContext, SDWebImageOptions * _Nonnull mutableOptions, SDImageCoderOptions * _Nonnull decodeOptions);
+
 /**
  This is the image cache protocol to provide custom image cache for `SDWebImageManager`.
  Though the best practice to custom image cache, is to write your own class which conform `SDMemoryCache` or `SDDiskCache` protocol for `SDImageCache` class (See more on `SDImageCacheConfig.memoryCacheClass & SDImageCacheConfig.diskCacheClass`).
@@ -68,22 +81,23 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn
 
  @param key The image cache key
  @param options A mask to specify options to use for this query
- @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
+ @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. Pass `.callbackQueue` to control callback queue
  @param completionBlock The completion block. Will not get called if the operation is cancelled
  @return The operation for this query
  */
 - (nullable id<SDWebImageOperation>)queryImageForKey:(nullable NSString *)key
                                              options:(SDWebImageOptions)options
                                              context:(nullable SDWebImageContext *)context
-                                          completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock;
+                                          completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock API_DEPRECATED_WITH_REPLACEMENT("queryImageForKey:options:context:cacheType:completion:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
+@optional
 /**
  Query the cached image from image cache for given key. The operation can be used to cancel the query.
  If image is cached in memory, completion is called synchronously, else asynchronously and depends on the options arg (See `SDWebImageQueryDiskSync`)
 
  @param key The image cache key
  @param options A mask to specify options to use for this query
- @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
+ @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. Pass `.callbackQueue` to control callback queue
  @param cacheType Specify where to query the cache from. By default we use `.all`, which means both memory cache and disk cache. You can choose to query memory only or disk only as well. Pass `.none` is invalid and callback with nil immediately.
  @param completionBlock The completion block. Will not get called if the operation is cancelled
  @return The operation for this query
@@ -94,21 +108,43 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn
                                            cacheType:(SDImageCacheType)cacheType
                                           completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock;
 
+@required
+/**
+ Store the image into image cache for the given key. If cache type is memory only, completion is called synchronously, else asynchronously.
+
+ @param image The image to store
+ @param imageData The image data to be used for disk storage
+ @param key The image cache key
+ @param cacheType The image store op cache type
+ @param completionBlock A block executed after the operation is finished
+ */
+- (void)storeImage:(nullable UIImage *)image
+         imageData:(nullable NSData *)imageData
+            forKey:(nullable NSString *)key
+         cacheType:(SDImageCacheType)cacheType
+        completion:(nullable SDWebImageNoParamsBlock)completionBlock API_DEPRECATED_WITH_REPLACEMENT("storeImage:imageData:forKey:options:context:cacheType:completion:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
+
+@optional
 /**
  Store the image into image cache for the given key. If cache type is memory only, completion is called synchronously, else asynchronously.
 
  @param image The image to store
  @param imageData The image data to be used for disk storage
  @param key The image cache key
+ @param options A mask to specify options to use for this store
+ @param context The context options to use. Pass `.callbackQueue` to control callback queue
  @param cacheType The image store op cache type
  @param completionBlock A block executed after the operation is finished
  */
 - (void)storeImage:(nullable UIImage *)image
          imageData:(nullable NSData *)imageData
             forKey:(nullable NSString *)key
+           options:(SDWebImageOptions)options
+           context:(nullable SDWebImageContext *)context
          cacheType:(SDImageCacheType)cacheType
         completion:(nullable SDWebImageNoParamsBlock)completionBlock;
 
+#pragma mark - Deprecated because SDWebImageManager does not use these APIs
 /**
  Remove the image from image cache for the given key. If cache type is memory only, completion is called synchronously, else asynchronously.
 
@@ -118,7 +154,7 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn
  */
 - (void)removeImageForKey:(nullable NSString *)key
                 cacheType:(SDImageCacheType)cacheType
-               completion:(nullable SDWebImageNoParamsBlock)completionBlock;
+               completion:(nullable SDWebImageNoParamsBlock)completionBlock API_DEPRECATED("No longer use. Cast to cache instance and call its API", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 /**
  Check if image cache contains the image for the given key (does not load the image). If image is cached in memory, completion is called synchronously, else asynchronously.
@@ -129,7 +165,7 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn
  */
 - (void)containsImageForKey:(nullable NSString *)key
                   cacheType:(SDImageCacheType)cacheType
-                 completion:(nullable SDImageCacheContainsCompletionBlock)completionBlock;
+                 completion:(nullable SDImageCacheContainsCompletionBlock)completionBlock API_DEPRECATED("No longer use. Cast to cache instance and call its API", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 /**
  Clear all the cached images for image cache. If cache type is memory only, completion is called synchronously, else asynchronously.
@@ -138,6 +174,6 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn
  @param completionBlock A block executed after the operation is finished
  */
 - (void)clearWithCacheType:(SDImageCacheType)cacheType
-                completion:(nullable SDWebImageNoParamsBlock)completionBlock;
+                completion:(nullable SDWebImageNoParamsBlock)completionBlock API_DEPRECATED("No longer use. Cast to cache instance and call its API", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 @end

+ 82 - 23
Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.m

@@ -13,36 +13,91 @@
 #import "UIImage+Metadata.h"
 #import "SDInternalMacros.h"
 
-UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
-    UIImage *image;
+#import <CoreServices/CoreServices.h>
+
+SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext * _Nullable context, SDWebImageOptions options, NSString * _Nonnull cacheKey) {
     BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
     NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
-    CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
+    CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); // Use cache key to detect scale
     NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio];
     NSValue *thumbnailSizeValue;
     BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages);
-    if (shouldScaleDown) {
-        CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4;
-        CGFloat dimension = ceil(sqrt(thumbnailPixels));
-        thumbnailSizeValue = @(CGSizeMake(dimension, dimension));
+    NSNumber *scaleDownLimitBytesValue = context[SDWebImageContextImageScaleDownLimitBytes];
+    if (!scaleDownLimitBytesValue && shouldScaleDown) {
+        // Use the default limit bytes
+        scaleDownLimitBytesValue = @(SDImageCoderHelper.defaultScaleDownLimitBytes);
     }
     if (context[SDWebImageContextImageThumbnailPixelSize]) {
         thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
     }
+    NSString *typeIdentifierHint = context[SDWebImageContextImageTypeIdentifierHint];
+    NSString *fileExtensionHint;
+    if (!typeIdentifierHint) {
+        // UTI has high priority
+        fileExtensionHint = cacheKey.pathExtension; // without dot
+        if (fileExtensionHint.length == 0) {
+            // Ignore file extension which is empty
+            fileExtensionHint = nil;
+        }
+    }
+    
+    // First check if user provided decode options
+    SDImageCoderMutableOptions *mutableCoderOptions;
+    if (context[SDWebImageContextImageDecodeOptions] != nil) {
+        mutableCoderOptions = [NSMutableDictionary dictionaryWithDictionary:context[SDWebImageContextImageDecodeOptions]];
+    } else {
+        mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:6];
+    }
     
-    SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
+    // Override individual options
     mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
     mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
     mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
     mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
-    mutableCoderOptions[SDImageCoderWebImageContext] = context;
-    SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
+    mutableCoderOptions[SDImageCoderDecodeTypeIdentifierHint] = typeIdentifierHint;
+    mutableCoderOptions[SDImageCoderDecodeFileExtensionHint] = fileExtensionHint;
+    mutableCoderOptions[SDImageCoderDecodeScaleDownLimitBytes] = scaleDownLimitBytesValue;
     
-    // Grab the image coder
-    id<SDImageCoder> imageCoder;
-    if ([context[SDWebImageContextImageCoder] conformsToProtocol:@protocol(SDImageCoder)]) {
-        imageCoder = context[SDWebImageContextImageCoder];
+    return [mutableCoderOptions copy];
+}
+
+void SDSetDecodeOptionsToContext(SDWebImageMutableContext * _Nonnull mutableContext, SDWebImageOptions * _Nonnull mutableOptions, SDImageCoderOptions * _Nonnull decodeOptions) {
+    if ([decodeOptions[SDImageCoderDecodeFirstFrameOnly] boolValue]) {
+        *mutableOptions |= SDWebImageDecodeFirstFrameOnly;
     } else {
+        *mutableOptions &= ~SDWebImageDecodeFirstFrameOnly;
+    }
+    
+    mutableContext[SDWebImageContextImageScaleFactor] = decodeOptions[SDImageCoderDecodeScaleFactor];
+    mutableContext[SDWebImageContextImagePreserveAspectRatio] = decodeOptions[SDImageCoderDecodePreserveAspectRatio];
+    mutableContext[SDWebImageContextImageThumbnailPixelSize] = decodeOptions[SDImageCoderDecodeThumbnailPixelSize];
+    mutableContext[SDWebImageContextImageScaleDownLimitBytes] = decodeOptions[SDImageCoderDecodeScaleDownLimitBytes];
+    
+    NSString *typeIdentifierHint = decodeOptions[SDImageCoderDecodeTypeIdentifierHint];
+    if (!typeIdentifierHint) {
+        NSString *fileExtensionHint = decodeOptions[SDImageCoderDecodeFileExtensionHint];
+        if (fileExtensionHint) {
+            typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, kUTTypeImage);
+            // Ignore dynamic UTI
+            if (UTTypeIsDynamic((__bridge CFStringRef)typeIdentifierHint)) {
+                typeIdentifierHint = nil;
+            }
+        }
+    }
+    mutableContext[SDWebImageContextImageTypeIdentifierHint] = typeIdentifierHint;
+}
+
+UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
+    NSCParameterAssert(imageData);
+    NSCParameterAssert(cacheKey);
+    UIImage *image;
+    SDImageCoderOptions *coderOptions = SDGetDecodeOptionsFromContext(context, options, cacheKey);
+    BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
+    CGFloat scale = [coderOptions[SDImageCoderDecodeScaleFactor] doubleValue];
+    
+    // Grab the image coder
+    id<SDImageCoder> imageCoder = context[SDWebImageContextImageCoder];
+    if (!imageCoder) {
         imageCoder = [SDImageCodersManager sharedManager];
     }
     
@@ -68,17 +123,21 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS
         image = [imageCoder decodedImageWithData:imageData options:coderOptions];
     }
     if (image) {
-        BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
-        if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
-            // `SDAnimatedImage` do not decode
-            shouldDecode = NO;
-        } else if (image.sd_isAnimated) {
-            // animated image do not decode
-            shouldDecode = NO;
+        SDImageForceDecodePolicy policy = SDImageForceDecodePolicyAutomatic;
+        NSNumber *polivyValue = context[SDWebImageContextImageForceDecodePolicy];
+        if (polivyValue != nil) {
+            policy = polivyValue.unsignedIntegerValue;
         }
-        if (shouldDecode) {
-            image = [SDImageCoderHelper decodedImageWithImage:image];
+        // TODO: Deprecated, remove in SD 6.0...
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+        if (SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage)) {
+            policy = SDImageForceDecodePolicyNever;
         }
+#pragma clang diagnostic pop
+        image = [SDImageCoderHelper decodedImageWithImage:image policy:policy];
+        // assign the decode options, to let manager check whether to re-decode if needed
+        image.sd_decodeOptions = coderOptions;
     }
     
     return image;

+ 14 - 10
Pods/SDWebImage/SDWebImage/Core/SDImageCachesManager.m

@@ -130,6 +130,10 @@
 }
 
 - (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock {
+    [self storeImage:image imageData:imageData forKey:key options:0 context:nil cacheType:cacheType completion:completionBlock];
+}
+
+- (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock {
     if (!key) {
         return;
     }
@@ -138,28 +142,28 @@
     if (count == 0) {
         return;
     } else if (count == 1) {
-        [caches.firstObject storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock];
+        [caches.firstObject storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock];
         return;
     }
     switch (self.storeOperationPolicy) {
         case SDImageCachesManagerOperationPolicyHighestOnly: {
             id<SDImageCache> cache = caches.lastObject;
-            [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock];
+            [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock];
         }
             break;
         case SDImageCachesManagerOperationPolicyLowestOnly: {
             id<SDImageCache> cache = caches.firstObject;
-            [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock];
+            [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock];
         }
             break;
         case SDImageCachesManagerOperationPolicyConcurrent: {
             SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new];
             [operation beginWithTotalCount:caches.count];
-            [self concurrentStoreImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation];
+            [self concurrentStoreImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation];
         }
             break;
         case SDImageCachesManagerOperationPolicySerial: {
-            [self serialStoreImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator];
+            [self serialStoreImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator];
         }
             break;
         default:
@@ -315,11 +319,11 @@
     }
 }
 
-- (void)concurrentStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<SDImageCache>> *)enumerator operation:(SDImageCachesManagerOperation *)operation {
+- (void)concurrentStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<SDImageCache>> *)enumerator operation:(SDImageCachesManagerOperation *)operation {
     NSParameterAssert(enumerator);
     NSParameterAssert(operation);
     for (id<SDImageCache> cache in enumerator) {
-        [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:^{
+        [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:^{
             if (operation.isCancelled) {
                 // Cancelled
                 return;
@@ -462,7 +466,7 @@
     }];
 }
 
-- (void)serialStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<SDImageCache>> *)enumerator {
+- (void)serialStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<SDImageCache>> *)enumerator {
     NSParameterAssert(enumerator);
     id<SDImageCache> cache = enumerator.nextObject;
     if (!cache) {
@@ -473,10 +477,10 @@
         return;
     }
     @weakify(self);
-    [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:^{
+    [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:^{
         @strongify(self);
         // Next
-        [self serialStoreImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock enumerator:enumerator];
+        [self serialStoreImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:enumerator];
     }];
 }
 

+ 63 - 2
Pods/SDWebImage/SDWebImage/Core/SDImageCoder.h

@@ -9,6 +9,7 @@
 #import <Foundation/Foundation.h>
 #import "SDWebImageCompat.h"
 #import "NSData+ImageContentType.h"
+#import "SDImageFrame.h"
 
 typedef NSString * SDImageCoderOption NS_STRING_ENUM;
 typedef NSDictionary<SDImageCoderOption, id> SDImageCoderOptions;
@@ -44,6 +45,48 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodePreserveAs
  */
 FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeThumbnailPixelSize;
 
+/**
+ A NSString value indicating the source image's file extension. Example: "jpg", "nef", "tif", don't prefix the dot
+ Some image file format share the same data structure but has different tag explanation, like TIFF and NEF/SRW, see https://en.wikipedia.org/wiki/TIFF
+ Changing the file extension cause the different image result. The coder (like ImageIO) may use file extension to choose the correct parser
+ @note However, different UTType may share the same file extension, like `public.jpeg` and `public.jpeg-2000` both use `.jpg`. If you want detail control, use `TypeIdentifierHint` below
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeFileExtensionHint;
+
+/**
+ A NSString value (UTI) indicating the source image's file extension. Example: "public.jpeg-2000", "com.nikon.raw-image", "public.tiff"
+ Some image file format share the same data structure but has different tag explanation, like TIFF and NEF/SRW, see https://en.wikipedia.org/wiki/TIFF
+ Changing the file extension cause the different image result. The coder (like ImageIO) may use file extension to choose the correct parser
+ @note If you provide `TypeIdentifierHint`, the `FileExtensionHint` option above will be ignored (because UTType has high priority)
+ @note If you really don't want any hint which effect the image result, pass `NSNull.null` instead
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeTypeIdentifierHint;
+
+/**
+ A BOOL value indicating whether to use lazy-decoding. Defaults to NO on animated image coder, but defaults to YES on static image coder.
+ CGImageRef, this image object typically support lazy-decoding, via the `CGDataProviderCreateDirectAccess` or `CGDataProviderCreateSequential`
+ Which allows you to provide a lazy-called callback to access bitmap buffer, so that you can achieve lazy-decoding when consumer actually need bitmap buffer
+ UIKit on iOS use heavy on this and ImageIO codec prefers to lazy-decoding for common Hardware-Accelerate format like JPEG/PNG/HEIC
+ But however, the consumer may access bitmap buffer when running on main queue, like CoreAnimation layer render image. So this is a trade-off
+ You can force us to disable the lazy-decoding and always allocate bitmap buffer on RAM, but this may have higher ratio of OOM (out of memory)
+ @note The default value is NO for animated image coder (means `animatedImageFrameAtIndex:`)
+ @note The default value is YES for static image coder (means `decodedImageWithData:`)
+ @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`.
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeUseLazyDecoding;
+
+/**
+ A NSUInteger value to provide the limit bytes during decoding. This can help to avoid OOM on large frame count animated image or large pixel static image when you don't know how much RAM it occupied before decoding
+ The decoder will do these logic based on limit bytes:
+ 1. Get the total frame count (static image means 1)
+ 2. Calculate the `framePixelSize` width/height to `sqrt(limitBytes / frameCount / bytesPerPixel)`, keeping aspect ratio (at least 1x1)
+ 3. If the `framePixelSize < originalImagePixelSize`, then do thumbnail decoding (see `SDImageCoderDecodeThumbnailPixelSize`) use the `framePixelSize` and `preseveAspectRatio = YES`
+ 4. Else, use the full pixel decoding (small than limit bytes)
+ 5. Whatever result, this does not effect the animated/static behavior of image. So even if you set `limitBytes = 1 && frameCount = 100`, we will stll create animated image with each frame `1x1` pixel size.
+ @note You can use the logic from `+[SDImageCoder scaledSizeWithImageSize:limitBytes:bytesPerPixel:frameCount:]`
+ @note This option has higher priority than `.decodeThumbnailPixelSize`
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleDownLimitBytes;
 
 // These options are for image encoding
 /**
@@ -91,9 +134,11 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeEmbedThumb
  A SDWebImageContext object which hold the original context options from top-level API. (SDWebImageContext)
  This option is ignored for all built-in coders and take no effect.
  But this may be useful for some custom coders, because some business logic may dependent on things other than image or image data information only.
+ Only the unknown context from top-level API (See SDWebImageDefine.h) may be passed in during image loading.
  See `SDWebImageContext` for more detailed information.
+ @warning Deprecated. This does nothing from 5.14.0. Use `SDWebImageContextImageDecodeOptions` to pass additional information in top-level API, and use `SDImageCoderOptions` to retrieve options from coder.
  */
-FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext API_DEPRECATED("The coder component will be seperated from Core subspec in the future. Update your code to not rely on this context option.", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext API_DEPRECATED("No longer supported. Use SDWebImageContextDecodeOptions in loader API to provide options. Use SDImageCoderOptions in coder API to retrieve options.", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0));
 
 #pragma mark - Coder
 /**
@@ -140,7 +185,8 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext
 
 /**
  Encode the image to image data.
- @note This protocol may supports encode animated image frames. You can use `+[SDImageCoderHelper framesFromAnimatedImage:]` to assemble an animated image with frames.
+ @note This protocol may supports encode animated image frames. You can use `+[SDImageCoderHelper framesFromAnimatedImage:]` to assemble an animated image with frames. But this consume time is not always reversible. In 5.15.0, we introduce `encodedDataWithFrames` API for better animated image encoding. Use that instead.
+ @note Which means, this just forward to `encodedDataWithFrames([SDImageFrame(image: image, duration: 0], image.sd_imageLoopCount))`
 
  @param image The image to be encoded
  @param format The image format to encode, you should note `SDImageFormatUndefined` format is also  possible
@@ -151,6 +197,21 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext
                                    format:(SDImageFormat)format
                                   options:(nullable SDImageCoderOptions *)options;
 
+#pragma mark - Animated Encoding
+@optional
+/**
+ Encode the animated image frames to image data.
+
+ @param frames The animated image frames to be encoded, should be at least 1 element, or it will fallback to static image encode.
+ @param loopCount The final animated image loop count. 0 means infinity loop. This config ignore each frame's `sd_imageLoopCount`
+ @param format The image format to encode, you should note `SDImageFormatUndefined` format is also  possible
+ @param options A dictionary containing any encoding options. Pass @{SDImageCoderEncodeCompressionQuality: @(1)} to specify compression quality.
+ @return The encoded image data
+ */
+- (nullable NSData *)encodedDataWithFrames:(nonnull NSArray<SDImageFrame *>*)frames
+                                 loopCount:(NSUInteger)loopCount
+                                    format:(SDImageFormat)format
+                                   options:(nullable SDImageCoderOptions *)options;
 @end
 
 #pragma mark - Progressive Coder

+ 4 - 0
Pods/SDWebImage/SDWebImage/Core/SDImageCoder.m

@@ -12,6 +12,10 @@ SDImageCoderOption const SDImageCoderDecodeFirstFrameOnly = @"decodeFirstFrameOn
 SDImageCoderOption const SDImageCoderDecodeScaleFactor = @"decodeScaleFactor";
 SDImageCoderOption const SDImageCoderDecodePreserveAspectRatio = @"decodePreserveAspectRatio";
 SDImageCoderOption const SDImageCoderDecodeThumbnailPixelSize = @"decodeThumbnailPixelSize";
+SDImageCoderOption const SDImageCoderDecodeFileExtensionHint = @"decodeFileExtensionHint";
+SDImageCoderOption const SDImageCoderDecodeTypeIdentifierHint = @"decodeTypeIdentifierHint";
+SDImageCoderOption const SDImageCoderDecodeUseLazyDecoding = @"decodeUseLazyDecoding";
+SDImageCoderOption const SDImageCoderDecodeScaleDownLimitBytes = @"decodeScaleDownLimitBytes";
 
 SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly";
 SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality";

+ 101 - 3
Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.h

@@ -10,6 +10,45 @@
 #import "SDWebImageCompat.h"
 #import "SDImageFrame.h"
 
+/// The options controls how we force pre-draw the image (to avoid lazy-decoding). Which need OS's framework compatibility
+typedef NS_ENUM(NSUInteger, SDImageCoderDecodeSolution) {
+    /// automatically choose the solution based on image format, hardware, OS version. This keep balance for compatibility and performance. Default after SDWebImage 5.13.0
+    SDImageCoderDecodeSolutionAutomatic,
+    /// always use CoreGraphics to draw on bitmap context and trigger decode. Best compatibility. Default before SDWebImage 5.13.0
+    SDImageCoderDecodeSolutionCoreGraphics,
+    /// available on iOS/tvOS 15+, use UIKit's new CGImageDecompressor/CMPhoto to decode. Best performance. If failed, will fallback to CoreGraphics as well
+    SDImageCoderDecodeSolutionUIKit
+};
+
+/// The policy to force-decode the origin CGImage (produced by Image Coder Plugin)
+/// Some CGImage may be lazy, or not lazy, but need extra copy to render on screen
+/// The force-decode step help to `pre-process` to get the best suitable CGImage to render, which can increase frame rate
+/// The downside is that force-decode may consume RAM and CPU, and may loss the `lazy` support (lazy CGImage can be purged when memory warning, and re-created if need), see more: `SDImageCoderDecodeUseLazyDecoding`
+typedef NS_ENUM(NSUInteger, SDImageForceDecodePolicy) {
+    /// Based on input CGImage's colorspace, alignment, bitmapinfo, if it may trigger `CA::copy_image` extra copy, we will force-decode, else don't
+    SDImageForceDecodePolicyAutomatic,
+    /// Never force decode input CGImage
+    SDImageForceDecodePolicyNever,
+    /// Always force decode input CGImage (only once)
+    SDImageForceDecodePolicyAlways
+};
+
+/// Byte alignment the bytes size with alignment
+/// - Parameters:
+///   - size: The bytes size
+///   - alignment: The alignment, in bytes
+static inline size_t SDByteAlign(size_t size, size_t alignment) {
+    return ((size + (alignment - 1)) / alignment) * alignment;
+}
+
+/// The pixel format about the information to call `CGImageCreate` suitable for current hardware rendering
+typedef struct SDImagePixelFormat {
+    /// Typically is pre-multiplied RGBA8888 for alpha image, RGBX8888 for non-alpha image.
+    CGBitmapInfo bitmapInfo;
+    /// Typically is 32, the 8 pixels bytesPerRow.
+    size_t alignment;
+} SDImagePixelFormat;
+
 /**
  Provide some common helper methods for building the image decoder/encoder.
  */
@@ -35,16 +74,31 @@
  */
 + (NSArray<SDImageFrame *> * _Nullable)framesFromAnimatedImage:(UIImage * _Nullable)animatedImage NS_SWIFT_NAME(frames(from:));
 
+#pragma mark - Preferred Rendering Format
+/// For coders who use `CGImageCreate`, use the information below to create an effient CGImage which can be render on GPU without Core Animation's extra copy (`CA::Render::copy_image`), which can be debugged using `Color Copied Image` in Xcode Instruments
+/// `CGImageCreate`'s `bytesPerRow`, `space`, `bitmapInfo` params should use the information below.
 /**
  Return the shared device-dependent RGB color space. This follows The Get Rule.
- On iOS, it's created with deviceRGB (if available, use sRGB).
- On macOS, it's from the screen colorspace (if failed, use deviceRGB)
  Because it's shared, you should not retain or release this object.
+ Typically is sRGB for iOS, screen color space (like Color LCD) for macOS.
  
  @return The device-dependent RGB color space
  */
 + (CGColorSpaceRef _Nonnull)colorSpaceGetDeviceRGB CF_RETURNS_NOT_RETAINED;
 
+/**
+ Tthis returns the pixel format **Preferred from current hardward && OS using runtime detection**
+ @param containsAlpha Whether the image to render contains alpha channel
+ */
++ (SDImagePixelFormat)preferredPixelFormat:(BOOL)containsAlpha;
+
+/**
+ Check whether CGImage is hardware supported to rendering on screen, without the trigger of `CA::Render::copy_image`
+ You can debug the copied image by using Xcode's `Color Copied Image`, the copied image will turn Cyan and occupy double RAM for bitmap buffer.
+ Typically, when the CGImage's using the method above (`colorspace` / `alignment` / `bitmapInfo`) can render withtout the copy.
+ */
++ (BOOL)CGImageIsHardwareSupported:(_Nonnull CGImageRef)cgImage;
+
 /**
  Check whether CGImage contains alpha channel.
  
@@ -76,6 +130,7 @@
 /**
  Create a scaled CGImage by the provided CGImage and size. This follows The Create Rule and you are response to call release after usage.
  It will detect whether the image size matching the scale size, if not, stretch the image to the target size.
+ @note If you need to keep aspect ratio, you can calculate the scale size by using `scaledSizeWithImageSize` first.
  
  @param cgImage The CGImage
  @param size The scale size in pixel.
@@ -83,23 +138,66 @@
  */
 + (CGImageRef _Nullable)CGImageCreateScaled:(_Nonnull CGImageRef)cgImage size:(CGSize)size CF_RETURNS_RETAINED;
 
+/** Scale the image size based on provided scale size, whether or not to preserve aspect ratio, whether or not to scale up.
+ @note For example, if you implements thumnail decoding, pass `shouldScaleUp` to NO to avoid the calculated size larger than image size.
+ 
+ @param imageSize The image size (in pixel or point defined by caller)
+ @param scaleSize The scale size (in pixel or point defined by caller)
+ @param preserveAspectRatio Whether or not to preserve aspect ratio
+ @param shouldScaleUp Whether or not to scale up (or scale down only)
+ */
++ (CGSize)scaledSizeWithImageSize:(CGSize)imageSize scaleSize:(CGSize)scaleSize preserveAspectRatio:(BOOL)preserveAspectRatio shouldScaleUp:(BOOL)shouldScaleUp;
+
+/// Calculate the limited image size with the bytes, when using `SDImageCoderDecodeScaleDownLimitBytes`. This preserve aspect ratio and never scale up
+/// @param imageSize The image size (in pixel or point defined by caller)
+/// @param limitBytes The limit bytes
+/// @param bytesPerPixel The bytes per pixel
+/// @param frameCount The image frame count, 0 means 1 frame as well
++ (CGSize)scaledSizeWithImageSize:(CGSize)imageSize limitBytes:(NSUInteger)limitBytes bytesPerPixel:(NSUInteger)bytesPerPixel frameCount:(NSUInteger)frameCount;
 /**
- Return the decoded image by the provided image. This one unlike `CGImageCreateDecoded:`, will not decode the image which contains alpha channel or animated image
+ Return the decoded image by the provided image. This one unlike `CGImageCreateDecoded:`, will not decode the image which contains alpha channel or animated image. On iOS 15+, this may use `UIImage.preparingForDisplay()` to use CMPhoto for better performance than the old solution.
  @param image The image to be decoded
+ @note This translate to `decodedImageWithImage:policy:` with automatic policy
  @return The decoded image
  */
 + (UIImage * _Nullable)decodedImageWithImage:(UIImage * _Nullable)image;
 
+/**
+ Return the decoded image by the provided image. This one unlike `CGImageCreateDecoded:`, will not decode the image which contains alpha channel or animated image. On iOS 15+, this may use `UIImage.preparingForDisplay()` to use CMPhoto for better performance than the old solution.
+ @param image The image to be decoded
+ @param policy The force decode policy to decode image, will effect the check whether input image need decode
+ @return The decoded image
+ */
++ (UIImage * _Nullable)decodedImageWithImage:(UIImage * _Nullable)image policy:(SDImageForceDecodePolicy)policy;
+
 /**
  Return the decoded and probably scaled down image by the provided image. If the image pixels bytes size large than the limit bytes, will try to scale down. Or just works as `decodedImageWithImage:`, never scale up.
  @warning You should not pass too small bytes, the suggestion value should be larger than 1MB. Even we use Tile Decoding to avoid OOM, however, small bytes will consume much more CPU time because we need to iterate more times to draw each tile.
 
  @param image The image to be decoded and scaled down
  @param bytes The limit bytes size. Provide 0 to use the build-in limit.
+ @note This translate to `decodedAndScaledDownImageWithImage:limitBytes:policy:` with automatic policy
  @return The decoded and probably scaled down image
  */
 + (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes;
 
+/**
+ Return the decoded and probably scaled down image by the provided image. If the image pixels bytes size large than the limit bytes, will try to scale down. Or just works as `decodedImageWithImage:`, never scale up.
+ @warning You should not pass too small bytes, the suggestion value should be larger than 1MB. Even we use Tile Decoding to avoid OOM, however, small bytes will consume much more CPU time because we need to iterate more times to draw each tile.
+
+ @param image The image to be decoded and scaled down
+ @param bytes The limit bytes size. Provide 0 to use the build-in limit.
+ @param policy The force decode policy to decode image, will effect the check whether input image need decode
+ @return The decoded and probably scaled down image
+ */
++ (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes policy:(SDImageForceDecodePolicy)policy;
+
+/**
+ Control the default force decode solution. Available solutions  in `SDImageCoderDecodeSolution`.
+ @note Defaults to `SDImageCoderDecodeSolutionAutomatic`, which prefers to use UIKit for JPEG/HEIF, and fallback on CoreGraphics. If you want control on your hand, set the other solution.
+ */
+@property (class, readwrite) SDImageCoderDecodeSolution defaultDecodeSolution;
+
 /**
  Control the default limit bytes to scale down largest images.
  This value must be larger than 4 Bytes (at least 1x1 pixel). Defaults to 60MB on iOS/tvOS, 90MB on macOS, 30MB on watchOS.

+ 401 - 118
Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.m

@@ -16,12 +16,99 @@
 #import "UIImage+Metadata.h"
 #import "SDInternalMacros.h"
 #import "SDGraphicsImageRenderer.h"
+#import "SDInternalMacros.h"
+#import "SDDeviceHelper.h"
 #import <Accelerate/Accelerate.h>
 
-static inline size_t SDByteAlign(size_t size, size_t alignment) {
-    return ((size + (alignment - 1)) / alignment) * alignment;
+#define kCGColorSpaceDeviceRGB CFSTR("kCGColorSpaceDeviceRGB")
+
+#if SD_UIKIT
+static inline UIImage *SDImageDecodeUIKit(UIImage *image) {
+    // See: https://developer.apple.com/documentation/uikit/uiimage/3750834-imagebypreparingfordisplay
+    // Need CGImage-based
+    if (@available(iOS 15, tvOS 15, *)) {
+        UIImage *decodedImage = [image imageByPreparingForDisplay];
+        if (decodedImage) {
+            SDImageCopyAssociatedObject(image, decodedImage);
+            decodedImage.sd_isDecoded = YES;
+            return decodedImage;
+        }
+    }
+    return nil;
 }
 
+static inline UIImage *SDImageDecodeAndScaleDownUIKit(UIImage *image, CGSize destResolution) {
+    // See: https://developer.apple.com/documentation/uikit/uiimage/3750835-imagebypreparingthumbnailofsize
+    // Need CGImage-based
+    if (@available(iOS 15, tvOS 15, *)) {
+        // Calculate thumbnail point size
+        CGFloat scale = image.scale ?: 1;
+        CGSize thumbnailSize = CGSizeMake(destResolution.width / scale, destResolution.height / scale);
+        UIImage *decodedImage = [image imageByPreparingThumbnailOfSize:thumbnailSize];
+        if (decodedImage) {
+            SDImageCopyAssociatedObject(image, decodedImage);
+            decodedImage.sd_isDecoded = YES;
+            return decodedImage;
+        }
+    }
+    return nil;
+}
+
+static inline BOOL SDImageSupportsHardwareHEVCDecoder(void) {
+    static dispatch_once_t onceToken;
+    static BOOL supportsHardware = NO;
+    dispatch_once(&onceToken, ^{
+        SEL DeviceInfoSelector = SD_SEL_SPI(deviceInfoForKey:);
+        NSString *HEVCDecoder8bitSupported = @"N8lZxRgC7lfdRS3dRLn+Ag";
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+        if ([UIDevice.currentDevice respondsToSelector:DeviceInfoSelector] && [UIDevice.currentDevice performSelector:DeviceInfoSelector withObject:HEVCDecoder8bitSupported]) {
+            supportsHardware = YES;
+        }
+#pragma clang diagnostic pop
+    });
+    return supportsHardware;
+}
+#endif
+
+static UIImage * _Nonnull SDImageGetAlphaDummyImage(void) {
+    static dispatch_once_t onceToken;
+    static UIImage *dummyImage;
+    dispatch_once(&onceToken, ^{
+        SDGraphicsImageRendererFormat *format = [SDGraphicsImageRendererFormat preferredFormat];
+        format.scale = 1;
+        format.opaque = NO;
+        CGSize size = CGSizeMake(1, 1);
+        SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format];
+        dummyImage = [renderer imageWithActions:^(CGContextRef  _Nonnull context) {
+            CGContextSetFillColorWithColor(context, UIColor.redColor.CGColor);
+            CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
+        }];
+        NSCAssert(dummyImage, @"The sample alpha image (1x1 pixels) returns nil, OS bug ?");
+    });
+    return dummyImage;
+}
+
+static UIImage * _Nonnull SDImageGetNonAlphaDummyImage(void) {
+    static dispatch_once_t onceToken;
+    static UIImage *dummyImage;
+    dispatch_once(&onceToken, ^{
+        SDGraphicsImageRendererFormat *format = [SDGraphicsImageRendererFormat preferredFormat];
+        format.scale = 1;
+        format.opaque = YES;
+        CGSize size = CGSizeMake(1, 1);
+        SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format];
+        dummyImage = [renderer imageWithActions:^(CGContextRef  _Nonnull context) {
+            CGContextSetFillColorWithColor(context, UIColor.redColor.CGColor);
+            CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
+        }];
+        NSCAssert(dummyImage, @"The sample non-alpha image (1x1 pixels) returns nil, OS bug ?");
+    });
+    return dummyImage;
+}
+
+static SDImageCoderDecodeSolution kDefaultDecodeSolution = SDImageCoderDecodeSolutionAutomatic;
+
 static const size_t kBytesPerPixel = 4;
 static const size_t kBitsPerComponent = 8;
 
@@ -42,6 +129,13 @@ static CGFloat kDestImageLimitBytes = 30.f * kBytesPerMB;
 
 static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to overlap the seems where tiles meet.
 
+#if SD_MAC
+@interface SDAnimatedImageRep (Private)
+/// This wrap the animated image frames for legacy animated image coder API (`encodedDataWithImage:`).
+@property (nonatomic, readwrite, weak) NSArray<SDImageFrame *> *frames;
+@end
+#endif
+
 @implementation SDImageCoderHelper
 
 + (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
@@ -89,13 +183,11 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     }
     
     for (size_t i = 0; i < frameCount; i++) {
-        @autoreleasepool {
-            SDImageFrame *frame = frames[i];
-            NSTimeInterval frameDuration = frame.duration;
-            CGImageRef frameImageRef = frame.image.CGImage;
-            NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}};
-            CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
-        }
+        SDImageFrame *frame = frames[i];
+        NSTimeInterval frameDuration = frame.duration;
+        CGImageRef frameImageRef = frame.image.CGImage;
+        NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}};
+        CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
     }
     // Finalize the destination.
     if (CGImageDestinationFinalize(imageDestination) == NO) {
@@ -109,6 +201,7 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
     NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
     imageRep.size = size;
+    imageRep.frames = frames; // Weak assign to avoid effect lazy semantic of NSBitmapImageRep
     animatedImage = [[NSImage alloc] initWithSize:size];
     [animatedImage addRepresentation:imageRep];
 #endif
@@ -121,7 +214,7 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
         return nil;
     }
     
-    NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
+    NSMutableArray<SDImageFrame *> *frames;
     NSUInteger frameCount = 0;
     
 #if SD_UIKIT || SD_WATCH
@@ -130,6 +223,7 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     if (frameCount == 0) {
         return nil;
     }
+    frames = [NSMutableArray arrayWithCapacity:frameCount];
     
     NSTimeInterval avgDuration = animatedImage.duration / frameCount;
     if (avgDuration == 0) {
@@ -160,6 +254,14 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     
     NSRect imageRect = NSMakeRect(0, 0, animatedImage.size.width, animatedImage.size.height);
     NSImageRep *imageRep = [animatedImage bestRepresentationForRect:imageRect context:nil hints:nil];
+    // Check weak assigned frames firstly
+    if ([imageRep isKindOfClass:[SDAnimatedImageRep class]]) {
+        SDAnimatedImageRep *animatedImageRep = (SDAnimatedImageRep *)imageRep;
+        if (animatedImageRep.frames) {
+            return animatedImageRep.frames;
+        }
+    }
+    
     NSBitmapImageRep *bitmapImageRep;
     if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
         bitmapImageRep = (NSBitmapImageRep *)imageRep;
@@ -171,32 +273,102 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     if (frameCount == 0) {
         return nil;
     }
+    frames = [NSMutableArray arrayWithCapacity:frameCount];
     CGFloat scale = animatedImage.scale;
     
     for (size_t i = 0; i < frameCount; i++) {
-        @autoreleasepool {
-            // NSBitmapImageRep need to manually change frame. "Good taste" API
-            [bitmapImageRep setProperty:NSImageCurrentFrame withValue:@(i)];
-            NSTimeInterval frameDuration = [[bitmapImageRep valueForProperty:NSImageCurrentFrameDuration] doubleValue];
-            NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapImageRep.CGImage scale:scale orientation:kCGImagePropertyOrientationUp];
-            SDImageFrame *frame = [SDImageFrame frameWithImage:frameImage duration:frameDuration];
-            [frames addObject:frame];
-        }
+        // NSBitmapImageRep need to manually change frame. "Good taste" API
+        [bitmapImageRep setProperty:NSImageCurrentFrame withValue:@(i)];
+        NSTimeInterval frameDuration = [[bitmapImageRep valueForProperty:NSImageCurrentFrameDuration] doubleValue];
+        NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapImageRep.CGImage scale:scale orientation:kCGImagePropertyOrientationUp];
+        SDImageFrame *frame = [SDImageFrame frameWithImage:frameImage duration:frameDuration];
+        [frames addObject:frame];
     }
 #endif
     
-    return frames;
+    return [frames copy];
 }
 
 + (CGColorSpaceRef)colorSpaceGetDeviceRGB {
     static CGColorSpaceRef colorSpace;
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
+#if SD_MAC
+        NSScreen *mainScreen = nil;
+        if (@available(macOS 10.12, *)) {
+            mainScreen = [NSScreen mainScreen];
+        } else {
+            mainScreen = [NSScreen screens].firstObject;
+        }
+        colorSpace = mainScreen.colorSpace.CGColorSpace;
+#else
         colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
+#endif
     });
     return colorSpace;
 }
 
++ (SDImagePixelFormat)preferredPixelFormat:(BOOL)containsAlpha {
+    CGImageRef cgImage;
+    if (containsAlpha) {
+        cgImage = SDImageGetAlphaDummyImage().CGImage;
+    } else {
+        cgImage = SDImageGetNonAlphaDummyImage().CGImage;
+    }
+    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage);
+    size_t bitsPerPixel = 8;
+    if (SD_OPTIONS_CONTAINS(bitmapInfo, kCGBitmapFloatComponents)) {
+        bitsPerPixel = 16;
+    }
+    size_t components = 4; // Hardcode now
+    // https://github.com/path/FastImageCache#byte-alignment
+    // A properly aligned bytes-per-row value must be a multiple of 8 pixels × bytes per pixel.
+    size_t alignment = (bitsPerPixel / 8) * components * 8;
+    SDImagePixelFormat pixelFormat = {
+        .bitmapInfo = bitmapInfo,
+        .alignment = alignment
+    };
+    return pixelFormat;
+}
+
++ (BOOL)CGImageIsHardwareSupported:(CGImageRef)cgImage {
+    BOOL supported = YES;
+    // 1. Check byte alignment
+    size_t bytesPerRow = CGImageGetBytesPerRow(cgImage);
+    BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
+    SDImagePixelFormat pixelFormat = [self preferredPixelFormat:hasAlpha];
+    if (SDByteAlign(bytesPerRow, pixelFormat.alignment) == bytesPerRow) {
+        // byte aligned, OK
+        supported &= YES;
+    } else {
+        // not aligned
+        supported &= NO;
+    }
+    if (!supported) return supported;
+    
+    // 2. Check color space
+    CGColorSpaceRef colorSpace = CGImageGetColorSpace(cgImage);
+    CGColorSpaceRef perferredColorSpace = [self colorSpaceGetDeviceRGB];
+    if (colorSpace == perferredColorSpace) {
+        return supported;
+    } else {
+        if (@available(iOS 10.0, tvOS 10.0, macOS 10.6, watchOS 3.0, *)) {
+            NSString *colorspaceName = (__bridge_transfer NSString *)CGColorSpaceCopyName(colorSpace);
+            // Seems sRGB/deviceRGB always supported, P3 not always
+            if ([colorspaceName isEqualToString:(__bridge NSString *)kCGColorSpaceDeviceRGB]
+                || [colorspaceName isEqualToString:(__bridge NSString *)kCGColorSpaceSRGB]) {
+                supported &= YES;
+            } else {
+                supported &= NO;
+            }
+            return supported;
+        } else {
+            // Fallback on earlier versions
+            return supported;
+        }
+    }
+}
+
 + (BOOL)CGImageContainsAlpha:(CGImageRef)cgImage {
     if (!cgImage) {
         return NO;
@@ -241,16 +413,8 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
     // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
     // Check #3330 for more detail about why this bitmap is choosen.
-    CGBitmapInfo bitmapInfo;
-    if (hasAlpha) {
-        // iPhone GPU prefer to use BGRA8888, see: https://forums.raywenderlich.com/t/why-mtlpixelformat-bgra8unorm/53489
-        // BGRA8888
-        bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;
-    } else {
-        // BGR888 previously works on iOS 8~iOS 14, however, iOS 15+ will result a black image. FB9958017
-        // RGB888
-        bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
-    }
+    // From v5.17.0, use runtime detection of bitmap info instead of hardcode.
+    CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:hasAlpha].bitmapInfo;
     CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
     if (!context) {
         return NULL;
@@ -285,16 +449,8 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
     // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
     // Check #3330 for more detail about why this bitmap is choosen.
-    CGBitmapInfo bitmapInfo;
-    if (hasAlpha) {
-        // iPhone GPU prefer to use BGRA8888, see: https://forums.raywenderlich.com/t/why-mtlpixelformat-bgra8unorm/53489
-        // BGRA8888
-        bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;
-    } else {
-        // BGR888 previously works on iOS 8~iOS 14, however, iOS 15+ will result a black image. FB9958017
-        // RGB888
-        bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
-    }
+    // From v5.17.0, use runtime detection of bitmap info instead of hardcode.
+    CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:hasAlpha].bitmapInfo;
     vImage_CGImageFormat format = (vImage_CGImageFormat) {
         .bitsPerComponent = 8,
         .bitsPerPixel = 32,
@@ -325,89 +481,195 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     return outputImage;
 }
 
++ (CGSize)scaledSizeWithImageSize:(CGSize)imageSize scaleSize:(CGSize)scaleSize preserveAspectRatio:(BOOL)preserveAspectRatio shouldScaleUp:(BOOL)shouldScaleUp {
+    CGFloat width = imageSize.width;
+    CGFloat height = imageSize.height;
+    CGFloat resultWidth;
+    CGFloat resultHeight;
+    
+    if (width <= 0 || height <= 0 || scaleSize.width <= 0 || scaleSize.height <= 0) {
+        // Protect
+        resultWidth = width;
+        resultHeight = height;
+    } else {
+        // Scale to fit
+        if (preserveAspectRatio) {
+            CGFloat pixelRatio = width / height;
+            CGFloat scaleRatio = scaleSize.width / scaleSize.height;
+            if (pixelRatio > scaleRatio) {
+                resultWidth = scaleSize.width;
+                resultHeight = ceil(scaleSize.width / pixelRatio);
+            } else {
+                resultHeight = scaleSize.height;
+                resultWidth = ceil(scaleSize.height * pixelRatio);
+            }
+        } else {
+            // Stretch
+            resultWidth = scaleSize.width;
+            resultHeight = scaleSize.height;
+        }
+        if (!shouldScaleUp) {
+            // Scale down only
+            resultWidth = MIN(width, resultWidth);
+            resultHeight = MIN(height, resultHeight);
+        }
+    }
+    
+    return CGSizeMake(resultWidth, resultHeight);
+}
+
++ (CGSize)scaledSizeWithImageSize:(CGSize)imageSize limitBytes:(NSUInteger)limitBytes bytesPerPixel:(NSUInteger)bytesPerPixel frameCount:(NSUInteger)frameCount {
+    if (CGSizeEqualToSize(imageSize, CGSizeZero)) return CGSizeMake(1, 1);
+    NSUInteger totalFramePixelSize = limitBytes / bytesPerPixel / (frameCount ?: 1);
+    CGFloat ratio = imageSize.height / imageSize.width;
+    CGFloat width = sqrt(totalFramePixelSize / ratio);
+    CGFloat height = width * ratio;
+    width = MAX(1, floor(width));
+    height = MAX(1, floor(height));
+    CGSize size = CGSizeMake(width, height);
+    
+    return size;
+}
+
 + (UIImage *)decodedImageWithImage:(UIImage *)image {
-    if (![self shouldDecodeImage:image]) {
+    return [self decodedImageWithImage:image policy:SDImageForceDecodePolicyAutomatic];
+}
+
++ (UIImage *)decodedImageWithImage:(UIImage *)image policy:(SDImageForceDecodePolicy)policy {
+    if (![self shouldDecodeImage:image policy:policy]) {
         return image;
     }
     
+    UIImage *decodedImage;
+    SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution;
+#if SD_UIKIT
+    if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) {
+        // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :(
+        SDImageFormat format = image.sd_imageFormat;
+        if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) {
+            decodedImage = SDImageDecodeUIKit(image);
+        } else if (format == SDImageFormatJPEG) {
+            decodedImage = SDImageDecodeUIKit(image);
+        }
+    } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) {
+        // Arbitrarily call CMPhoto
+        decodedImage = SDImageDecodeUIKit(image);
+    }
+    if (decodedImage) {
+        return decodedImage;
+    }
+#endif
+    
     CGImageRef imageRef = image.CGImage;
     if (!imageRef) {
+        // Only decode for CGImage-based
         return image;
     }
-    BOOL hasAlpha = [self CGImageContainsAlpha:imageRef];
-    // Prefer to use new Image Renderer to re-draw image, instead of low-level CGBitmapContext and CGContextDrawImage
-    // This can keep both OS compatible and don't fight with Apple's performance optimization
-    SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init];
-    format.opaque = !hasAlpha;
-    format.scale = image.scale;
-    CGSize imageSize = image.size;
-    SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format];
-    UIImage *decodedImage = [renderer imageWithActions:^(CGContextRef  _Nonnull context) {
-            [image drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
-    }];
+    
+    if (decodeSolution == SDImageCoderDecodeSolutionCoreGraphics) {
+        CGImageRef decodedImageRef = [self CGImageCreateDecoded:imageRef];
+#if SD_MAC
+        decodedImage = [[UIImage alloc] initWithCGImage:decodedImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
+#else
+        decodedImage = [[UIImage alloc] initWithCGImage:decodedImageRef scale:image.scale orientation:image.imageOrientation];
+#endif
+        CGImageRelease(decodedImageRef);
+    } else {
+        BOOL hasAlpha = [self CGImageContainsAlpha:imageRef];
+        // Prefer to use new Image Renderer to re-draw image, instead of low-level CGBitmapContext and CGContextDrawImage
+        // This can keep both OS compatible and don't fight with Apple's performance optimization
+        SDGraphicsImageRendererFormat *format = SDGraphicsImageRendererFormat.preferredFormat;
+        format.opaque = !hasAlpha;
+        format.scale = image.scale;
+        CGSize imageSize = image.size;
+        SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format];
+        decodedImage = [renderer imageWithActions:^(CGContextRef  _Nonnull context) {
+                [image drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
+        }];
+    }
     SDImageCopyAssociatedObject(image, decodedImage);
     decodedImage.sd_isDecoded = YES;
     return decodedImage;
 }
 
 + (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes {
-    if (![self shouldDecodeImage:image]) {
+    return [self decodedAndScaledDownImageWithImage:image limitBytes:bytes policy:SDImageForceDecodePolicyAutomatic];
+}
+
++ (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes policy:(SDImageForceDecodePolicy)policy {
+    if (![self shouldDecodeImage:image policy:policy]) {
         return image;
     }
     
-    if (![self shouldScaleDownImage:image limitBytes:bytes]) {
-        return [self decodedImageWithImage:image];
-    }
-    
     CGFloat destTotalPixels;
     CGFloat tileTotalPixels;
     if (bytes == 0) {
-        bytes = kDestImageLimitBytes;
+        bytes = [self defaultScaleDownLimitBytes];
     }
+    bytes = MAX(bytes, kBytesPerPixel);
     destTotalPixels = bytes / kBytesPerPixel;
     tileTotalPixels = destTotalPixels / 3;
-    CGContextRef destContext = NULL;
+    
+    CGImageRef sourceImageRef = image.CGImage;
+    if (!sourceImageRef) {
+        // Only decode for CGImage-based
+        return image;
+    }
+    CGSize sourceResolution = CGSizeZero;
+    sourceResolution.width = CGImageGetWidth(sourceImageRef);
+    sourceResolution.height = CGImageGetHeight(sourceImageRef);
+    
+    if (![self shouldScaleDownImagePixelSize:sourceResolution limitBytes:bytes]) {
+        return [self decodedImageWithImage:image];
+    }
+    
+    CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
+    // Determine the scale ratio to apply to the input image
+    // that results in an output image of the defined size.
+    // see kDestImageSizeMB, and how it relates to destTotalPixels.
+    CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
+    CGSize destResolution = CGSizeZero;
+    destResolution.width = MAX(1, (int)(sourceResolution.width * imageScale));
+    destResolution.height = MAX(1, (int)(sourceResolution.height * imageScale));
+    
+    UIImage *decodedImage;
+#if SD_UIKIT
+    SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution;
+    if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) {
+        // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :(
+        SDImageFormat format = image.sd_imageFormat;
+        if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) {
+            decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
+        } else if (format == SDImageFormatJPEG) {
+            decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
+        }
+    } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) {
+        // Arbitrarily call CMPhoto
+        decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
+    }
+    if (decodedImage) {
+        return decodedImage;
+    }
+#endif
     
     // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
     // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
     @autoreleasepool {
-        CGImageRef sourceImageRef = image.CGImage;
-        
-        CGSize sourceResolution = CGSizeZero;
-        sourceResolution.width = CGImageGetWidth(sourceImageRef);
-        sourceResolution.height = CGImageGetHeight(sourceImageRef);
-        CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
-        // Determine the scale ratio to apply to the input image
-        // that results in an output image of the defined size.
-        // see kDestImageSizeMB, and how it relates to destTotalPixels.
-        CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
-        CGSize destResolution = CGSizeZero;
-        destResolution.width = MAX(1, (int)(sourceResolution.width * imageScale));
-        destResolution.height = MAX(1, (int)(sourceResolution.height * imageScale));
-        
         // device color space
         CGColorSpaceRef colorspaceRef = [self colorSpaceGetDeviceRGB];
         BOOL hasAlpha = [self CGImageContainsAlpha:sourceImageRef];
         
         // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
         // Check #3330 for more detail about why this bitmap is choosen.
-        CGBitmapInfo bitmapInfo;
-        if (hasAlpha) {
-            // iPhone GPU prefer to use BGRA8888, see: https://forums.raywenderlich.com/t/why-mtlpixelformat-bgra8unorm/53489
-            // BGRA8888
-            bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;
-        } else {
-            // BGR888 previously works on iOS 8~iOS 14, however, iOS 15+ will result a black image. FB9958017
-            // RGB888
-            bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
-        }
-        destContext = CGBitmapContextCreate(NULL,
-                                            destResolution.width,
-                                            destResolution.height,
-                                            kBitsPerComponent,
-                                            0,
-                                            colorspaceRef,
-                                            bitmapInfo);
+        // From v5.17.0, use runtime detection of bitmap info instead of hardcode.
+        CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:hasAlpha].bitmapInfo;
+        CGContextRef destContext = CGBitmapContextCreate(NULL,
+                                                         destResolution.width,
+                                                         destResolution.height,
+                                                         kBitsPerComponent,
+                                                         0,
+                                                         colorspaceRef,
+                                                         bitmapInfo);
         
         if (destContext == NULL) {
             return image;
@@ -454,19 +716,17 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
         sourceTile.size.height += sourceSeemOverlap;
         destTile.size.height += kDestSeemOverlap;
         for( int y = 0; y < iterations; ++y ) {
-            @autoreleasepool {
-                sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
-                destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
-                sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
-                if( y == iterations - 1 && remainder ) {
-                    float dify = destTile.size.height;
-                    destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
-                    dify -= destTile.size.height;
-                    destTile.origin.y = MIN(0, destTile.origin.y + dify);
-                }
-                CGContextDrawImage( destContext, destTile, sourceTileImageRef );
-                CGImageRelease( sourceTileImageRef );
+            sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
+            destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
+            sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
+            if( y == iterations - 1 && remainder ) {
+                float dify = destTile.size.height;
+                destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
+                dify -= destTile.size.height;
+                destTile.origin.y = MIN(0, destTile.origin.y + dify);
             }
+            CGContextDrawImage( destContext, destTile, sourceTileImageRef );
+            CGImageRelease( sourceTileImageRef );
         }
         
         CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
@@ -475,20 +735,25 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
             return image;
         }
 #if SD_MAC
-        UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
+        decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
 #else
-        UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
+        decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
 #endif
         CGImageRelease(destImageRef);
-        if (destImage == nil) {
-            return image;
-        }
-        SDImageCopyAssociatedObject(image, destImage);
-        destImage.sd_isDecoded = YES;
-        return destImage;
+        SDImageCopyAssociatedObject(image, decodedImage);
+        decodedImage.sd_isDecoded = YES;
+        return decodedImage;
     }
 }
 
++ (SDImageCoderDecodeSolution)defaultDecodeSolution {
+    return kDefaultDecodeSolution;
+}
+
++ (void)setDefaultDecodeSolution:(SDImageCoderDecodeSolution)defaultDecodeSolution {
+    kDefaultDecodeSolution = defaultDecodeSolution;
+}
+
 + (NSUInteger)defaultScaleDownLimitBytes {
     return kDestImageLimitBytes;
 }
@@ -571,11 +836,15 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
 #endif
 
 #pragma mark - Helper Function
-+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
++ (BOOL)shouldDecodeImage:(nullable UIImage *)image policy:(SDImageForceDecodePolicy)policy {
     // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
     if (image == nil) {
         return NO;
     }
+    // Check policy (never)
+    if (policy == SDImageForceDecodePolicyNever) {
+        return NO;
+    }
     // Avoid extra decode
     if (image.sd_isDecoded) {
         return NO;
@@ -588,18 +857,32 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     if (image.sd_isVector) {
         return NO;
     }
-    
+    // Check policy (always)
+    if (policy == SDImageForceDecodePolicyAlways) {
+        return YES;
+    } else {
+        // Check policy (automatic)
+        CGImageRef cgImage = image.CGImage;
+        if (cgImage) {
+            CFStringRef uttype = CGImageGetUTType(cgImage);
+            if (uttype) {
+                // Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier`
+                return YES;
+            } else {
+                // Now, let's check if the CGImage is hardware supported (not byte-aligned will cause extra copy)
+                BOOL isSupported = [SDImageCoderHelper CGImageIsHardwareSupported:cgImage];
+                return !isSupported;
+            }
+        }
+    }
+
     return YES;
 }
 
-+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image limitBytes:(NSUInteger)bytes {
++ (BOOL)shouldScaleDownImagePixelSize:(CGSize)sourceResolution limitBytes:(NSUInteger)bytes {
     BOOL shouldScaleDown = YES;
     
-    CGImageRef sourceImageRef = image.CGImage;
-    CGSize sourceResolution = CGSizeZero;
-    sourceResolution.width = CGImageGetWidth(sourceImageRef);
-    sourceResolution.height = CGImageGetHeight(sourceImageRef);
-    float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
+    CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
     if (sourceTotalPixels <= 0) {
         return NO;
     }
@@ -609,7 +892,7 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     }
     bytes = MAX(bytes, kBytesPerPixel);
     destTotalPixels = bytes / kBytesPerPixel;
-    float imageScale = destTotalPixels / sourceTotalPixels;
+    CGFloat imageScale = destTotalPixels / sourceTotalPixels;
     if (imageScale < 1) {
         shouldScaleDown = YES;
     } else {

+ 15 - 0
Pods/SDWebImage/SDWebImage/Core/SDImageCodersManager.m

@@ -127,4 +127,19 @@
     return nil;
 }
 
+- (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options {
+    if (!frames || frames.count < 1) {
+        return nil;
+    }
+    NSArray<id<SDImageCoder>> *coders = self.coders;
+    for (id<SDImageCoder> coder in coders.reverseObjectEnumerator) {
+        if ([coder canEncodeToFormat:format]) {
+            if ([coder respondsToSelector:@selector(encodedDataWithFrames:loopCount:format:options:)]) {
+                return [coder encodedDataWithFrames:frames loopCount:loopCount format:format options:options];
+            }
+        }
+    }
+    return nil;
+}
+
 @end

+ 9 - 1
Pods/SDWebImage/SDWebImage/Core/SDImageFrame.h

@@ -24,6 +24,11 @@
  */
 @property (nonatomic, readonly, assign) NSTimeInterval duration;
 
+/// Create a frame instance with specify image and duration
+/// @param image current frame's image
+/// @param duration current frame's duration
+- (nonnull instancetype)initWithImage:(nonnull UIImage *)image duration:(NSTimeInterval)duration;
+
 /**
  Create a frame instance with specify image and duration
 
@@ -31,6 +36,9 @@
  @param duration current frame's duration
  @return frame instance
  */
-+ (instancetype _Nonnull)frameWithImage:(UIImage * _Nonnull)image duration:(NSTimeInterval)duration;
++ (nonnull instancetype)frameWithImage:(nonnull UIImage *)image duration:(NSTimeInterval)duration;
+
+- (nonnull instancetype)init NS_UNAVAILABLE;
++ (nonnull instancetype)new  NS_UNAVAILABLE;
 
 @end

+ 10 - 4
Pods/SDWebImage/SDWebImage/Core/SDImageFrame.m

@@ -17,11 +17,17 @@
 
 @implementation SDImageFrame
 
+- (instancetype)initWithImage:(UIImage *)image duration:(NSTimeInterval)duration {
+    self = [super init];
+    if (self) {
+        _image = image;
+        _duration = duration;
+    }
+    return self;
+}
+
 + (instancetype)frameWithImage:(UIImage *)image duration:(NSTimeInterval)duration {
-    SDImageFrame *frame = [[SDImageFrame alloc] init];
-    frame.image = image;
-    frame.duration = duration;
-    
+    SDImageFrame *frame = [[SDImageFrame alloc] initWithImage:image duration:duration];
     return frame;
 }
 

+ 18 - 7
Pods/SDWebImage/SDWebImage/Core/SDImageGraphics.m

@@ -17,7 +17,13 @@ static void *kNSGraphicsContextScaleFactorKey;
 static CGContextRef SDCGContextCreateBitmapContext(CGSize size, BOOL opaque, CGFloat scale) {
     if (scale == 0) {
         // Match `UIGraphicsBeginImageContextWithOptions`, reset to the scale factor of the device’s main screen if scale is 0.
-        scale = [NSScreen mainScreen].backingScaleFactor;
+        NSScreen *mainScreen = nil;
+        if (@available(macOS 10.12, *)) {
+            mainScreen = [NSScreen mainScreen];
+        } else {
+            mainScreen = [NSScreen screens].firstObject;
+        }
+        scale = mainScreen.backingScaleFactor ?: 1.0f;
     }
     size_t width = ceil(size.width * scale);
     size_t height = ceil(size.height * scale);
@@ -26,14 +32,13 @@ static CGContextRef SDCGContextCreateBitmapContext(CGSize size, BOOL opaque, CGF
     CGColorSpaceRef space = [SDImageCoderHelper colorSpaceGetDeviceRGB];
     // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
     // Check #3330 for more detail about why this bitmap is choosen.
+    // From v5.17.0, use runtime detection of bitmap info instead of hardcode.
+    // However, macOS's runtime detection will also call this function, cause recursive, so still hardcode here
     CGBitmapInfo bitmapInfo;
     if (!opaque) {
-        // iPhone GPU prefer to use BGRA8888, see: https://forums.raywenderlich.com/t/why-mtlpixelformat-bgra8unorm/53489
-        // BGRA8888
-        bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;
+        // [NSImage imageWithSize:flipped:drawingHandler:] returns float(16-bits) RGBA8888 on alpha image, which we don't need
+        bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast;
     } else {
-        // BGR888 previously works on iOS 8~iOS 14, however, iOS 15+ will result a black image. FB9958017
-        // RGB888
         bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
     }
     CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, space, bitmapInfo);
@@ -106,7 +111,13 @@ UIImage * SDGraphicsGetImageFromCurrentImageContext(void) {
     }
     if (!scale) {
         // reset to the scale factor of the device’s main screen if scale is 0.
-        scale = [NSScreen mainScreen].backingScaleFactor;
+        NSScreen *mainScreen = nil;
+        if (@available(macOS 10.12, *)) {
+            mainScreen = [NSScreen mainScreen];
+        } else {
+            mainScreen = [NSScreen screens].firstObject;
+        }
+        scale = mainScreen.backingScaleFactor ?: 1.0f;
     }
     NSImage *image = [[NSImage alloc] initWithCGImage:imageRef scale:scale orientation:kCGImagePropertyOrientationUp];
     CGImageRelease(imageRef);

+ 0 - 1
Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.h

@@ -7,7 +7,6 @@
 */
 
 #import <Foundation/Foundation.h>
-#import <ImageIO/ImageIO.h>
 #import "SDImageCoder.h"
 
 /**

+ 291 - 80
Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.m

@@ -13,12 +13,40 @@
 #import "SDImageCoderHelper.h"
 #import "SDAnimatedImageRep.h"
 #import "UIImage+ForceDecode.h"
+#import "SDInternalMacros.h"
+
+#import <ImageIO/ImageIO.h>
+#import <CoreServices/CoreServices.h>
+
+#if SD_CHECK_CGIMAGE_RETAIN_SOURCE
+#import <dlfcn.h>
+
+// SPI to check thread safe during Example and Test
+static CGImageSourceRef (*SDCGImageGetImageSource)(CGImageRef);
+#endif
 
-// Specify DPI for vector format in CGImageSource, like PDF
-static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizationDPI";
 // Specify File Size for lossy format encoding, like JPEG
 static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
 
+// This strip the un-wanted CGImageProperty, like the internal CGImageSourceRef in iOS 15+
+// However, CGImageCreateCopy still keep those CGImageProperty, not suit for our use case
+static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
+    if (!image) return nil;
+    size_t width = CGImageGetWidth(image);
+    size_t height = CGImageGetHeight(image);
+    size_t bitsPerComponent = CGImageGetBitsPerComponent(image);
+    size_t bitsPerPixel = CGImageGetBitsPerPixel(image);
+    size_t bytesPerRow = CGImageGetBytesPerRow(image);
+    CGColorSpaceRef space = CGImageGetColorSpace(image);
+    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image);
+    CGDataProviderRef provider = CGImageGetDataProvider(image);
+    const CGFloat *decode = CGImageGetDecode(image);
+    bool shouldInterpolate = CGImageGetShouldInterpolate(image);
+    CGColorRenderingIntent intent = CGImageGetRenderingIntent(image);
+    CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, provider, decode, shouldInterpolate, intent);
+    return newImage;
+}
+
 @interface SDImageIOCoderFrame : NSObject
 
 @property (nonatomic, assign) NSUInteger index; // Frame index (zero based)
@@ -32,6 +60,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
 @implementation SDImageIOAnimatedCoder {
     size_t _width, _height;
     CGImageSourceRef _imageSource;
+    BOOL _incremental;
+    SD_LOCK_DECLARE(_lock); // Lock only apply for incremental animation decoding
     NSData *_imageData;
     CGFloat _scale;
     NSUInteger _loopCount;
@@ -40,6 +70,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     BOOL _finished;
     BOOL _preserveAspectRatio;
     CGSize _thumbnailSize;
+    NSUInteger _limitBytes;
+    BOOL _lazyDecode;
 }
 
 - (void)dealloc
@@ -152,12 +184,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
 }
 
 + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
-    NSDictionary *options = @{
-        (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
-        (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
-    };
     NSTimeInterval frameDuration = 0.1;
-    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
+    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, NULL);
     if (!cfFrameProperties) {
         return frameDuration;
     }
@@ -187,23 +215,30 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     return frameDuration;
 }
 
-+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize options:(NSDictionary *)options {
-    // Some options need to pass to `CGImageSourceCopyPropertiesAtIndex` before `CGImageSourceCreateImageAtIndex`, or ImageIO will ignore them because they parse once :)
++ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode animatedImage:(BOOL)animatedImage {
+    // `animatedImage` means called from `SDAnimatedImageProvider.animatedImageFrameAtIndex`
+    NSDictionary *options;
+    if (animatedImage) {
+        if (!lazyDecode) {
+            options = @{
+                // image decoding and caching should happen at image creation time.
+                (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
+            };
+        } else {
+            options = @{
+                // image decoding will happen at rendering time
+                (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(NO),
+            };
+        }
+    }
     // Parse the image properties
-    NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
+    NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, NULL);
     CGFloat pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue];
     CGFloat pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue];
     CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue];
     if (!exifOrientation) {
         exifOrientation = kCGImagePropertyOrientationUp;
     }
-    
-    CFStringRef uttype = CGImageSourceGetType(source);
-    // Check vector format
-    BOOL isVector = NO;
-    if ([NSData sd_imageFormatFromUTType:uttype] == SDImageFormatPDF) {
-        isVector = YES;
-    }
 
     NSMutableDictionary *decodingOptions;
     if (options) {
@@ -214,22 +249,6 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     CGImageRef imageRef;
     BOOL createFullImage = thumbnailSize.width == 0 || thumbnailSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height);
     if (createFullImage) {
-        if (isVector) {
-            if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
-                // Provide the default pixel count for vector images, simply just use the screen size
-#if SD_WATCH
-                thumbnailSize = WKInterfaceDevice.currentDevice.screenBounds.size;
-#elif SD_UIKIT
-                thumbnailSize = UIScreen.mainScreen.bounds.size;
-#elif SD_MAC
-                thumbnailSize = NSScreen.mainScreen.frame.size;
-#endif
-            }
-            CGFloat maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height);
-            NSUInteger DPIPerPixel = 2;
-            NSUInteger rasterizationDPI = maxPixelSize * DPIPerPixel;
-            decodingOptions[kSDCGImageSourceRasterizationDPI] = @(rasterizationDPI);
-        }
         imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
     } else {
         decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio);
@@ -238,9 +257,9 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             CGFloat pixelRatio = pixelWidth / pixelHeight;
             CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height;
             if (pixelRatio > thumbnailRatio) {
-                maxPixelSize = thumbnailSize.width;
+                maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.width / pixelRatio);
             } else {
-                maxPixelSize = thumbnailSize.height;
+                maxPixelSize = MAX(thumbnailSize.height, thumbnailSize.height * pixelRatio);
             }
         } else {
             maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height);
@@ -252,6 +271,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     if (!imageRef) {
         return nil;
     }
+    BOOL isDecoded = NO;
     // Thumbnail image post-process
     if (!createFullImage) {
         if (preserveAspectRatio) {
@@ -260,8 +280,45 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         } else {
             // `CGImageSourceCreateThumbnailAtIndex` take only pixel dimension, if not `preserveAspectRatio`, we should manual scale to the target size
             CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:imageRef size:thumbnailSize];
-            CGImageRelease(imageRef);
-            imageRef = scaledImageRef;
+            if (scaledImageRef) {
+                CGImageRelease(imageRef);
+                imageRef = scaledImageRef;
+                isDecoded = YES;
+            }
+        }
+    }
+    // Check whether output CGImage is decoded
+    if (!lazyDecode) {
+        if (!isDecoded) {
+            // Use CoreGraphics to trigger immediately decode
+            CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef];
+            if (decodedImageRef) {
+                CGImageRelease(imageRef);
+                imageRef = decodedImageRef;
+                isDecoded = YES;
+            }
+        }
+    } else if (animatedImage) {
+        // iOS 15+, CGImageRef now retains CGImageSourceRef internally. To workaround its thread-safe issue, we have to strip CGImageSourceRef, using Force-Decode (or have to use SPI `CGImageSetImageSource`), See: https://github.com/SDWebImage/SDWebImage/issues/3273
+        if (@available(iOS 15, tvOS 15, *)) {
+            // User pass `lazyDecode == YES`, but we still have to strip the CGImageSourceRef
+            // CGImageRef newImageRef = CGImageCreateCopy(imageRef); // This one does not strip the CGImageProperty
+            CGImageRef newImageRef = SDCGImageCreateCopy(imageRef);
+            if (newImageRef) {
+                CGImageRelease(imageRef);
+                imageRef = newImageRef;
+            }
+#if SD_CHECK_CGIMAGE_RETAIN_SOURCE
+            // Assert here to check CGImageRef should not retain the CGImageSourceRef and has possible thread-safe issue (this is behavior on iOS 15+)
+            // If assert hit, fire issue to https://github.com/SDWebImage/SDWebImage/issues and we update the condition for this behavior check
+            static dispatch_once_t onceToken;
+            dispatch_once(&onceToken, ^{
+                SDCGImageGetImageSource = dlsym(RTLD_DEFAULT, "CGImageGetImageSource");
+            });
+            if (SDCGImageGetImageSource) {
+                NSCAssert(!SDCGImageGetImageSource(imageRef), @"Animated Coder created CGImageRef should not retain CGImageSourceRef, which may cause thread-safe issue without lock");
+            }
+#endif
         }
     }
     
@@ -272,6 +329,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation];
 #endif
     CGImageRelease(imageRef);
+    image.sd_isDecoded = isDecoded;
+    
     return image;
 }
 
@@ -306,34 +365,88 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         preserveAspectRatio = preserveAspectRatioValue.boolValue;
     }
     
+    BOOL lazyDecode = YES; // Defaults YES for static image coder
+    NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
+    if (lazyDecodeValue != nil) {
+        lazyDecode = lazyDecodeValue.boolValue;
+    }
+    
+    NSUInteger limitBytes = 0;
+    NSNumber *limitBytesValue = options[SDImageCoderDecodeScaleDownLimitBytes];
+    if (limitBytesValue != nil) {
+        limitBytes = limitBytesValue.unsignedIntegerValue;
+    }
+    
 #if SD_MAC
     // If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG)
     // Which decode frames in time and reduce memory usage
-    if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
+    if (limitBytes == 0 && (thumbnailSize.width == 0 || thumbnailSize.height == 0)) {
         SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
-        NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
-        imageRep.size = size;
-        NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
-        [animatedImage addRepresentation:imageRep];
-        return animatedImage;
+        if (imageRep) {
+            NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
+            imageRep.size = size;
+            NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
+            [animatedImage addRepresentation:imageRep];
+            animatedImage.sd_imageFormat = self.class.imageFormat;
+            return animatedImage;
+        }
     }
 #endif
     
-    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
+    NSString *typeIdentifierHint = options[SDImageCoderDecodeTypeIdentifierHint];
+    if (!typeIdentifierHint) {
+        // Check file extension and convert to UTI, from: https://stackoverflow.com/questions/1506251/getting-an-uniform-type-identifier-for-a-given-extension
+        NSString *fileExtensionHint = options[SDImageCoderDecodeFileExtensionHint];
+        if (fileExtensionHint) {
+            typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, kUTTypeImage);
+            // Ignore dynamic UTI
+            if (UTTypeIsDynamic((__bridge CFStringRef)typeIdentifierHint)) {
+                typeIdentifierHint = nil;
+            }
+        }
+    } else if ([typeIdentifierHint isEqual:NSNull.null]) {
+        // Hack if user don't want to imply file extension
+        typeIdentifierHint = nil;
+    }
+    
+    NSDictionary *creatingOptions = nil;
+    if (typeIdentifierHint) {
+        creatingOptions = @{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : typeIdentifierHint};
+    }
+    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)creatingOptions);
+    if (!source) {
+        // Try again without UTType hint, the call site from user may provide the wrong UTType
+        source = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
+    }
     if (!source) {
         return nil;
     }
-    size_t count = CGImageSourceGetCount(source);
+    
+    size_t frameCount = CGImageSourceGetCount(source);
     UIImage *animatedImage;
     
+    // Parse the image properties
+    NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
+    size_t width = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue];
+    size_t height = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue];
+    // Scale down to limit bytes if need
+    if (limitBytes > 0) {
+        // Hack since ImageIO public API (not CGImageDecompressor/CMPhoto) always return back RGBA8888 CGImage
+        CGSize imageSize = CGSizeMake(width, height);
+        CGSize framePixelSize = [SDImageCoderHelper scaledSizeWithImageSize:imageSize limitBytes:limitBytes bytesPerPixel:4 frameCount:frameCount];
+        // Override thumbnail size
+        thumbnailSize = framePixelSize;
+        preserveAspectRatio = YES;
+    }
+    
     BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
-    if (decodeFirstFrame || count <= 1) {
-        animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
+    if (decodeFirstFrame || frameCount <= 1) {
+        animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
     } else {
-        NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
+        NSMutableArray<SDImageFrame *> *frames = [NSMutableArray arrayWithCapacity:frameCount];
         
-        for (size_t i = 0; i < count; i++) {
-            UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
+        for (size_t i = 0; i < frameCount; i++) {
+            UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
             if (!image) {
                 continue;
             }
@@ -366,6 +479,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     if (self) {
         NSString *imageUTType = self.class.imageUTType;
         _imageSource = CGImageSourceCreateIncremental((__bridge CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : imageUTType});
+        _incremental = YES;
         CGFloat scale = 1;
         NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
         if (scaleFactor != nil) {
@@ -388,6 +502,19 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             preserveAspectRatio = preserveAspectRatioValue.boolValue;
         }
         _preserveAspectRatio = preserveAspectRatio;
+        NSUInteger limitBytes = 0;
+        NSNumber *limitBytesValue = options[SDImageCoderDecodeScaleDownLimitBytes];
+        if (limitBytesValue != nil) {
+            limitBytes = limitBytesValue.unsignedIntegerValue;
+        }
+        _limitBytes = limitBytes;
+        BOOL lazyDecode = NO; // Defaults NO for animated image coder
+        NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
+        if (lazyDecodeValue != nil) {
+            lazyDecode = lazyDecodeValue.boolValue;
+        }
+        _lazyDecode = lazyDecode;
+        SD_LOCK_INIT(_lock);
 #if SD_UIKIT
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
 #endif
@@ -396,6 +523,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
 }
 
 - (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished {
+    NSCParameterAssert(_incremental);
     if (_finished) {
         return;
     }
@@ -409,11 +537,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
     
     if (_width + _height == 0) {
-        NSDictionary *options = @{
-            (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
-            (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
-        };
-        CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, (__bridge CFDictionaryRef)options);
+        CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
         if (properties) {
             CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
             if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
@@ -423,11 +547,24 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         }
     }
     
+    SD_LOCK(_lock);
     // For animated image progressive decoding because the frame count and duration may be changed.
     [self scanAndCheckFramesValidWithImageSource:_imageSource];
+    SD_UNLOCK(_lock);
+    
+    // Scale down to limit bytes if need
+    if (_limitBytes > 0) {
+        // Hack since ImageIO public API (not CGImageDecompressor/CMPhoto) always return back RGBA8888 CGImage
+        CGSize imageSize = CGSizeMake(_width, _height);
+        CGSize framePixelSize = [SDImageCoderHelper scaledSizeWithImageSize:imageSize limitBytes:_limitBytes bytesPerPixel:4 frameCount:_frameCount];
+        // Override thumbnail size
+        _thumbnailSize = framePixelSize;
+        _preserveAspectRatio = YES;
+    }
 }
 
 - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
+    NSCParameterAssert(_incremental);
     UIImage *image;
     
     if (_width + _height > 0) {
@@ -437,7 +574,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         if (scaleFactor != nil) {
             scale = MAX([scaleFactor doubleValue], 1);
         }
-        image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:nil];
+        image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO];
         if (image) {
             image.sd_imageFormat = self.class.imageFormat;
         }
@@ -455,19 +592,31 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     if (!image) {
         return nil;
     }
-    CGImageRef imageRef = image.CGImage;
-    if (!imageRef) {
-        // Earily return, supports CGImage only
+    if (format != self.class.imageFormat) {
         return nil;
     }
     
-    if (format != self.class.imageFormat) {
+    NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
+    if (!frames || frames.count == 0) {
+        SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:0];
+        frames = @[frame];
+    }
+    return [self encodedDataWithFrames:frames loopCount:image.sd_imageLoopCount format:format options:options];
+}
+
+- (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options {
+    UIImage *image = frames.firstObject.image; // Primary image
+    if (!image) {
+        return nil;
+    }
+    CGImageRef imageRef = image.CGImage;
+    if (!imageRef) {
+        // Earily return, supports CGImage only
         return nil;
     }
     
     NSMutableData *imageData = [NSMutableData data];
     CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
-    NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
     
     // Create an image destination. Animated Image does not support EXIF image orientation TODO
     // The `CGImageDestinationCreateWithData` will log a warning when count is 0, use 1 instead.
@@ -477,6 +626,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         return nil;
     }
     NSMutableDictionary *properties = [NSMutableDictionary dictionary];
+#if SD_UIKIT || SD_WATCH
+    CGImagePropertyOrientation exifOrientation = [SDImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
+#else
+    CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp;
+#endif
+    properties[(__bridge NSString *)kCGImagePropertyOrientation] = @(exifOrientation);
     // Encoding Options
     double compressionQuality = 1;
     if (options[SDImageCoderEncodeCompressionQuality]) {
@@ -499,13 +654,15 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     CGFloat pixelWidth = (CGFloat)CGImageGetWidth(imageRef);
     CGFloat pixelHeight = (CGFloat)CGImageGetHeight(imageRef);
     CGFloat finalPixelSize = 0;
-    if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && pixelWidth > maxPixelSize.width && pixelHeight > maxPixelSize.height) {
+    BOOL encodeFullImage = maxPixelSize.width == 0 || maxPixelSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= maxPixelSize.width && pixelHeight <= maxPixelSize.height);
+    if (!encodeFullImage) {
+        // Thumbnail Encoding
         CGFloat pixelRatio = pixelWidth / pixelHeight;
         CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height;
         if (pixelRatio > maxPixelSizeRatio) {
-            finalPixelSize = maxPixelSize.width;
+            finalPixelSize = MAX(maxPixelSize.width, maxPixelSize.width / pixelRatio);
         } else {
-            finalPixelSize = maxPixelSize.height;
+            finalPixelSize = MAX(maxPixelSize.height, maxPixelSize.height * pixelRatio);
         }
         properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize);
     }
@@ -522,12 +679,11 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail);
     
     BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
-    if (encodeFirstFrame || frames.count == 0) {
+    if (encodeFirstFrame || frames.count <= 1) {
         // for static single images
         CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
     } else {
         // for animated images
-        NSUInteger loopCount = image.sd_imageLoopCount;
         NSDictionary *containerProperties = @{
             self.class.dictionaryProperty: @{self.class.loopCountProperty : @(loopCount)}
         };
@@ -591,6 +747,31 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             preserveAspectRatio = preserveAspectRatioValue.boolValue;
         }
         _preserveAspectRatio = preserveAspectRatio;
+        NSUInteger limitBytes = 0;
+        NSNumber *limitBytesValue = options[SDImageCoderDecodeScaleDownLimitBytes];
+        if (limitBytesValue != nil) {
+            limitBytes = limitBytesValue.unsignedIntegerValue;
+        }
+        _limitBytes = limitBytes;
+        // Parse the image properties
+        NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
+        _width = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue];
+        _height = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue];
+        // Scale down to limit bytes if need
+        if (_limitBytes > 0) {
+            // Hack since ImageIO public API (not CGImageDecompressor/CMPhoto) always return back RGBA8888 CGImage
+            CGSize imageSize = CGSizeMake(_width, _height);
+            CGSize framePixelSize = [SDImageCoderHelper scaledSizeWithImageSize:imageSize limitBytes:_limitBytes bytesPerPixel:4 frameCount:_frameCount];
+            // Override thumbnail size
+            _thumbnailSize = framePixelSize;
+            _preserveAspectRatio = YES;
+        }
+        BOOL lazyDecode = NO; // Defaults NO for animated image coder
+        NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
+        if (lazyDecodeValue != nil) {
+            lazyDecode = lazyDecodeValue.boolValue;
+        }
+        _lazyDecode = lazyDecode;
         _imageSource = imageSource;
         _imageData = data;
 #if SD_UIKIT
@@ -606,17 +787,21 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     }
     NSUInteger frameCount = CGImageSourceGetCount(imageSource);
     NSUInteger loopCount = [self.class imageLoopCountWithSource:imageSource];
-    NSMutableArray<SDImageIOCoderFrame *> *frames = [NSMutableArray array];
+    _loopCount = loopCount;
     
+    NSMutableArray<SDImageIOCoderFrame *> *frames = [NSMutableArray arrayWithCapacity:frameCount];
     for (size_t i = 0; i < frameCount; i++) {
         SDImageIOCoderFrame *frame = [[SDImageIOCoderFrame alloc] init];
         frame.index = i;
         frame.duration = [self.class frameDurationAtIndex:i source:imageSource];
         [frames addObject:frame];
     }
+    if (frames.count != frameCount) {
+        // frames not match, do not override current value
+        return NO;
+    }
     
     _frameCount = frameCount;
-    _loopCount = loopCount;
     _frames = [frames copy];
     
     return YES;
@@ -635,27 +820,53 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
 }
 
 - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
-    if (index >= _frameCount) {
-        return 0;
+    NSTimeInterval duration;
+    // Incremental Animation decoding may update frames when new bytes available
+    // Which should use lock to ensure frame count and frames match, ensure atomic logic
+    if (_incremental) {
+        SD_LOCK(_lock);
+        if (index >= _frames.count) {
+            SD_UNLOCK(_lock);
+            return 0;
+        }
+        duration = _frames[index].duration;
+        SD_UNLOCK(_lock);
+    } else {
+        if (index >= _frames.count) {
+            return 0;
+        }
+        duration = _frames[index].duration;
     }
-    return _frames[index].duration;
+    return duration;
 }
 
 - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
-    if (index >= _frameCount) {
-        return nil;
+    UIImage *image;
+    // Incremental Animation decoding may update frames when new bytes available
+    // Which should use lock to ensure frame count and frames match, ensure atomic logic
+    if (_incremental) {
+        SD_LOCK(_lock);
+        if (index >= _frames.count) {
+            SD_UNLOCK(_lock);
+            return nil;
+        }
+        image = [self safeAnimatedImageFrameAtIndex:index];
+        SD_UNLOCK(_lock);
+    } else {
+        if (index >= _frames.count) {
+            return nil;
+        }
+        image = [self safeAnimatedImageFrameAtIndex:index];
     }
-    // Animated Image should not use the CGContext solution to force decode. Prefers to use Image/IO built in method, which is safer and memory friendly, see https://github.com/SDWebImage/SDWebImage/issues/2961
-    NSDictionary *options = @{
-        (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
-        (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
-    };
-    UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:options];
+    return image;
+}
+
+- (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
+    UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:YES];
     if (!image) {
         return nil;
     }
     image.sd_imageFormat = self.class.imageFormat;
-    image.sd_isDecoded = YES;
     return image;
 }
 

+ 140 - 12
Pods/SDWebImage/SDWebImage/Core/SDImageIOCoder.m

@@ -9,10 +9,13 @@
 #import "SDImageIOCoder.h"
 #import "SDImageCoderHelper.h"
 #import "NSImage+Compatibility.h"
-#import <ImageIO/ImageIO.h>
 #import "UIImage+Metadata.h"
+#import "SDImageGraphics.h"
 #import "SDImageIOAnimatedCoderInternal.h"
 
+#import <ImageIO/ImageIO.h>
+#import <CoreServices/CoreServices.h>
+
 // Specify File Size for lossy format encoding, like JPEG
 static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
 
@@ -24,6 +27,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     BOOL _finished;
     BOOL _preserveAspectRatio;
     CGSize _thumbnailSize;
+    BOOL _lazyDecode;
 }
 
 - (void)dealloc {
@@ -52,6 +56,67 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     return coder;
 }
 
+#pragma mark - Bitmap PDF representation
++ (UIImage *)createBitmapPDFWithData:(nonnull NSData *)data pageNumber:(NSUInteger)pageNumber targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio {
+    NSParameterAssert(data);
+    UIImage *image;
+    
+    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
+    if (!provider) {
+        return nil;
+    }
+    CGPDFDocumentRef document = CGPDFDocumentCreateWithProvider(provider);
+    CGDataProviderRelease(provider);
+    if (!document) {
+        return nil;
+    }
+    
+    // `CGPDFDocumentGetPage` page number is 1-indexed.
+    CGPDFPageRef page = CGPDFDocumentGetPage(document, pageNumber + 1);
+    if (!page) {
+        CGPDFDocumentRelease(document);
+        return nil;
+    }
+    
+    CGPDFBox box = kCGPDFMediaBox;
+    CGRect rect = CGPDFPageGetBoxRect(page, box);
+    CGRect targetRect = rect;
+    if (!CGSizeEqualToSize(targetSize, CGSizeZero)) {
+        targetRect = CGRectMake(0, 0, targetSize.width, targetSize.height);
+    }
+    
+    CGFloat xRatio = targetRect.size.width / rect.size.width;
+    CGFloat yRatio = targetRect.size.height / rect.size.height;
+    CGFloat xScale = preserveAspectRatio ? MIN(xRatio, yRatio) : xRatio;
+    CGFloat yScale = preserveAspectRatio ? MIN(xRatio, yRatio) : yRatio;
+    
+    // `CGPDFPageGetDrawingTransform` will only scale down, but not scale up, so we need calculate the actual scale again
+    CGRect drawRect = CGRectMake( 0, 0, targetRect.size.width / xScale, targetRect.size.height / yScale);
+    CGAffineTransform scaleTransform = CGAffineTransformMakeScale(xScale, yScale);
+    CGAffineTransform transform = CGPDFPageGetDrawingTransform(page, box, drawRect, 0, preserveAspectRatio);
+    
+    SDGraphicsBeginImageContextWithOptions(targetRect.size, NO, 0);
+    CGContextRef context = SDGraphicsGetCurrentContext();
+    
+#if SD_UIKIT || SD_WATCH
+    // Core Graphics coordinate system use the bottom-left, UIKit use the flipped one
+    CGContextTranslateCTM(context, 0, targetRect.size.height);
+    CGContextScaleCTM(context, 1, -1);
+#endif
+    
+    CGContextConcatCTM(context, scaleTransform);
+    CGContextConcatCTM(context, transform);
+    
+    CGContextDrawPDFPage(context, page);
+    
+    image = SDGraphicsGetImageFromCurrentImageContext();
+    SDGraphicsEndImageContext();
+    
+    CGPDFDocumentRelease(document);
+    
+    return image;
+}
+
 #pragma mark - Decode
 - (BOOL)canDecodeFromData:(nullable NSData *)data {
     return YES;
@@ -83,18 +148,73 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         preserveAspectRatio = preserveAspectRatioValue.boolValue;
     }
     
-    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
+    // Check vector format
+    if ([NSData sd_imageFormatForImageData:data] == SDImageFormatPDF) {
+        // History before iOS 16, ImageIO can decode PDF with rasterization size, but can't ever :(
+        // So, use CoreGraphics to decode PDF (copy code from SDWebImagePDFCoder, may do refactor in the future)
+        UIImage *image;
+        NSUInteger pageNumber = 0; // Still use first page, may added options is user want
+#if SD_MAC
+        // If don't use thumbnail, prefers the built-in generation of vector image
+        // macOS's `NSImage` supports PDF built-in rendering
+        if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
+            NSPDFImageRep *imageRep = [[NSPDFImageRep alloc] initWithData:data];
+            if (imageRep) {
+                imageRep.currentPage = pageNumber;
+                image = [[NSImage alloc] initWithSize:imageRep.size];
+                [image addRepresentation:imageRep];
+                image.sd_imageFormat = SDImageFormatPDF;
+                return image;
+            }
+        }
+#endif
+        image = [self.class createBitmapPDFWithData:data pageNumber:pageNumber targetSize:thumbnailSize preserveAspectRatio:preserveAspectRatio];
+        image.sd_imageFormat = SDImageFormatPDF;
+        return image;
+    }
+    
+    BOOL lazyDecode = YES; // Defaults YES for static image coder
+    NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
+    if (lazyDecodeValue != nil) {
+        lazyDecode = lazyDecodeValue.boolValue;
+    }
+    
+    NSString *typeIdentifierHint = options[SDImageCoderDecodeTypeIdentifierHint];
+    if (!typeIdentifierHint) {
+        // Check file extension and convert to UTI, from: https://stackoverflow.com/questions/1506251/getting-an-uniform-type-identifier-for-a-given-extension
+        NSString *fileExtensionHint = options[SDImageCoderDecodeFileExtensionHint];
+        if (fileExtensionHint) {
+            typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, kUTTypeImage);
+            // Ignore dynamic UTI
+            if (UTTypeIsDynamic((__bridge CFStringRef)typeIdentifierHint)) {
+                typeIdentifierHint = nil;
+            }
+        }
+    } else if ([typeIdentifierHint isEqual:NSNull.null]) {
+        // Hack if user don't want to imply file extension
+        typeIdentifierHint = nil;
+    }
+    
+    NSDictionary *creatingOptions = nil;
+    if (typeIdentifierHint) {
+        creatingOptions = @{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : typeIdentifierHint};
+    }
+    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)creatingOptions);
+    if (!source) {
+        // Try again without UTType hint, the call site from user may provide the wrong UTType
+        source = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
+    }
     if (!source) {
         return nil;
     }
     
-    UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
+    CFStringRef uttype = CGImageSourceGetType(source);
+    SDImageFormat imageFormat = [NSData sd_imageFormatFromUTType:uttype];
+    
+    UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
     CFRelease(source);
-    if (!image) {
-        return nil;
-    }
     
-    image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
+    image.sd_imageFormat = imageFormat;
     return image;
 }
 
@@ -130,6 +250,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             preserveAspectRatio = preserveAspectRatioValue.boolValue;
         }
         _preserveAspectRatio = preserveAspectRatio;
+        BOOL lazyDecode = YES; // Defaults YES for static image coder
+        NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
+        if (lazyDecodeValue != nil) {
+            lazyDecode = lazyDecodeValue.boolValue;
+        }
+        _lazyDecode = lazyDecode;
 #if SD_UIKIT
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
 #endif
@@ -180,7 +306,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         if (scaleFactor != nil) {
             scale = MAX([scaleFactor doubleValue], 1);
         }
-        image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:nil];
+        image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO];
         if (image) {
             CFStringRef uttype = CGImageSourceGetType(_imageSource);
             image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype];
@@ -252,14 +378,16 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     }
     CGFloat pixelWidth = (CGFloat)CGImageGetWidth(imageRef);
     CGFloat pixelHeight = (CGFloat)CGImageGetHeight(imageRef);
-    if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && pixelWidth > maxPixelSize.width && pixelHeight > maxPixelSize.height) {
+    CGFloat finalPixelSize = 0;
+    BOOL encodeFullImage = maxPixelSize.width == 0 || maxPixelSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= maxPixelSize.width && pixelHeight <= maxPixelSize.height);
+    if (!encodeFullImage) {
+        // Thumbnail Encoding
         CGFloat pixelRatio = pixelWidth / pixelHeight;
         CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height;
-        CGFloat finalPixelSize;
         if (pixelRatio > maxPixelSizeRatio) {
-            finalPixelSize = maxPixelSize.width;
+            finalPixelSize = MAX(maxPixelSize.width, maxPixelSize.width / pixelRatio);
         } else {
-            finalPixelSize = maxPixelSize.height;
+            finalPixelSize = MAX(maxPixelSize.height, maxPixelSize.height * pixelRatio);
         }
         properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize);
     }

+ 2 - 2
Pods/SDWebImage/SDWebImage/Core/SDImageLoader.h

@@ -81,7 +81,7 @@ FOUNDATION_EXPORT void SDImageLoaderSetProgressiveCoder(id<SDWebImageOperation>
  @param url The image URL to be loaded.
  @return YES to continue download, NO to stop download.
  */
-- (BOOL)canRequestImageForURL:(nullable NSURL *)url API_DEPRECATED("Use canRequestImageForURL:options:context: instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
+- (BOOL)canRequestImageForURL:(nullable NSURL *)url API_DEPRECATED_WITH_REPLACEMENT("canRequestImageForURL:options:context:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 @optional
 /**
@@ -125,7 +125,7 @@ FOUNDATION_EXPORT void SDImageLoaderSetProgressiveCoder(id<SDWebImageOperation>
  @return Whether to block this url or not. Return YES to mark this URL as failed.
  */
 - (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url
-                              error:(nonnull NSError *)error API_DEPRECATED("Use shouldBlockFailedURLWithURL:error:options:context: instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
+                              error:(nonnull NSError *)error API_DEPRECATED_WITH_REPLACEMENT("shouldBlockFailedURLWithURL:error:options:context:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 @optional
 /**

+ 35 - 67
Pods/SDWebImage/SDWebImage/Core/SDImageLoader.m

@@ -13,6 +13,7 @@
 #import "SDAnimatedImage.h"
 #import "UIImage+Metadata.h"
 #import "SDInternalMacros.h"
+#import "SDImageCacheDefine.h"
 #import "objc/runtime.h"
 
 SDWebImageContextOption const SDWebImageContextLoaderCachedImage = @"loaderCachedImage";
@@ -41,34 +42,13 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS
     } else {
         cacheKey = imageURL.absoluteString;
     }
+    SDImageCoderOptions *coderOptions = SDGetDecodeOptionsFromContext(context, options, cacheKey);
     BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
-    NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
-    CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
-    NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio];
-    NSValue *thumbnailSizeValue;
-    BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages);
-    if (shouldScaleDown) {
-        CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4;
-        CGFloat dimension = ceil(sqrt(thumbnailPixels));
-        thumbnailSizeValue = @(CGSizeMake(dimension, dimension));
-    }
-    if (context[SDWebImageContextImageThumbnailPixelSize]) {
-        thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
-    }
-    
-    SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
-    mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
-    mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
-    mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
-    mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
-    mutableCoderOptions[SDImageCoderWebImageContext] = context;
-    SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
+    CGFloat scale = [coderOptions[SDImageCoderDecodeScaleFactor] doubleValue];
     
     // Grab the image coder
-    id<SDImageCoder> imageCoder;
-    if ([context[SDWebImageContextImageCoder] conformsToProtocol:@protocol(SDImageCoder)]) {
-        imageCoder = context[SDWebImageContextImageCoder];
-    } else {
+    id<SDImageCoder> imageCoder = context[SDWebImageContextImageCoder];
+    if (!imageCoder) {
         imageCoder = [SDImageCodersManager sharedManager];
     }
     
@@ -94,18 +74,21 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS
         image = [imageCoder decodedImageWithData:imageData options:coderOptions];
     }
     if (image) {
-        BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
-        if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
-            // `SDAnimatedImage` do not decode
-            shouldDecode = NO;
-        } else if (image.sd_isAnimated) {
-            // animated image do not decode
-            shouldDecode = NO;
+        SDImageForceDecodePolicy policy = SDImageForceDecodePolicyAutomatic;
+        NSNumber *polivyValue = context[SDWebImageContextImageForceDecodePolicy];
+        if (polivyValue != nil) {
+            policy = polivyValue.unsignedIntegerValue;
         }
-        
-        if (shouldDecode) {
-            image = [SDImageCoderHelper decodedImageWithImage:image];
+        // TODO: Deprecated, remove in SD 6.0...
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+        if (SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage)) {
+            policy = SDImageForceDecodePolicyNever;
         }
+#pragma clang diagnostic pop
+        image = [SDImageCoderHelper decodedImageWithImage:image policy:policy];
+        // assign the decode options, to let manager check whether to re-decode if needed
+        image.sd_decodeOptions = coderOptions;
     }
     
     return image;
@@ -124,35 +107,16 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im
     } else {
         cacheKey = imageURL.absoluteString;
     }
+    SDImageCoderOptions *coderOptions = SDGetDecodeOptionsFromContext(context, options, cacheKey);
     BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
-    NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
-    CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
-    NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio];
-    NSValue *thumbnailSizeValue;
-    BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages);
-    if (shouldScaleDown) {
-        CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4;
-        CGFloat dimension = ceil(sqrt(thumbnailPixels));
-        thumbnailSizeValue = @(CGSizeMake(dimension, dimension));
-    }
-    if (context[SDWebImageContextImageThumbnailPixelSize]) {
-        thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
-    }
-    
-    SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
-    mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
-    mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
-    mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
-    mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
-    mutableCoderOptions[SDImageCoderWebImageContext] = context;
-    SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
+    CGFloat scale = [coderOptions[SDImageCoderDecodeScaleFactor] doubleValue];
     
     // Grab the progressive image coder
     id<SDProgressiveImageCoder> progressiveCoder = SDImageLoaderGetProgressiveCoder(operation);
     if (!progressiveCoder) {
         id<SDProgressiveImageCoder> imageCoder = context[SDWebImageContextImageCoder];
         // Check the progressive coder if provided
-        if ([imageCoder conformsToProtocol:@protocol(SDProgressiveImageCoder)]) {
+        if ([imageCoder respondsToSelector:@selector(initIncrementalWithOptions:)]) {
             progressiveCoder = [[[imageCoder class] alloc] initIncrementalWithOptions:coderOptions];
         } else {
             // We need to create a new instance for progressive decoding to avoid conflicts
@@ -175,7 +139,7 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im
     if (!decodeFirstFrame) {
         // check whether we should use `SDAnimatedImage`
         Class animatedImageClass = context[SDWebImageContextAnimatedImageClass];
-        if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)] && [progressiveCoder conformsToProtocol:@protocol(SDAnimatedImageCoder)]) {
+        if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)] && [progressiveCoder respondsToSelector:@selector(animatedImageFrameAtIndex:)]) {
             image = [[animatedImageClass alloc] initWithAnimatedCoder:(id<SDAnimatedImageCoder>)progressiveCoder scale:scale];
             if (image) {
                 // Progressive decoding does not preload frames
@@ -191,17 +155,21 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im
         image = [progressiveCoder incrementalDecodedImageWithOptions:coderOptions];
     }
     if (image) {
-        BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
-        if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
-            // `SDAnimatedImage` do not decode
-            shouldDecode = NO;
-        } else if (image.sd_isAnimated) {
-            // animated image do not decode
-            shouldDecode = NO;
+        SDImageForceDecodePolicy policy = SDImageForceDecodePolicyAutomatic;
+        NSNumber *polivyValue = context[SDWebImageContextImageForceDecodePolicy];
+        if (polivyValue != nil) {
+            policy = polivyValue.unsignedIntegerValue;
         }
-        if (shouldDecode) {
-            image = [SDImageCoderHelper decodedImageWithImage:image];
+        // TODO: Deprecated, remove in SD 6.0...
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+        if (SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage)) {
+            policy = SDImageForceDecodePolicyNever;
         }
+#pragma clang diagnostic pop
+        image = [SDImageCoderHelper decodedImageWithImage:image policy:policy];
+        // assign the decode options, to let manager check whether to re-decode if needed
+        image.sd_decodeOptions = coderOptions;
         // mark the image as progressive (completed one are not mark as progressive)
         image.sd_isIncremental = !finished;
     }

+ 1 - 1
Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheSerializer.h

@@ -19,7 +19,7 @@ typedef NSData * _Nullable(^SDWebImageCacheSerializerBlock)(UIImage * _Nonnull i
 
 /// Provide the image data associated to the image and store to disk cache
 /// @param image The loaded image
-/// @param data The original loaded image data
+/// @param data The original loaded image data. May be nil when image is transformed (UIImage.sd_isTransformed = YES)
 /// @param imageURL The image URL
 - (nullable NSData *)cacheDataWithImage:(nonnull UIImage *)image originalData:(nullable NSData *)data imageURL:(nullable NSURL *)imageURL;
 

+ 74 - 6
Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.h

@@ -105,6 +105,8 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
     /**
      * By default, placeholder images are loaded while the image is loading. This flag will delay the loading
      * of the placeholder image until after the image has finished loading.
+     * @note This is used to treate placeholder as an **Error Placeholder** but not **Loading Placeholder** by defaults. if the image loading is cancelled or error, the placeholder will be always set.
+     * @note Therefore, if you want both **Error Placeholder** and **Loading Placeholder** exist, use `SDWebImageAvoidAutoSetImage` to manually set the two placeholders and final loaded image by your hand depends on loading result.
      */
     SDWebImageDelayPlaceholder = 1 << 8,
     
@@ -125,9 +127,10 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
      * By default, images are decoded respecting their original size.
      * This flag will scale down the images to a size compatible with the constrained memory of devices.
      * To control the limit memory bytes, check `SDImageCoderHelper.defaultScaleDownLimitBytes` (Defaults to 60MB on iOS)
-     * This will actually translate to use context option `.imageThumbnailPixelSize` from v5.5.0 (Defaults to (3966, 3966) on iOS). Previously does not.
-     * This flags effect the progressive and animated images as well from v5.5.0. Previously does not.
-     * @note If you need detail controls, it's better to use context option `imageThumbnailPixelSize` and `imagePreserveAspectRatio` instead.
+     * (from 5.16.0) This will actually translate to use context option `SDWebImageContextImageScaleDownLimitBytes`, which check and calculate the thumbnail pixel size occupied small than limit bytes (including animated image)
+     * (from 5.5.0) This flags effect the progressive and animated images as well
+     * @note If you need detail controls, it's better to use context option `imageScaleDownBytes` instead.
+     * @warning This does not effect the cache key. So which means, this will effect the global cache even next time you query without this option. Pay attention when you use this on global options (It's always recommended to use request-level option for different pipeline)
      */
     SDWebImageScaleDownLargeImages = 1 << 11,
     
@@ -167,9 +170,11 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
     
     /**
      * By default, we will decode the image in the background during cache query and download from the network. This can help to improve performance because when rendering image on the screen, it need to be firstly decoded. But this happen on the main queue by Core Animation.
-     * However, this process may increase the memory usage as well. If you are experiencing a issue due to excessive memory consumption, This flag can prevent decode the image.
+     * However, this process may increase the memory usage as well. If you are experiencing an issue due to excessive memory consumption, This flag can prevent decode the image.
+     * @note 5.14.0 introduce `SDImageCoderDecodeUseLazyDecoding`, use that for better control from codec, instead of post-processing. Which acts the similar like this option but works for SDAnimatedImage as well (this one does not)
+     * @deprecated Deprecated in v5.17.0, if you don't want force-decode, pass [.imageForceDecodePolicy] = [SDImageForceDecodePolicy.never] in context option
      */
-    SDWebImageAvoidDecodeImage = 1 << 18,
+    SDWebImageAvoidDecodeImage API_DEPRECATED("Use SDWebImageContextImageForceDecodePolicy instead", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0)) = 1 << 18,
     
     /**
      * By default, we decode the animated image. This flag can force decode the first frame only and produce the static image.
@@ -205,7 +210,7 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
 };
 
 
-#pragma mark - Context Options
+#pragma mark - Manager Context Options
 
 /**
  A String to be used as the operation key for view category to store the image load operation. This is used for view instance which supports different image loading process. If nil, will use the class name as operation key. (NSString *)
@@ -218,6 +223,15 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextSetIma
  */
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustomManager API_DEPRECATED("Use individual context option like .imageCache, .imageLoader and .imageTransformer instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
+/**
+ A `SDCallbackQueue` instance which controls the `Cache`/`Manager`/`Loader`'s callback queue for their completionBlock.
+ This is useful for user who call these 3 components in non-main queue and want to avoid callback in main queue.
+ @note For UI callback (`sd_setImageWithURL`), we will still use main queue to dispatch, means if you specify a global queue, it will enqueue from the global queue to main queue.
+ @note This does not effect the components' working queue (for example, `Cache` still query disk on internal ioQueue, `Loader` still do network on URLSessionConfiguration.delegateQueue), change those config if you need.
+ Defaults to nil. Which means main queue.
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCallbackQueue;
+
 /**
  A id<SDImageCache> instance which conforms to `SDImageCache` protocol. It's used to override the image manager's cache during the image loading pipeline.
  In other word, if you just want to specify a custom cache during image loading, you don't need to re-create a dummy SDWebImageManager instance with the cache. If not provided, use the image manager's cache (id<SDImageCache>)
@@ -239,9 +253,29 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageC
 
 /**
  A id<SDImageTransformer> instance which conforms `SDImageTransformer` protocol. It's used for image transform after the image load finished and store the transformed image to cache. If you provide one, it will ignore the `transformer` in manager and use provided one instead. If you pass NSNull, the transformer feature will be disabled. (id<SDImageTransformer>)
+ @note When this value is used, we will trigger image transform after downloaded, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key)
  */
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageTransformer;
 
+#pragma mark - Force Decode Options
+
+/**
+ A  NSNumber instance which store the`SDImageForceDecodePolicy` enum. This is used to control how current image loading should force-decode the decoded image (CGImage, typically). See more what's force-decode means in `SDImageForceDecodePolicy` comment.
+ Defaults to `SDImageForceDecodePolicyAutomatic`, which will detect the input CGImage's metadata, and only force-decode if the input CGImage can not directly render on screen (need extra CoreAnimation Copied Image and increase RAM usage).
+ @note If you want to always the force-decode for this image request, pass `SDImageForceDecodePolicyAlways`, for example, some WebP images which does not created by ImageIO.
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageForceDecodePolicy;
+
+#pragma mark - Image Decoder Context Options
+
+/**
+ A Dictionary (SDImageCoderOptions) value, which pass the extra decoding options to the SDImageCoder. Introduced in SDWebImage 5.14.0
+ You can pass additional decoding related options to the decoder, extensible and control by you. And pay attention this dictionary may be retained by decoded image via `UIImage.sd_decodeOptions` 
+ This context option replace the deprecated `SDImageCoderWebImageContext`, which may cause retain cycle (cache -> image -> options -> context -> cache)
+ @note There are already individual options below like `.imageScaleFactor`, `.imagePreserveAspectRatio`, each of individual options will override the same filed for this dictionary.
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageDecodeOptions;
+
 /**
  A CGFloat raw value which specify the image scale factor. The number should be greater than or equal to 1.0. If not provide or the number is invalid, we will use the cache key to specify the scale factor. (NSNumber)
  */
@@ -257,9 +291,41 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageP
  A CGSize raw value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.imagePreserveAspectRatio`) the value size.
  @note When you pass `.preserveAspectRatio == NO`, the thumbnail image is stretched to match each dimension. When `.preserveAspectRatio == YES`, the thumbnail image's width is limited to pixel size's width, the thumbnail image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both.
  Defaults to CGSizeZero, which means no thumbnail generation at all. (NSValue)
+ @note When this value is used, we will trigger thumbnail decoding for url, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key)
  */
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageThumbnailPixelSize;
 
+/**
+ A NSString value (UTI) indicating the source image's file extension. Example: "public.jpeg-2000", "com.nikon.raw-image", "public.tiff"
+ Some image file format share the same data structure but has different tag explanation, like TIFF and NEF/SRW, see https://en.wikipedia.org/wiki/TIFF
+ Changing the file extension cause the different image result. The coder (like ImageIO) may use file extension to choose the correct parser
+ @note If you don't provide this option, we will use the `URL.path` as file extension to calculate the UTI hint
+ @note If you really don't want any hint which effect the image result, pass `NSNull.null` instead
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageTypeIdentifierHint;
+
+/**
+ A NSUInteger value to provide the limit bytes during decoding. This can help to avoid OOM on large frame count animated image or large pixel static image when you don't know how much RAM it occupied before decoding
+ The decoder will do these logic based on limit bytes:
+ 1. Get the total frame count (static image means 1)
+ 2. Calculate the `framePixelSize` width/height to `sqrt(limitBytes / frameCount / bytesPerPixel)`, keeping aspect ratio (at least 1x1)
+ 3. If the `framePixelSize < originalImagePixelSize`, then do thumbnail decoding (see `SDImageCoderDecodeThumbnailPixelSize`) use the `framePixelSize` and `preseveAspectRatio = YES`
+ 4. Else, use the full pixel decoding (small than limit bytes)
+ 5. Whatever result, this does not effect the animated/static behavior of image. So even if you set `limitBytes = 1 && frameCount = 100`, we will stll create animated image with each frame `1x1` pixel size.
+ @note This option has higher priority than `.imageThumbnailPixelSize`
+ @warning This does not effect the cache key. So which means, this will effect the global cache even next time you query without this option. Pay attention when you use this on global options (It's always recommended to use request-level option for different pipeline)
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageScaleDownLimitBytes;
+
+#pragma mark - Cache Context Options
+
+/**
+ A Dictionary (SDImageCoderOptions) value, which pass the extra encode options to the SDImageCoder. Introduced in SDWebImage 5.15.0
+ You can pass encode options like `compressionQuality`, `maxFileSize`, `maxPixelSize` to control the encoding related thing, this is used inside `SDImageCache` during store logic.
+ @note For developer who use custom cache protocol (not SDImageCache instance), they need to upgrade and use these options for encoding.
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageEncodeOptions;
+
 /**
  A SDImageCacheType raw value which specify the source of cache to query. Specify `SDImageCacheTypeDisk` to query from disk cache only; `SDImageCacheTypeMemory` to query from memory only. And `SDImageCacheTypeAll` to query from both memory cache and disk cache. Specify `SDImageCacheTypeNone` is invalid and totally ignore the cache query.
  If not provide or the value is invalid, we will use `SDImageCacheTypeAll`. (NSNumber)
@@ -298,6 +364,8 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextOrigin
  */
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextAnimatedImageClass;
 
+#pragma mark - Download Context Options
+
 /**
  A id<SDWebImageDownloaderRequestModifier> instance to modify the image download request. It's used for downloader to modify the original request from URL and options. If you provide one, it will ignore the `requestModifier` in downloader and use provided one instead. (id<SDWebImageDownloaderRequestModifier>)
  */

+ 36 - 4
Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.m

@@ -9,11 +9,12 @@
 #import "SDWebImageDefine.h"
 #import "UIImage+Metadata.h"
 #import "NSImage+Compatibility.h"
+#import "SDAnimatedImage.h"
 #import "SDAssociatedObject.h"
 
 #pragma mark - Image scale
 
-static inline NSArray<NSNumber *> * _Nonnull SDImageScaleFactors() {
+static inline NSArray<NSNumber *> * _Nonnull SDImageScaleFactors(void) {
     return @[@2, @3];
 }
 
@@ -28,7 +29,13 @@ inline CGFloat SDImageScaleFactorForKey(NSString * _Nullable key) {
 #elif SD_UIKIT
     if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)])
 #elif SD_MAC
-    if ([[NSScreen mainScreen] respondsToSelector:@selector(backingScaleFactor)])
+    NSScreen *mainScreen = nil;
+    if (@available(macOS 10.12, *)) {
+        mainScreen = [NSScreen mainScreen];
+    } else {
+        mainScreen = [NSScreen screens].firstObject;
+    }
+    if ([mainScreen respondsToSelector:@selector(backingScaleFactor)])
 #endif
     {
         // a@2x.png -> 8
@@ -75,13 +82,32 @@ inline UIImage * _Nullable SDScaledImageForScaleFactor(CGFloat scale, UIImage *
         return image;
     }
     UIImage *scaledImage;
+    // Check SDAnimatedImage support for shortcut
+    if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
+        if ([image respondsToSelector:@selector(animatedCoder)]) {
+            id<SDAnimatedImageCoder> coder = [(id<SDAnimatedImage>)image animatedCoder];
+            if (coder) {
+                scaledImage = [[image.class alloc] initWithAnimatedCoder:coder scale:scale];
+            }
+        } else {
+            // Some class impl does not support `animatedCoder`, keep for compatibility
+            NSData *data = [(id<SDAnimatedImage>)image animatedImageData];
+            if (data) {
+                scaledImage = [[image.class alloc] initWithData:data scale:scale];
+            }
+        }
+        if (scaledImage) {
+            return scaledImage;
+        }
+    }
     if (image.sd_isAnimated) {
         UIImage *animatedImage;
 #if SD_UIKIT || SD_WATCH
         // `UIAnimatedImage` images share the same size and scale.
-        NSMutableArray<UIImage *> *scaledImages = [NSMutableArray array];
+        NSArray<UIImage *> *images = image.images;
+        NSMutableArray<UIImage *> *scaledImages = [NSMutableArray arrayWithCapacity:images.count];
         
-        for (UIImage *tempImage in image.images) {
+        for (UIImage *tempImage in images) {
             UIImage *tempScaledImage = [[UIImage alloc] initWithCGImage:tempImage.CGImage scale:scale orientation:tempImage.imageOrientation];
             [scaledImages addObject:tempScaledImage];
         }
@@ -120,13 +146,19 @@ inline UIImage * _Nullable SDScaledImageForScaleFactor(CGFloat scale, UIImage *
 
 SDWebImageContextOption const SDWebImageContextSetImageOperationKey = @"setImageOperationKey";
 SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager";
+SDWebImageContextOption const SDWebImageContextCallbackQueue = @"callbackQueue";
 SDWebImageContextOption const SDWebImageContextImageCache = @"imageCache";
 SDWebImageContextOption const SDWebImageContextImageLoader = @"imageLoader";
 SDWebImageContextOption const SDWebImageContextImageCoder = @"imageCoder";
 SDWebImageContextOption const SDWebImageContextImageTransformer = @"imageTransformer";
+SDWebImageContextOption const SDWebImageContextImageForceDecodePolicy = @"imageForceDecodePolicy";
+SDWebImageContextOption const SDWebImageContextImageDecodeOptions = @"imageDecodeOptions";
 SDWebImageContextOption const SDWebImageContextImageScaleFactor = @"imageScaleFactor";
 SDWebImageContextOption const SDWebImageContextImagePreserveAspectRatio = @"imagePreserveAspectRatio";
 SDWebImageContextOption const SDWebImageContextImageThumbnailPixelSize = @"imageThumbnailPixelSize";
+SDWebImageContextOption const SDWebImageContextImageTypeIdentifierHint = @"imageTypeIdentifierHint";
+SDWebImageContextOption const SDWebImageContextImageScaleDownLimitBytes = @"imageScaleDownLimitBytes";
+SDWebImageContextOption const SDWebImageContextImageEncodeOptions = @"imageEncodeOptions";
 SDWebImageContextOption const SDWebImageContextQueryCacheType = @"queryCacheType";
 SDWebImageContextOption const SDWebImageContextStoreCacheType = @"storeCacheType";
 SDWebImageContextOption const SDWebImageContextOriginalQueryCacheType = @"originalQueryCacheType";

+ 7 - 1
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.h

@@ -74,8 +74,10 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
     /**
      * By default, we will decode the image in the background during cache query and download from the network. This can help to improve performance because when rendering image on the screen, it need to be firstly decoded. But this happen on the main queue by Core Animation.
      * However, this process may increase the memory usage as well. If you are experiencing a issue due to excessive memory consumption, This flag can prevent decode the image.
+     * @note 5.14.0 introduce `SDImageCoderDecodeUseLazyDecoding`, use that for better control from codec, instead of post-processing. Which acts the similar like this option but works for SDAnimatedImage as well (this one does not)
+     * @deprecated Deprecated in v5.17.0, if you don't want force-decode, pass [.imageForceDecodePolicy] = [SDImageForceDecodePolicy.never] in context option
      */
-    SDWebImageDownloaderAvoidDecodeImage = 1 << 9,
+    SDWebImageDownloaderAvoidDecodeImage API_DEPRECATED("Use SDWebImageContextImageForceDecodePolicy instead", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0)) = 1 << 9,
     
     /**
      * By default, we decode the animated image. This flag can force decode the first frame only and produce the static image.
@@ -95,9 +97,13 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
     SDWebImageDownloaderMatchAnimatedImageClass = 1 << 12,
 };
 
+/// Posed when URLSessionTask started (`resume` called))
 FOUNDATION_EXPORT NSNotificationName _Nonnull const SDWebImageDownloadStartNotification;
+/// Posed when URLSessionTask get HTTP response (`didReceiveResponse:completionHandler:` called)
 FOUNDATION_EXPORT NSNotificationName _Nonnull const SDWebImageDownloadReceiveResponseNotification;
+/// Posed when URLSessionTask stoped (`didCompleteWithError:` with error or `cancel` called)
 FOUNDATION_EXPORT NSNotificationName _Nonnull const SDWebImageDownloadStopNotification;
+/// Posed when URLSessionTask finished with success  (`didCompleteWithError:` without error)
 FOUNDATION_EXPORT NSNotificationName _Nonnull const SDWebImageDownloadFinishNotification;
 
 typedef SDImageLoaderProgressBlock SDWebImageDownloaderProgressBlock;

+ 67 - 18
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.m

@@ -10,7 +10,10 @@
 #import "SDWebImageDownloaderConfig.h"
 #import "SDWebImageDownloaderOperation.h"
 #import "SDWebImageError.h"
+#import "SDWebImageCacheKeyFilter.h"
+#import "SDImageCacheDefine.h"
 #import "SDInternalMacros.h"
+#import "objc/runtime.h"
 
 NSNotificationName const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
 NSNotificationName const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
@@ -18,6 +21,22 @@ NSNotificationName const SDWebImageDownloadStopNotification = @"SDWebImageDownlo
 NSNotificationName const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";
 
 static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
+static void * SDWebImageDownloaderOperationKey = &SDWebImageDownloaderOperationKey;
+
+BOOL SDWebImageDownloaderOperationGetCompleted(id<SDWebImageDownloaderOperation> operation) {
+    NSCParameterAssert(operation);
+    NSNumber *value = objc_getAssociatedObject(operation, SDWebImageDownloaderOperationKey);
+    if (value != nil) {
+        return value.boolValue;
+    } else {
+        return NO;
+    }
+}
+
+void SDWebImageDownloaderOperationSetCompleted(id<SDWebImageDownloaderOperation> operation, BOOL isCompleted) {
+    NSCParameterAssert(operation);
+    objc_setAssociatedObject(operation, SDWebImageDownloaderOperationKey, @(isCompleted), OBJC_ASSOCIATION_RETAIN);
+}
 
 @interface SDWebImageDownloadToken ()
 
@@ -97,7 +116,7 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
         [_config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) options:0 context:SDWebImageDownloaderContext];
         _downloadQueue = [NSOperationQueue new];
         _downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads;
-        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
+        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader.downloadQueue";
         _URLOperations = [NSMutableDictionary new];
         NSMutableDictionary<NSString *, NSString *> *headerDictionary = [NSMutableDictionary dictionary];
         NSString *userAgent = nil;
@@ -204,11 +223,28 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
         return nil;
     }
     
-    SD_LOCK(_operationsLock);
     id downloadOperationCancelToken;
+    // When different thumbnail size download with same url, we need to make sure each callback called with desired size
+    id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
+    NSString *cacheKey;
+    if (cacheKeyFilter) {
+        cacheKey = [cacheKeyFilter cacheKeyForURL:url];
+    } else {
+        cacheKey = url.absoluteString;
+    }
+    SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey);
+    SD_LOCK(_operationsLock);
     NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
     // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
-    if (!operation || operation.isFinished || operation.isCancelled) {
+    BOOL shouldNotReuseOperation;
+    if (operation) {
+        @synchronized (operation) {
+            shouldNotReuseOperation = operation.isFinished || operation.isCancelled || SDWebImageDownloaderOperationGetCompleted(operation);
+        }
+    } else {
+        shouldNotReuseOperation = YES;
+    }
+    if (shouldNotReuseOperation) {
         operation = [self createDownloaderOperationWithUrl:url options:options context:context];
         if (!operation) {
             SD_UNLOCK(_operationsLock);
@@ -228,9 +264,9 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
             [self.URLOperations removeObjectForKey:url];
             SD_UNLOCK(self->_operationsLock);
         };
-        self.URLOperations[url] = operation;
+        [self.URLOperations setObject:operation forKey:url];
         // Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers.
-        downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
+        downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
         // Add operation to operation queue only after all configuration done according to Apple's doc.
         // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
         [self.downloadQueue addOperation:operation];
@@ -238,16 +274,7 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
         // When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue)
         // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.
         @synchronized (operation) {
-            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
-        }
-        if (!operation.isExecuting) {
-            if (options & SDWebImageDownloaderHighPriority) {
-                operation.queuePriority = NSOperationQueuePriorityHigh;
-            } else if (options & SDWebImageDownloaderLowPriority) {
-                operation.queuePriority = NSOperationQueuePriorityLow;
-            } else {
-                operation.queuePriority = NSOperationQueuePriorityNormal;
-            }
+            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
         }
     }
     SD_UNLOCK(_operationsLock);
@@ -260,6 +287,21 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
     return token;
 }
 
+#pragma mark Helper methods
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
++ (SDWebImageOptions)imageOptionsFromDownloaderOptions:(SDWebImageDownloaderOptions)downloadOptions {
+    SDWebImageOptions options = 0;
+    if (downloadOptions & SDWebImageDownloaderScaleDownLargeImages) options |= SDWebImageScaleDownLargeImages;
+    if (downloadOptions & SDWebImageDownloaderDecodeFirstFrameOnly) options |= SDWebImageDecodeFirstFrameOnly;
+    if (downloadOptions & SDWebImageDownloaderPreloadAllFrames) options |= SDWebImagePreloadAllFrames;
+    if (downloadOptions & SDWebImageDownloaderAvoidDecodeImage) options |= SDWebImageAvoidDecodeImage;
+    if (downloadOptions & SDWebImageDownloaderMatchAnimatedImageClass) options |= SDWebImageMatchAnimatedImageClass;
+    
+    return options;
+}
+#pragma clang diagnostic pop
+
 - (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
                                                                                   options:(SDWebImageDownloaderOptions)options
                                                                                   context:(nullable SDWebImageContext *)context {
@@ -330,9 +372,7 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
     
     // Operation Class
     Class operationClass = self.config.operationClass;
-    if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
-        // Custom operation class
-    } else {
+    if (!operationClass) {
         operationClass = [SDWebImageDownloaderOperation class];
     }
     NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
@@ -478,6 +518,12 @@ didReceiveResponse:(NSURLResponse *)response
     
     // Identify the operation that runs this task and pass it the delegate method
     NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
+    if (dataOperation) {
+        @synchronized (dataOperation) {
+            // Mark the downloader operation `isCompleted = YES`, no longer re-use this operation when new request comes in
+            SDWebImageDownloaderOperationSetCompleted(dataOperation, YES);
+        }
+    }
     if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
         [dataOperation URLSession:session task:task didCompleteWithError:error];
     }
@@ -582,6 +628,8 @@ didReceiveResponse:(NSURLResponse *)response
     return YES;
 }
 
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
 - (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock {
     UIImage *cachedImage = context[SDWebImageContextLoaderCachedImage];
     
@@ -608,6 +656,7 @@ didReceiveResponse:(NSURLResponse *)response
     
     return [self downloadImageWithURL:url options:downloaderOptions context:context progress:progressBlock completed:completedBlock];
 }
+#pragma clang diagnostic pop
 
 - (BOOL)shouldBlockFailedURLWithURL:(NSURL *)url error:(NSError *)error {
     return [self shouldBlockFailedURLWithURL:url error:error options:0 context:nil];

+ 8 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderConfig.m

@@ -7,6 +7,7 @@
  */
 
 #import "SDWebImageDownloaderConfig.h"
+#import "SDWebImageDownloaderOperation.h"
 
 static SDWebImageDownloaderConfig * _defaultDownloaderConfig;
 
@@ -48,5 +49,12 @@ static SDWebImageDownloaderConfig * _defaultDownloaderConfig;
     return config;
 }
 
+- (void)setOperationClass:(Class)operationClass {
+    if (operationClass) {
+        NSAssert([operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)], @"Custom downloader operation class must subclass NSOperation and conform to `SDWebImageDownloaderOperation` protocol");
+    }
+    _operationClass = operationClass;
+}
+
 
 @end

+ 19 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.h

@@ -29,6 +29,10 @@
 - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                             completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
 
+- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
+                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
+                        decodeOptions:(nullable SDImageCoderOptions *)decodeOptions;
+
 - (BOOL)cancel:(nullable id)token;
 
 @property (strong, nonatomic, readonly, nullable) NSURLRequest *request;
@@ -160,6 +164,21 @@
 - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                             completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
 
+/**
+ *  Adds handlers for progress and completion, and optional decode options (which need another image other than the initial one). Returns a token that can be passed to -cancel: to cancel this set of
+ *  callbacks.
+ *
+ *  @param progressBlock  the block executed when a new chunk of data arrives.
+ *                        @note the progress block is executed on a background queue
+ *  @param completedBlock the block executed when the download is done.
+ *                        @note the completed block is executed on the main queue for success. If errors are found, there is a chance the block will be executed on a background queue
+ *  @param decodeOptions The optional decode options, used when in thumbnail decoding for current completion block callback. For example, request <url1, {thumbnail: 100x100}> and then <url1, {thumbnail: 200x200}>, we may callback these two completion block with different size.
+ *  @return the token to use to cancel this set of handlers
+ */
+- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
+                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
+                        decodeOptions:(nullable SDImageCoderOptions *)decodeOptions;
+
 /**
  *  Cancels a set of callbacks. Once all callbacks are canceled, the operation is cancelled.
  *

+ 178 - 67
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.m

@@ -11,15 +11,43 @@
 #import "SDInternalMacros.h"
 #import "SDWebImageDownloaderResponseModifier.h"
 #import "SDWebImageDownloaderDecryptor.h"
+#import "SDImageCacheDefine.h"
+#import "SDCallbackQueue.h"
 
-static NSString *const kProgressCallbackKey = @"progress";
-static NSString *const kCompletedCallbackKey = @"completed";
+BOOL SDWebImageDownloaderOperationGetCompleted(id<SDWebImageDownloaderOperation> operation); // Private currently, mark open if needed
 
-typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
+// A handler to represent individual request
+@interface SDWebImageDownloaderOperationToken : NSObject
+
+@property (nonatomic, copy, nullable) SDWebImageDownloaderCompletedBlock completedBlock;
+@property (nonatomic, copy, nullable) SDWebImageDownloaderProgressBlock progressBlock;
+@property (nonatomic, copy, nullable) SDImageCoderOptions *decodeOptions;
+
+@end
+
+@implementation SDWebImageDownloaderOperationToken
+
+- (BOOL)isEqual:(id)other {
+    if (nil == other) {
+      return NO;
+    }
+    if (self == other) {
+      return YES;
+    }
+    if (![other isKindOfClass:[self class]]) {
+      return NO;
+    }
+    SDWebImageDownloaderOperationToken *object = (SDWebImageDownloaderOperationToken *)other;
+    // warn: only compare decodeOptions, ignore pointer, use `removeObjectIdenticalTo`
+    BOOL result = [self.decodeOptions isEqualToDictionary:object.decodeOptions];
+    return result;
+}
+
+@end
 
 @interface SDWebImageDownloaderOperation ()
 
-@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
+@property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageDownloaderOperationToken *> *callbackTokens;
 
 @property (assign, nonatomic, readwrite) SDWebImageDownloaderOptions options;
 @property (copy, nonatomic, readwrite, nullable) SDWebImageContext *context;
@@ -48,6 +76,8 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
 @property (strong, nonatomic, readwrite, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
 
 @property (strong, nonatomic, nonnull) NSOperationQueue *coderQueue; // the serial operation queue to do image decoding
+
+@property (strong, nonatomic, nonnull) NSMapTable<SDImageCoderOptions *, UIImage *> *imageMap; // each variant of image is weak-referenced to avoid too many re-decode during downloading
 #if SD_UIKIT
 @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
 #endif
@@ -75,15 +105,17 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
         _request = [request copy];
         _options = options;
         _context = [context copy];
-        _callbackBlocks = [NSMutableArray new];
+        _callbackTokens = [NSMutableArray new];
         _responseModifier = context[SDWebImageContextDownloadResponseModifier];
         _decryptor = context[SDWebImageContextDownloadDecryptor];
         _executing = NO;
         _finished = NO;
         _expectedSize = 0;
         _unownedSession = session;
-        _coderQueue = [NSOperationQueue new];
+        _coderQueue = [[NSOperationQueue alloc] init];
         _coderQueue.maxConcurrentOperationCount = 1;
+        _coderQueue.name = @"com.hackemist.SDWebImageDownloaderOperation.coderQueue";
+        _imageMap = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:1];
 #if SD_UIKIT
         _backgroundTaskId = UIBackgroundTaskInvalid;
 #endif
@@ -93,33 +125,31 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
 
 - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                             completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
-    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
-    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
-    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
-    @synchronized (self) {
-        [self.callbackBlocks addObject:callbacks];
-    }
-    return callbacks;
+    return [self addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:nil];
 }
 
-- (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
-    NSMutableArray<id> *callbacks;
+- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
+                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
+                        decodeOptions:(nullable SDImageCoderOptions *)decodeOptions {
+    if (!completedBlock && !progressBlock && !decodeOptions) return nil;
+    SDWebImageDownloaderOperationToken *token = [SDWebImageDownloaderOperationToken new];
+    token.completedBlock = completedBlock;
+    token.progressBlock = progressBlock;
+    token.decodeOptions = decodeOptions;
     @synchronized (self) {
-        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
+        [self.callbackTokens addObject:token];
     }
-    // We need to remove [NSNull null] because there might not always be a progress block for each callback
-    [callbacks removeObjectIdenticalTo:[NSNull null]];
-    return [callbacks copy]; // strip mutability here
+    
+    return token;
 }
 
 - (BOOL)cancel:(nullable id)token {
-    if (!token) return NO;
+    if (![token isKindOfClass:SDWebImageDownloaderOperationToken.class]) return NO;
     
     BOOL shouldCancel = NO;
     @synchronized (self) {
-        NSMutableArray *tempCallbackBlocks = [self.callbackBlocks mutableCopy];
-        [tempCallbackBlocks removeObjectIdenticalTo:token];
-        if (tempCallbackBlocks.count == 0) {
+        NSArray *tokens = self.callbackTokens;
+        if (tokens.count == 1 && [tokens indexOfObjectIdenticalTo:token] != NSNotFound) {
             shouldCancel = YES;
         }
     }
@@ -129,14 +159,9 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
     } else {
         // Only callback this token's completion block
         @synchronized (self) {
-            [self.callbackBlocks removeObjectIdenticalTo:token];
+            [self.callbackTokens removeObjectIdenticalTo:token];
         }
-        SDWebImageDownloaderCompletedBlock completedBlock = [token valueForKey:kCompletedCallbackKey];
-        dispatch_main_async_safe(^{
-            if (completedBlock) {
-                completedBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}], YES);
-            }
-        });
+        [self callCompletionBlockWithToken:token image:nil imageData:nil error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] finished:YES];
     }
     return shouldCancel;
 }
@@ -209,17 +234,20 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
     if (self.dataTask) {
         if (self.options & SDWebImageDownloaderHighPriority) {
             self.dataTask.priority = NSURLSessionTaskPriorityHigh;
-            self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
         } else if (self.options & SDWebImageDownloaderLowPriority) {
             self.dataTask.priority = NSURLSessionTaskPriorityLow;
-            self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
         } else {
             self.dataTask.priority = NSURLSessionTaskPriorityDefault;
-            self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
         }
         [self.dataTask resume];
-        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
-            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
+        NSArray<SDWebImageDownloaderOperationToken *> *tokens;
+        @synchronized (self) {
+            tokens = [self.callbackTokens copy];
+        }
+        for (SDWebImageDownloaderOperationToken *token in tokens) {
+            if (token.progressBlock) {
+                token.progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
+            }
         }
         __block typeof(self) strongSelf = self;
         dispatch_async(dispatch_get_main_queue(), ^{
@@ -275,7 +303,7 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
 
 - (void)reset {
     @synchronized (self) {
-        [self.callbackBlocks removeAllObjects];
+        [self.callbackTokens removeAllObjects];
         self.dataTask = nil;
         
         if (self.ownedSession) {
@@ -306,7 +334,7 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
     [self didChangeValueForKey:@"isExecuting"];
 }
 
-- (BOOL)isConcurrent {
+- (BOOL)isAsynchronous {
     return YES;
 }
 
@@ -374,8 +402,14 @@ didReceiveResponse:(NSURLResponse *)response
     }
     
     if (valid) {
-        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
-            progressBlock(0, expected, self.request.URL);
+        NSArray<SDWebImageDownloaderOperationToken *> *tokens;
+        @synchronized (self) {
+            tokens = [self.callbackTokens copy];
+        }
+        for (SDWebImageDownloaderOperationToken *token in tokens) {
+            if (token.progressBlock) {
+                token.progressBlock(0, expected, self.request.URL);
+            }
         }
     } else {
         // Status code invalid and marked as cancelled. Do not call `[self.dataTask cancel]` which may mass up URLSession life cycle
@@ -398,10 +432,16 @@ didReceiveResponse:(NSURLResponse *)response
     [self.imageData appendData:data];
     
     self.receivedSize = self.imageData.length;
+    NSArray<SDWebImageDownloaderOperationToken *> *tokens;
+    @synchronized (self) {
+        tokens = [self.callbackTokens copy];
+    }
     if (self.expectedSize == 0) {
         // Unknown expectedSize, immediately call progressBlock and return
-        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
-            progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
+        for (SDWebImageDownloaderOperationToken *token in tokens) {
+            if (token.progressBlock) {
+                token.progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
+            }
         }
         return;
     }
@@ -420,6 +460,8 @@ didReceiveResponse:(NSURLResponse *)response
     
     // Using data decryptor will disable the progressive decoding, since there are no support for progressive decrypt
     BOOL supportProgressive = (self.options & SDWebImageDownloaderProgressiveLoad) && !self.decryptor;
+    // When multiple thumbnail decoding use different size, this progressive decoding will cause issue because each callback assume called with different size's image, can not share the same decoding part
+    // We currently only pick the first thumbnail size, see #3423 talks
     // Progressive decoding Only decode partial image, full image in `URLSession:task:didCompleteWithError:`
     if (supportProgressive && !finished) {
         // Get the image data
@@ -434,6 +476,12 @@ didReceiveResponse:(NSURLResponse *)response
                 if (!self) {
                     return;
                 }
+                // When cancelled or transfer finished (`didCompleteWithError`), cancel the progress callback, only completed block is called and enough
+                @synchronized (self) {
+                    if (self.isCancelled || SDWebImageDownloaderOperationGetCompleted(self)) {
+                        return;
+                    }
+                }
                 UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, NO, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
                 if (image) {
                     // We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding.
@@ -444,8 +492,10 @@ didReceiveResponse:(NSURLResponse *)response
         }
     }
     
-    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
-        progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
+    for (SDWebImageDownloaderOperationToken *token in tokens) {
+        if (token.progressBlock) {
+            token.progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
+        }
     }
 }
 
@@ -471,7 +521,9 @@ didReceiveResponse:(NSURLResponse *)response
     // If we already cancel the operation or anything mark the operation finished, don't callback twice
     if (self.isFinished) return;
     
-    @synchronized(self) {
+    NSArray<SDWebImageDownloaderOperationToken *> *tokens;
+    @synchronized (self) {
+        tokens = [self.callbackTokens copy];
         self.dataTask = nil;
         __block typeof(self) strongSelf = self;
         dispatch_async(dispatch_get_main_queue(), ^{
@@ -491,7 +543,7 @@ didReceiveResponse:(NSURLResponse *)response
         [self callCompletionBlocksWithError:error];
         [self done];
     } else {
-        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
+        if (tokens.count > 0) {
             NSData *imageData = self.imageData;
             self.imageData = nil;
             // data decryptor
@@ -514,28 +566,63 @@ didReceiveResponse:(NSURLResponse *)response
                     // decode the image in coder queue, cancel all previous decoding process
                     [self.coderQueue cancelAllOperations];
                     @weakify(self);
-                    [self.coderQueue addOperationWithBlock:^{
+                    for (SDWebImageDownloaderOperationToken *token in tokens) {
+                        [self.coderQueue addOperationWithBlock:^{
+                            @strongify(self);
+                            if (!self) {
+                                return;
+                            }
+                            UIImage *image;
+                            // check if we already decode this variant of image for current callback
+                            if (token.decodeOptions) {
+                                image = [self.imageMap objectForKey:token.decodeOptions];
+                            }
+                            if (!image) {
+                                // check if we already use progressive decoding, use that to produce faster decoding
+                                id<SDProgressiveImageCoder> progressiveCoder = SDImageLoaderGetProgressiveCoder(self);
+                                SDWebImageOptions options = [[self class] imageOptionsFromDownloaderOptions:self.options];
+                                SDWebImageContext *context;
+                                if (token.decodeOptions) {
+                                    SDWebImageMutableContext *mutableContext = [NSMutableDictionary dictionaryWithDictionary:self.context];
+                                    SDSetDecodeOptionsToContext(mutableContext, &options, token.decodeOptions);
+                                    context = [mutableContext copy];
+                                } else {
+                                    context = self.context;
+                                }
+                                if (progressiveCoder) {
+                                    image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, YES, self, options, context);
+                                } else {
+                                    image = SDImageLoaderDecodeImageData(imageData, self.request.URL, options, context);
+                                }
+                                if (image && token.decodeOptions) {
+                                    [self.imageMap setObject:image forKey:token.decodeOptions];
+                                }
+                            }
+                            CGSize imageSize = image.size;
+                            if (imageSize.width == 0 || imageSize.height == 0) {
+                                NSString *description = image == nil ? @"Downloaded image decode failed" : @"Downloaded image has 0 pixels";
+                                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : description}];
+                                [self callCompletionBlockWithToken:token image:nil imageData:nil error:error finished:YES];
+                            } else {
+                                [self callCompletionBlockWithToken:token image:image imageData:imageData error:nil finished:YES];
+                            }
+                        }];
+                    }
+                    // call [self done] after all completed block was dispatched
+                    dispatch_block_t doneBlock = ^{
                         @strongify(self);
                         if (!self) {
                             return;
                         }
-                        // check if we already use progressive decoding, use that to produce faster decoding
-                        id<SDProgressiveImageCoder> progressiveCoder = SDImageLoaderGetProgressiveCoder(self);
-                        UIImage *image;
-                        if (progressiveCoder) {
-                            image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, YES, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
-                        } else {
-                            image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
-                        }
-                        CGSize imageSize = image.size;
-                        if (imageSize.width == 0 || imageSize.height == 0) {
-                            NSString *description = image == nil ? @"Downloaded image decode failed" : @"Downloaded image has 0 pixels";
-                            [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : description}]];
-                        } else {
-                            [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
-                        }
                         [self done];
-                    }];
+                    };
+                    if (@available(iOS 13, tvOS 13, macOS 10.15, watchOS 6, *)) {
+                        // seems faster than `addOperationWithBlock`
+                        [self.coderQueue addBarrierBlock:doneBlock];
+                    } else {
+                        // serial queue, this does the same effect in semantics
+                        [self.coderQueue addOperationWithBlock:doneBlock];
+                    }
                 }
             } else {
                 [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
@@ -584,6 +671,8 @@ didReceiveResponse:(NSURLResponse *)response
 }
 
 #pragma mark Helper methods
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
 + (SDWebImageOptions)imageOptionsFromDownloaderOptions:(SDWebImageDownloaderOptions)downloadOptions {
     SDWebImageOptions options = 0;
     if (downloadOptions & SDWebImageDownloaderScaleDownLargeImages) options |= SDWebImageScaleDownLargeImages;
@@ -594,6 +683,7 @@ didReceiveResponse:(NSURLResponse *)response
     
     return options;
 }
+#pragma clang diagnostic pop
 
 - (BOOL)shouldContinueWhenAppEntersBackground {
     return SD_OPTIONS_CONTAINS(self.options, SDWebImageDownloaderContinueInBackground);
@@ -607,12 +697,33 @@ didReceiveResponse:(NSURLResponse *)response
                             imageData:(nullable NSData *)imageData
                                 error:(nullable NSError *)error
                              finished:(BOOL)finished {
-    NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
-    dispatch_main_async_safe(^{
-        for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
-            completedBlock(image, imageData, error, finished);
+    NSArray<SDWebImageDownloaderOperationToken *> *tokens;
+    @synchronized (self) {
+        tokens = [self.callbackTokens copy];
+    }
+    for (SDWebImageDownloaderOperationToken *token in tokens) {
+        SDWebImageDownloaderCompletedBlock completedBlock = token.completedBlock;
+        if (completedBlock) {
+            SDCallbackQueue *queue = self.context[SDWebImageContextCallbackQueue];
+            [(queue ?: SDCallbackQueue.mainQueue) async:^{
+                completedBlock(image, imageData, error, finished);
+            }];
         }
-    });
+    }
+}
+
+- (void)callCompletionBlockWithToken:(nonnull SDWebImageDownloaderOperationToken *)token
+                               image:(nullable UIImage *)image
+                           imageData:(nullable NSData *)imageData
+                               error:(nullable NSError *)error
+                            finished:(BOOL)finished {
+    SDWebImageDownloaderCompletedBlock completedBlock = token.completedBlock;
+    if (completedBlock) {
+        SDCallbackQueue *queue = self.context[SDWebImageContextCallbackQueue];
+        [(queue ?: SDCallbackQueue.mainQueue) async:^{
+            completedBlock(image, imageData, error, finished);
+        }];
+    }
 }
 
 @end

+ 1 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageError.h

@@ -9,6 +9,7 @@
 
 #import "SDWebImageCompat.h"
 
+/// An error domain represent SDWebImage loading system with custom codes
 FOUNDATION_EXPORT NSErrorDomain const _Nonnull SDWebImageErrorDomain;
 
 /// The response instance for invalid download response (NSURLResponse *)

+ 1 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageIndicator.m

@@ -12,6 +12,7 @@
 
 #if SD_MAC
 #import <QuartzCore/QuartzCore.h>
+#import <CoreImage/CIFilter.h>
 #endif
 
 #pragma mark - Activity Indicator

+ 3 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.h

@@ -29,6 +29,9 @@ typedef void(^SDInternalCompletionBlock)(UIImage * _Nullable image, NSData * _Nu
  */
 - (void)cancel;
 
+/// Whether the operation has been cancelled.
+@property (nonatomic, assign, readonly, getter=isCancelled) BOOL cancelled;
+
 /**
  The cache operation from the image cache query
  */

+ 229 - 162
Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.m

@@ -13,6 +13,7 @@
 #import "SDAssociatedObject.h"
 #import "SDWebImageError.h"
 #import "SDInternalMacros.h"
+#import "SDCallbackQueue.h"
 
 static id<SDImageCache> _defaultImageCache;
 static id<SDImageLoader> _defaultImageLoader;
@@ -112,6 +113,26 @@ static id<SDImageLoader> _defaultImageLoader;
     return key;
 }
 
+- (nullable NSString *)originalCacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context {
+    if (!url) {
+        return @"";
+    }
+    
+    NSString *key;
+    // Cache Key Filter
+    id<SDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
+    if (context[SDWebImageContextCacheKeyFilter]) {
+        cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
+    }
+    if (cacheKeyFilter) {
+        key = [cacheKeyFilter cacheKeyForURL:url];
+    } else {
+        key = url.absoluteString;
+    }
+    
+    return key;
+}
+
 - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context {
     if (!url) {
         return @"";
@@ -150,7 +171,7 @@ static id<SDImageLoader> _defaultImageLoader;
     id<SDImageTransformer> transformer = self.transformer;
     if (context[SDWebImageContextImageTransformer]) {
         transformer = context[SDWebImageContextImageTransformer];
-        if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) {
+        if ([transformer isEqual:NSNull.null]) {
             transformer = nil;
         }
     }
@@ -193,11 +214,14 @@ static id<SDImageLoader> _defaultImageLoader;
         isFailedUrl = [self.failedURLs containsObject:url];
         SD_UNLOCK(_failedURLsLock);
     }
+    
+    // Preprocess the options and context arg to decide the final the result for manager
+    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
 
     if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
         NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
         NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
-        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
+        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] queue:result.context[SDWebImageContextCallbackQueue] url:url];
         return operation;
     }
 
@@ -205,10 +229,19 @@ static id<SDImageLoader> _defaultImageLoader;
     [self.runningOperations addObject:operation];
     SD_UNLOCK(_runningOperationsLock);
     
-    // Preprocess the options and context arg to decide the final the result for manager
-    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
+    // Start the entry to load image from cache, the longest steps are below
+    // Steps without transformer:
+    // 1. query image from cache, miss
+    // 2. download data and image
+    // 3. store image to cache
     
-    // Start the entry to load image from cache
+    // Steps with transformer:
+    // 1. query transformed image from cache, miss
+    // 2. query original image from cache, miss
+    // 3. download data and image
+    // 4. do transform in CPU
+    // 5. store original image to cache
+    // 6. store transformed image to cache
     [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
 
     return operation;
@@ -254,10 +287,8 @@ static id<SDImageLoader> _defaultImageLoader;
                             progress:(nullable SDImageLoaderProgressBlock)progressBlock
                            completed:(nullable SDInternalCompletionBlock)completedBlock {
     // Grab the image cache to use
-    id<SDImageCache> imageCache;
-    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
-        imageCache = context[SDWebImageContextImageCache];
-    } else {
+    id<SDImageCache> imageCache = context[SDWebImageContextImageCache];
+    if (!imageCache) {
         imageCache = self.imageCache;
     }
     // Get the query cache type
@@ -269,21 +300,26 @@ static id<SDImageLoader> _defaultImageLoader;
     // Check whether we should query cache
     BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
     if (shouldQueryCache) {
+        // transformed cache key
         NSString *key = [self cacheKeyForURL:url context:context];
         @weakify(operation);
         operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
             @strongify(operation);
             if (!operation || operation.isCancelled) {
                 // Image combined operation cancelled by user
-                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
+                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] queue:context[SDWebImageContextCallbackQueue] url:url];
                 [self safelyRemoveOperationFromRunning:operation];
                 return;
-            } else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
-                // Have a chance to query original cache instead of downloading
-                [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
-                return;
+            } else if (!cachedImage) {
+                NSString *originKey = [self originalCacheKeyForURL:url context:context];
+                BOOL mayInOriginalCache = ![key isEqualToString:originKey];
+                // Have a chance to query original cache instead of downloading, then applying transform
+                // Thumbnail decoding is done inside SDImageCache's decoding part, which does not need post processing for transform
+                if (mayInOriginalCache) {
+                    [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
+                    return;
+                }
             }
-            
             // Continue download process
             [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
         }];
@@ -301,14 +337,11 @@ static id<SDImageLoader> _defaultImageLoader;
                                     progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                    completed:(nullable SDInternalCompletionBlock)completedBlock {
     // Grab the image cache to use, choose standalone original cache firstly
-    id<SDImageCache> imageCache;
-    if ([context[SDWebImageContextOriginalImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
-        imageCache = context[SDWebImageContextOriginalImageCache];
-    } else {
+    id<SDImageCache> imageCache = context[SDWebImageContextOriginalImageCache];
+    if (!imageCache) {
         // if no standalone cache available, use default cache
-        if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
-            imageCache = context[SDWebImageContextImageCache];
-        } else {
+        imageCache = context[SDWebImageContextImageCache];
+        if (!imageCache) {
             imageCache = self.imageCache;
         }
     }
@@ -321,32 +354,30 @@ static id<SDImageLoader> _defaultImageLoader;
     // Check whether we should query original cache
     BOOL shouldQueryOriginalCache = (originalQueryCacheType != SDImageCacheTypeNone);
     if (shouldQueryOriginalCache) {
-        // Disable transformer for original cache key generation
-        SDWebImageMutableContext *tempContext = [context mutableCopy];
-        tempContext[SDWebImageContextImageTransformer] = [NSNull null];
-        NSString *key = [self cacheKeyForURL:url context:tempContext];
+        // Get original cache key generation without transformer
+        NSString *key = [self originalCacheKeyForURL:url context:context];
         @weakify(operation);
         operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:originalQueryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
             @strongify(operation);
             if (!operation || operation.isCancelled) {
                 // Image combined operation cancelled by user
-                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
+                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] queue:context[SDWebImageContextCallbackQueue] url:url];
                 [self safelyRemoveOperationFromRunning:operation];
                 return;
-            } else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
+            } else if (!cachedImage) {
                 // Original image cache miss. Continue download process
-                [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:originalQueryCacheType progress:progressBlock completed:completedBlock];
+                [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
                 return;
             }
                         
-            // Use the store cache process instead of downloading, and ignore .refreshCached option for now
-            [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:cachedImage downloadedData:cachedData finished:YES progress:progressBlock completed:completedBlock];
+            // Skip downloading and continue transform process, and ignore .refreshCached option for now
+            [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:cachedImage originalData:cachedData cacheType:cacheType finished:YES completed:completedBlock];
             
             [self safelyRemoveOperationFromRunning:operation];
         }];
     } else {
         // Continue download process
-        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:originalQueryCacheType progress:progressBlock completed:completedBlock];
+        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
     }
 }
 
@@ -360,11 +391,14 @@ static id<SDImageLoader> _defaultImageLoader;
                               cacheType:(SDImageCacheType)cacheType
                                progress:(nullable SDImageLoaderProgressBlock)progressBlock
                               completed:(nullable SDInternalCompletionBlock)completedBlock {
+    // Mark the cache operation end
+    @synchronized (operation) {
+        operation.cacheOperation = nil;
+    }
+    
     // Grab the image loader to use
-    id<SDImageLoader> imageLoader;
-    if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
-        imageLoader = context[SDWebImageContextImageLoader];
-    } else {
+    id<SDImageLoader> imageLoader = context[SDWebImageContextImageLoader];
+    if (!imageLoader) {
         imageLoader = self.imageLoader;
     }
     
@@ -381,7 +415,7 @@ static id<SDImageLoader> _defaultImageLoader;
         if (cachedImage && options & SDWebImageRefreshCached) {
             // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
             // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
-            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
+            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];
             // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
             SDWebImageMutableContext *mutableContext;
             if (context) {
@@ -398,14 +432,14 @@ static id<SDImageLoader> _defaultImageLoader;
             @strongify(operation);
             if (!operation || operation.isCancelled) {
                 // Image combined operation cancelled by user
-                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] url:url];
+                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] queue:context[SDWebImageContextCallbackQueue] url:url];
             } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
                 // Image refresh hit the NSURLCache cache, do not call the completion block
             } else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
                 // Download operation cancelled by user before sending the request, don't block failed URL
-                [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
+                [self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];
             } else if (error) {
-                [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
+                [self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];
                 BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error options:options context:context];
                 
                 if (shouldBlockFailedURL) {
@@ -419,8 +453,8 @@ static id<SDImageLoader> _defaultImageLoader;
                     [self.failedURLs removeObject:url];
                     SD_UNLOCK(self->_failedURLsLock);
                 }
-                // Continue store cache process
-                [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
+                // Continue transform process
+                [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock];
             }
             
             if (finished) {
@@ -428,103 +462,128 @@ static id<SDImageLoader> _defaultImageLoader;
             }
         }];
     } else if (cachedImage) {
-        [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
+        [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];
         [self safelyRemoveOperationFromRunning:operation];
     } else {
         // Image not in cache and download disallowed by delegate
-        [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
+        [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];
         [self safelyRemoveOperationFromRunning:operation];
     }
 }
 
-// Store cache process
-- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
-                                      url:(nonnull NSURL *)url
-                                  options:(SDWebImageOptions)options
-                                  context:(SDWebImageContext *)context
-                          downloadedImage:(nullable UIImage *)downloadedImage
-                           downloadedData:(nullable NSData *)downloadedData
-                                 finished:(BOOL)finished
-                                 progress:(nullable SDImageLoaderProgressBlock)progressBlock
-                                completed:(nullable SDInternalCompletionBlock)completedBlock {
-    // Grab the image cache to use, choose standalone original cache firstly
-    id<SDImageCache> imageCache;
-    if ([context[SDWebImageContextOriginalImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
-        imageCache = context[SDWebImageContextOriginalImageCache];
+// Transform process
+- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
+                                     url:(nonnull NSURL *)url
+                                 options:(SDWebImageOptions)options
+                                 context:(SDWebImageContext *)context
+                           originalImage:(nullable UIImage *)originalImage
+                            originalData:(nullable NSData *)originalData
+                               cacheType:(SDImageCacheType)cacheType
+                                finished:(BOOL)finished
+                               completed:(nullable SDInternalCompletionBlock)completedBlock {
+    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
+    if ([transformer isEqual:NSNull.null]) {
+        transformer = nil;
+    }
+    // transformer check
+    BOOL shouldTransformImage = originalImage && transformer;
+    shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
+    shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage));
+    // thumbnail check
+    BOOL isThumbnail = originalImage.sd_isThumbnail;
+    NSData *cacheData = originalData;
+    UIImage *cacheImage = originalImage;
+    if (isThumbnail) {
+        cacheData = nil; // thumbnail don't store full size data
+        originalImage = nil; // thumbnail don't have full size image
+    }
+    
+    if (shouldTransformImage) {
+        // transformed cache key
+        NSString *key = [self cacheKeyForURL:url context:context];
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+            // Case that transformer on thumbnail, which this time need full pixel image
+            UIImage *transformedImage = [transformer transformedImageWithImage:cacheImage forKey:key];
+            if (transformedImage) {
+                transformedImage.sd_isTransformed = YES;
+                [self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:transformedImage originalData:originalData cacheData:nil cacheType:cacheType finished:finished completed:completedBlock];
+            } else {
+                [self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:cacheImage originalData:originalData cacheData:cacheData cacheType:cacheType finished:finished completed:completedBlock];
+            }
+        });
     } else {
+        [self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:cacheImage originalData:originalData cacheData:cacheData cacheType:cacheType finished:finished completed:completedBlock];
+    }
+}
+
+// Store origin cache process
+- (void)callStoreOriginCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
+                                            url:(nonnull NSURL *)url
+                                        options:(SDWebImageOptions)options
+                                        context:(SDWebImageContext *)context
+                                  originalImage:(nullable UIImage *)originalImage
+                                     cacheImage:(nullable UIImage *)cacheImage
+                                   originalData:(nullable NSData *)originalData
+                                      cacheData:(nullable NSData *)cacheData
+                                      cacheType:(SDImageCacheType)cacheType
+                                       finished:(BOOL)finished
+                                      completed:(nullable SDInternalCompletionBlock)completedBlock {
+    // Grab the image cache to use, choose standalone original cache firstly
+    id<SDImageCache> imageCache = context[SDWebImageContextOriginalImageCache];
+    if (!imageCache) {
         // if no standalone cache available, use default cache
-        if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
-            imageCache = context[SDWebImageContextImageCache];
-        } else {
+        imageCache = context[SDWebImageContextImageCache];
+        if (!imageCache) {
             imageCache = self.imageCache;
         }
     }
-    // the target image store cache type
-    SDImageCacheType storeCacheType = SDImageCacheTypeAll;
-    if (context[SDWebImageContextStoreCacheType]) {
-        storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
-    }
     // the original store image cache type
     SDImageCacheType originalStoreCacheType = SDImageCacheTypeDisk;
     if (context[SDWebImageContextOriginalStoreCacheType]) {
         originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue];
     }
-    // Disable transformer for original cache key generation
-    SDWebImageMutableContext *tempContext = [context mutableCopy];
-    tempContext[SDWebImageContextImageTransformer] = [NSNull null];
-    NSString *key = [self cacheKeyForURL:url context:tempContext];
-    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
-    if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) {
-        transformer = nil;
-    }
     id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
     
-    BOOL shouldTransformImage = downloadedImage && transformer;
-    shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
-    shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isVector || (options & SDWebImageTransformVectorImage));
-    BOOL shouldCacheOriginal = downloadedImage && finished;
+    // If the original cacheType is disk, since we don't need to store the original data again
+    // Strip the disk from the originalStoreCacheType
+    if (cacheType == SDImageCacheTypeDisk) {
+        if (originalStoreCacheType == SDImageCacheTypeDisk) originalStoreCacheType = SDImageCacheTypeNone;
+        if (originalStoreCacheType == SDImageCacheTypeAll) originalStoreCacheType = SDImageCacheTypeMemory;
+    }
     
-    // if available, store original image to cache
-    if (shouldCacheOriginal) {
-        // normally use the store cache type, but if target image is transformed, use original store cache type instead
-        SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
-        if (cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) {
-            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
-                @autoreleasepool {
-                    NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
-                    [self storeImage:downloadedImage imageData:cacheData forKey:key imageCache:imageCache cacheType:targetStoreCacheType options:options context:context completion:^{
-                        // Continue transform process
-                        [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
-                    }];
-                }
-            });
-        } else {
-            [self storeImage:downloadedImage imageData:downloadedData forKey:key imageCache:imageCache cacheType:targetStoreCacheType options:options context:context completion:^{
-                // Continue transform process
-                [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
+    // Get original cache key generation without transformer
+    NSString *key = [self originalCacheKeyForURL:url context:context];
+    if (finished && cacheSerializer && (originalStoreCacheType == SDImageCacheTypeDisk || originalStoreCacheType == SDImageCacheTypeAll)) {
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+            NSData *newOriginalData = [cacheSerializer cacheDataWithImage:originalImage originalData:originalData imageURL:url];
+            // Store original image and data
+            [self storeImage:originalImage imageData:newOriginalData forKey:key options:options context:context imageCache:imageCache cacheType:originalStoreCacheType finished:finished completion:^{
+                // Continue store cache process, transformed data is nil
+                [self callStoreCacheProcessForOperation:operation url:url options:options context:context image:cacheImage data:cacheData cacheType:cacheType finished:finished completed:completedBlock];
             }];
-        }
+        });
     } else {
-        // Continue transform process
-        [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
+        // Store original image and data
+        [self storeImage:originalImage imageData:originalData forKey:key options:options context:context imageCache:imageCache cacheType:originalStoreCacheType finished:finished completion:^{
+            // Continue store cache process, transformed data is nil
+            [self callStoreCacheProcessForOperation:operation url:url options:options context:context image:cacheImage data:cacheData cacheType:cacheType finished:finished completed:completedBlock];
+        }];
     }
 }
 
-// Transform process
-- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
-                                     url:(nonnull NSURL *)url
-                                 options:(SDWebImageOptions)options
-                                 context:(SDWebImageContext *)context
-                           originalImage:(nullable UIImage *)originalImage
-                            originalData:(nullable NSData *)originalData
-                                finished:(BOOL)finished
-                                progress:(nullable SDImageLoaderProgressBlock)progressBlock
-                               completed:(nullable SDInternalCompletionBlock)completedBlock {
+// Store normal cache process
+- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
+                                      url:(nonnull NSURL *)url
+                                  options:(SDWebImageOptions)options
+                                  context:(SDWebImageContext *)context
+                                    image:(nullable UIImage *)image
+                                     data:(nullable NSData *)data
+                                cacheType:(SDImageCacheType)cacheType
+                                 finished:(BOOL)finished
+                                completed:(nullable SDInternalCompletionBlock)completedBlock {
     // Grab the image cache to use
-    id<SDImageCache> imageCache;
-    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
-        imageCache = context[SDWebImageContextImageCache];
-    } else {
+    id<SDImageCache> imageCache = context[SDWebImageContextImageCache];
+    if (!imageCache) {
         imageCache = self.imageCache;
     }
     // the target image store cache type
@@ -532,41 +591,23 @@ static id<SDImageLoader> _defaultImageLoader;
     if (context[SDWebImageContextStoreCacheType]) {
         storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
     }
-    // transformed cache key
-    NSString *key = [self cacheKeyForURL:url context:context];
-    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
-    if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) {
-        transformer = nil;
-    }
     id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
     
-    BOOL shouldTransformImage = originalImage && transformer;
-    shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
-    shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage));
-    // if available, store transformed image to cache
-    if (shouldTransformImage) {
+    // transformed cache key
+    NSString *key = [self cacheKeyForURL:url context:context];
+    if (finished && cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
-            @autoreleasepool {
-                UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
-                if (transformedImage && finished) {
-                    BOOL imageWasTransformed = ![transformedImage isEqual:originalImage];
-                    NSData *cacheData;
-                    // pass nil if the image was transformed, so we can recalculate the data from the image
-                    if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
-                        cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : originalData) imageURL:url];
-                    } else {
-                        cacheData = (imageWasTransformed ? nil : originalData);
-                    }
-                    [self storeImage:transformedImage imageData:cacheData forKey:key imageCache:imageCache cacheType:storeCacheType options:options context:context completion:^{
-                        [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
-                    }];
-                } else {
-                    [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
-                }
-            }
+            NSData *newData = [cacheSerializer cacheDataWithImage:image originalData:data imageURL:url];
+            // Store image and data
+            [self storeImage:image imageData:newData forKey:key options:options context:context imageCache:imageCache cacheType:storeCacheType finished:finished completion:^{
+                [self callCompletionBlockForOperation:operation completion:completedBlock image:image data:data error:nil cacheType:cacheType finished:finished queue:context[SDWebImageContextCallbackQueue] url:url];
+            }];
         });
     } else {
-        [self callCompletionBlockForOperation:operation completion:completedBlock image:originalImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
+        // Store image and data
+        [self storeImage:image imageData:data forKey:key options:options context:context imageCache:imageCache cacheType:storeCacheType finished:finished completion:^{
+            [self callCompletionBlockForOperation:operation completion:completedBlock image:image data:data error:nil cacheType:cacheType finished:finished queue:context[SDWebImageContextCallbackQueue] url:url];
+        }];
     }
 }
 
@@ -584,20 +625,38 @@ static id<SDImageLoader> _defaultImageLoader;
 - (void)storeImage:(nullable UIImage *)image
          imageData:(nullable NSData *)data
             forKey:(nullable NSString *)key
-        imageCache:(nonnull id<SDImageCache>)imageCache
-         cacheType:(SDImageCacheType)cacheType
            options:(SDWebImageOptions)options
            context:(nullable SDWebImageContext *)context
+        imageCache:(nonnull id<SDImageCache>)imageCache
+         cacheType:(SDImageCacheType)cacheType
+          finished:(BOOL)finished
         completion:(nullable SDWebImageNoParamsBlock)completion {
     BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);
+    // Ignore progressive data cache
+    if (!finished) {
+        if (completion) {
+            completion();
+        }
+        return;
+    }
     // Check whether we should wait the store cache finished. If not, callback immediately
-    [imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{
-        if (waitStoreCache) {
-            if (completion) {
-                completion();
+    if ([imageCache respondsToSelector:@selector(storeImage:imageData:forKey:options:context:cacheType:completion:)]) {
+        [imageCache storeImage:image imageData:data forKey:key options:options context:context cacheType:cacheType completion:^{
+            if (waitStoreCache) {
+                if (completion) {
+                    completion();
+                }
             }
-        }
-    }];
+        }];
+    } else {
+        [imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{
+            if (waitStoreCache) {
+                if (completion) {
+                    completion();
+                }
+            }
+        }];
+    }
     if (!waitStoreCache) {
         if (completion) {
             completion();
@@ -608,8 +667,9 @@ static id<SDImageLoader> _defaultImageLoader;
 - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
                              completion:(nullable SDInternalCompletionBlock)completionBlock
                                   error:(nullable NSError *)error
+                                  queue:(nullable SDCallbackQueue *)queue
                                     url:(nullable NSURL *)url {
-    [self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES url:url];
+    [self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES queue:queue url:url];
 }
 
 - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
@@ -619,22 +679,21 @@ static id<SDImageLoader> _defaultImageLoader;
                                   error:(nullable NSError *)error
                               cacheType:(SDImageCacheType)cacheType
                                finished:(BOOL)finished
+                                  queue:(nullable SDCallbackQueue *)queue
                                     url:(nullable NSURL *)url {
-    dispatch_main_async_safe(^{
-        if (completionBlock) {
+    if (completionBlock) {
+        [(queue ?: SDCallbackQueue.mainQueue) async:^{
             completionBlock(image, data, error, cacheType, finished, url);
-        }
-    });
+        }];
+    }
 }
 
 - (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url
                               error:(nonnull NSError *)error
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context {
-    id<SDImageLoader> imageLoader;
-    if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
-        imageLoader = context[SDWebImageContextImageLoader];
-    } else {
+    id<SDImageLoader> imageLoader = context[SDWebImageContextImageLoader];
+    if (!imageLoader) {
         imageLoader = self.imageLoader;
     }
     // Check whether we should block failed url
@@ -696,12 +755,20 @@ static id<SDImageLoader> _defaultImageLoader;
 
 @implementation SDWebImageCombinedOperation
 
+- (BOOL)isCancelled {
+    // Need recursive lock (user's cancel block may check isCancelled), do not use SD_LOCK
+    @synchronized (self) {
+        return _cancelled;
+    }
+}
+
 - (void)cancel {
+    // Need recursive lock (user's cancel block may check isCancelled), do not use SD_LOCK
     @synchronized(self) {
-        if (self.isCancelled) {
+        if (_cancelled) {
             return;
         }
-        self.cancelled = YES;
+        _cancelled = YES;
         if (self.cacheOperation) {
             [self.cacheOperation cancel];
             self.cacheOperation = nil;

+ 6 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageOperation.h

@@ -11,8 +11,14 @@
 /// A protocol represents cancelable operation.
 @protocol SDWebImageOperation <NSObject>
 
+/// Cancel the operation
 - (void)cancel;
 
+@optional
+
+/// Whether the operation has been cancelled.
+@property (nonatomic, assign, readonly, getter=isCancelled) BOOL cancelled;
+
 @end
 
 /// NSOperation conform to `SDWebImageOperation`.

+ 29 - 4
Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.h

@@ -76,20 +76,23 @@ typedef void(^SDWebImagePrefetcherCompletionBlock)(NSUInteger noOfFinishedUrls,
 
 /**
  * The options for prefetcher. Defaults to SDWebImageLowPriority.
+ * @deprecated Prefetcher is designed to be used shared and should not effect others. So in 5.15.0 we added API  `prefetchURLs:options:context:`. If you want global control, try to use `SDWebImageOptionsProcessor` in manager level.
  */
-@property (nonatomic, assign) SDWebImageOptions options;
+@property (nonatomic, assign) SDWebImageOptions options API_DEPRECATED("Use individual prefetch options param instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 /**
  * The context for prefetcher. Defaults to nil.
+ * @deprecated Prefetcher is designed to be used shared and should not effect others. So in 5.15.0 we added API  `prefetchURLs:options:context:`. If you want global control, try to use `SDWebImageOptionsProcessor` in `SDWebImageManager.optionsProcessor`.
  */
-@property (nonatomic, copy, nullable) SDWebImageContext *context;
+@property (nonatomic, copy, nullable) SDWebImageContext *context API_DEPRECATED("Use individual prefetch context param instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 /**
  * Queue options for prefetcher when call the progressBlock, completionBlock and delegate methods. Defaults to Main Queue.
- * @note The call is asynchronously to avoid blocking target queue.
+ * @deprecated 5.15.0 introduce SDCallbackQueue, use that is preferred and has higher priority. The set/get to this property will translate to that instead.
+ * @note The call is asynchronously to avoid blocking target queue. (see SDCallbackPolicyDispatch)
  * @note The delegate queue should be set before any prefetching start and may not be changed during prefetching to avoid thread-safe problem.
  */
-@property (strong, nonatomic, nonnull) dispatch_queue_t delegateQueue;
+@property (strong, nonatomic, nonnull) dispatch_queue_t delegateQueue API_DEPRECATED("Use SDWebImageContextCallbackQueue context param instead, see SDCallbackQueue", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0));
 
 /**
  * The delegate for the prefetcher. Defaults to nil.
@@ -134,6 +137,28 @@ typedef void(^SDWebImagePrefetcherCompletionBlock)(NSUInteger noOfFinishedUrls,
                                           progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
                                          completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;
 
+/**
+ * Assign list of URLs to let SDWebImagePrefetcher to queue the prefetching. It based on the image manager so the image may from the cache and network according to the `options` property.
+ * Prefetching is separate to each other, which means the progressBlock and completionBlock you provide is bind to the prefetching for the list of urls.
+ * Attention that call this will not cancel previous fetched urls. You should keep the token return by this to cancel or cancel all the prefetch.
+ *
+ * @param urls            list of URLs to prefetch
+ * @param options         The options to use when downloading the image. @see SDWebImageOptions for the possible values.
+ * @param context         A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
+ * @param progressBlock   block to be called when progress updates;
+ *                        first parameter is the number of completed (successful or not) requests,
+ *                        second parameter is the total number of images originally requested to be prefetched
+ * @param completionBlock block to be called when the current prefetching is completed
+ *                        first param is the number of completed (successful or not) requests,
+ *                        second parameter is the number of skipped requests
+ * @return the token to cancel the current prefetching.
+ */
+- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
+                                           options:(SDWebImageOptions)options
+                                           context:(nullable SDWebImageContext *)context
+                                          progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
+                                         completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;
+
 /**
  * Remove and cancel all the prefeching for the prefetcher.
  */

+ 79 - 43
Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.m

@@ -8,9 +8,16 @@
 
 #import "SDWebImagePrefetcher.h"
 #import "SDAsyncBlockOperation.h"
+#import "SDCallbackQueue.h"
 #import "SDInternalMacros.h"
 #import <stdatomic.h>
 
+@interface SDCallbackQueue ()
+
+@property (nonatomic, strong, nonnull) dispatch_queue_t queue;
+
+@end
+
 @interface SDWebImagePrefetchToken () {
     @public
     // Though current implementation, `SDWebImageManager` completion block is always on main queue. But however, there is no guarantee in docs. And we may introduce config to specify custom queue in the future.
@@ -30,6 +37,8 @@
 @property (nonatomic, strong) NSPointerArray *loadOperations;
 @property (nonatomic, strong) NSPointerArray *prefetchOperations;
 @property (nonatomic, weak) SDWebImagePrefetcher *prefetcher;
+@property (nonatomic, assign) SDWebImageOptions options;
+@property (nonatomic, copy, nullable) SDWebImageContext *context;
 @property (nonatomic, copy, nullable) SDWebImagePrefetcherCompletionBlock completionBlock;
 @property (nonatomic, copy, nullable) SDWebImagePrefetcherProgressBlock progressBlock;
 
@@ -40,6 +49,7 @@
 @property (strong, nonatomic, nonnull) SDWebImageManager *manager;
 @property (strong, atomic, nonnull) NSMutableSet<SDWebImagePrefetchToken *> *runningTokens;
 @property (strong, nonatomic, nonnull) NSOperationQueue *prefetchQueue;
+@property (strong, nonatomic, nullable) SDCallbackQueue *callbackQueue;
 
 @end
 
@@ -63,7 +73,6 @@
         _manager = manager;
         _runningTokens = [NSMutableSet set];
         _options = SDWebImageLowPriority;
-        _delegateQueue = dispatch_get_main_queue();
         _prefetchQueue = [NSOperationQueue new];
         self.maxConcurrentPrefetchCount = 3;
     }
@@ -78,6 +87,17 @@
     return self.prefetchQueue.maxConcurrentOperationCount;
 }
 
+- (void)setDelegateQueue:(dispatch_queue_t)delegateQueue {
+    // Deprecate and translate to SDCallbackQueue
+    _callbackQueue = [[SDCallbackQueue alloc] initWithDispatchQueue:delegateQueue];
+    _callbackQueue.policy = SDCallbackPolicyDispatch;
+}
+
+- (dispatch_queue_t)delegateQueue {
+    // Deprecate and translate to SDCallbackQueue
+    return (_callbackQueue ?: SDCallbackQueue.mainQueue).queue;
+}
+
 #pragma mark - Prefetch
 - (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls {
     return [self prefetchURLs:urls progress:nil completed:nil];
@@ -86,6 +106,14 @@
 - (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
                                           progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
                                          completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock {
+    return [self prefetchURLs:urls options:self.options context:self.context progress:progressBlock completed:completionBlock];
+}
+
+- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
+                                           options:(SDWebImageOptions)options
+                                           context:(nullable SDWebImageContext *)context
+                                          progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
+                                         completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock {
     if (!urls || urls.count == 0) {
         if (completionBlock) {
             completionBlock(0, 0);
@@ -95,6 +123,8 @@
     SDWebImagePrefetchToken *token = [SDWebImagePrefetchToken new];
     token.prefetcher = self;
     token.urls = urls;
+    token.options = options;
+    token.context = context;
     token->_skippedCount = 0;
     token->_finishedCount = 0;
     token->_totalCount = token.urls.count;
@@ -111,49 +141,47 @@
 
 - (void)startPrefetchWithToken:(SDWebImagePrefetchToken * _Nonnull)token {
     for (NSURL *url in token.urls) {
-        @autoreleasepool {
-            @weakify(self);
-            SDAsyncBlockOperation *prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^(SDAsyncBlockOperation * _Nonnull asyncOperation) {
+        @weakify(self);
+        SDAsyncBlockOperation *prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^(SDAsyncBlockOperation * _Nonnull asyncOperation) {
+            @strongify(self);
+            if (!self || asyncOperation.isCancelled) {
+                return;
+            }
+            id<SDWebImageOperation> operation = [self.manager loadImageWithURL:url options:token.options context:token.context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
                 @strongify(self);
-                if (!self || asyncOperation.isCancelled) {
+                if (!self) {
                     return;
                 }
-                id<SDWebImageOperation> operation = [self.manager loadImageWithURL:url options:self.options context:self.context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
-                    @strongify(self);
-                    if (!self) {
-                        return;
-                    }
-                    if (!finished) {
-                        return;
-                    }
-                    atomic_fetch_add_explicit(&(token->_finishedCount), 1, memory_order_relaxed);
-                    if (error) {
-                        // Add last failed
-                        atomic_fetch_add_explicit(&(token->_skippedCount), 1, memory_order_relaxed);
-                    }
-                    
-                    // Current operation finished
-                    [self callProgressBlockForToken:token imageURL:imageURL];
-                    
-                    if (atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed) == token->_totalCount) {
-                        // All finished
-                        if (!atomic_flag_test_and_set_explicit(&(token->_isAllFinished), memory_order_relaxed)) {
-                            [self callCompletionBlockForToken:token];
-                            [self removeRunningToken:token];
-                        }
+                if (!finished) {
+                    return;
+                }
+                atomic_fetch_add_explicit(&(token->_finishedCount), 1, memory_order_relaxed);
+                if (error) {
+                    // Add last failed
+                    atomic_fetch_add_explicit(&(token->_skippedCount), 1, memory_order_relaxed);
+                }
+                
+                // Current operation finished
+                [self callProgressBlockForToken:token imageURL:imageURL];
+                
+                if (atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed) == token->_totalCount) {
+                    // All finished
+                    if (!atomic_flag_test_and_set_explicit(&(token->_isAllFinished), memory_order_relaxed)) {
+                        [self callCompletionBlockForToken:token];
+                        [self removeRunningToken:token];
                     }
-                    [asyncOperation complete];
-                }];
-                NSAssert(operation != nil, @"Operation should not be nil, [SDWebImageManager loadImageWithURL:options:context:progress:completed:] break prefetch logic");
-                SD_LOCK(token->_loadOperationsLock);
-                [token.loadOperations addPointer:(__bridge void *)operation];
-                SD_UNLOCK(token->_loadOperationsLock);
+                }
+                [asyncOperation complete];
             }];
-            SD_LOCK(token->_prefetchOperationsLock);
-            [token.prefetchOperations addPointer:(__bridge void *)prefetchOperation];
-            SD_UNLOCK(token->_prefetchOperationsLock);
-            [self.prefetchQueue addOperation:prefetchOperation];
-        }
+            NSAssert(operation != nil, @"Operation should not be nil, [SDWebImageManager loadImageWithURL:options:context:progress:completed:] break prefetch logic");
+            SD_LOCK(token->_loadOperationsLock);
+            [token.loadOperations addPointer:(__bridge void *)operation];
+            SD_UNLOCK(token->_loadOperationsLock);
+        }];
+        SD_LOCK(token->_prefetchOperationsLock);
+        [token.prefetchOperations addPointer:(__bridge void *)prefetchOperation];
+        SD_UNLOCK(token->_prefetchOperationsLock);
+        [self.prefetchQueue addOperation:prefetchOperation];
     }
 }
 
@@ -175,14 +203,18 @@
     NSUInteger tokenTotalCount = [self tokenTotalCount];
     NSUInteger finishedCount = atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed);
     NSUInteger totalCount = token->_totalCount;
-    dispatch_async(self.delegateQueue, ^{
+    SDCallbackQueue *queue = token.context[SDWebImageContextCallbackQueue];
+    if (!queue) {
+        queue = self.callbackQueue;
+    }
+    [(queue ?: SDCallbackQueue.mainQueue) async:^{
         if (shouldCallDelegate) {
             [self.delegate imagePrefetcher:self didPrefetchURL:url finishedCount:tokenFinishedCount totalCount:tokenTotalCount];
         }
         if (token.progressBlock) {
             token.progressBlock(finishedCount, totalCount);
         }
-    });
+    }];
 }
 
 - (void)callCompletionBlockForToken:(SDWebImagePrefetchToken *)token {
@@ -194,14 +226,18 @@
     NSUInteger tokenSkippedCount = [self tokenSkippedCount];
     NSUInteger finishedCount = atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed);
     NSUInteger skippedCount = atomic_load_explicit(&(token->_skippedCount), memory_order_relaxed);
-    dispatch_async(self.delegateQueue, ^{
+    SDCallbackQueue *queue = token.context[SDWebImageContextCallbackQueue];
+    if (!queue) {
+        queue = self.callbackQueue;
+    }
+    [(queue ?: SDCallbackQueue.mainQueue) async:^{
         if (shoulCallDelegate) {
             [self.delegate imagePrefetcher:self didFinishWithTotalCount:tokenTotalCount skippedCount:tokenSkippedCount];
         }
         if (token.completionBlock) {
             token.completionBlock(finishedCount, skippedCount);
         }
-    });
+    }];
 }
 
 #pragma mark - Helper

+ 6 - 0
Pods/SDWebImage/SDWebImage/Core/UIImage+ForceDecode.h

@@ -15,6 +15,12 @@
 
 /**
  A bool value indicating whether the image has already been decoded. This can help to avoid extra force decode.
+ Force decode is used for 2 cases:
+ -- 1. for ImageIO created image (via `CGImageCreateWithImageSource` SPI), it's lazy and we trigger the decode before rendering
+ -- 2. for non-ImageIO created image (via `CGImageCreate` API), we can ensure it's alignment is suitable to render on screen without copy by CoreAnimation
+ @note For coder plugin developer, always use the SDImageCoderHelper's `colorSpaceGetDeviceRGB`/`preferredPixelFormat` to create CGImage.
+ @note For more information why force decode, see: https://github.com/path/FastImageCache#byte-alignment
+ @note From v5.17.0, the default value is always NO. Use `SDImageForceDecodePolicy` to control complicated policy.
  */
 @property (nonatomic, assign) BOOL sd_isDecoded;
 

+ 2 - 1
Pods/SDWebImage/SDWebImage/Core/UIImage+ForceDecode.m

@@ -9,12 +9,13 @@
 #import "UIImage+ForceDecode.h"
 #import "SDImageCoderHelper.h"
 #import "objc/runtime.h"
+#import "NSImage+Compatibility.h"
 
 @implementation UIImage (ForceDecode)
 
 - (BOOL)sd_isDecoded {
     NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isDecoded));
-    return value.boolValue;
+    return [value boolValue];
 }
 
 - (void)setSd_isDecoded:(BOOL)sd_isDecoded {

+ 21 - 0
Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.h

@@ -8,6 +8,7 @@
 
 #import "SDWebImageCompat.h"
 #import "NSData+ImageContentType.h"
+#import "SDImageCoder.h"
 
 /**
  UIImage category for image metadata, including animation, loop count, format, incremental, etc.
@@ -65,4 +66,24 @@
  */
 @property (nonatomic, assign) BOOL sd_isIncremental;
 
+/**
+ A bool value indicating that the image is transformed from original image, so the image data may not always match original download one.
+ */
+@property (nonatomic, assign) BOOL sd_isTransformed;
+
+/**
+ A bool value indicating that the image is using thumbnail decode with smaller size, so the image data may not always match original download one.
+ @note This just check `sd_decodeOptions[.decodeThumbnailPixelSize] > CGSize.zero`
+ */
+@property (nonatomic, assign, readonly) BOOL sd_isThumbnail;
+
+/**
+ A dictionary value contains the decode options when decoded from SDWebImage loading system (say, `SDImageCacheDecodeImageData/SDImageLoaderDecode[Progressive]ImageData`)
+ It may not always available and only image decoding related options will be saved. (including [.decodeScaleFactor, .decodeThumbnailPixelSize, .decodePreserveAspectRatio, .decodeFirstFrameOnly])
+ @note This is used to identify and check the image is from thumbnail decoding, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key)
+ @warning You should not store object inside which keep strong reference to image itself, which will cause retain cycle.
+ @warning This API exist only because of current SDWebImageDownloader bad design which does not callback the context we call it. There will be refactor in future (API break), use with caution.
+ */
+@property (nonatomic, copy) SDImageCoderOptions *sd_decodeOptions;
+
 @end

+ 36 - 4
Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.m

@@ -166,10 +166,8 @@
         return imageFormat;
     }
     // Check CGImage's UTType, may return nil for non-Image/IO based image
-    if (@available(iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0, *)) {
-        CFStringRef uttype = CGImageGetUTType(self.CGImage);
-        imageFormat = [NSData sd_imageFormatFromUTType:uttype];
-    }
+    CFStringRef uttype = CGImageGetUTType(self.CGImage);
+    imageFormat = [NSData sd_imageFormatFromUTType:uttype];
     return imageFormat;
 }
 
@@ -186,4 +184,38 @@
     return value.boolValue;
 }
 
+- (void)setSd_isTransformed:(BOOL)sd_isTransformed {
+    objc_setAssociatedObject(self, @selector(sd_isTransformed), @(sd_isTransformed), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+- (BOOL)sd_isTransformed {
+    NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isTransformed));
+    return value.boolValue;
+}
+
+- (void)setSd_decodeOptions:(SDImageCoderOptions *)sd_decodeOptions {
+    objc_setAssociatedObject(self, @selector(sd_decodeOptions), sd_decodeOptions, OBJC_ASSOCIATION_COPY_NONATOMIC);
+}
+
+-(BOOL)sd_isThumbnail {
+    CGSize thumbnailSize = CGSizeZero;
+    NSValue *thumbnailSizeValue = self.sd_decodeOptions[SDImageCoderDecodeThumbnailPixelSize];
+    if (thumbnailSizeValue != nil) {
+    #if SD_MAC
+        thumbnailSize = thumbnailSizeValue.sizeValue;
+    #else
+        thumbnailSize = thumbnailSizeValue.CGSizeValue;
+    #endif
+    }
+    return thumbnailSize.width > 0 && thumbnailSize.height > 0;
+}
+
+- (SDImageCoderOptions *)sd_decodeOptions {
+    SDImageCoderOptions *value = objc_getAssociatedObject(self, @selector(sd_decodeOptions));
+    if ([value isKindOfClass:NSDictionary.class]) {
+        return value;
+    }
+    return nil;
+}
+
 @end

+ 4 - 0
Pods/SDWebImage/SDWebImage/Core/UIImage+Transform.h

@@ -8,9 +8,13 @@
 
 #import "SDWebImageCompat.h"
 
+/// The scale mode to apply when image drawing on a container with different sizes.
 typedef NS_ENUM(NSUInteger, SDImageScaleMode) {
+    /// The option to scale the content to fit the size of itself by changing the aspect ratio of the content if necessary.
     SDImageScaleModeFill = 0,
+    /// The option to scale the content to fit the size of the view by maintaining the aspect ratio. Any remaining area of the view’s bounds is transparent.
     SDImageScaleModeAspectFit = 1,
+    /// The option to scale the content to fill the size of the view. Some portion of the content may be clipped to fill the view’s bounds.
     SDImageScaleModeAspectFill = 2
 };
 

+ 159 - 34
Pods/SDWebImage/SDWebImage/Core/UIImage+Transform.m

@@ -57,7 +57,96 @@ static inline CGRect SDCGRectFitWithScaleMode(CGRect rect, CGSize size, SDImageS
     return rect;
 }
 
-static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitmapInfo) {
+static inline UIColor * SDGetColorFromGrayscale(Pixel_88 pixel, CGBitmapInfo bitmapInfo, CGColorSpaceRef cgColorSpace) {
+    // Get alpha info, byteOrder info
+    CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
+    CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
+    CGFloat w = 0, a = 1;
+    
+    BOOL byteOrderNormal = NO;
+    switch (byteOrderInfo) {
+        case kCGBitmapByteOrderDefault: {
+            byteOrderNormal = YES;
+        } break;
+        case kCGBitmapByteOrder32Little: {
+        } break;
+        case kCGBitmapByteOrder32Big: {
+            byteOrderNormal = YES;
+        } break;
+        default: break;
+    }
+    switch (alphaInfo) {
+        case kCGImageAlphaPremultipliedFirst:
+        case kCGImageAlphaFirst: {
+            if (byteOrderNormal) {
+                // AW
+                a = pixel[0] / 255.0;
+                w = pixel[1] / 255.0;
+            } else {
+                // WA
+                w = pixel[0] / 255.0;
+                a = pixel[1] / 255.0;
+            }
+        }
+            break;
+        case kCGImageAlphaPremultipliedLast:
+        case kCGImageAlphaLast: {
+            if (byteOrderNormal) {
+                // WA
+                w = pixel[0] / 255.0;
+                a = pixel[1] / 255.0;
+            } else {
+                // AW
+                a = pixel[0] / 255.0;
+                w = pixel[1] / 255.0;
+            }
+        }
+            break;
+        case kCGImageAlphaNone: {
+            // W
+            w = pixel[0] / 255.0;
+        }
+            break;
+        case kCGImageAlphaNoneSkipLast: {
+            if (byteOrderNormal) {
+                // WX
+                w = pixel[0] / 255.0;
+            } else {
+                // XW
+                a = pixel[1] / 255.0;
+            }
+        }
+            break;
+        case kCGImageAlphaNoneSkipFirst: {
+            if (byteOrderNormal) {
+                // XW
+                a = pixel[1] / 255.0;
+            } else {
+                // WX
+                a = pixel[0] / 255.0;
+            }
+        }
+            break;
+        case kCGImageAlphaOnly: {
+            // A
+            a = pixel[0] / 255.0;
+        }
+            break;
+        default:
+            break;
+    }
+#if SD_MAC
+    // Mac supports ColorSync, to ensure the same bahvior, we convert color to sRGB
+    NSColorSpace *colorSpace = [[NSColorSpace alloc] initWithCGColorSpace:cgColorSpace];
+    CGFloat components[2] = {w, a};
+    NSColor *color = [NSColor colorWithColorSpace:colorSpace components:components count:2];
+    return [color colorUsingColorSpace:NSColorSpace.genericGamma22GrayColorSpace];
+#else
+    return [UIColor colorWithWhite:w alpha:a];
+#endif
+}
+
+static inline UIColor * SDGetColorFromRGBA(Pixel_8888 pixel, CGBitmapInfo bitmapInfo, CGColorSpaceRef cgColorSpace) {
     // Get alpha info, byteOrder info
     CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
     CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
@@ -68,8 +157,10 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma
         case kCGBitmapByteOrderDefault: {
             byteOrderNormal = YES;
         } break;
+        case kCGBitmapByteOrder16Little:
         case kCGBitmapByteOrder32Little: {
         } break;
+        case kCGBitmapByteOrder16Big:
         case kCGBitmapByteOrder32Big: {
             byteOrderNormal = YES;
         } break;
@@ -160,8 +251,15 @@ static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitma
         default:
             break;
     }
-    
+#if SD_MAC
+    // Mac supports ColorSync, to ensure the same bahvior, we convert color to sRGB
+    NSColorSpace *colorSpace = [[NSColorSpace alloc] initWithCGColorSpace:cgColorSpace];
+    CGFloat components[4] = {r, g, b, a};
+    NSColor *color = [NSColor colorWithColorSpace:colorSpace components:components count:4];
+    return [color colorUsingColorSpace:NSColorSpace.sRGBColorSpace];
+#else
     return [UIColor colorWithRed:r green:g blue:b alpha:a];
+#endif
 }
 
 #if SD_UIKIT || SD_MAC
@@ -470,18 +568,37 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
     size_t components = CGImageGetBitsPerPixel(imageRef) / CGImageGetBitsPerComponent(imageRef);
     CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
     
-    CFRange range = CFRangeMake(bytesPerRow * point.y + components * point.x, 4);
+    CFRange range = CFRangeMake(bytesPerRow * point.y + components * point.x, components);
     if (CFDataGetLength(data) < range.location + range.length) {
         CFRelease(data);
         CGImageRelease(imageRef);
         return nil;
     }
-    Pixel_8888 pixel = {0};
-    CFDataGetBytes(data, range, pixel);
-    CFRelease(data);
-    CGImageRelease(imageRef);
-    // Convert to color
-    return SDGetColorFromPixel(pixel, bitmapInfo);
+    // Get color space for transform
+    CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef);
+    
+    // greyscale
+    if (components == 2) {
+        Pixel_88 pixel = {0};
+        CFDataGetBytes(data, range, pixel);
+        CFRelease(data);
+        CGImageRelease(imageRef);
+        // Convert to color
+        return SDGetColorFromGrayscale(pixel, bitmapInfo, colorSpace);
+    } else if (components == 3 || components == 4) {
+        // RGB/RGBA
+        Pixel_8888 pixel = {0};
+        CFDataGetBytes(data, range, pixel);
+        CFRelease(data);
+        CGImageRelease(imageRef);
+        // Convert to color
+        return SDGetColorFromRGBA(pixel, bitmapInfo, colorSpace);
+    } else {
+        NSLog(@"Unsupported components: %zu", components);
+        CFRelease(data);
+        CGImageRelease(imageRef);
+        return nil;
+    }
 }
 
 - (nullable NSArray<UIColor *> *)sd_colorsWithRect:(CGRect)rect {
@@ -539,17 +656,34 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
     // Convert to color
     CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
     NSMutableArray<UIColor *> *colors = [NSMutableArray arrayWithCapacity:CGRectGetWidth(rect) * CGRectGetHeight(rect)];
-    for (size_t index = start; index < end; index += 4) {
+    // ColorSpace
+    CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef);
+    for (size_t index = start; index < end; index += components) {
         if (index >= row * bytesPerRow + col * components) {
             // Index beyond the end of current row, go next row
             row++;
             index = row * bytesPerRow + CGRectGetMinX(rect) * components;
-            index -= 4;
+            index -= components;
             continue;
         }
-        Pixel_8888 pixel = {pixels[index], pixels[index+1], pixels[index+2], pixels[index+3]};
-        UIColor *color = SDGetColorFromPixel(pixel, bitmapInfo);
-        [colors addObject:color];
+        UIColor *color;
+        if (components == 2) {
+            Pixel_88 pixel = {pixels[index], pixel[index+1]};
+            color = SDGetColorFromGrayscale(pixel, bitmapInfo, colorSpace);
+        } else {
+            if (components == 3) {
+                Pixel_8888 pixel = {pixels[index], pixels[index+1], pixels[index+2], 0};
+                color = SDGetColorFromRGBA(pixel, bitmapInfo, colorSpace);
+            } else if (components == 4) {
+                Pixel_8888 pixel = {pixels[index], pixels[index+1], pixels[index+2], pixels[index+3]};
+                color = SDGetColorFromRGBA(pixel, bitmapInfo, colorSpace);
+            } else {
+                NSLog(@"Unsupported components: %zu", components);
+            }
+        }
+        if (color) {
+            [colors addObject:color];
+        }
     }
     CFRelease(data);
     CGImageRelease(imageRef);
@@ -588,15 +722,8 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
 #endif
     
     CGImageRef imageRef = self.CGImage;
-    
-    //convert to BGRA if it isn't
-    if (CGImageGetBitsPerPixel(imageRef) != 32 ||
-        CGImageGetBitsPerComponent(imageRef) != 8 ||
-        !((CGImageGetBitmapInfo(imageRef) & kCGBitmapAlphaInfoMask))) {
-        SDGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
-        [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
-        imageRef = SDGraphicsGetImageFromCurrentImageContext().CGImage;
-        SDGraphicsEndImageContext();
+    if (!imageRef) {
+        return nil;
     }
     
     vImage_Buffer effect = {}, scratch = {};
@@ -613,7 +740,7 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
     };
     
     vImage_Error err;
-    err = vImageBuffer_InitWithCGImage(&effect, &format, NULL, imageRef, kvImageNoFlags);
+    err = vImageBuffer_InitWithCGImage(&effect, &format, NULL, imageRef, kvImageNoFlags); // vImage will convert to format we requests, no need `vImageConvert`
     if (err != kvImageNoError) {
         NSLog(@"UIImage+Transform error: vImageBuffer_InitWithCGImage returned error code %zi for inputImage: %@", err, self);
         return nil;
@@ -627,6 +754,7 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
     input = &effect;
     output = &scratch;
     
+    // See: https://developer.apple.com/library/archive/samplecode/UIImageEffects/Introduction/Intro.html
     if (hasBlur) {
         // A description of how to compute the box kernel width from the Gaussian
         // radius (aka standard deviation) appears in the SVG spec:
@@ -643,19 +771,16 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
         if (inputRadius - 2.0 < __FLT_EPSILON__) inputRadius = 2.0;
         uint32_t radius = floor(inputRadius * 3.0 * sqrt(2 * M_PI) / 4 + 0.5);
         radius |= 1; // force radius to be odd so that the three box-blur methodology works.
-        int iterations;
-        if (blurRadius * scale < 0.5) iterations = 1;
-        else if (blurRadius * scale < 1.5) iterations = 2;
-        else iterations = 3;
         NSInteger tempSize = vImageBoxConvolve_ARGB8888(input, output, NULL, 0, 0, radius, radius, NULL, kvImageGetTempBufferSize | kvImageEdgeExtend);
         void *temp = malloc(tempSize);
-        for (int i = 0; i < iterations; i++) {
-            vImageBoxConvolve_ARGB8888(input, output, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend);
-            vImage_Buffer *tmp = input;
-            input = output;
-            output = tmp;
-        }
+        vImageBoxConvolve_ARGB8888(input, output, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend);
+        vImageBoxConvolve_ARGB8888(output, input, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend);
+        vImageBoxConvolve_ARGB8888(input, output, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend);
         free(temp);
+        
+        vImage_Buffer *tmp = input;
+        input = output;
+        output = tmp;
     }
     
     CGImageRef effectCGImage = NULL;

+ 8 - 7
Pods/SDWebImage/SDWebImage/Core/UIView+WebCache.h

@@ -71,14 +71,15 @@ typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable ima
  *   block is called a last time with the full image and the last parameter set to YES.
  *
  *   The last parameter is the original image URL
+ *  @return The returned operation for cancelling cache and download operation, typically type is `SDWebImageCombinedOperation`
  */
-- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
-                  placeholderImage:(nullable UIImage *)placeholder
-                           options:(SDWebImageOptions)options
-                           context:(nullable SDWebImageContext *)context
-                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
-                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
-                         completed:(nullable SDInternalCompletionBlock)completedBlock;
+- (nullable id<SDWebImageOperation>)sd_internalSetImageWithURL:(nullable NSURL *)url
+                                              placeholderImage:(nullable UIImage *)placeholder
+                                                       options:(SDWebImageOptions)options
+                                                       context:(nullable SDWebImageContext *)context
+                                                 setImageBlock:(nullable SDSetImageBlock)setImageBlock
+                                                      progress:(nullable SDImageLoaderProgressBlock)progressBlock
+                                                     completed:(nullable SDInternalCompletionBlock)completedBlock;
 
 /**
  * Cancel the current image load

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott