Repository: Tencent/MMKV
Branch: master
Commit: f7f0f47fa940
Files: 614
Total size: 16.0 MB
Directory structure:
gitextract_t9qzaftc/
├── .clang-format
├── .dockerignore
├── .github/
│ ├── FUNDING.yml
│ └── ISSUE_TEMPLATE/
│ └── bug_report.md
├── .gitignore
├── .gitmodules
├── Android/
│ └── MMKV/
│ ├── .gitignore
│ ├── build.gradle
│ ├── checkstyle.xml
│ ├── debug.keystore
│ ├── gradle/
│ │ ├── android-publish-private.gradle
│ │ ├── android-publish.gradle
│ │ ├── build_library.gradle
│ │ ├── check.gradle
│ │ └── wrapper/
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── mmkv/
│ │ ├── .gitignore
│ │ ├── CMakeLists.txt
│ │ ├── build.gradle
│ │ ├── gradle.properties
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ ├── androidTest/
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java/
│ │ │ └── com/
│ │ │ └── tencent/
│ │ │ └── mmkv/
│ │ │ ├── MMKVTest.java
│ │ │ └── MMKVTestService.java
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── aidl/
│ │ │ └── com/
│ │ │ └── tencent/
│ │ │ └── mmkv/
│ │ │ └── ParcelableMMKV.aidl
│ │ ├── cpp/
│ │ │ ├── flutter-bridge.cpp
│ │ │ └── native-bridge.cpp
│ │ └── java/
│ │ └── com/
│ │ └── tencent/
│ │ └── mmkv/
│ │ ├── MMKV.java
│ │ ├── MMKVConfig.java
│ │ ├── MMKVContentChangeNotification.java
│ │ ├── MMKVContentProvider.java
│ │ ├── MMKVHandler.java
│ │ ├── MMKVLogLevel.java
│ │ ├── MMKVProcessUtil.java
│ │ ├── MMKVRecoverStrategic.java
│ │ ├── NameSpace.java
│ │ ├── NativeBuffer.java
│ │ ├── ParcelableMMKV.java
│ │ └── UnsupportedArchitectureException.java
│ ├── mmkvannotation/
│ │ ├── .gitignore
│ │ ├── build.gradle
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── dalvik/
│ │ └── annotation/
│ │ └── optimization/
│ │ └── FastNative.java
│ ├── mmkvdemo/
│ │ ├── .gitignore
│ │ ├── build.gradle
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── aidl/
│ │ │ └── com/
│ │ │ └── tencent/
│ │ │ └── mmkvdemo/
│ │ │ └── IAshmemMMKV.aidl
│ │ ├── cpp/
│ │ │ ├── CMakeLists.txt
│ │ │ └── mmkvdemo.cpp
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── tencent/
│ │ │ └── mmkvdemo/
│ │ │ ├── Baseline.java
│ │ │ ├── BenchMarkBaseService.java
│ │ │ ├── FakeInfo.java
│ │ │ ├── Info.java
│ │ │ ├── MainActivity.java
│ │ │ ├── MultiProcessSharedPreferences.java
│ │ │ ├── MyApplication.java
│ │ │ ├── MyService.java
│ │ │ ├── MyService_1.java
│ │ │ ├── SQLiteKV.java
│ │ │ └── TestParcelable.java
│ │ ├── kotlin/
│ │ │ └── KotlinUsecase.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ └── ic_launcher_background.xml
│ │ ├── drawable-v24/
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── layout/
│ │ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ └── values/
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ ├── pmd-ruleset.xml
│ ├── proguard-rules/
│ │ ├── proguard-rules-android-lib.pro
│ │ └── proguard-rules-test.pro
│ └── settings.gradle
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Core/
│ ├── .gitignore
│ ├── CMakeLists.txt
│ ├── CodedInputData.cpp
│ ├── CodedInputData.h
│ ├── CodedInputDataCrypt.cpp
│ ├── CodedInputDataCrypt.h
│ ├── CodedInputDataCrypt_OSX.cpp
│ ├── CodedInputData_OSX.cpp
│ ├── CodedOutputData.cpp
│ ├── CodedOutputData.h
│ ├── Core.xcodeproj/
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace/
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata/
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata/
│ │ └── xcschemes/
│ │ ├── Core.xcscheme
│ │ └── MMKVWatchCore.xcscheme
│ ├── InterProcessLock.cpp
│ ├── InterProcessLock.h
│ ├── InterProcessLock_Android.cpp
│ ├── InterProcessLock_Win32.cpp
│ ├── KeyValueHolder.cpp
│ ├── KeyValueHolder.h
│ ├── MMBuffer.cpp
│ ├── MMBuffer.h
│ ├── MMKV.cpp
│ ├── MMKV.h
│ ├── MMKVHandler.h
│ ├── MMKVLog.cpp
│ ├── MMKVLog.h
│ ├── MMKVLog_Android.cpp
│ ├── MMKVMetaInfo.hpp
│ ├── MMKVPredef.h
│ ├── MMKV_Android.cpp
│ ├── MMKV_IO.cpp
│ ├── MMKV_IO.h
│ ├── MMKV_OSX.cpp
│ ├── MMKV_OSX.h
│ ├── MemoryFile.cpp
│ ├── MemoryFile.h
│ ├── MemoryFile_Android.cpp
│ ├── MemoryFile_Linux.cpp
│ ├── MemoryFile_OSX.cpp
│ ├── MemoryFile_Win32.cpp
│ ├── MiniPBCoder.cpp
│ ├── MiniPBCoder.h
│ ├── MiniPBCoder_OSX.cpp
│ ├── PBEncodeItem.hpp
│ ├── PBUtility.cpp
│ ├── PBUtility.h
│ ├── ScopedLock.hpp
│ ├── ThreadLock.cpp
│ ├── ThreadLock.h
│ ├── ThreadLock_Win32.cpp
│ ├── aes/
│ │ ├── AESCrypt.cpp
│ │ ├── AESCrypt.h
│ │ └── openssl/
│ │ ├── openssl_aes.h
│ │ ├── openssl_aes_core.cpp
│ │ ├── openssl_aes_locl.h
│ │ ├── openssl_aesv8-armx.S
│ │ ├── openssl_arm_arch.h
│ │ ├── openssl_cfb128.cpp
│ │ ├── openssl_md32_common.h
│ │ ├── openssl_md5.h
│ │ ├── openssl_md5_dgst.cpp
│ │ ├── openssl_md5_locl.h
│ │ ├── openssl_md5_one.cpp
│ │ └── openssl_opensslconf.h
│ ├── core.vcxproj
│ ├── core.vcxproj.filters
│ ├── crc32/
│ │ ├── CMakeLists.txt
│ │ ├── Checksum.h
│ │ ├── crc32_armv8.cpp
│ │ └── zlib/
│ │ ├── crc32.cpp
│ │ ├── crc32.h
│ │ ├── zconf.h
│ │ └── zutil.h
│ └── fakeinclude/
│ └── MMKVCore/
│ ├── MMBuffer.h
│ ├── MMKV.h
│ ├── MMKVHandler.h
│ ├── MMKVLog.h
│ ├── MMKVPredef.h
│ ├── MemoryFile.h
│ ├── MiniPBCoder.h
│ ├── ScopedLock.hpp
│ └── ThreadLock.h
├── Dockerfile
├── LICENSE.TXT
├── MMKV.podspec
├── MMKV.xcworkspace/
│ ├── contents.xcworkspacedata
│ └── xcshareddata/
│ └── IDEWorkspaceChecks.plist
├── MMKVAppExtension.podspec
├── MMKVCore.podspec
├── MMKVWatchExtension.podspec
├── Makefile
├── OpenHarmony/
│ ├── .gitignore
│ ├── AppScope/
│ │ ├── app.json5
│ │ └── resources/
│ │ └── base/
│ │ └── element/
│ │ └── string.json
│ ├── MMKV/
│ │ ├── .gitignore
│ │ ├── BuildProfile.ets
│ │ ├── CHANGELOG.md
│ │ ├── Index.ets
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── build-profile.json5
│ │ ├── consumer-rules.txt
│ │ ├── example/
│ │ │ ├── Index.ets
│ │ │ └── Util.ets
│ │ ├── hvigorfile.ts
│ │ ├── obfuscation-rules.txt
│ │ ├── oh-package.json5
│ │ └── src/
│ │ └── main/
│ │ ├── cpp/
│ │ │ ├── CMakeLists.txt
│ │ │ ├── flutter-bridge.cpp
│ │ │ ├── native_bridge.cpp
│ │ │ └── types/
│ │ │ └── libmmkv/
│ │ │ ├── index.d.ts
│ │ │ └── oh-package.json5
│ │ ├── ets/
│ │ │ └── utils/
│ │ │ ├── MMKV.ets
│ │ │ ├── MMKVConfig.ets
│ │ │ ├── MMKVHandler.ets
│ │ │ ├── MMKVLogLevel.ets
│ │ │ ├── NativeBuffer.ets
│ │ │ └── Util.ts
│ │ └── module.json5
│ ├── build-profile.json5
│ ├── dependencies/
│ │ ├── hvigor-4.3.0.tgz
│ │ └── hvigor-ohos-plugin-4.3.0.tgz
│ ├── entry/
│ │ ├── .gitignore
│ │ ├── build-profile.json5
│ │ ├── hvigorfile.ts
│ │ ├── obfuscation-rules.txt
│ │ ├── oh-package.json5
│ │ └── src/
│ │ ├── main/
│ │ │ ├── cpp/
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── napi_init.cpp
│ │ │ │ └── types/
│ │ │ │ └── libentry/
│ │ │ │ ├── index.d.ts
│ │ │ │ └── oh-package.json5
│ │ │ ├── ets/
│ │ │ │ ├── Util/
│ │ │ │ │ ├── Baseline.ets
│ │ │ │ │ ├── MyMMKVHandler.ets
│ │ │ │ │ └── Util.ets
│ │ │ │ ├── entryability/
│ │ │ │ │ └── EntryAbility.ets
│ │ │ │ ├── pages/
│ │ │ │ │ └── Index.ets
│ │ │ │ └── process/
│ │ │ │ └── DemoProcess.ets
│ │ │ ├── module.json5
│ │ │ └── resources/
│ │ │ ├── base/
│ │ │ │ ├── element/
│ │ │ │ │ ├── color.json
│ │ │ │ │ └── string.json
│ │ │ │ ├── media/
│ │ │ │ │ └── layered_image.json
│ │ │ │ └── profile/
│ │ │ │ └── main_pages.json
│ │ │ ├── en_US/
│ │ │ │ └── element/
│ │ │ │ └── string.json
│ │ │ └── zh_CN/
│ │ │ └── element/
│ │ │ └── string.json
│ │ ├── mock/
│ │ │ └── mock-config.json5
│ │ ├── ohosTest/
│ │ │ ├── ets/
│ │ │ │ ├── test/
│ │ │ │ │ ├── Ability.test.ets
│ │ │ │ │ └── List.test.ets
│ │ │ │ ├── testability/
│ │ │ │ │ ├── TestAbility.ets
│ │ │ │ │ └── pages/
│ │ │ │ │ └── Index.ets
│ │ │ │ └── testrunner/
│ │ │ │ └── OpenHarmonyTestRunner.ets
│ │ │ ├── module.json5
│ │ │ └── resources/
│ │ │ └── base/
│ │ │ ├── element/
│ │ │ │ ├── color.json
│ │ │ │ └── string.json
│ │ │ └── profile/
│ │ │ └── test_pages.json
│ │ └── test/
│ │ ├── List.test.ets
│ │ └── LocalUnit.test.ets
│ ├── hvigor/
│ │ └── hvigor-config.json5
│ ├── hvigorfile.ts
│ └── oh-package.json5
├── POSIX/
│ ├── CMakeLists.txt
│ ├── demo/
│ │ ├── CMakeLists.txt
│ │ ├── TestInterProcessLock.cpp
│ │ ├── UnitTest.cpp
│ │ ├── demo.cpp
│ │ └── process.cpp
│ ├── golang/
│ │ ├── .gitignore
│ │ ├── CMakeLists.txt
│ │ ├── callback.go
│ │ ├── go.mod
│ │ ├── golang-bridge.cpp
│ │ ├── golang-bridge.h
│ │ ├── mmkv.go
│ │ ├── mmkv_test.go
│ │ └── test/
│ │ ├── go.mod
│ │ └── main.go
│ └── src/
│ ├── CMakeLists.txt
│ └── libmmkv.cpp
├── Package.swift
├── Python/
│ ├── CMakeLists.txt
│ ├── README.md
│ ├── demo.py
│ ├── libmmkv_python.cpp
│ ├── setup.py
│ └── unit_test.py
├── README.md
├── README_CN.md
├── SECURITY.md
├── Script/
│ ├── dumpJavaSignature.py
│ └── formatCode.py
├── Win32/
│ ├── .clang-format
│ ├── Win32.sln
│ ├── Win32Demo/
│ │ ├── Win32Demo.cpp
│ │ ├── Win32Demo.vcxproj
│ │ ├── Win32Demo.vcxproj.filters
│ │ ├── pch.cpp
│ │ └── pch.h
│ └── Win32DemoProcess/
│ ├── Win32DemoProcess.cpp
│ ├── Win32DemoProcess.vcxproj
│ ├── Win32DemoProcess.vcxproj.filters
│ ├── pch.cpp
│ └── pch.h
├── flutter/
│ ├── .gitignore
│ ├── .metadata
│ ├── .vscode/
│ │ ├── launch.json
│ │ └── settings.json
│ ├── mmkv/
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── analysis_options.yaml
│ │ ├── example/
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ ├── android/
│ │ │ │ ├── .gitignore
│ │ │ │ ├── app/
│ │ │ │ │ ├── build.gradle
│ │ │ │ │ └── src/
│ │ │ │ │ ├── main/
│ │ │ │ │ │ ├── AndroidManifest.xml
│ │ │ │ │ │ ├── java/
│ │ │ │ │ │ │ └── com/
│ │ │ │ │ │ │ └── tencent/
│ │ │ │ │ │ │ └── mmkv_example/
│ │ │ │ │ │ │ └── MainActivity.java
│ │ │ │ │ │ └── res/
│ │ │ │ │ │ ├── drawable/
│ │ │ │ │ │ │ └── launch_background.xml
│ │ │ │ │ │ └── values/
│ │ │ │ │ │ └── styles.xml
│ │ │ │ │ └── profile/
│ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ ├── build.gradle
│ │ │ │ ├── gradle/
│ │ │ │ │ └── wrapper/
│ │ │ │ │ └── gradle-wrapper.properties
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ └── settings_aar.gradle
│ │ │ ├── ios/
│ │ │ │ ├── .gitignore
│ │ │ │ ├── Flutter/
│ │ │ │ │ ├── AppFrameworkInfo.plist
│ │ │ │ │ ├── Debug.xcconfig
│ │ │ │ │ ├── Release.xcconfig
│ │ │ │ │ └── ephemeral/
│ │ │ │ │ ├── flutter_lldb_helper.py
│ │ │ │ │ └── flutter_lldbinit
│ │ │ │ ├── Podfile
│ │ │ │ ├── Runner/
│ │ │ │ │ ├── AppDelegate.h
│ │ │ │ │ ├── AppDelegate.m
│ │ │ │ │ ├── Assets.xcassets/
│ │ │ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ │ │ └── Contents.json
│ │ │ │ │ │ └── LaunchImage.imageset/
│ │ │ │ │ │ ├── Contents.json
│ │ │ │ │ │ └── README.md
│ │ │ │ │ ├── Base.lproj/
│ │ │ │ │ │ ├── LaunchScreen.storyboard
│ │ │ │ │ │ └── Main.storyboard
│ │ │ │ │ ├── Info.plist
│ │ │ │ │ ├── Runner.entitlements
│ │ │ │ │ └── main.m
│ │ │ │ ├── Runner.xcodeproj/
│ │ │ │ │ ├── project.pbxproj
│ │ │ │ │ ├── project.xcworkspace/
│ │ │ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ │ │ └── xcshareddata/
│ │ │ │ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ │ │ │ └── WorkspaceSettings.xcsettings
│ │ │ │ │ └── xcshareddata/
│ │ │ │ │ └── xcschemes/
│ │ │ │ │ └── Runner.xcscheme
│ │ │ │ └── Runner.xcworkspace/
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ └── xcshareddata/
│ │ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ │ └── WorkspaceSettings.xcsettings
│ │ │ ├── lib/
│ │ │ │ └── main.dart
│ │ │ ├── linux/
│ │ │ │ ├── .gitignore
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── flutter/
│ │ │ │ │ ├── CMakeLists.txt
│ │ │ │ │ ├── generated_plugin_registrant.cc
│ │ │ │ │ ├── generated_plugin_registrant.h
│ │ │ │ │ └── generated_plugins.cmake
│ │ │ │ └── runner/
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── main.cc
│ │ │ │ ├── my_application.cc
│ │ │ │ └── my_application.h
│ │ │ ├── macos/
│ │ │ │ ├── .gitignore
│ │ │ │ ├── Flutter/
│ │ │ │ │ ├── Flutter-Debug.xcconfig
│ │ │ │ │ ├── Flutter-Release.xcconfig
│ │ │ │ │ └── GeneratedPluginRegistrant.swift
│ │ │ │ ├── Podfile
│ │ │ │ ├── Runner/
│ │ │ │ │ ├── AppDelegate.swift
│ │ │ │ │ ├── Assets.xcassets/
│ │ │ │ │ │ └── AppIcon.appiconset/
│ │ │ │ │ │ └── Contents.json
│ │ │ │ │ ├── Base.lproj/
│ │ │ │ │ │ └── MainMenu.xib
│ │ │ │ │ ├── Configs/
│ │ │ │ │ │ ├── AppInfo.xcconfig
│ │ │ │ │ │ ├── Debug.xcconfig
│ │ │ │ │ │ ├── Release.xcconfig
│ │ │ │ │ │ └── Warnings.xcconfig
│ │ │ │ │ ├── DebugProfile.entitlements
│ │ │ │ │ ├── Info.plist
│ │ │ │ │ ├── MainFlutterWindow.swift
│ │ │ │ │ └── Release.entitlements
│ │ │ │ ├── Runner.xcodeproj/
│ │ │ │ │ ├── project.pbxproj
│ │ │ │ │ ├── project.xcworkspace/
│ │ │ │ │ │ └── xcshareddata/
│ │ │ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ │ │ └── xcshareddata/
│ │ │ │ │ └── xcschemes/
│ │ │ │ │ └── Runner.xcscheme
│ │ │ │ ├── Runner.xcworkspace/
│ │ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ │ └── xcshareddata/
│ │ │ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ │ │ └── WorkspaceSettings.xcsettings
│ │ │ │ └── RunnerTests/
│ │ │ │ └── RunnerTests.swift
│ │ │ ├── ohos/
│ │ │ │ ├── .gitignore
│ │ │ │ ├── AppScope/
│ │ │ │ │ ├── app.json5
│ │ │ │ │ └── resources/
│ │ │ │ │ └── base/
│ │ │ │ │ └── element/
│ │ │ │ │ └── string.json
│ │ │ │ ├── build-profile.json5
│ │ │ │ ├── entry/
│ │ │ │ │ ├── .gitignore
│ │ │ │ │ ├── build-profile.json5
│ │ │ │ │ ├── hvigorfile.ts
│ │ │ │ │ ├── oh-package.json5
│ │ │ │ │ └── src/
│ │ │ │ │ ├── main/
│ │ │ │ │ │ ├── ets/
│ │ │ │ │ │ │ ├── entryability/
│ │ │ │ │ │ │ │ └── EntryAbility.ets
│ │ │ │ │ │ │ ├── pages/
│ │ │ │ │ │ │ │ └── Index.ets
│ │ │ │ │ │ │ └── plugins/
│ │ │ │ │ │ │ └── GeneratedPluginRegistrant.ets
│ │ │ │ │ │ ├── module.json5
│ │ │ │ │ │ └── resources/
│ │ │ │ │ │ ├── base/
│ │ │ │ │ │ │ ├── element/
│ │ │ │ │ │ │ │ ├── color.json
│ │ │ │ │ │ │ │ └── string.json
│ │ │ │ │ │ │ └── profile/
│ │ │ │ │ │ │ └── main_pages.json
│ │ │ │ │ │ ├── en_US/
│ │ │ │ │ │ │ └── element/
│ │ │ │ │ │ │ └── string.json
│ │ │ │ │ │ └── zh_CN/
│ │ │ │ │ │ └── element/
│ │ │ │ │ │ └── string.json
│ │ │ │ │ └── ohosTest/
│ │ │ │ │ ├── ets/
│ │ │ │ │ │ ├── test/
│ │ │ │ │ │ │ ├── Ability.test.ets
│ │ │ │ │ │ │ └── List.test.ets
│ │ │ │ │ │ ├── testability/
│ │ │ │ │ │ │ ├── TestAbility.ets
│ │ │ │ │ │ │ └── pages/
│ │ │ │ │ │ │ └── Index.ets
│ │ │ │ │ │ └── testrunner/
│ │ │ │ │ │ └── OpenHarmonyTestRunner.ts
│ │ │ │ │ ├── module.json5
│ │ │ │ │ └── resources/
│ │ │ │ │ └── base/
│ │ │ │ │ ├── element/
│ │ │ │ │ │ ├── color.json
│ │ │ │ │ │ └── string.json
│ │ │ │ │ └── profile/
│ │ │ │ │ └── test_pages.json
│ │ │ │ ├── hvigor/
│ │ │ │ │ └── hvigor-config.json5
│ │ │ │ ├── hvigorfile.ts
│ │ │ │ └── oh-package.json5
│ │ │ ├── pubspec.yaml
│ │ │ └── windows/
│ │ │ ├── .gitignore
│ │ │ ├── CMakeLists.txt
│ │ │ ├── flutter/
│ │ │ │ ├── CMakeLists.txt
│ │ │ │ ├── generated_plugin_registrant.cc
│ │ │ │ ├── generated_plugin_registrant.h
│ │ │ │ └── generated_plugins.cmake
│ │ │ └── runner/
│ │ │ ├── CMakeLists.txt
│ │ │ ├── Runner.rc
│ │ │ ├── flutter_window.cpp
│ │ │ ├── flutter_window.h
│ │ │ ├── main.cpp
│ │ │ ├── resource.h
│ │ │ ├── runner.exe.manifest
│ │ │ ├── utils.cpp
│ │ │ ├── utils.h
│ │ │ ├── win32_window.cpp
│ │ │ └── win32_window.h
│ │ ├── lib/
│ │ │ └── mmkv.dart
│ │ ├── mmkv.iml
│ │ ├── pubspec.yaml
│ │ ├── test/
│ │ │ └── mmkv_test.dart
│ │ └── tool/
│ │ ├── fix_mmkv_plugin_name.rb
│ │ └── mmkvpodhelper.rb
│ ├── mmkv.iml
│ ├── mmkv_android/
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── android/
│ │ │ ├── .gitignore
│ │ │ ├── build.gradle
│ │ │ ├── gradle/
│ │ │ │ └── wrapper/
│ │ │ │ ├── gradle-wrapper.jar
│ │ │ │ └── gradle-wrapper.properties
│ │ │ ├── gradle.properties
│ │ │ ├── gradlew
│ │ │ ├── gradlew.bat
│ │ │ ├── settings.gradle
│ │ │ └── src/
│ │ │ └── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java/
│ │ │ └── com/
│ │ │ └── tencent/
│ │ │ └── mmkv/
│ │ │ └── MMKVPlugin.java
│ │ ├── lib/
│ │ │ └── mmkv_android.dart
│ │ └── pubspec.yaml
│ ├── mmkv_ios/
│ │ ├── .gitignore
│ │ ├── .metadata
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── darwin/
│ │ │ ├── .gitignore
│ │ │ ├── Classes/
│ │ │ │ ├── MMKVPlugin.h
│ │ │ │ ├── MMKVPlugin.mm
│ │ │ │ ├── flutter-bridge.h
│ │ │ │ └── flutter-bridge.mm
│ │ │ └── mmkv_ios.podspec
│ │ ├── lib/
│ │ │ └── mmkv_ios.dart
│ │ └── pubspec.yaml
│ ├── mmkv_linux/
│ │ ├── .gitignore
│ │ ├── .metadata
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── analysis_options.yaml
│ │ ├── lib/
│ │ │ └── mmkv_linux.dart
│ │ ├── linux/
│ │ │ ├── CMakeLists.txt
│ │ │ ├── flutter-bridge.cpp
│ │ │ ├── include/
│ │ │ │ └── mmkv_linux/
│ │ │ │ └── mmkv_linux_plugin.h
│ │ │ └── mmkv_linux_plugin.cc
│ │ └── pubspec.yaml
│ ├── mmkv_ohos/
│ │ ├── .gitignore
│ │ ├── .metadata
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── analysis_options.yaml
│ │ ├── lib/
│ │ │ └── mmkv_ohos.dart
│ │ ├── ohos/
│ │ │ ├── .gitignore
│ │ │ ├── build-profile.json5
│ │ │ ├── hvigorfile.ts
│ │ │ ├── index.ets
│ │ │ ├── oh-package.json5
│ │ │ └── src/
│ │ │ └── main/
│ │ │ ├── ets/
│ │ │ │ └── components/
│ │ │ │ └── plugin/
│ │ │ │ └── MMKVPlugin.ets
│ │ │ └── module.json5
│ │ └── pubspec.yaml
│ ├── mmkv_platform_interface/
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── lib/
│ │ │ ├── mmkv_platform_ffi.dart
│ │ │ └── mmkv_platform_interface.dart
│ │ └── pubspec.yaml
│ └── mmkv_win32/
│ ├── .gitignore
│ ├── .metadata
│ ├── CHANGELOG.md
│ ├── LICENSE
│ ├── README.md
│ ├── analysis_options.yaml
│ ├── lib/
│ │ └── mmkv_win32.dart
│ └── pubspec.yaml
└── iOS/
├── MMKV/
│ ├── MMKV/
│ │ ├── AutoCleanInfo.hpp
│ │ ├── MMKV.h
│ │ ├── MMKVAppExtension/
│ │ │ ├── include/
│ │ │ │ └── MMKVAppExtension/
│ │ │ │ └── MMKV.h
│ │ │ └── libMMKV.mm
│ │ ├── MMKVHandler.h
│ │ ├── Resources/
│ │ │ ├── Info.plist
│ │ │ └── PrivacyInfo.xcprivacy
│ │ ├── fakeinclude/
│ │ │ └── MMKV/
│ │ │ └── MMKV.h
│ │ └── libMMKV.mm
│ └── MMKV.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata/
│ └── xcschemes/
│ ├── MMKV For App Extension.xcscheme
│ ├── MMKV Static.xcscheme
│ ├── MMKV.xcscheme
│ └── MMKVWatchExtension.xcscheme
└── MMKVDemo/
├── Config.xcconfig
├── MMKVCatalystDemo/
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Assets.xcassets/
│ │ ├── AccentColor.colorset/
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj/
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ ├── MMKVCatalystDemo.entitlements
│ ├── SceneDelegate.h
│ ├── SceneDelegate.m
│ ├── ViewController.h
│ ├── ViewController.mm
│ └── main.m
├── MMKVDemo/
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── DemoSwiftUsage.swift
│ ├── MMKVDemo-Bridging-Header.h
│ ├── Resources/
│ │ ├── Assets.xcassets/
│ │ │ └── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ ├── Base.lproj/
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ ├── Info.plist
│ │ ├── MMKVDemo.entitlements
│ │ ├── testReadOnly
│ │ └── testReadOnly.crc
│ ├── TestMMKVCpp.cpp
│ ├── TestMMKVCpp.hpp
│ ├── ViewController+TestCaseBad.h
│ ├── ViewController+TestCaseBad.mm
│ ├── ViewController.h
│ ├── ViewController.mm
│ └── main.m
├── MMKVDemo.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata/
│ └── xcschemes/
│ ├── MMKVDemo.xcscheme
│ ├── MMKVMacDemo.xcscheme
│ ├── MMKVTodayExtensionDemo.xcscheme
│ ├── WatchApp (Notification).xcscheme
│ ├── WatchApp.xcscheme
│ └── kvdemo.xcscheme
├── MMKVDemoTests/
│ ├── Info.plist
│ ├── MMKVDemoTests.mm
│ └── MMKVPerformanceTest.mm
├── MMKVMacDemo/
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Assets.xcassets/
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj/
│ │ └── Main.storyboard
│ ├── Info.plist
│ ├── MMKVMacDemo.entitlements
│ ├── ViewController.h
│ ├── ViewController.mm
│ └── main.m
├── MMKVTodayExtensionDemo/
│ ├── Base.lproj/
│ │ └── MainInterface.storyboard
│ ├── Info.plist
│ ├── MMKVTodayExtensionDemo.entitlements
│ ├── TodayViewController.h
│ └── TodayViewController.m
├── MMKVVisionDemo/
│ ├── Assets.xcassets/
│ │ ├── AppIcon.solidimagestack/
│ │ │ ├── Back.solidimagestacklayer/
│ │ │ │ ├── Content.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── Front.solidimagestacklayer/
│ │ │ │ ├── Content.imageset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ └── Middle.solidimagestacklayer/
│ │ │ ├── Content.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── ContentView.swift
│ ├── Info.plist
│ ├── MMKVVisionDemoApp.swift
│ └── Preview Content/
│ └── Preview Assets.xcassets/
│ └── Contents.json
├── WatchApp/
│ ├── Assets.xcassets/
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj/
│ │ └── Interface.storyboard
│ └── Info.plist
├── WatchApp Extension/
│ ├── Assets.xcassets/
│ │ ├── Complication.complicationset/
│ │ │ ├── Circular.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── Extra Large.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Graphic Bezel.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Graphic Circular.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Graphic Corner.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Graphic Extra Large.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Graphic Large Rectangular.imageset/
│ │ │ │ └── Contents.json
│ │ │ ├── Modular.imageset/
│ │ │ │ └── Contents.json
│ │ │ └── Utilitarian.imageset/
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── ExtensionDelegate.h
│ ├── ExtensionDelegate.mm
│ ├── Info.plist
│ ├── InterfaceController.h
│ ├── InterfaceController.m
│ ├── NotificationController.h
│ ├── NotificationController.m
│ └── PushNotificationPayload.apns
└── kvdemo/
├── AppDelegate.h
├── AppDelegate.m
├── Assets.xcassets/
│ ├── AccentColor.colorset/
│ │ └── Contents.json
│ ├── AppIcon.appiconset/
│ │ └── Contents.json
│ └── Contents.json
├── Base.lproj/
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Info.plist
├── SceneDelegate.h
├── SceneDelegate.m
├── ViewController.h
├── ViewController.m
├── kvdemo.entitlements
└── main.m
================================================
FILE CONTENTS
================================================
================================================
FILE: .clang-format
================================================
---
BasedOnStyle: LLVM
IndentWidth: 4
TabWidth: 4
AlwaysBreakTemplateDeclarations: true
AllowShortFunctionsOnASingleLine: InlineOnly
#AllowShortLambdasOnASingleLine: Inline
BreakAfterJavaFieldAnnotations: true
#BreakBeforeBraces: Linux
SpaceAfterCStyleCast: true
IndentCaseLabels: true
AccessModifierOffset: -4
BreakBeforeBraces: Custom
BraceWrapping:
AfterNamespace: false
AfterClass: false
AfterFunction: false
BreakConstructorInitializersBeforeComma: true
ConstructorInitializerAllOnOneLineOrOnePerLine: true
BinPackParameters: false
ReflowComments: false
ObjCBlockIndentWidth: 4
---
Language: Cpp
ColumnLimit: 120
IndentPPDirectives: AfterHash
---
Language: ObjC
ColumnLimit: 0
#UseTab: ForIndentation
---
Language: Java
ColumnLimit: 120
AllowShortFunctionsOnASingleLine: None
BreakBeforeBinaryOperators: NonAssignment
================================================
FILE: .dockerignore
================================================
# 排除特定文件
output/
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [lingol]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
---
## Note:
An crash issue will be **ignored & closed** in a week **without logs**.
### The language of MMKV
> e.g. Objective-C, Swift, Java, or Kotlin
### The version of MMKV
> e.g. v1.2.2
> Note: For versions older than the latest version, please upgrade before posting any issue.
> We don't have much time for old version tech support.
### The platform of MMKV
> e.g. iOS or Android
### The installation of MMKV
> e.g. Cocoapods, Maven, or Git clone
### What's the issue?
> Post the outputs or screenshots for errors.
>
> Explain what you want by example or code **in English**.
> If you have a crash/OOM/ANR on Android, please provide **symbolize stack traces**. https://developer.android.com/ndk/guides/ndk-stack.html
### What's the log of MMKV when that happened?
> Your **detail logs**. As much as possible.
> For how to forward MMKV's log, you can checkout the wiki on each platform.
> An crash issue will be **ignored & closed** in a week **without logs**.
================================================
FILE: .gitignore
================================================
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
## Other
*.xccheckout
*.moved-aside
*.xcuserstate
*.xcscmblueprint
#python
venv
## OS X
.DS_Store
## Xcode
DerivedData
## Visual Studio
.vs
[Dd]ebug/
[Rr]elease/
*.user
*.VC.opendb
*.VC.db
ipch/
#cmake
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Linux/Makefile
cmake-build-debug/
cmake-build-release/
build/
build_*/
dist/
*.egg-info/
.cmake
#CLion
.idea
#Android Studio
.cxx
pubspec.lock
Podfile.lock
#docker
output/
#swiftpm
.swiftpm
================================================
FILE: .gitmodules
================================================
[submodule "Python/pybind11"]
path = Python/pybind11
url = https://github.com/pybind/pybind11.git
================================================
FILE: Android/MMKV/.gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.DS_Store
/build
/captures
.externalNativeBuild
.idea/caches/build_file_checksums.ser
.idea/
================================================
FILE: Android/MMKV/build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '2.2.20'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
ext {
minSdkVersion = 23
compileSdk = 35
targetSdkVersion = compileSdk
supportLibVersion = "25.4.0"
javaVersion = JavaVersion.VERSION_11
defaultsToPrivateOnly = true
GROUP = 'com.tencent'
VERSION_NAME = "${VERSION_NAME_PREFIX}${VERSION_NAME_SUFFIX}"
POM_PACKAGING = "pom"
POM_DESCRIPTION = "MMKV for Android"
POM_URL = "https://github.com/Tencent/MMKV"
POM_SCM_URL = "https://github.com/Tencent/MMKV"
POM_SCM_CONNECTION="scm:git:github.com/Tencent/MMKV.git"
POM_SCM_DEV_CONNECTION="scm:git:ssh://git@github.com/Tencent/MMKV.git"
POM_ISSUE_URL = 'https://github.com/Tencent/MMKV/issues'
POM_LICENCE_NAME = "BSD License"
POM_LICENCE_URL = "https://opensource.org/licenses/BSD-3-Clause"
POM_DEVELOPER_ID = "Tencent Wechat"
POM_DEVELOPER_NAME = "Tencent Wechat, Inc."
}
================================================
FILE: Android/MMKV/checkstyle.xml
================================================
================================================
FILE: Android/MMKV/gradle/android-publish-private.gradle
================================================
// OSS_ANDROID_TEMPLATE_FILE_HEADER
/**
* New android-publish gradle script (for private use only, without bintray support)
*
* 1. Multiple flavors support.
* 2. Provide PRIVATE_RELEASE_REPOSITORY_URL and PRIVATE_SNAPSHOT_REPOSITORY_URL with gradle.properties or local.properties.
* 3. Provide PRIVATE_REPOSITORY_USERNAME and PRIVATE_REPOSITORY_PASSWORD if needed.
*/
apply plugin: 'digital.wup.android-maven-publish'
apply plugin: 'signing'
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
def getReleaseRepositoryUrl() {
return hasProperty('PRIVATE_RELEASE_REPOSITORY_URL') ? PRIVATE_RELEASE_REPOSITORY_URL : readPropertyFromLocalProperties('PRIVATE_RELEASE_REPOSITORY_URL')
}
def getSnapshotRepositoryUrl() {
return hasProperty('PRIVATE_SNAPSHOT_REPOSITORY_URL') ? PRIVATE_SNAPSHOT_REPOSITORY_URL : readPropertyFromLocalProperties('PRIVATE_SNAPSHOT_REPOSITORY_URL')
}
def readPropertyFromLocalProperties(String key) {
Properties properties = new Properties()
try {
properties.load(project.rootProject.file('local.properties').newDataInputStream())
} catch (Exception e) {
println("load local properties failed msg:${e.message}")
}
return properties.getProperty(key)
}
def getRepositoryUsername() {
return hasProperty('PRIVATE_REPOSITORY_USERNAME') ? PRIVATE_REPOSITORY_USERNAME : readPropertyFromLocalProperties('PRIVATE_REPOSITORY_USERNAME')
}
def getRepositoryPassword() {
return hasProperty('PRIVATE_REPOSITORY_PASSWORD') ? PRIVATE_REPOSITORY_PASSWORD : readPropertyFromLocalProperties('PRIVATE_REPOSITORY_PASSWORD')
}
def pomConfig = {
scm {
url POM_SCM_URL
}
licenses {
license {
name POM_LICENCE_NAME
url POM_LICENCE_URL
distribution POM_LICENCE_DIST
}
}
developers {
developer {
id POM_DEVELOPER_ID
name POM_DEVELOPER_NAME
}
}
}
android.libraryVariants.all { variant ->
// Skipped debug variants
if (variant.buildType.name == "debug") {
return
}
def hasFlavors = !variant.flavorName.isEmpty()
def artifactIdSuffix = hasFlavors ? variant.flavorName.replace('_', '-').capitalize() : ''
variant.productFlavors.each { flavor ->
def flavorArtifactIdSuffix = flavor.ext.has('artifactIdSuffix') ? flavor.ext.artifactIdSuffix : flavor.name
if (!flavorArtifactIdSuffix.isEmpty()) {
artifactIdSuffix = artifactIdSuffix.replace(flavor.name.capitalize(), "-${flavorArtifactIdSuffix}")
} else {
artifactIdSuffix = artifactIdSuffix.replace(flavor.name.capitalize(), "")
}
}
if (!artifactIdSuffix.isEmpty() && !artifactIdSuffix.startsWith('-')) {
artifactIdSuffix = '-' + artifactIdSuffix
}
def curArtifactId = "${POM_ARTIFACT_ID}${artifactIdSuffix}"
/**
* Includes
*/
def sourceDirs = variant.sourceSets.collect {
it.javaDirectories // TODO: kotlin sources
}
def javadoc = task("${variant.name}Javadoc", type: Javadoc) {
source = variant.javaCompile.source // TODO: deprecated
options.encoding = 'utf-8'
destinationDir = file("${buildDir}/docs/javadoc${hasFlavors ? artifactIdSuffix : ""}")
ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdk}/android.jar"
classpath += files(ext.androidJar)
classpath += files(configurations.compile)
exclude '**/BuildConfig.java'
exclude '**/R.java'
failOnError false
}
def javadocJar = task("${variant.name}JavadocJar", type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
def sourcesJar = task("${variant.name}SourcesJar", type: Jar) {
classifier = 'sources'
from sourceDirs
}
def jniSymbolsJar = task("${variant.name}SymbolJar", type: Jar, dependsOn: 'build') {
classifier = "so-symbols"
boolean hasNativeBuildTask = false
tasks.each { task ->
if (task.getName().startsWith("externalNativeBuild")) {
hasNativeBuildTask = true
}
}
if (!hasNativeBuildTask) {
return
}
if (hasFlavors) {
variant.productFlavors.each { flavor ->
from file("build/intermediates/cmake/${flavor.name}/release/obj/")
}
} else {
from file("build/intermediates/cmake/release/obj/")
}
}
// require gradle 4.x +
if (GradleVersion.current() < GradleVersion.version('4.0')) {
throw new GradleException('android-publish.gradle need Gradle 4.0 or newer')
}
def publicationName = "component${variant.name.capitalize()}"
// Declare publications
publishing.publications {
"$publicationName"(MavenPublication) {
artifactId curArtifactId
groupId groupId
version version
from components.findByName("android${variant.name.capitalize()}")
artifact javadocJar
artifact jniSymbolsJar
}
}
}
/**
* Setup custom maven repo
*/
publishing.repositories {
maven {
url "${isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl()}"
credentials {
username "${getRepositoryUsername()}"
password "${getRepositoryPassword()}"
}
}
}
if (JavaVersion.current().isJava8Compatible()) {
allprojects {
tasks.withType(Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
}
}
}
task buildAndPublishToLocalMaven(type: Copy, dependsOn: ['build', 'publishToMavenLocal']) {
group = 'publishing'
// save artifacts files to artifacts folder
from configurations.archives.allArtifacts.files
into "${rootProject.buildDir}/outputs/artifacts/"
rename { String fileName ->
fileName.replace("release.aar", "${version}.aar")
}
doLast {
println "* published to maven local: ${project.group}:${project.name}:${project.version}"
}
}
task buildAndPublishRepo(type: Copy, dependsOn: ['build', 'publish']) {
group = "publishing"
// save artifacts files to artifacts folder
from configurations.archives.allArtifacts.files
into "${rootProject.buildDir}/outputs/artifacts/"
rename { String fileName ->
fileName.replace("release.aar", "${version}.aar")
}
doLast {
println "* published to repo: ${project.group}:${project.name}:${project.version}"
}
}
apply from: rootProject.file('gradle/check.gradle')
================================================
FILE: Android/MMKV/gradle/android-publish.gradle
================================================
import java.util.regex.Matcher
import java.util.regex.Pattern
// OSS_ANDROID_TEMPLATE_FILE_HEADER
/**
* New android-publish gradle script
*
* 1. Multiple flavors support.
* 2. Provide RELEASE_REPOSITORY_URL and SNAPSHOT_REPOSITORY_URL with gradle.properties or local.properties.
* 3. Provide REPOSITORY_USERNAME and REPOSITORY_PASSWORD if needed.
*/
apply plugin: 'maven-publish'
apply plugin: 'signing'
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
def getReleaseRepositoryUrl() {
return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL : readPropertyFromLocalProperties('RELEASE_REPOSITORY_URL')
}
def getSnapshotRepositoryUrl() {
return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL : readPropertyFromLocalProperties('SNAPSHOT_REPOSITORY_URL')
}
def readPropertyFromLocalProperties(String key) {
Properties properties = new Properties()
try {
properties.load(project.rootProject.file('local.properties').newDataInputStream())
} catch (Exception e) {
println("load local properties failed msg:${e.message}")
}
return properties.getProperty(key)
}
def getRepositoryUsername() {
return hasProperty('REPOSITORY_USERNAME') ? REPOSITORY_USERNAME : readPropertyFromLocalProperties('REPOSITORY_USERNAME')
}
def getRepositoryPassword() {
return hasProperty('REPOSITORY_PASSWORD') ? REPOSITORY_PASSWORD : readPropertyFromLocalProperties('REPOSITORY_PASSWORD')
}
def curArtifactId
android.libraryVariants.all { variant ->
// Skipped debug variants
if (variant.buildType.name == "debug") {
return
}
// Skipped non publishing variants
// println('android.defaultPublishConfig=' + android.defaultPublishConfig)
// println('variant.flavorName=' + variant.flavorName)
// if (!android.defaultPublishConfig.startsWith(variant.flavorName)) {
// return
// }
def hasFlavors = !variant.flavorName.isEmpty()
def artifactIdSuffix = hasFlavors ? variant.flavorName.replace('_', '-').capitalize() : ''
variant.productFlavors.each { flavor ->
def flavorArtifactIdSuffix = flavor.ext.has('artifactIdSuffix') ? flavor.ext.artifactIdSuffix : flavor.name
if (!flavorArtifactIdSuffix.isEmpty()) {
artifactIdSuffix = artifactIdSuffix.replace(flavor.name.capitalize(), "-${flavorArtifactIdSuffix}")
} else {
artifactIdSuffix = artifactIdSuffix.replace(flavor.name.capitalize(), "")
}
}
if (!artifactIdSuffix.isEmpty() && !artifactIdSuffix.startsWith('-')) {
artifactIdSuffix = '-' + artifactIdSuffix
}
curArtifactId = "${POM_ARTIFACT_ID}${artifactIdSuffix}"
def jniJar = task("${variant.name}JniJar", type: Jar, dependsOn: 'build') {
}
def javadoc = task("${variant.name}Javadoc", type: Javadoc) {
source = variant.javaCompileProvider.get().source
options.encoding = 'utf-8'
destinationDir = file("${buildDir}/docs/javadoc${hasFlavors ? artifactIdSuffix : ""}")
ext.androidJar = "${android.sdkDirectory}/platforms/${android.compileSdk}/android.jar"
classpath += files(ext.androidJar)
classpath += files(variant.javaCompileProvider.get().classpath)
classpath += files(configurations.javadocDeps)
exclude '**/BuildConfig.java'
exclude '**/R.java'
failOnError = false
}
def javadocJar = task("${variant.name}JavadocJar", type: Jar, dependsOn: javadoc) {
archiveClassifier = 'javadoc'
from javadoc.destinationDir
}
def jniSymbolsJar = task("${variant.name}SymbolJar", type: Jar, dependsOn: 'build') {
archiveClassifier = "so-symbols"
boolean hasNativeBuildTask = false
tasks.each { task ->
if (task.getName().startsWith("externalNativeBuild")) {
hasNativeBuildTask = true
}
}
if (!hasNativeBuildTask) {
return
}
if (hasFlavors) {
variant.productFlavors.each { flavor ->
from file("build/intermediates/cmake/${flavor.name}Release/obj/")
}
} else {
from file("build/intermediates/cmake/release/obj/")
}
}
// Custom task to package headers into a ZIP file
def headerZip = task("${variant.name}Header", type: Zip, dependsOn: 'build') {
archiveClassifier = "header"
from('../../../Core/include') {
exclude '*/MemoryFile.h'
exclude '*/MMKVLog.h'
}
archiveFileName = 'headers.zip'
destinationDirectory = file("$buildDir/outputs/headers")
}
def publicationName = "component${variant.name.capitalize()}"
// Declare publications
publishing.publications {
"$publicationName"(MavenPublication) {
groupId groupId
artifactId curArtifactId
version version
from components.findByName("android${variant.name.capitalize()}")
if (hasFlavors) {
variant.productFlavors.each { flavor ->
artifact("build/outputs/aar/${project.getName()}-${flavor.name}-release.aar")
}
} else {
artifact("build/outputs/aar/${project.getName()}-release.aar")
}
artifact javadocJar
artifact jniSymbolsJar
artifact headerZip
pom {
name = 'MMKV'
description = POM_DESCRIPTION
url = POM_URL
licenses {
license {
name = POM_LICENCE_NAME
url = POM_LICENCE_URL
}
}
developers {
developer {
id = POM_DEVELOPER_ID
name = POM_DEVELOPER_NAME
}
}
scm {
url = POM_SCM_URL
connection = POM_SCM_CONNECTION
developerConnection = POM_SCM_DEV_CONNECTION
}
// write the dependency of the library into pom
withXml {
def dependenciesNode = asNode().appendNode('dependencies')
project.configurations.all { configuration ->
def name = configuration.name
if (name != "implementation" && name != "compile" && name != "api") {
return
}
configuration.dependencies.each {
if (it.name == "unspecified") {
return
}
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', it.group)
dependencyNode.appendNode('artifactId', it.name)
dependencyNode.appendNode('version', it.version)
if (name == "api" || name == "compile") {
dependencyNode.appendNode("scope", "compile")
} else {
dependencyNode.appendNode("scope", "runtime")
}
}
}
}
}
}
}
}
signing {
sign publishing.publications
}
publishing.repositories {
maven {
url "${isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl()}"
credentials {
username "${getRepositoryUsername()}"
password "${getRepositoryPassword()}"
}
}
}
if (JavaVersion.current().isJava8Compatible()) {
allprojects {
tasks.withType(Javadoc) {
options.addStringOption('Xdoclint:none', '-quiet')
}
}
}
task buildAndPublishToLocalMaven(type: Copy, dependsOn: ['build', 'publishToMavenLocal']) {
group = 'publishing'
// save artifacts files to artifacts folder
from "$buildDir/outputs/aar"
include '*.aar'
into "${rootProject.buildDir}/outputs/artifacts"
rename { String fileName ->
fileName.replace("release.aar", "${version}.aar")
}
doLast {
println "* published to maven local: ${project.group}:${project.name}:${project.version}"
}
}
task buildAndPublishRepo(type: Copy, dependsOn: ['build', 'publish']) {
group = "publishing"
// save artifacts files to artifacts folder
from configurations.archives.allArtifacts.files
into "${rootProject.buildDir}/outputs/artifacts/"
rename { String fileName ->
fileName.replace("release.aar", "${version}.aar")
}
doLast {
println "* published to repo: ${project.group}:${project.name}:${project.version}"
}
}
//tasks.register('closeCentralRepository', Exec) {
// // This task will only run if the 'centralUsername' property exists
//// onlyIf { project.hasProperty('centralUsername') && project.hasProperty('centralToken') }
//
// // Define your namespace here
// def namespace = "com.tencent" // <-- IMPORTANT: Replace with your actual namespace
//
// group = "publishing"
// description = "Closes the repository on the Central Publisher Portal."
//
// commandLine "curl", "-X", "POST",
// "-u", "${getRepositoryUsername()}:${getRepositoryPassword()}",
// "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/${namespace}"
//}
//
//// Find your specific publish task and make it trigger the new task
//// Replace 'publish' with the actual name of your main publishing task if it's different.
//tasks.named('publish') {
// finalizedBy 'closeCentralRepository'
//}
apply from: rootProject.file('gradle/check.gradle')
================================================
FILE: Android/MMKV/gradle/build_library.gradle
================================================
// OSS_ANDROID_TEMPLATE_FILE_HEADER
/**
* Optional android libraries gradle file
*
* Simply apply from: rootProject.file('gradle/build_library.gradle') if needed
*
*/
apply plugin: 'com.android.library'
android {
compileSdk rootProject.ext.compileSdk
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
}
signingConfigs {
configDebug {
keyAlias 'key0'
keyPassword 'mmkv.wxg'
storeFile rootProject.file('debug.keystore')
storePassword 'mmkv.wxg'
}
configRelease {
keyAlias 'key0'
keyPassword 'mmkv.wxg'
storeFile rootProject.file('debug.keystore')
storePassword 'mmkv.wxg'
}
}
buildTypes {
debug {
debuggable true
signingConfig signingConfigs.configDebug
minifyEnabled false
}
release {
signingConfig signingConfigs.configRelease
minifyEnabled true
proguardFile getDefaultProguardFile('proguard-android.txt')
proguardFile project.file('proguard-rules-android-lib.pro') // NOTE: default rules for android library if this file exists
}
}
}
// Publication
version = rootProject.ext.VERSION_NAME
group = rootProject.ext.GROUP
//def check_private_only = project.hasProperty('PRIVATE_ONLY') ? PRIVATE_ONLY.toBoolean() : rootProject.ext.defaultsToPrivateOnly // Dangerous, turn on for default
//
//if (check_private_only) {
// // Publish to private only repositories
// apply from: rootProject.file('gradle/android-publish-private.gradle')
//
//} else {
// // Publish to public repositories
// apply from: rootProject.file('gradle/android-publish.gradle')
//}
================================================
FILE: Android/MMKV/gradle/check.gradle
================================================
// OSS_ANDROID_TEMPLATE_FILE_HEADER
apply plugin: 'checkstyle'
checkstyle {
configFile rootProject.file('checkstyle.xml')
toolVersion '6.19'
ignoreFailures false
showViolations true
}
task('checkstyle', type: Checkstyle) {
source 'src/main/java'
include '**/*.java'
classpath = files()
}
check.dependsOn('checkstyle')
//apply plugin: 'pmd'
//
//pmd {
// toolVersion '5.4.0'
//}
//
//task pmd(type: Pmd) {
// targetJdk = TargetJdk.VERSION_1_7
//
// description 'Run pmd'
// group 'verification'
//
// // If ruleSets is not empty, it seems to contain some
// // defaults which override rules in the ruleset file...
// ruleSets = []
// ruleSetFiles = rootProject.files('pmd-ruleset.xml')
// source = fileTree('src/main/java')
// ignoreFailures = false
//
// reports {
// xml.enabled = false
// html.enabled = true
// }
//}
//
//check.dependsOn('pmd')
//apply plugin: 'findbugs'
//
//def classTree = 'build/intermediates/classes/debug'
//
//if (project.plugins.hasPlugin('java')) {
// classTree = 'build/classes'
//}
//task findbugs(type: FindBugs) {
//
// description 'Run findbugs'
// group 'verification'
//
// classes = fileTree(classTree)
// source = fileTree('src/main/java/')
// classpath = files()
//
// effort = 'default'
//
// excludeFilter = rootProject.file("findbugs-exclude.xml")
//
// reports {
// xml.enabled = false
// html.enabled = true
// }
// ignoreFailures = true
//}
//check.dependsOn('findbugs')
================================================
FILE: Android/MMKV/gradle/wrapper/gradle-wrapper.properties
================================================
#Fri Oct 16 21:16:39 CST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
================================================
FILE: Android/MMKV/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
android.enableJetifier=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
VERSION_NAME_PREFIX=2.4.0
#VERSION_NAME_SUFFIX=-SNAPSHOT
VERSION_NAME_SUFFIX=
RELEASE_REPOSITORY_URL=https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2
SNAPSHOT_REPOSITORY_URL=https://ossrh-staging-api.central.sonatype.com/content/repositories/snapshots
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
================================================
FILE: Android/MMKV/gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: Android/MMKV/gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: Android/MMKV/mmkv/.gitignore
================================================
/build
================================================
FILE: Android/MMKV/mmkv/CMakeLists.txt
================================================
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
project(mmkv)
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.0)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_subdirectory(../../../Core Core)
add_library( mmkv
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-bridge.cpp
src/main/cpp/flutter-bridge.cpp
)
set_target_properties(mmkv PROPERTIES
CXX_STANDARD 20
CXX_EXTENSIONS OFF
POSITION_INDEPENDENT_CODE ON
)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( mmkv
# Links the target library to the log library
# included in the NDK.
${log-lib}
core
# aes
)
================================================
FILE: Android/MMKV/mmkv/build.gradle
================================================
plugins {
id 'com.android.library'
id 'maven-publish'
id 'signing'
// id 'org.jetbrains.dokka' version '1.4.32' // For Kotlin projects
}
apply from: rootProject.file('gradle/build_library.gradle')
android {
namespace "com.tencent.mmkv"
compileSdk rootProject.ext.compileSdk
publishing {
multipleVariants("DefaultCppRelease") {
includeBuildTypeValues('release')
includeFlavorDimensionAndValues('stl_mode', 'DefaultCpp')
withJavadocJar()
}
multipleVariants("StaticCppRelease") {
includeBuildTypeValues('release')
includeFlavorDimensionAndValues('stl_mode', 'StaticCpp')
withJavadocJar()
}
multipleVariants("SharedCppRelease") {
includeBuildTypeValues('release')
includeFlavorDimensionAndValues('stl_mode', 'SharedCpp')
withJavadocJar()
}
}
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
abiFilters 'arm64-v8a', 'x86_64'
}
}
ndkVersion "28.1.13356709"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
}
buildTypes {
release {
minifyEnabled false
externalNativeBuild {
cmake {
cppFlags "-fvisibility=hidden", "-funwind-tables", "-fasynchronous-unwind-tables", "-O2"
}
}
consumerProguardFiles 'proguard-rules.pro'
// debuggable true
// jniDebuggable true
}
debug {
jniDebuggable true
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
version "3.18.0+"
}
}
packagingOptions {
jniLibs {
useLegacyPackaging true
}
}
flavorDimensions = ["stl_mode"]
productFlavors {
DefaultCpp {
dimension "stl_mode"
ext.artifactIdSuffix = ''
externalNativeBuild {
cmake {
arguments = ["-DANDROID_STL=c++_static"]
cppFlags "-DMMKV_STL_SHARED=0"
}
}
}
StaticCpp {
dimension "stl_mode"
ext.artifactIdSuffix = 'static'
externalNativeBuild {
cmake {
arguments = ["-DANDROID_STL=c++_static"]
cppFlags "-DMMKV_STL_SHARED=0"
}
}
// not working
// packagingOptions {
// exclude "prefab/modules/**"
// }
}
SharedCpp {
dimension "stl_mode"
ext.artifactIdSuffix = 'shared'
externalNativeBuild {
cmake {
arguments = ["-DANDROID_STL=c++_shared"]
cppFlags "-DMMKV_STL_SHARED=1"
}
}
}
}
buildFeatures {
prefab true
prefabPublishing true
aidl true
}
prefab {
mmkv {
headers "../../../Core/include"
}
}
}
afterEvaluate {
android.libraryVariants.configureEach { variant ->
if (variant.buildType.name == "release") {
def abiFilters = ['arm64-v8a', 'x86_64']
def strippedLibsDir = "$buildDir/intermediates/stripped_native_libs/${variant.name}/strip${variant.name}DebugSymbols/out/lib"
// Create per-variant copy task
tasks.register("syncStrippedLibsToPrefab${variant.name.capitalize()}", Copy) {
from(strippedLibsDir) {
include "**/libmmkv.so"
}
into "$buildDir/intermediates/prefab_package/${variant.name.capitalize()}/prefab/modules/mmkv/libs"
eachFile {
// println('hhh before:' + path)
for (abi in abiFilters) {
if (path.contains("${abi}/")) {
path = path.replace("${abi}/", "android.${abi}/")
break
}
}
// println('hhh after:' + path)
}
// Hook into build process
dependsOn("strip${variant.name.capitalize()}DebugSymbols")
}
// Hook into build process
tasks.named("externalNativeBuild${variant.name.capitalize()}") {
finalizedBy("syncStrippedLibsToPrefab${variant.name.capitalize()}")
}
tasks.named("bundle${variant.name.capitalize()}Aar") {
dependsOn("syncStrippedLibsToPrefab${variant.name.capitalize()}")
}
tasks.named("bundle${variant.name.capitalize()}LocalLintAar") {
dependsOn("syncStrippedLibsToPrefab${variant.name.capitalize()}")
}
// Generate combined artifact suffix from all flavor dimensions
def flavorSuffixes = variant.productFlavors.collect { it.ext.artifactIdSuffix }
def artifactIdSuffix = flavorSuffixes.join("-")
def fullArtifactId = "${POM_ARTIFACT_ID}${artifactIdSuffix ? "-$artifactIdSuffix" : ""}"
// 1. Find the task that merges native libs (unstripped).
// Name pattern is typically: mergeReleaseNativeLibs or mergeFlavorReleaseNativeLibs
def mergeTaskName = "merge${variant.name.capitalize()}NativeLibs"
def mergeTask = tasks.findByName(mergeTaskName)
if (!mergeTask) {
println "Warning: Could not find Native Merge task: $mergeTaskName"
return // Skip creating the symbols jar if we can't find the source
}
// Create debug symbols task
def symbolsTask = tasks.register("bundleSymbols${variant.name.capitalize()}", Jar) {
// 2. FIX: Don't use a hardcoded path string.
// Use the output files of the merge task directly.
from(mergeTask.outputs.files)
// 3. Ensure we only grab the .so files (the merge task might output a tree structure like /lib/arm64/...)
include '**/*.so'
archiveClassifier.set("so-symbols")
destinationDirectory.set(file("$buildDir/intermediates/so-symbols/${variant.name}"))
dependsOn mergeTask
}
// Create publication per variant
publishing.publications.create(variant.name, MavenPublication) {
from components.findByName(variant.name)
groupId = group
artifactId = fullArtifactId
version = version
// Add debug symbols
artifact symbolsTask
// Configure POM
pom {
name = 'MMKV'
description = POM_DESCRIPTION
url = POM_URL
licenses {
license {
name = POM_LICENCE_NAME
url = POM_LICENCE_URL
}
}
developers {
developer {
id = POM_DEVELOPER_ID
name = POM_DEVELOPER_NAME
}
}
scm {
url = POM_SCM_URL
connection = POM_SCM_CONNECTION
developerConnection = POM_SCM_DEV_CONNECTION
}
}
}
}
}
// Configure repositories and signing
publishing.repositories {
maven {
name = "MavenCentral"
url = RELEASE_REPOSITORY_URL
credentials {
username = REPOSITORY_USERNAME
password = REPOSITORY_PASSWORD
}
}
}
signing {
// Ensure signing only targets publications that were successfully created
sign publishing.publications.matching {
it.name in android.libraryVariants.collect { v -> v.name }
}
}
}
configurations {
javadocDeps
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.annotation:annotation:1.9.1'
javadocDeps 'androidx.annotation:annotation:1.9.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
compileOnly project(':mmkvannotation')
}
================================================
FILE: Android/MMKV/mmkv/gradle.properties
================================================
POM_ARTIFACT_ID=mmkv
PRIVATE_ONLY=false
================================================
FILE: Android/MMKV/mmkv/proguard-rules.pro
================================================
# Keep all native methods, their classes and any classes in their descriptors
-keepclasseswithmembers,includedescriptorclasses class com.tencent.mmkv.** {
native ;
long nativeHandle;
private static *** onMMKVCRCCheckFail(***);
private static *** onMMKVFileLengthError(***);
private static *** mmkvLogImp(...);
private static *** onContentChangedByOuterProcess(***);
private static *** onMMKVContentLoadSuccessfully(***);
}
================================================
FILE: Android/MMKV/mmkv/src/androidTest/AndroidManifest.xml
================================================
================================================
FILE: Android/MMKV/mmkv/src/androidTest/java/com/tencent/mmkv/MMKVTest.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
import static org.junit.Assert.*;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
import androidx.test.InstrumentationRegistry;
import java.util.HashSet;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
public class MMKVTest {
static MMKV mmkv;
static final String KeyNotExist = "Key_Not_Exist";
static final float Delta = 0.000001f;
@BeforeClass
public static void setUp() throws Exception {
Context appContext = InstrumentationRegistry.getTargetContext();
MMKV.initialize(appContext);
mmkv = MMKV.mmkvWithID("unitTest", MMKV.SINGLE_PROCESS_MODE, "UnitTestCryptKey");
}
@AfterClass
public static void tearDown() throws Exception {
mmkv.clearAll();
}
@Test
public void testBool() {
boolean ret = mmkv.encode("bool", true);
assertEquals(ret, true);
boolean value = mmkv.decodeBool("bool");
assertEquals(value, true);
value = mmkv.decodeBool(KeyNotExist);
assertEquals(value, false);
value = mmkv.decodeBool(KeyNotExist, true);
assertEquals(value, true);
}
@Test
public void testInt() {
boolean ret = mmkv.encode("int", Integer.MAX_VALUE);
assertEquals(ret, true);
int value = mmkv.decodeInt("int");
assertEquals(value, Integer.MAX_VALUE);
value = mmkv.decodeInt(KeyNotExist);
assertEquals(value, 0);
value = mmkv.decodeInt(KeyNotExist, -1);
assertEquals(value, -1);
}
@Test
public void testLong() {
boolean ret = mmkv.encode("long", Long.MAX_VALUE);
assertEquals(ret, true);
long value = mmkv.decodeLong("long");
assertEquals(value, Long.MAX_VALUE);
value = mmkv.decodeLong(KeyNotExist);
assertEquals(value, 0);
value = mmkv.decodeLong(KeyNotExist, -1);
assertEquals(value, -1);
}
@Test
public void testFloat() {
boolean ret = mmkv.encode("float", Float.MAX_VALUE);
assertEquals(ret, true);
float value = mmkv.decodeFloat("float");
assertEquals(value, Float.MAX_VALUE, Delta);
value = mmkv.decodeFloat(KeyNotExist);
assertEquals(value, 0, Delta);
value = mmkv.decodeFloat(KeyNotExist, -1);
assertEquals(value, -1, Delta);
}
@Test
public void testDouble() {
boolean ret = mmkv.encode("double", Double.MAX_VALUE);
assertEquals(ret, true);
double value = mmkv.decodeDouble("double");
assertEquals(value, Double.MAX_VALUE, Delta);
value = mmkv.decodeDouble(KeyNotExist);
assertEquals(value, 0, Delta);
value = mmkv.decodeDouble(KeyNotExist, -1);
assertEquals(value, -1, Delta);
}
@Test
public void testString() {
String str = "Hello 2018 world cup 世界杯";
boolean ret = mmkv.encode("string", str);
assertEquals(ret, true);
String value = mmkv.decodeString("string");
assertEquals(value, str);
value = mmkv.decodeString(KeyNotExist);
assertEquals(value, null);
value = mmkv.decodeString(KeyNotExist, "Empty");
assertEquals(value, "Empty");
}
@Test
public void testStringSet() {
HashSet set = new HashSet();
set.add("W");
set.add("e");
set.add("C");
set.add("h");
set.add("a");
set.add("t");
boolean ret = mmkv.encode("string_set", set);
assertEquals(ret, true);
HashSet value = (HashSet) mmkv.decodeStringSet("string_set");
assertEquals(value, set);
value = (HashSet) mmkv.decodeStringSet(KeyNotExist);
assertEquals(value, null);
set = new HashSet();
set.add("W");
value = (HashSet) mmkv.decodeStringSet(KeyNotExist, set);
assertEquals(value, set);
}
@Test
public void testBytes() {
byte[] bytes = {'m', 'm', 'k', 'v'};
boolean ret = mmkv.encode("bytes", bytes);
assertEquals(ret, true);
byte[] value = mmkv.decodeBytes("bytes");
assertArrayEquals(value, bytes);
}
@Test
public void testRemove() {
boolean ret = mmkv.encode("bool_1", true);
ret &= mmkv.encode("int_1", Integer.MIN_VALUE);
ret &= mmkv.encode("long_1", Long.MIN_VALUE);
ret &= mmkv.encode("float_1", Float.MIN_VALUE);
ret &= mmkv.encode("double_1", Double.MIN_VALUE);
ret &= mmkv.encode("string_1", "hello");
HashSet set = new HashSet();
set.add("W");
set.add("e");
set.add("C");
set.add("h");
set.add("a");
set.add("t");
ret &= mmkv.encode("string_set_1", set);
byte[] bytes = {'m', 'm', 'k', 'v'};
ret &= mmkv.encode("bytes_1", bytes);
assertEquals(ret, true);
{
long count = mmkv.count();
mmkv.removeValueForKey("bool_1");
mmkv.removeValuesForKeys(new String[] {"int_1", "long_1"});
long newCount = mmkv.count();
assertEquals(count, newCount + 3);
}
boolean bValue = mmkv.decodeBool("bool_1");
assertEquals(bValue, false);
int iValue = mmkv.decodeInt("int_1");
assertEquals(iValue, 0);
long lValue = mmkv.decodeLong("long_1");
assertEquals(lValue, 0);
float fValue = mmkv.decodeFloat("float_1");
assertEquals(fValue, Float.MIN_VALUE, Delta);
double dValue = mmkv.decodeDouble("double_1");
assertEquals(dValue, Double.MIN_VALUE, Delta);
String sValue = mmkv.decodeString("string_1");
assertEquals(sValue, "hello");
HashSet hashSet = (HashSet) mmkv.decodeStringSet("string_set_1");
assertEquals(hashSet, set);
byte[] byteValue = mmkv.decodeBytes("bytes_1");
assertArrayEquals(bytes, byteValue);
}
@Test
public void testIPCUpdateInt() {
MMKV mmkv = MMKV.mmkvWithID(MMKVTestService.SharedMMKVID, MMKV.MULTI_PROCESS_MODE);
mmkv.encode(MMKVTestService.SharedMMKVKey, 1024);
Context appContext = InstrumentationRegistry.getTargetContext();
Intent intent = new Intent(appContext, MMKVTestService.class);
intent.putExtra(MMKVTestService.CMD_Key, MMKVTestService.CMD_Update);
appContext.startService(intent);
SystemClock.sleep(1000 * 3);
int value = mmkv.decodeInt(MMKVTestService.SharedMMKVKey);
assertEquals(value, 1024 + 1);
}
@Test
public void testIPCLock() {
Context appContext = InstrumentationRegistry.getTargetContext();
Intent intent = new Intent(appContext, MMKVTestService.class);
intent.putExtra(MMKVTestService.CMD_Key, MMKVTestService.CMD_Lock);
appContext.startService(intent);
SystemClock.sleep(1000 * 3);
MMKV mmkv = MMKV.mmkvWithID(MMKVTestService.SharedMMKVID, MMKV.MULTI_PROCESS_MODE);
boolean ret = mmkv.tryLock();
assertEquals(ret, false);
intent.putExtra(MMKVTestService.CMD_Key, MMKVTestService.CMD_Kill);
appContext.startService(intent);
SystemClock.sleep(1000 * 3);
ret = mmkv.tryLock();
assertEquals(ret, true);
}
}
================================================
FILE: Android/MMKV/mmkv/src/androidTest/java/com/tencent/mmkv/MMKVTestService.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.Process;
import androidx.annotation.Nullable;
public class MMKVTestService extends Service {
public static final String SharedMMKVID = "SharedMMKVID";
public static final String SharedMMKVKey = "SharedMMKVKey";
public static final String CMD_Key = "CMD_Key";
public static final String CMD_Update = "CMD_Update";
public static final String CMD_Lock = "CMD_Lock";
public static final String CMD_Kill = "CMD_Kill";
@Override
public void onCreate() {
super.onCreate();
MMKV.initialize(this);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
MMKV mmkv = MMKV.mmkvWithID(SharedMMKVID, MMKV.MULTI_PROCESS_MODE);
String cmd = intent.getStringExtra(CMD_Key);
switch (cmd) {
case CMD_Update:
int value = mmkv.decodeInt(SharedMMKVKey);
value += 1;
mmkv.encode(SharedMMKVKey, value);
break;
case CMD_Lock:
mmkv.lock();
break;
case CMD_Kill:
stopSelf();
break;
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
Process.killProcess(Process.myPid());
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
================================================
FILE: Android/MMKV/mmkv/src/main/AndroidManifest.xml
================================================
================================================
FILE: Android/MMKV/mmkv/src/main/aidl/com/tencent/mmkv/ParcelableMMKV.aidl
================================================
// MMKV.aidl
package com.tencent.mmkv;
parcelable ParcelableMMKV;
================================================
FILE: Android/MMKV/mmkv/src/main/cpp/flutter-bridge.cpp
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2020 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include
#ifndef MMKV_DISABLE_FLUTTER
# include
# include
# include
# include
using namespace mmkv;
using namespace std;
namespace mmkv {
extern int g_android_api;
extern string g_android_tmpDir;
}
# ifdef MMKV_EXPORT
# undef MMKV_EXPORT
# endif
# define MMKV_EXPORT extern "C" __attribute__((visibility("default"))) __attribute__((used))
using LogCallback_t = void (*)(uint32_t level, const char *file, int32_t line, const char *funcname, const char *message);
using ErrorCallback_t = int (*)(const char *mmapID, int32_t errorType);
using ContenctChangeCallback_t = void (*)(const char *mmapID);
class FlutterMMKVHandler : public mmkv::MMKVHandler {
public:
LogCallback_t logCallback = nullptr;
ErrorCallback_t errorCallback = nullptr;
ContenctChangeCallback_t contentChangeCallback = nullptr;
ContenctChangeCallback_t contentLoadedCallback = nullptr;
void mmkvLog(MMKVLogLevel level, const char *file, int line, const char *function, const std::string &message) override {
if (logCallback) {
logCallback(level, file, line, function, message.c_str());
}
}
MMKVRecoverStrategic onMMKVCRCCheckFail(const std::string &mmapID) override {
if (errorCallback) {
return (MMKVRecoverStrategic) errorCallback(mmapID.c_str(), MMKVCRCCheckFail);
}
return OnErrorDiscard;
}
MMKVRecoverStrategic onMMKVFileLengthError(const std::string &mmapID) override {
if (errorCallback) {
return (MMKVRecoverStrategic) errorCallback(mmapID.c_str(), MMKVFileLength);
}
return OnErrorDiscard;
}
void onContentChangedByOuterProcess(const std::string &mmapID) override {
if (contentChangeCallback) {
contentChangeCallback(mmapID.c_str());
}
}
void onMMKVContentLoadSuccessfully(const std::string &mmapID) override {
if (contentLoadedCallback) {
contentLoadedCallback(mmapID.c_str());
}
}
};
static FlutterMMKVHandler g_flutterHandler;
MMKV_EXPORT void *mmkvInitialize_v2(const char *rootDir, const char *cacheDir, int32_t sdkInt, int32_t logLevel, LogCallback_t callback) {
if (!rootDir) {
return nullptr;
}
if (cacheDir) {
g_android_tmpDir = string(cacheDir);
}
g_android_api = sdkInt;
#ifdef MMKV_STL_SHARED
MMKVInfo("current API level = %d, libc++_shared=%d", g_android_api, MMKV_STL_SHARED);
#else
MMKVInfo("current API level = %d, libc++_shared=?", g_android_api);
#endif
if (callback) {
g_flutterHandler.logCallback = callback;
MMKV::initializeMMKV(rootDir, (MMKVLogLevel) logLevel, &g_flutterHandler);
} else {
MMKV::initializeMMKV(rootDir, (MMKVLogLevel) logLevel);
}
return (void *) MMKV::getRootDir().c_str();
}
MMKV_EXPORT void mmkvInitialize_v1(const char *rootDir, const char *cacheDir, int32_t sdkInt, int32_t logLevel) {
mmkvInitialize_v2(rootDir, cacheDir, sdkInt, logLevel, nullptr);
}
MMKV_EXPORT void mmkvInitialize(const char *rootDir, int32_t logLevel) {
mmkvInitialize_v2(rootDir, nullptr, 0, logLevel, nullptr);
}
MMKV_EXPORT void *getMMKVWithID(const char *mmapID, int32_t mode, const char *cryptKey, const char *rootPath,
size_t expectedCapacity, bool fromNameSpace, bool aes256, int32_t enableKeyExpire,
int32_t expiredInSeconds, bool enableCompareBeforeSet, int32_t recover,
uint32_t itemSizeLimit) {
MMKV *kv = nullptr;
if (!mmapID) {
return kv;
}
string str = mmapID;
auto config = MMKVConfig();
config.mode = (MMKVMode) mode;
config.aes256 = aes256;
config.expectedCapacity = expectedCapacity;
if (enableKeyExpire >= 0) {
config.enableKeyExpire = (enableKeyExpire != 0);
}
config.expiredInSeconds = expiredInSeconds;
config.enableCompareBeforeSet = enableCompareBeforeSet;
if (recover >= 0) {
config.recover = static_cast(recover);
}
config.itemSizeLimit = itemSizeLimit;
bool done = false;
if (cryptKey) {
string crypt = cryptKey;
if (!crypt.empty()) {
config.cryptKey = &crypt;
if (rootPath) {
string path = rootPath;
if (fromNameSpace) {
auto ns = MMKV::nameSpace(path);
config.rootPath = &ns.getRootDir();
kv = ns.mmkvWithID(str, config);
} else {
config.rootPath = &path;
kv = MMKV::mmkvWithID(str, config);
}
} else {
kv = MMKV::mmkvWithID(str, config);
}
done = true;
}
}
if (!done) {
if (rootPath) {
string path = rootPath;
if (fromNameSpace) {
auto ns = MMKV::nameSpace(path);
config.rootPath = &ns.getRootDir();
kv = ns.mmkvWithID(str, config);
} else {
config.rootPath = &path;
kv = MMKV::mmkvWithID(str, config);
}
} else {
kv = MMKV::mmkvWithID(str, config);
}
}
return kv;
}
MMKV_EXPORT void *getDefaultMMKV(int32_t mode, const char *cryptKey, bool aes256, size_t expectedCapacity,
int32_t enableKeyExpire, int32_t expiredInSeconds, bool enableCompareBeforeSet,
int32_t recover, uint32_t itemSizeLimit) {
MMKV *kv = nullptr;
auto config = MMKVConfig();
config.mode = (MMKVMode) mode;
config.aes256 = aes256;
config.expectedCapacity = expectedCapacity;
if (enableKeyExpire >= 0) {
config.enableKeyExpire = (enableKeyExpire != 0);
}
config.expiredInSeconds = expiredInSeconds;
config.enableCompareBeforeSet = enableCompareBeforeSet;
if (recover >= 0) {
config.recover = static_cast(recover);
}
config.itemSizeLimit = itemSizeLimit;
if (cryptKey) {
string crypt = cryptKey;
if (crypt.length() > 0) {
config.cryptKey = &crypt;
kv = MMKV::defaultMMKV(config);
}
}
if (!kv) {
kv = MMKV::defaultMMKV(config);
}
return kv;
}
MMKV_EXPORT const char *mmapID(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
return kv->mmapID().c_str();
}
return nullptr;
}
MMKV_EXPORT bool encodeBool(void *handle, const char *oKey, bool value) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->set((bool) value, key);
}
return false;
}
MMKV_EXPORT bool encodeBool_v2(void *handle, const char *oKey, bool value, uint32_t expiration) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->set((bool) value, key, expiration);
}
return false;
}
MMKV_EXPORT bool decodeBool(void *handle, const char *oKey, bool defaultValue) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->getBool(key, defaultValue);
}
return defaultValue;
}
MMKV_EXPORT bool encodeInt32(void *handle, const char *oKey, int32_t value) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->set((int32_t) value, key);
}
return false;
}
MMKV_EXPORT bool encodeInt32_v2(void *handle, const char *oKey, int32_t value, uint32_t expiration) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->set((int32_t) value, key, expiration);
}
return false;
}
MMKV_EXPORT int32_t decodeInt32(void *handle, const char *oKey, int32_t defaultValue) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->getInt32(key, defaultValue);
}
return defaultValue;
}
MMKV_EXPORT bool encodeInt64(void *handle, const char *oKey, int64_t value) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->set((int64_t) value, key);
}
return false;
}
MMKV_EXPORT bool encodeInt64_v2(void *handle, const char *oKey, int64_t value, uint32_t expiration) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->set((int64_t) value, key, expiration);
}
return false;
}
MMKV_EXPORT int64_t decodeInt64(void *handle, const char *oKey, int64_t defaultValue) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->getInt64(key, defaultValue);
}
return defaultValue;
}
MMKV_EXPORT bool encodeDouble(void *handle, const char *oKey, double value) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->set((double) value, key);
}
return false;
}
MMKV_EXPORT bool encodeDouble_v2(void *handle, const char *oKey, double value, uint32_t expiration) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->set((double) value, key, expiration);
}
return false;
}
MMKV_EXPORT double decodeDouble(void *handle, const char *oKey, double defaultValue) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
return kv->getDouble(key, defaultValue);
}
return defaultValue;
}
MMKV_EXPORT bool encodeBytes(void *handle, const char *oKey, void *oValue, uint64_t length) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
if (oValue) {
auto value = MMBuffer(oValue, static_cast(length), MMBufferNoCopy);
return kv->set(value, key);
} else {
kv->removeValueForKey(key);
return true;
}
}
return false;
}
MMKV_EXPORT bool encodeBytes_v2(void *handle, const char *oKey, void *oValue, uint64_t length, uint32_t expiration) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
if (oValue) {
auto value = MMBuffer(oValue, static_cast(length), MMBufferNoCopy);
return kv->set(value, key, expiration);
} else {
kv->removeValueForKey(key);
return true;
}
}
return false;
}
MMKV_EXPORT void *decodeBytes(void *handle, const char *oKey, uint64_t *lengthPtr) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
auto key = string(oKey);
mmkv::MMBuffer value;
auto hasValue = kv->getBytes(key, value);
if (hasValue) {
if (value.length() > 0) {
if (value.isStoredOnStack()) {
auto result = malloc(value.length());
if (result) {
memcpy(result, value.getPtr(), value.length());
*lengthPtr = value.length();
}
return result;
}
void *result = value.getPtr();
*lengthPtr = value.length();
value.detach();
return result;
}
*lengthPtr = 0;
// this ptr is intended for checking existence of the value
// don't free this ptr
return value.getPtr();
}
}
return nullptr;
}
# ifndef MMKV_DISABLE_CRYPT
MMKV_EXPORT bool reKey(void *handle, char *oKey, uint64_t length, bool aes256) {
MMKV *kv = static_cast(handle);
if (kv) {
if (oKey && length > 0) {
string key(oKey, length);
return kv->reKey(key, aes256);
} else {
return kv->reKey(string(), aes256);
}
}
return false;
}
MMKV_EXPORT void *cryptKey(void *handle, uint64_t *lengthPtr) {
MMKV *kv = static_cast(handle);
if (kv && lengthPtr) {
auto cryptKey = kv->cryptKey();
if (cryptKey.length() > 0) {
auto ptr = malloc(cryptKey.length());
if (ptr) {
memcpy(ptr, cryptKey.data(), cryptKey.length());
*lengthPtr = cryptKey.length();
return ptr;
}
}
}
return nullptr;
}
MMKV_EXPORT void checkReSetCryptKey(void *handle, char *oKey, uint64_t length, bool aes256) {
MMKV *kv = static_cast(handle);
if (kv) {
if (oKey && length > 0) {
string key(oKey, length);
kv->checkReSetCryptKey(&key, aes256);
} else {
kv->checkReSetCryptKey(nullptr, aes256);
}
}
}
# endif // MMKV_DISABLE_CRYPT
MMKV_EXPORT uint32_t valueSize(void *handle, char *oKey, bool actualSize) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
string key(oKey);
auto ret = kv->getValueSize(key, actualSize);
return static_cast(ret);
}
return 0;
}
MMKV_EXPORT int32_t writeValueToNB(void *handle, char *oKey, void *pointer, uint32_t size) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
string key(oKey);
return kv->writeValueToBuffer(key, pointer, size);
}
return -1;
}
MMKV_EXPORT uint64_t allKeys(void *handle, char ***keyArrayPtr, uint32_t **sizeArrayPtr, bool filterExpire) {
MMKV *kv = static_cast(handle);
if (kv) {
auto keys = kv->allKeys(filterExpire);
if (!keys.empty()) {
auto keyArray = (char **) malloc(keys.size() * sizeof(void *));
auto sizeArray = (uint32_t *) malloc(keys.size() * sizeof(uint32_t *));
if (!keyArray || !sizeArray) {
free(keyArray);
free(sizeArray);
return 0;
}
*keyArrayPtr = keyArray;
*sizeArrayPtr = sizeArray;
for (size_t index = 0; index < keys.size(); index++) {
auto &key = keys[index];
sizeArray[index] = static_cast(key.length());
keyArray[index] = (char *) malloc(key.length());
if (keyArray[index]) {
memcpy(keyArray[index], key.data(), key.length());
}
}
}
return keys.size();
}
return 0;
}
MMKV_EXPORT bool containsKey(void *handle, char *oKey) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
string key(oKey);
return kv->containsKey(key);
}
return false;
}
MMKV_EXPORT uint64_t count(void *handle, bool filterExpire) {
MMKV *kv = static_cast(handle);
if (kv) {
return kv->count(filterExpire);
}
return 0;
}
MMKV_EXPORT uint64_t totalSize(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
return kv->totalSize();
}
return 0;
}
MMKV_EXPORT uint64_t actualSize(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
return kv->actualSize();
}
return 0;
}
MMKV_EXPORT void removeValueForKey(void *handle, char *oKey) {
MMKV *kv = static_cast(handle);
if (kv && oKey) {
string key(oKey);
kv->removeValueForKey(key);
}
}
MMKV_EXPORT void removeValuesForKeys(void *handle, char **keyArray, uint32_t *sizeArray, uint64_t count) {
MMKV *kv = static_cast(handle);
if (kv && keyArray && sizeArray && count > 0) {
vector arrKeys;
arrKeys.reserve(count);
for (uint64_t index = 0; index < count; index++) {
if (sizeArray[index] > 0 && keyArray[index]) {
arrKeys.emplace_back(keyArray[index], sizeArray[index]);
}
}
if (!arrKeys.empty()) {
kv->removeValuesForKeys(arrKeys);
}
}
}
MMKV_EXPORT void clearAll(void *handle, bool keepSpace) {
MMKV *kv = static_cast(handle);
if (kv) {
kv->clearAll(keepSpace);
}
}
MMKV_EXPORT void mmkvSync(void *handle, bool sync) {
MMKV *kv = static_cast(handle);
if (kv) {
kv->sync((SyncFlag) sync);
}
}
MMKV_EXPORT void clearMemoryCache(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
kv->clearMemoryCache();
}
}
MMKV_EXPORT int32_t pageSize() {
return static_cast(DEFAULT_MMAP_SIZE);
}
MMKV_EXPORT const char *version() {
return MMKV_VERSION;
}
MMKV_EXPORT void trim(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
kv->trim();
}
}
MMKV_EXPORT void mmkvClose(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
kv->close();
}
}
MMKV_EXPORT void mmkvMemcpy(void *dst, const void *src, uint64_t size) {
memcpy(dst, src, size);
}
MMKV_EXPORT bool backupOne(const char *mmapID, const char *dstDir, const char *rootPath) {
if (rootPath) {
auto root = string(rootPath);
if (root.length() > 0) {
return MMKV::backupOneToDirectory(mmapID, dstDir, &root);
}
}
return MMKV::backupOneToDirectory(mmapID, dstDir);
}
MMKV_EXPORT bool restoreOne(const char *mmapID, const char *srcDir, const char *rootPath) {
if (rootPath) {
auto root = string(rootPath);
if (root.length() > 0) {
return MMKV::restoreOneFromDirectory(mmapID, srcDir, &root);
}
}
return MMKV::restoreOneFromDirectory(mmapID, srcDir);
}
MMKV_EXPORT uint64_t backupAll(const char *dstDir/*, const char *rootPath*/) {
// historically Android mistakenly use mmapKey as mmapID
// makes everything tricky with customize root
/*if (rootPath) {
auto root = string(rootPath);
if (root.length() > 0) {
return MMKV::backupAllToDirectory(dstDir, &root);
}
}*/
return MMKV::backupAllToDirectory(dstDir);
}
MMKV_EXPORT uint64_t restoreAll(const char *srcDir/*, const char *rootPath*/) {
// historically Android mistakenly use mmapKey as mmapID
// makes everything tricky with customize root
/*if (rootPath) {
auto root = string(rootPath);
if (root.length() > 0) {
return MMKV::restoreAllFromDirectory(srcDir, &root);
}
}*/
return MMKV::restoreAllFromDirectory(srcDir);
}
MMKV_EXPORT bool enableAutoExpire(void *handle, uint32_t expireDuration) {
MMKV *kv = static_cast(handle);
if (kv) {
return kv->enableAutoKeyExpire(expireDuration);
}
return false;
}
MMKV_EXPORT bool disableAutoExpire(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
return kv->disableAutoKeyExpire();
}
return false;
}
MMKV_EXPORT bool enableCompareBeforeSet(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
return kv->enableCompareBeforeSet();
}
return false;
}
MMKV_EXPORT bool disableCompareBeforeSet(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
return kv->disableCompareBeforeSet();
}
return false;
}
MMKV_EXPORT bool isFileValid(const char *mmapID, const char *rootPath) {
if (rootPath) {
auto root = string(rootPath);
if (root.length() > 0) {
return MMKV::isFileValid(mmapID, &root);
}
}
return MMKV::isFileValid(mmapID, nullptr);
}
MMKV_EXPORT bool removeStorage(const char *mmapID, const char *rootPath) {
if (rootPath) {
auto root = string(rootPath);
if (root.length() > 0) {
return MMKV::removeStorage(mmapID, &root);
}
}
return MMKV::removeStorage(mmapID, nullptr);
}
MMKV_EXPORT bool isMultiProcess(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
return kv->isMultiProcess();
}
return false;
}
MMKV_EXPORT bool isReadOnly(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
return kv->isReadOnly();
}
return false;
}
MMKV_EXPORT void registerErrorHandler(ErrorCallback_t callback) {
g_flutterHandler.errorCallback = callback;
if (callback || g_flutterHandler.logCallback || g_flutterHandler.contentChangeCallback || g_flutterHandler.contentLoadedCallback) {
MMKV::registerHandler(&g_flutterHandler);
}
}
MMKV_EXPORT void registerContentChangeNotify(ContenctChangeCallback_t callback) {
g_flutterHandler.contentChangeCallback = callback;
if (callback || g_flutterHandler.logCallback || g_flutterHandler.errorCallback || g_flutterHandler.contentLoadedCallback) {
MMKV::registerHandler(&g_flutterHandler);
}
}
MMKV_EXPORT void registerContentLoadedNotify(ContenctChangeCallback_t callback) {
g_flutterHandler.contentLoadedCallback = callback;
if (callback || g_flutterHandler.logCallback || g_flutterHandler.errorCallback || g_flutterHandler.contentChangeCallback) {
MMKV::registerHandler(&g_flutterHandler);
}
}
MMKV_EXPORT void checkContentChanged(void *handle) {
MMKV *kv = static_cast(handle);
if (kv) {
kv->checkContentChanged();
}
}
MMKV_EXPORT bool getNameSpace(const char *rootPath) {
if (rootPath) {
auto root = string(rootPath);
if (!root.empty()) {
MMKV::nameSpace(root);
return true;
}
}
return false;
}
MMKV_EXPORT bool checkExist(const char *mmapID, const char *rootPath) {
if (rootPath) {
auto root = string(rootPath);
if (root.length() > 0) {
return MMKV::checkExist(mmapID, &root);
}
}
return MMKV::checkExist(mmapID, nullptr);
}
MMKV_EXPORT size_t importFrom(void *handle, void *srcHandle) {
MMKV *kv = static_cast(handle);
MMKV *kvSrc = static_cast(srcHandle);
if (kv && kvSrc) {
return kv->importFrom(kvSrc);
}
return 0;
}
#endif // MMKV_DISABLE_FLUTTER
================================================
FILE: Android/MMKV/mmkv/src/main/cpp/native-bridge.cpp
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include
#ifdef MMKV_ANDROID
# include
# include
# include
# include
# include
# include
# include
# include
using namespace std;
using namespace mmkv;
static jclass g_cls = nullptr;
static jfieldID g_fileID = nullptr;
static jmethodID g_callbackOnCRCFailID = nullptr;
static jmethodID g_callbackOnFileLengthErrorID = nullptr;
static jmethodID g_mmkvLogID = nullptr;
static jmethodID g_callbackOnContentChange = nullptr;
static jmethodID g_callbackOnContentLoaded = nullptr;
static JavaVM *g_currentJVM = nullptr;
static int registerNativeMethods(JNIEnv *env, jclass cls);
extern "C" void internalLogWithLevel(MMKVLogLevel level, const char *filename, const char *func, int line, const char *format, ...);
extern MMKVLogLevel g_currentLogLevel;
namespace mmkv {
static void mmkvLog(MMKVLogLevel level, const char *file, int line, const char *function, const std::string &message);
typedef void (*AndroidLogHandler)(MMKVLogLevel level, const char *file, int line, const char *function, const char *message);
static AndroidLogHandler g_androidLogHandler = nullptr;
static void androidLogWrapper(MMKVLogLevel level, const char *file, int line, const char *function, const std::string &message);
static JNIEnv *getCurrentEnv();
static jstring string2jstring(JNIEnv *env, const string &str);
// C++ adapter class that bridges mmkv::MMKVHandler to JNI
class JNIMMKVHandler : public mmkv::MMKVHandler {
bool m_logRedirecting = false;
bool m_hasCallback = false;
bool m_wantsContentChange = false;
public:
void setLogRedirecting(bool logRedirecting) { m_logRedirecting = logRedirecting; }
void setHasCallback(bool hasCallback) { m_hasCallback = hasCallback; }
void setWantsContentChange(bool wantsContentChange) { m_wantsContentChange = wantsContentChange; }
bool isLogRedirecting() const { return m_logRedirecting; }
void mmkvLog(MMKVLogLevel level, const char *file, int line, const char *function, MMKVLog_t message) override {
if (m_logRedirecting) {
if (g_androidLogHandler) {
g_androidLogHandler(level, file, line, function, message.c_str());
} else {
mmkv::mmkvLog(level, file, line, function, message);
}
}
}
MMKVRecoverStrategic onMMKVCRCCheckFail(const std::string &mmapID) override {
if (!m_hasCallback) {
return OnErrorDiscard;
}
auto currentEnv = getCurrentEnv();
if (currentEnv && g_callbackOnCRCFailID) {
jstring str = string2jstring(currentEnv, mmapID);
auto strategic = currentEnv->CallStaticIntMethod(g_cls, g_callbackOnCRCFailID, str);
currentEnv->DeleteLocalRef(str);
return static_cast(strategic);
}
return OnErrorDiscard;
}
MMKVRecoverStrategic onMMKVFileLengthError(const std::string &mmapID) override {
if (!m_hasCallback) {
return OnErrorDiscard;
}
auto currentEnv = getCurrentEnv();
if (currentEnv && g_callbackOnFileLengthErrorID) {
jstring str = string2jstring(currentEnv, mmapID);
auto strategic = currentEnv->CallStaticIntMethod(g_cls, g_callbackOnFileLengthErrorID, str);
currentEnv->DeleteLocalRef(str);
return static_cast(strategic);
}
return OnErrorDiscard;
}
void onContentChangedByOuterProcess(const std::string &mmapID) override {
auto currentEnv = getCurrentEnv();
if (currentEnv && g_callbackOnContentChange) {
jstring str = string2jstring(currentEnv, mmapID);
currentEnv->CallStaticVoidMethod(g_cls, g_callbackOnContentChange, str);
currentEnv->DeleteLocalRef(str);
}
}
void onMMKVContentLoadSuccessfully(const std::string &mmapID) override {
auto currentEnv = getCurrentEnv();
if (currentEnv && g_callbackOnContentLoaded) {
jstring str = string2jstring(currentEnv, mmapID);
currentEnv->CallStaticVoidMethod(g_cls, g_callbackOnContentLoaded, str);
currentEnv->DeleteLocalRef(str);
}
}
};
static JNIMMKVHandler g_jniHandler;
}
#define InternalLogError(format, ...) \
internalLogWithLevel(MMKV_NAMESPACE_PREFIX::MMKVLogError, __MMKV_FILE_NAME__, __func__, __LINE__, format, ##__VA_ARGS__)
#define InternalLogInfo(format, ...) \
internalLogWithLevel(MMKV_NAMESPACE_PREFIX::MMKVLogInfo, __MMKV_FILE_NAME__, __func__, __LINE__, format, ##__VA_ARGS__)
extern "C" JNIEXPORT JNICALL jint JNI_OnLoad(JavaVM *vm, void *reserved) {
g_currentJVM = vm;
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
if (g_cls) {
env->DeleteGlobalRef(g_cls);
}
static const char *clsName = "com/tencent/mmkv/MMKV";
jclass instance = env->FindClass(clsName);
if (!instance) {
MMKVError("fail to locate class: %s", clsName);
return -2;
}
g_cls = reinterpret_cast(env->NewGlobalRef(instance));
if (!g_cls) {
MMKVError("fail to create global reference for %s", clsName);
return -3;
}
g_mmkvLogID =
env->GetStaticMethodID(g_cls, "mmkvLogImp", "(ILjava/lang/String;ILjava/lang/String;Ljava/lang/String;)V");
if (!g_mmkvLogID) {
MMKVError("fail to get method id for mmkvLogImp");
}
// every code from now on can use InternalLogXXX()
int ret = registerNativeMethods(env, g_cls);
if (ret != 0) {
InternalLogError("fail to register native methods for class %s, ret = %d", clsName, ret);
return -4;
}
g_fileID = env->GetFieldID(g_cls, "nativeHandle", "J");
if (!g_fileID) {
InternalLogError("fail to locate fileID");
return -5;
}
g_callbackOnCRCFailID = env->GetStaticMethodID(g_cls, "onMMKVCRCCheckFail", "(Ljava/lang/String;)I");
if (!g_callbackOnCRCFailID) {
InternalLogError("fail to get method id for onMMKVCRCCheckFail");
}
g_callbackOnFileLengthErrorID = env->GetStaticMethodID(g_cls, "onMMKVFileLengthError", "(Ljava/lang/String;)I");
if (!g_callbackOnFileLengthErrorID) {
InternalLogError("fail to get method id for onMMKVFileLengthError");
}
g_callbackOnContentChange =
env->GetStaticMethodID(g_cls, "onContentChangedByOuterProcess", "(Ljava/lang/String;)V");
if (!g_callbackOnContentChange) {
InternalLogError("fail to get method id for onContentChangedByOuterProcess()");
}
g_callbackOnContentLoaded =
env->GetStaticMethodID(g_cls, "onMMKVContentLoadSuccessfully", "(Ljava/lang/String;)V");
if (!g_callbackOnContentLoaded) {
InternalLogError("fail to get method id for onMMKVContentLoadSuccessfully()");
}
// Note: If you use NDK r23 or older, you can get API level by accessing android.os.Build.VERSION.SDK_INT
g_android_api = android_get_device_api_level();
#ifdef MMKV_STL_SHARED
InternalLogInfo("current API level = %d, libc++_shared=%d", g_android_api, MMKV_STL_SHARED);
#else
InternalLogInfo("current API level = %d, libc++_shared=?", g_android_api);
#endif
return JNI_VERSION_1_6;
}
//#define MMKV_JNI extern "C" JNIEXPORT JNICALL
# define MMKV_JNI static
namespace mmkv {
static string jstring2string(JNIEnv *env, jstring str);
MMKV_JNI void jniInitialize(JNIEnv *env, jobject obj, jstring rootDir, jstring cacheDir, jint logLevel, jboolean logReDirecting, jboolean hasCallback, jlong nativeLogHandler) {
if (!rootDir) {
return;
}
const char *kstr = env->GetStringUTFChars(rootDir, nullptr);
if (kstr) {
g_jniHandler.setLogRedirecting(logReDirecting == JNI_TRUE);
g_jniHandler.setHasCallback(hasCallback == JNI_TRUE);
if (logReDirecting && nativeLogHandler != 0) {
g_androidLogHandler = (AndroidLogHandler) nativeLogHandler;
} else {
g_androidLogHandler = nullptr;
}
mmkv::MMKVHandler *handler = (logReDirecting || hasCallback) ? &g_jniHandler : nullptr;
MMKV::initializeMMKV(kstr, (MMKVLogLevel) logLevel, handler);
env->ReleaseStringUTFChars(rootDir, kstr);
g_android_tmpDir = jstring2string(env, cacheDir);
if (hasCallback == JNI_TRUE || logReDirecting == JNI_TRUE) {
MMKV::registerHandler(&g_jniHandler);
} else {
MMKV::unRegisterHandler();
}
}
}
MMKV_JNI void onExit(JNIEnv *env, jobject obj) {
MMKV::onExit();
}
static MMKV *getMMKV(JNIEnv *env, jobject obj) {
jlong handle = env->GetLongField(obj, g_fileID);
return reinterpret_cast(handle);
}
static string jstring2string(JNIEnv *env, jstring str) {
if (str) {
const char *kstr = env->GetStringUTFChars(str, nullptr);
if (kstr) {
string result(kstr);
env->ReleaseStringUTFChars(str, kstr);
return result;
}
}
return "";
}
static jstring string2jstring(JNIEnv *env, const string &str) {
return env->NewStringUTF(str.c_str());
}
static vector jarray2vector(JNIEnv *env, jobjectArray array) {
vector keys;
if (array) {
jsize size = env->GetArrayLength(array);
keys.reserve(size);
for (jsize i = 0; i < size; i++) {
jstring str = (jstring) env->GetObjectArrayElement(array, i);
if (str) {
keys.push_back(jstring2string(env, str));
env->DeleteLocalRef(str);
}
}
}
return keys;
}
static jobjectArray vector2jarray(JNIEnv *env, const vector &arr) {
jobjectArray result = env->NewObjectArray(arr.size(), env->FindClass("java/lang/String"), nullptr);
if (result) {
for (size_t index = 0; index < arr.size(); index++) {
jstring value = string2jstring(env, arr[index]);
env->SetObjectArrayElement(result, index, value);
env->DeleteLocalRef(value);
}
}
return result;
}
static JNIEnv *getCurrentEnv() {
if (g_currentJVM) {
JNIEnv *currentEnv = nullptr;
auto ret = g_currentJVM->GetEnv(reinterpret_cast(¤tEnv), JNI_VERSION_1_6);
if (ret == JNI_OK) {
return currentEnv;
} else {
MMKVError("fail to get current JNIEnv: %d", ret);
}
}
return nullptr;
}
extern "C" void internalLogWithLevel(MMKVLogLevel level, const char *filename, const char *func, int line, const char *format, ...) {
if (level >= g_currentLogLevel) {
std::string message;
char buffer[16];
va_list args;
va_start(args, format);
auto length = std::vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
if (length < 0) { // something wrong
message = {};
} else if (length < sizeof(buffer)) {
message = std::string(buffer, static_cast(length));
} else {
message.resize(static_cast(length), '\0');
va_start(args, format);
std::vsnprintf(const_cast(message.data()), static_cast(length) + 1, format, args);
va_end(args);
}
if (g_cls && g_mmkvLogID) {
mmkvLog(level, filename, line, func, message);
} else {
_MMKVLogWithLevel(level, filename, func, line, message.c_str());
}
}
}
static void mmkvLog(MMKVLogLevel level, const char *file, int line, const char *function, const std::string &message) {
auto currentEnv = getCurrentEnv();
if (currentEnv && g_mmkvLogID) {
jstring oFile = string2jstring(currentEnv, string(file));
jstring oFunction = string2jstring(currentEnv, string(function));
jstring oMessage = string2jstring(currentEnv, message);
int readLevel = level;
currentEnv->CallStaticVoidMethod(g_cls, g_mmkvLogID, readLevel, oFile, line, oFunction, oMessage);
currentEnv->DeleteLocalRef(oMessage);
currentEnv->DeleteLocalRef(oFunction);
currentEnv->DeleteLocalRef(oFile);
}
}
static void androidLogWrapper(MMKVLogLevel level, const char *file, int line, const char *function, const std::string &message) {
g_androidLogHandler(level, file, line, function, message.c_str());
}
MMKV_JNI jlong getMMKVWithID(JNIEnv *env, jobject, jstring mmapID, jint mode, jstring cryptKey, jstring rootPath,
jlong expectedCapacity, jboolean aes256, jint enableKeyExpire, jint expiredInSeconds,
jboolean enableCompareBeforeSet, jint recover, jint itemSizeLimit) {
MMKV *kv = nullptr;
if (!mmapID) {
return (jlong) kv;
}
string str = jstring2string(env, mmapID);
auto config = MMKVConfig();
config.mode = (MMKVMode) mode;
config.aes256 = aes256;
config.expectedCapacity = expectedCapacity;
if (enableKeyExpire >= 0) {
config.enableKeyExpire = (enableKeyExpire != 0);
}
config.expiredInSeconds = expiredInSeconds;
config.enableCompareBeforeSet = enableCompareBeforeSet;
if (recover >= 0) {
config.recover = static_cast(recover);
}
config.itemSizeLimit = itemSizeLimit;
bool done = false;
if (cryptKey) {
string crypt = jstring2string(env, cryptKey);
if (crypt.length() > 0) {
config.cryptKey = &crypt;
if (rootPath) {
string path = jstring2string(env, rootPath);
config.rootPath = &path;
kv = MMKV::mmkvWithID(str, config);
} else {
kv = MMKV::mmkvWithID(str, config);
}
done = true;
}
}
if (!done) {
if (rootPath) {
string path = jstring2string(env, rootPath);
config.rootPath = &path;
kv = MMKV::mmkvWithID(str, config);
} else {
kv = MMKV::mmkvWithID(str, config);
}
}
return (jlong) kv;
}
MMKV_JNI jlong getDefaultMMKV(JNIEnv *env, jobject obj, jint mode, jstring cryptKey, jlong expectedCapacity,
jboolean aes256, jint enableKeyExpire, jint expiredInSeconds,
jboolean enableCompareBeforeSet, jint recover, jint itemSizeLimit) {
MMKV *kv = nullptr;
auto config = MMKVConfig();
config.mode = (MMKVMode) mode;
config.aes256 = aes256;
config.expectedCapacity = expectedCapacity;
if (enableKeyExpire >= 0) {
config.enableKeyExpire = (enableKeyExpire != 0);
}
config.expiredInSeconds = expiredInSeconds;
config.enableCompareBeforeSet = enableCompareBeforeSet;
if (recover >= 0) {
config.recover = static_cast(recover);
}
config.itemSizeLimit = itemSizeLimit;
if (cryptKey) {
string crypt = jstring2string(env, cryptKey);
if (crypt.length() > 0) {
config.cryptKey = &crypt;
kv = MMKV::defaultMMKV(config);
}
}
if (!kv) {
kv = MMKV::defaultMMKV(config);
}
return (jlong) kv;
}
MMKV_JNI jlong getMMKVWithAshmemFD(JNIEnv *env, jobject obj, jstring mmapID, jint fd, jint metaFD, jstring cryptKey,
jboolean aes256) {
MMKV *kv = nullptr;
if (!mmapID || fd < 0 || metaFD < 0) {
return (jlong) kv;
}
string id = jstring2string(env, mmapID);
if (cryptKey) {
string crypt = jstring2string(env, cryptKey);
if (crypt.length() > 0) {
kv = MMKV::mmkvWithAshmemFD(id, fd, metaFD, &crypt, aes256);
}
}
if (!kv) {
kv = MMKV::mmkvWithAshmemFD(id, fd, metaFD, nullptr, aes256);
}
return (jlong) kv;
}
MMKV_JNI jstring mmapID(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
return string2jstring(env, kv->mmapID());
}
return nullptr;
}
MMKV_JNI jint ashmemFD(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
return kv->ashmemFD();
}
return -1;
}
MMKV_JNI jint ashmemMetaFD(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
return kv->ashmemMetaFD();
}
return -1;
}
MMKV_JNI jboolean checkProcessMode(JNIEnv *env, jobject, jlong handle) {
MMKV *kv = reinterpret_cast(handle);
if (kv) {
return kv->checkProcessMode();
}
return false;
}
MMKV_JNI jboolean encodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean value) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->set((bool) value, key);
}
return (jboolean) false;
}
MMKV_JNI jboolean encodeBool_2(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean value, jint expiration) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->set((bool) value, key, (uint32_t) expiration);
}
return (jboolean) false;
}
MMKV_JNI jboolean decodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean defaultValue) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->getBool(key, defaultValue);
}
return defaultValue;
}
MMKV_JNI jboolean encodeInt(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint value) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->set((int32_t) value, key);
}
return (jboolean) false;
}
MMKV_JNI jboolean encodeInt_2(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint value, jint expiration) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->set((int32_t) value, key, (uint32_t) expiration);
}
return (jboolean) false;
}
MMKV_JNI jint decodeInt(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jint defaultValue) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jint) kv->getInt32(key, defaultValue);
}
return defaultValue;
}
MMKV_JNI jboolean encodeLong(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jlong value) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->set((int64_t) value, key);
}
return (jboolean) false;
}
MMKV_JNI jboolean encodeLong_2(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jlong value, jint expiration) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->set((int64_t) value, key, (uint32_t) expiration);
}
return (jboolean) false;
}
MMKV_JNI jlong decodeLong(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jlong defaultValue) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jlong) kv->getInt64(key, defaultValue);
}
return defaultValue;
}
MMKV_JNI jboolean encodeFloat(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jfloat value) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->set((float) value, key);
}
return (jboolean) false;
}
MMKV_JNI jboolean encodeFloat_2(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jfloat value, jint expiration) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->set((float) value, key, (uint32_t) expiration);
}
return (jboolean) false;
}
MMKV_JNI jfloat decodeFloat(JNIEnv *env, jobject, jlong handle, jstring oKey, jfloat defaultValue) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jfloat) kv->getFloat(key, defaultValue);
}
return defaultValue;
}
MMKV_JNI jboolean encodeDouble(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jdouble value) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->set((double) value, key);
}
return (jboolean) false;
}
MMKV_JNI jboolean encodeDouble_2(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jdouble value, jint expiration) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->set((double) value, key, (uint32_t) expiration);
}
return (jboolean) false;
}
MMKV_JNI jdouble decodeDouble(JNIEnv *env, jobject, jlong handle, jstring oKey, jdouble defaultValue) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jdouble) kv->getDouble(key, defaultValue);
}
return defaultValue;
}
MMKV_JNI jboolean encodeString(JNIEnv *env, jobject, jlong handle, jstring oKey, jstring oValue) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
if (oValue) {
string value = jstring2string(env, oValue);
return (jboolean) kv->set(value, key);
} else {
kv->removeValueForKey(key);
return (jboolean) true;
}
}
return (jboolean) false;
}
MMKV_JNI jboolean encodeString_2(JNIEnv *env, jobject, jlong handle, jstring oKey, jstring oValue, jint expiration) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
if (oValue) {
string value = jstring2string(env, oValue);
return (jboolean) kv->set(value, key, (uint32_t) expiration);
} else {
kv->removeValueForKey(key);
return (jboolean) true;
}
}
return (jboolean) false;
}
MMKV_JNI jstring decodeString(JNIEnv *env, jobject obj, jlong handle, jstring oKey, jstring oDefaultValue) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
string value;
bool hasValue = kv->getString(key, value);
if (hasValue) {
return string2jstring(env, value);
}
}
return oDefaultValue;
}
MMKV_JNI jboolean encodeBytes(JNIEnv *env, jobject, jlong handle, jstring oKey, jbyteArray oValue) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
if (oValue) {
MMBuffer value(0);
{
jsize len = env->GetArrayLength(oValue);
void *bufferPtr = env->GetPrimitiveArrayCritical(oValue, nullptr);
if (bufferPtr) {
value = MMBuffer(bufferPtr, len);
env->ReleasePrimitiveArrayCritical(oValue, bufferPtr, JNI_ABORT);
} else {
MMKVError("fail to get array: %s=%p", key.c_str(), oValue);
}
}
return (jboolean) kv->set(value, key);
} else {
kv->removeValueForKey(key);
return (jboolean) true;
}
}
return (jboolean) false;
}
MMKV_JNI jboolean encodeBytes_2(JNIEnv *env, jobject, jlong handle, jstring oKey, jbyteArray oValue, jint expiration) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
if (oValue) {
MMBuffer value(0);
{
jsize len = env->GetArrayLength(oValue);
void *bufferPtr = env->GetPrimitiveArrayCritical(oValue, nullptr);
if (bufferPtr) {
value = MMBuffer(bufferPtr, len);
env->ReleasePrimitiveArrayCritical(oValue, bufferPtr, JNI_ABORT);
} else {
MMKVError("fail to get array: %s=%p", key.c_str(), oValue);
}
}
return (jboolean) kv->set(value, key, (uint32_t) expiration);
} else {
kv->removeValueForKey(key);
return (jboolean) true;
}
}
return (jboolean) false;
}
MMKV_JNI jbyteArray decodeBytes(JNIEnv *env, jobject obj, jlong handle, jstring oKey) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
mmkv::MMBuffer value;
auto hasValue = kv->getBytes(key, value);
if (hasValue) {
jbyteArray result = env->NewByteArray(value.length());
env->SetByteArrayRegion(result, 0, value.length(), (const jbyte *) value.getPtr());
return result;
}
}
return nullptr;
}
MMKV_JNI jobjectArray allKeys(JNIEnv *env, jobject instance, jlong handle, jboolean filterExpire) {
MMKV *kv = reinterpret_cast(handle);
if (kv) {
vector keys = kv->allKeys((bool) filterExpire);
return vector2jarray(env, keys);
}
return nullptr;
}
MMKV_JNI jboolean containsKey(JNIEnv *env, jobject instance, jlong handle, jstring oKey) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->containsKey(key);
}
return (jboolean) false;
}
MMKV_JNI jlong count(JNIEnv *env, jobject instance, jlong handle, jboolean filterExpire) {
MMKV *kv = reinterpret_cast(handle);
if (kv) {
jlong size = kv->count((bool) filterExpire);
return size;
}
return 0;
}
MMKV_JNI jlong totalSize(JNIEnv *env, jobject instance, jlong handle) {
MMKV *kv = reinterpret_cast(handle);
if (kv) {
jlong size = kv->totalSize();
return size;
}
return 0;
}
MMKV_JNI jlong actualSize(JNIEnv *env, jobject instance, jlong handle) {
MMKV *kv = reinterpret_cast(handle);
if (kv) {
jlong size = kv->actualSize();
return size;
}
return 0;
}
MMKV_JNI void removeValueForKey(JNIEnv *env, jobject instance, jlong handle, jstring oKey) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
kv->removeValueForKey(key);
}
}
MMKV_JNI void removeValuesForKeys(JNIEnv *env, jobject instance, jobjectArray arrKeys) {
MMKV *kv = getMMKV(env, instance);
if (kv && arrKeys) {
vector keys = jarray2vector(env, arrKeys);
if (!keys.empty()) {
kv->removeValuesForKeys(keys);
}
}
}
MMKV_JNI void clearAll(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
kv->clearAll();
}
}
MMKV_JNI void sync(JNIEnv *env, jobject instance, jboolean sync) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
kv->sync((SyncFlag) sync);
}
}
MMKV_JNI jboolean isFileValid(JNIEnv *env, jclass type, jstring oMmapID, jstring rootPath) {
if (oMmapID) {
string mmapID = jstring2string(env, oMmapID);
if (!rootPath) {
return (jboolean) MMKV::isFileValid(mmapID, nullptr);
} else {
auto root = jstring2string(env, rootPath);
return (jboolean) MMKV::isFileValid(mmapID, &root);
}
}
return (jboolean) false;
}
MMKV_JNI jboolean removeStorage(JNIEnv *env, jclass type, jstring oMmapID, jstring rootPath) {
if (oMmapID) {
string mmapID = jstring2string(env, oMmapID);
if (!rootPath) {
return (jboolean) MMKV::removeStorage(mmapID, nullptr);
} else {
auto root = jstring2string(env, rootPath);
return (jboolean) MMKV::removeStorage(mmapID, &root);
}
}
return (jboolean) false;
}
MMKV_JNI jboolean encodeSet(JNIEnv *env, jobject, jlong handle, jstring oKey, jobjectArray arrStr) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
if (arrStr) {
vector value = jarray2vector(env, arrStr);
return (jboolean) kv->set(value, key);
} else {
kv->removeValueForKey(key);
return (jboolean) true;
}
}
return (jboolean) false;
}
MMKV_JNI jboolean encodeSet_2(JNIEnv *env, jobject, jlong handle, jstring oKey, jobjectArray arrStr, jint expiration) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
if (arrStr) {
vector value = jarray2vector(env, arrStr);
return (jboolean) kv->set(value, key, (uint32_t) expiration);
} else {
kv->removeValueForKey(key);
return (jboolean) true;
}
}
return (jboolean) false;
}
MMKV_JNI jobjectArray decodeStringSet(JNIEnv *env, jobject, jlong handle, jstring oKey) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
vector value;
bool hasValue = kv->getVector(key, value);
if (hasValue) {
return vector2jarray(env, value);
}
}
return nullptr;
}
MMKV_JNI void clearMemoryCache(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
kv->clearMemoryCache();
}
}
MMKV_JNI void lock(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
kv->lock();
}
}
MMKV_JNI void unlock(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
kv->unlock();
}
}
MMKV_JNI jboolean tryLock(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
return (jboolean) kv->try_lock();
}
return jboolean(false);
}
MMKV_JNI jint pageSize(JNIEnv *env, jclass type) {
return DEFAULT_MMAP_SIZE;
}
MMKV_JNI jstring version(JNIEnv *env, jclass type) {
return string2jstring(env, MMKV_VERSION);
}
# ifndef MMKV_DISABLE_CRYPT
MMKV_JNI jstring cryptKey(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
string cryptKey = kv->cryptKey();
if (cryptKey.length() > 0) {
return string2jstring(env, cryptKey);
}
}
return nullptr;
}
MMKV_JNI jboolean doReKey(JNIEnv *env, jobject instance, jstring cryptKey, jboolean aes256) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
string newKey;
if (cryptKey) {
newKey = jstring2string(env, cryptKey);
}
return (jboolean) kv->reKey(newKey, aes256);
}
return (jboolean) false;
}
MMKV_JNI void doCheckReSetCryptKey(JNIEnv *env, jobject instance, jstring cryptKey, jboolean aes256) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
string newKey;
if (cryptKey) {
newKey = jstring2string(env, cryptKey);
}
if (!cryptKey || newKey.empty()) {
kv->checkReSetCryptKey(nullptr, aes256);
} else {
kv->checkReSetCryptKey(&newKey, aes256);
}
}
}
# else
MMKV_JNI jstring cryptKey(JNIEnv *env, jobject instance) {
return nullptr;
}
# endif // MMKV_DISABLE_CRYPT
MMKV_JNI void trim(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
kv->trim();
}
}
MMKV_JNI void close(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
kv->close();
env->SetLongField(instance, g_fileID, 0);
}
}
MMKV_JNI jint valueSize(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean actualSize) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return static_cast(kv->getValueSize(key, (bool) actualSize));
}
return 0;
}
MMKV_JNI void setLogLevel(JNIEnv *env, jclass type, jint level) {
MMKV::setLogLevel((MMKVLogLevel) level);
}
MMKV_JNI void setCallbackHandler(JNIEnv *env, jclass type, jboolean logReDirecting, jboolean hasCallback, jlong nativeHandler) {
g_jniHandler.setLogRedirecting(logReDirecting == JNI_TRUE);
g_jniHandler.setHasCallback(hasCallback == JNI_TRUE);
if (logReDirecting && nativeHandler != 0) {
g_androidLogHandler = (AndroidLogHandler) nativeHandler;
} else {
g_androidLogHandler = nullptr;
}
if (hasCallback == JNI_TRUE || logReDirecting == JNI_TRUE) {
MMKV::registerHandler(&g_jniHandler);
} else {
MMKV::unRegisterHandler();
}
}
MMKV_JNI jlong createNB(JNIEnv *env, jobject instance, jint size) {
auto ptr = malloc(static_cast(size));
if (!ptr) {
MMKVError("fail to create NativeBuffer:%s", strerror(errno));
return 0;
}
return reinterpret_cast(ptr);
}
MMKV_JNI void destroyNB(JNIEnv *env, jobject instance, jlong pointer, jint size) {
free(reinterpret_cast(pointer));
}
MMKV_JNI jint writeValueToNB(JNIEnv *env, jobject instance, jlong handle, jstring oKey, jlong pointer, jint size) {
MMKV *kv = reinterpret_cast(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return kv->writeValueToBuffer(key, reinterpret_cast(pointer), size);
}
return -1;
}
MMKV_JNI void setWantsContentChangeNotify(JNIEnv *env, jclass type, jboolean notify) {
g_jniHandler.setWantsContentChange(notify == JNI_TRUE);
// ensure handler is registered when content change is wanted
if (notify == JNI_TRUE) {
MMKV::registerHandler(&g_jniHandler);
}
}
MMKV_JNI void checkContentChanged(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
kv->checkContentChanged();
}
}
MMKV_JNI jboolean backupOne(JNIEnv *env, jobject obj, jstring mmapID, jstring dstDir, jstring rootPath) {
if (rootPath) {
string root = jstring2string(env, rootPath);
if (root.length() > 0) {
return (jboolean) MMKV::backupOneToDirectory(jstring2string(env, mmapID), jstring2string(env, dstDir), &root);
}
}
return (jboolean) MMKV::backupOneToDirectory(jstring2string(env, mmapID), jstring2string(env, dstDir));
}
MMKV_JNI jboolean restoreOne(JNIEnv *env, jobject obj, jstring mmapID, jstring srcDir, jstring rootPath) {
if (rootPath) {
string root = jstring2string(env, rootPath);
if (root.length() > 0) {
return (jboolean) MMKV::restoreOneFromDirectory(jstring2string(env, mmapID), jstring2string(env, srcDir), &root);
}
}
return (jboolean) MMKV::restoreOneFromDirectory(jstring2string(env, mmapID), jstring2string(env, srcDir));
}
MMKV_JNI jlong backupAll(JNIEnv *env, jobject obj, jstring dstDir/*, jstring rootPath*/) {
// historically Android mistakenly use mmapKey as mmapID
// makes everything tricky with customize root
/*if (rootPath) {
string root = jstring2string(env, rootPath);
if (root.length() > 0) {
return (jlong) MMKV::backupAllToDirectory(jstring2string(env, dstDir), &root);
}
}*/
return (jlong) MMKV::backupAllToDirectory(jstring2string(env, dstDir));
}
MMKV_JNI jlong restoreAll(JNIEnv *env, jobject obj, jstring srcDir/*, jstring rootPath*/) {
// historically Android mistakenly use mmapKey as mmapID
// makes everything tricky with customize root
/*if (rootPath) {
string root = jstring2string(env, rootPath);
if (root.length() > 0) {
return (jlong) MMKV::restoreAllFromDirectory(jstring2string(env, srcDir), &root);
}
}*/
return (jlong) MMKV::restoreAllFromDirectory(jstring2string(env, srcDir));
}
MMKV_JNI jboolean enableAutoExpire(JNIEnv *env, jobject instance, jint expireDuration) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
return (jboolean) kv->enableAutoKeyExpire(expireDuration);
}
return (jboolean) false;
}
MMKV_JNI jboolean disableAutoExpire(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
return (jboolean) kv->disableAutoKeyExpire();
}
return (jboolean) false;
}
MMKV_JNI void enableCompareBeforeSet(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
kv->enableCompareBeforeSet();
}
}
MMKV_JNI void disableCompareBeforeSet(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
kv->disableCompareBeforeSet();
}
}
MMKV_JNI bool isCompareBeforeSetEnabled(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
return kv->isCompareBeforeSetEnabled();
}
return false;
}
MMKV_JNI bool isEncryptionEnabled(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
return kv->isEncryptionEnabled();
}
return false;
}
MMKV_JNI bool isExpirationEnabled(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
return kv->isExpirationEnabled();
}
return false;
}
MMKV_JNI void clearAllWithKeepingSpace(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
kv->clearAll(true);
}
}
MMKV_JNI jboolean isMultiProcess(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
return (jboolean) kv->isMultiProcess();
}
return jboolean(false);
}
MMKV_JNI jboolean isReadOnly(JNIEnv *env, jobject instance) {
MMKV *kv = getMMKV(env, instance);
if (kv) {
return (jboolean) kv->isReadOnly();
}
return jboolean(false);
}
MMKV_JNI jboolean getNameSpace(JNIEnv *env, jclass type, jstring rootPath) {
if (rootPath) {
auto root = jstring2string(env, rootPath);
if (!root.empty()) {
MMKV::nameSpace(root);
return (jboolean) true;
}
}
return (jboolean) false;
}
MMKV_JNI jboolean checkExist(JNIEnv *env, jclass type, jstring oMmapID, jstring rootPath) {
if (oMmapID) {
string mmapID = jstring2string(env, oMmapID);
if (!rootPath) {
return (jboolean) MMKV::checkExist(mmapID, nullptr);
} else {
auto root = jstring2string(env, rootPath);
return (jboolean) MMKV::checkExist(mmapID, &root);
}
}
return (jboolean) false;
}
MMKV_JNI void enableDisableProcessMode(JNIEnv *env, jclass type, jboolean notify) {
if (notify == JNI_TRUE) {
MMKV::enableDisableProcessMode(true);
} else {
MMKV::enableDisableProcessMode(false);
}
}
MMKV_JNI jlong importFrom(JNIEnv *env, jobject instance, jlong handle, jlong srcHandle) {
MMKV *kv = reinterpret_cast(handle);
MMKV *src = reinterpret_cast(srcHandle);
if (kv && src) {
jlong size = kv->importFrom(src);
return size;
}
return 0;
}
} // namespace mmkv
static JNINativeMethod g_methods[] = {
{"onExit", "()V", (void *) mmkv::onExit},
{"cryptKey", "()Ljava/lang/String;", (void *) mmkv::cryptKey},
# ifndef MMKV_DISABLE_CRYPT
{"doReKey", "(Ljava/lang/String;Z)Z", (void *) mmkv::doReKey},
{"doCheckReSetCryptKey", "(Ljava/lang/String;Z)V", (void *) mmkv::doCheckReSetCryptKey},
# endif
{"pageSize", "()I", (void *) mmkv::pageSize},
{"mmapID", "()Ljava/lang/String;", (void *) mmkv::mmapID},
{"version", "()Ljava/lang/String;", (void *) mmkv::version},
{"lock", "()V", (void *) mmkv::lock},
{"unlock", "()V", (void *) mmkv::unlock},
{"tryLock", "()Z", (void *) mmkv::tryLock},
{"allKeys", "(JZ)[Ljava/lang/String;", (void *) mmkv::allKeys},
{"removeValuesForKeys", "([Ljava/lang/String;)V", (void *) mmkv::removeValuesForKeys},
{"clearAll", "()V", (void *) mmkv::clearAll},
{"trim", "()V", (void *) mmkv::trim},
{"close", "()V", (void *) mmkv::close},
{"clearMemoryCache", "()V", (void *) mmkv::clearMemoryCache},
{"sync", "(Z)V", (void *) mmkv::sync},
{"isFileValid", "(Ljava/lang/String;Ljava/lang/String;)Z", (void *) mmkv::isFileValid},
{"removeStorage", "(Ljava/lang/String;Ljava/lang/String;)Z", (void *) mmkv::removeStorage},
{"ashmemFD", "()I", (void *) mmkv::ashmemFD},
{"ashmemMetaFD", "()I", (void *) mmkv::ashmemMetaFD},
{"jniInitialize", "(Ljava/lang/String;Ljava/lang/String;IZZJ)V", (void *) mmkv::jniInitialize},
{"getMMKVWithID", "(Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;JZIIZII)J", (void *) mmkv::getMMKVWithID},
{"getDefaultMMKV", "(ILjava/lang/String;JZIIZII)J", (void *) mmkv::getDefaultMMKV},
{"getMMKVWithAshmemFD", "(Ljava/lang/String;IILjava/lang/String;Z)J", (void *) mmkv::getMMKVWithAshmemFD},
{"encodeBool", "(JLjava/lang/String;Z)Z", (void *) mmkv::encodeBool},
{"encodeBool_2", "(JLjava/lang/String;ZI)Z", (void *) mmkv::encodeBool_2},
{"decodeBool", "(JLjava/lang/String;Z)Z", (void *) mmkv::decodeBool},
{"encodeInt", "(JLjava/lang/String;I)Z", (void *) mmkv::encodeInt},
{"encodeInt_2", "(JLjava/lang/String;II)Z", (void *) mmkv::encodeInt_2},
{"decodeInt", "(JLjava/lang/String;I)I", (void *) mmkv::decodeInt},
{"encodeLong", "(JLjava/lang/String;J)Z", (void *) mmkv::encodeLong},
{"encodeLong_2", "(JLjava/lang/String;JI)Z", (void *) mmkv::encodeLong_2},
{"decodeLong", "(JLjava/lang/String;J)J", (void *) mmkv::decodeLong},
{"encodeFloat", "(JLjava/lang/String;F)Z", (void *) mmkv::encodeFloat},
{"encodeFloat_2", "(JLjava/lang/String;FI)Z", (void *) mmkv::encodeFloat_2},
{"decodeFloat", "(JLjava/lang/String;F)F", (void *) mmkv::decodeFloat},
{"encodeDouble", "(JLjava/lang/String;D)Z", (void *) mmkv::encodeDouble},
{"encodeDouble_2", "(JLjava/lang/String;DI)Z", (void *) mmkv::encodeDouble_2},
{"decodeDouble", "(JLjava/lang/String;D)D", (void *) mmkv::decodeDouble},
{"encodeString", "(JLjava/lang/String;Ljava/lang/String;)Z", (void *) mmkv::encodeString},
{"encodeString_2", "(JLjava/lang/String;Ljava/lang/String;I)Z", (void *) mmkv::encodeString_2},
{"decodeString", "(JLjava/lang/String;Ljava/lang/String;)Ljava/lang/String;", (void *) mmkv::decodeString},
{"encodeSet", "(JLjava/lang/String;[Ljava/lang/String;)Z", (void *) mmkv::encodeSet},
{"encodeSet_2", "(JLjava/lang/String;[Ljava/lang/String;I)Z", (void *) mmkv::encodeSet_2},
{"decodeStringSet", "(JLjava/lang/String;)[Ljava/lang/String;", (void *) mmkv::decodeStringSet},
{"encodeBytes", "(JLjava/lang/String;[B)Z", (void *) mmkv::encodeBytes},
{"encodeBytes_2", "(JLjava/lang/String;[BI)Z", (void *) mmkv::encodeBytes_2},
{"decodeBytes", "(JLjava/lang/String;)[B", (void *) mmkv::decodeBytes},
{"containsKey", "(JLjava/lang/String;)Z", (void *) mmkv::containsKey},
{"count", "(JZ)J", (void *) mmkv::count},
{"totalSize", "(J)J", (void *) mmkv::totalSize},
{"actualSize", "(J)J", (void *) mmkv::actualSize},
{"removeValueForKey", "(JLjava/lang/String;)V", (void *) mmkv::removeValueForKey},
{"valueSize", "(JLjava/lang/String;Z)I", (void *) mmkv::valueSize},
{"setLogLevel", "(I)V", (void *) mmkv::setLogLevel},
{"setCallbackHandler", "(ZZJ)V", (void *) mmkv::setCallbackHandler},
{"createNB", "(I)J", (void *) mmkv::createNB},
{"destroyNB", "(JI)V", (void *) mmkv::destroyNB},
{"writeValueToNB", "(JLjava/lang/String;JI)I", (void *) mmkv::writeValueToNB},
{"setWantsContentChangeNotify", "(Z)V", (void *) mmkv::setWantsContentChangeNotify},
{"checkContentChangedByOuterProcess", "()V", (void *) mmkv::checkContentChanged},
{"checkProcessMode", "(J)Z", (void *) mmkv::checkProcessMode},
{"backupOneToDirectory", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z", (void *) mmkv::backupOne},
{"restoreOneMMKVFromDirectory", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z", (void *) mmkv::restoreOne},
{"backupAllToDirectory", "(Ljava/lang/String;)J", (void *) mmkv::backupAll},
{"restoreAllFromDirectory", "(Ljava/lang/String;)J", (void *) mmkv::restoreAll},
{"enableAutoKeyExpire", "(I)Z", (void *) mmkv::enableAutoExpire},
{"disableAutoKeyExpire", "()Z", (void *) mmkv::disableAutoExpire},
{"nativeEnableCompareBeforeSet", "()V", (void *) mmkv::enableCompareBeforeSet},
{"disableCompareBeforeSet", "()V", (void *) mmkv::disableCompareBeforeSet},
{"isCompareBeforeSetEnabled", "()Z", (void *) mmkv::isCompareBeforeSetEnabled},
{"isEncryptionEnabled", "()Z", (void *) mmkv::isEncryptionEnabled},
{"isExpirationEnabled", "()Z", (void *) mmkv::isExpirationEnabled},
{"clearAllWithKeepingSpace", "()V", (void *) mmkv::clearAllWithKeepingSpace},
{"isMultiProcess", "()Z", (void *) mmkv::isMultiProcess},
{"isReadOnly", "()Z", (void *) mmkv::isReadOnly},
{"getNameSpace", "(Ljava/lang/String;)Z", (void *)mmkv::getNameSpace},
{"checkExist", "(Ljava/lang/String;Ljava/lang/String;)Z", (void *) mmkv::checkExist},
{"enableDisableProcessMode", "(Z)V", (void *) mmkv::enableDisableProcessMode},
{"importFrom", "(JJ)J", (void *) mmkv::importFrom},
};
static int registerNativeMethods(JNIEnv *env, jclass cls) {
return env->RegisterNatives(cls, g_methods, sizeof(g_methods) / sizeof(g_methods[0]));
}
#endif // MMKV_ANDROID
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKV.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.jetbrains.annotations.Contract;
import dalvik.annotation.optimization.FastNative;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* An highly efficient, reliable, multi-process key-value storage framework.
* THE PERFECT drop-in replacement for SharedPreferences and MultiProcessSharedPreferences.
*/
public class MMKV implements SharedPreferences, SharedPreferences.Editor {
private static final EnumMap recoverIndex;
private static final EnumMap logLevel2Index;
private static final MMKVLogLevel[] index2LogLevel;
private static final Set checkedHandleSet;
static {
recoverIndex = new EnumMap<>(MMKVRecoverStrategic.class);
recoverIndex.put(MMKVRecoverStrategic.OnErrorDiscard, 0);
recoverIndex.put(MMKVRecoverStrategic.OnErrorRecover, 1);
logLevel2Index = new EnumMap<>(MMKVLogLevel.class);
logLevel2Index.put(MMKVLogLevel.LevelDebug, 0);
logLevel2Index.put(MMKVLogLevel.LevelInfo, 1);
logLevel2Index.put(MMKVLogLevel.LevelWarning, 2);
logLevel2Index.put(MMKVLogLevel.LevelError, 3);
logLevel2Index.put(MMKVLogLevel.LevelNone, 4);
index2LogLevel = new MMKVLogLevel[]{MMKVLogLevel.LevelDebug, MMKVLogLevel.LevelInfo, MMKVLogLevel.LevelWarning,
MMKVLogLevel.LevelError, MMKVLogLevel.LevelNone};
checkedHandleSet = new HashSet();
}
/**
* The interface for providing a 3rd library loader (the ReLinker https://github.com/KeepSafe/ReLinker, etc).
*/
public interface LibLoader {
void loadLibrary(String libName);
}
/**
* Initialize MMKV with default configuration.
* You must call one of the initialize() methods on App startup process before using MMKV.
*
* @param context The context of Android App, usually from Application.
* @return The root folder of MMKV, defaults to $(FilesDir)/mmkv.
*/
public static String initialize(@NonNull Context context) {
String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
MMKVLogLevel logLevel = BuildConfig.DEBUG ? MMKVLogLevel.LevelDebug : MMKVLogLevel.LevelInfo;
return initialize(context, root, null, logLevel, null);
}
/**
* Initialize MMKV with customize log level.
* You must call one of the initialize() methods on App startup process before using MMKV.
*
* @param context The context of Android App, usually from Application.
* @param logLevel The log level of MMKV, defaults to {@link MMKVLogLevel#LevelInfo}.
* @return The root folder of MMKV, defaults to $(FilesDir)/mmkv.
*/
public static String initialize(@NonNull Context context, MMKVLogLevel logLevel) {
String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
return initialize(context, root, null, logLevel, null);
}
/**
* Initialize MMKV with a 3rd library loader.
* You must call one of the initialize() methods on App startup process before using MMKV.
*
* @param context The context of Android App, usually from Application.
* @param loader The 3rd library loader (for example, the ReLinker .
* @return The root folder of MMKV, defaults to $(FilesDir)/mmkv.
*/
public static String initialize(@NonNull Context context, LibLoader loader) {
String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
MMKVLogLevel logLevel = BuildConfig.DEBUG ? MMKVLogLevel.LevelDebug : MMKVLogLevel.LevelInfo;
return initialize(context, root, loader, logLevel, null);
}
/**
* Initialize MMKV with a 3rd library loader, and customize log level.
* You must call one of the initialize() methods on App startup process before using MMKV.
*
* @param context The context of Android App, usually from Application.
* @param loader The 3rd library loader (for example, the ReLinker .
* @param logLevel The log level of MMKV, defaults to {@link MMKVLogLevel#LevelInfo}.
* @return The root folder of MMKV, defaults to $(FilesDir)/mmkv.
*/
public static String initialize(@NonNull Context context, LibLoader loader, MMKVLogLevel logLevel) {
String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
return initialize(context, root, loader, logLevel, null);
}
/**
* Initialize MMKV with customize root folder.
* You must call one of the initialize() methods on App startup process before using MMKV.
*
* @param context The context of Android App, usually from Application.
* @param rootDir The root folder of MMKV, defaults to $(FilesDir)/mmkv.
* @return The root folder of MMKV.
*/
public static String initialize(Context context, String rootDir) {
MMKVLogLevel logLevel = BuildConfig.DEBUG ? MMKVLogLevel.LevelDebug : MMKVLogLevel.LevelInfo;
return initialize(context, rootDir, null, logLevel, null);
}
/**
* Initialize MMKV with customize root folder, and log level.
* You must call one of the initialize() methods on App startup process before using MMKV.
*
* @param context The context of Android App, usually from Application.
* @param rootDir The root folder of MMKV, defaults to $(FilesDir)/mmkv.
* @param logLevel The log level of MMKV, defaults to {@link MMKVLogLevel#LevelInfo}.
* @return The root folder of MMKV.
*/
public static String initialize(Context context, String rootDir, MMKVLogLevel logLevel) {
return initialize(context, rootDir, null, logLevel, null);
}
/**
* Initialize MMKV with customize root folder, and a 3rd library loader.
* You must call one of the initialize() methods on App startup process before using MMKV.
*
* @param context The context of Android App, usually from Application.
* @param rootDir The root folder of MMKV, defaults to $(FilesDir)/mmkv.
* @param loader The 3rd library loader (for example, the ReLinker .
* @return The root folder of MMKV.
*/
public static String initialize(Context context, String rootDir, LibLoader loader) {
MMKVLogLevel logLevel = BuildConfig.DEBUG ? MMKVLogLevel.LevelDebug : MMKVLogLevel.LevelInfo;
return initialize(context, rootDir, loader, logLevel, null);
}
/**
* Initialize MMKV with customize settings.
* You must call one of the initialize() methods on App startup process before using MMKV.
*
* @param context The context of Android App, usually from Application.
* @param rootDir The root folder of MMKV, defaults to $(FilesDir)/mmkv.
* @param loader The 3rd library loader (for example, the ReLinker .
* @param logLevel The log level of MMKV, defaults to {@link MMKVLogLevel#LevelInfo}.
* @return The root folder of MMKV.
*/
public static String initialize(Context context, String rootDir, LibLoader loader, MMKVLogLevel logLevel) {
return initialize(context, rootDir, loader, logLevel, null);
}
public static String initialize(@NonNull Context context, String rootDir, LibLoader loader, MMKVLogLevel logLevel, MMKVHandler handler) {
if (!Process.is64Bit()) {
throw new UnsupportedArchitectureException("MMKV 2.0+ requires 64-bit App, use 1.3.x instead.");
}
String cacheDir = context.getCacheDir().getAbsolutePath();
gCallbackHandler = handler;
boolean hasCallback = false;
long nativeLogHandler = 0;
if (handler != null) {
hasCallback = true;
if (handler.wantLogRedirecting()) {
gWantLogReDirecting = true;
nativeLogHandler = handler.getNativeLogHandler();
}
}
String ret = doInitialize(rootDir, cacheDir, loader, logLevel, gWantLogReDirecting, hasCallback, nativeLogHandler);
if (handler != null && handler.wantContentChangeNotification()) {
setWantsContentChangeNotify(true);
}
// disable process mode in release build
// FIXME: Find a better way to getApplicationInfo() without using context.
// If any one knows how, you're welcome to make a contribution.
if ((context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) == 0) {
disableProcessModeChecker();
} else {
enableProcessModeChecker();
}
return ret;
}
private static String doInitialize(String rootDir, String cacheDir, LibLoader loader, MMKVLogLevel logLevel, boolean wantLogReDirecting, boolean hasCallback, long nativeHandler) {
tryLoadNativeLib(loader);
jniInitialize(rootDir, cacheDir, logLevel2Int(logLevel), wantLogReDirecting, hasCallback, nativeHandler);
MMKV.rootDir = rootDir;
return MMKV.rootDir;
}
private static boolean isNativeLibLoaded = false;
private static void tryLoadNativeLib(@Nullable LibLoader loader) {
if (isNativeLibLoaded) {
return;
}
if (loader != null) {
if (BuildConfig.FLAVOR.equals("SharedCpp")) {
loader.loadLibrary("c++_shared");
}
loader.loadLibrary("mmkv");
} else {
if (BuildConfig.FLAVOR.equals("SharedCpp")) {
System.loadLibrary("c++_shared");
}
System.loadLibrary("mmkv");
}
isNativeLibLoaded = true;
}
/**
* @deprecated This method is deprecated due to failing to automatically disable checkProcessMode() without Context.
* Use the {@link #initialize(Context, String)} method instead.
*/
@Deprecated
public static String initialize(String rootDir) {
MMKVLogLevel logLevel = BuildConfig.DEBUG ? MMKVLogLevel.LevelDebug : MMKVLogLevel.LevelInfo;
return doInitialize(rootDir, rootDir + "/.tmp", null, logLevel, false, false, 0);
}
/**
* @deprecated This method is deprecated due to failing to automatically disable checkProcessMode() without Context.
* Use the {@link #initialize(Context, String, MMKVLogLevel)} method instead.
*/
@Deprecated
public static String initialize(String rootDir, MMKVLogLevel logLevel) {
return doInitialize(rootDir, rootDir + "/.tmp", null, logLevel, false, false, 0);
}
/**
* @deprecated This method is deprecated due to failing to automatically disable checkProcessMode() without Context.
* Use the {@link #initialize(Context, String, LibLoader)} method instead.
*/
@Deprecated
public static String initialize(String rootDir, LibLoader loader) {
MMKVLogLevel logLevel = BuildConfig.DEBUG ? MMKVLogLevel.LevelDebug : MMKVLogLevel.LevelInfo;
return doInitialize(rootDir, rootDir + "/.tmp", loader, logLevel, false, false, 0);
}
/**
* @deprecated This method is deprecated due to failing to automatically disable checkProcessMode() without Context.
* Use the {@link #initialize(Context, String, LibLoader, MMKVLogLevel)} method instead.
*/
@Deprecated
public static String initialize(String rootDir, LibLoader loader, MMKVLogLevel logLevel) {
return doInitialize(rootDir, rootDir + "/.tmp", loader, logLevel, false, false, 0);
}
/**
* @param dir the customize root directory of a NameSpace
* @return a NameSpace with custom root dir
* @throws RuntimeException if there's an runtime error.
*/
public static NameSpace nameSpace(String dir) throws RuntimeException {
tryLoadNativeLib(null);
if (getNameSpace(dir)) {
return new NameSpace(dir);
}
throw new RuntimeException("Fail to get NameSpace[" + dir + "] in JNI.");
}
/**
* identical with the original MMKV with the global root dir
* @throws RuntimeException if there's an runtime error.
*/
public static NameSpace defaultNameSpace() throws RuntimeException {
if (rootDir == null) {
throw new IllegalStateException("You should Call MMKV.initialize() first.");
}
return new NameSpace(rootDir);
}
static private String rootDir = null;
/**
* @return The root folder of MMKV, defaults to $(FilesDir)/mmkv.
*/
public static String getRootDir() {
return rootDir;
}
@Contract(pure = true)
private static int logLevel2Int(@NonNull MMKVLogLevel level) {
int realLevel;
switch (level) {
case LevelDebug:
realLevel = 0;
break;
case LevelWarning:
realLevel = 2;
break;
case LevelError:
realLevel = 3;
break;
case LevelNone:
realLevel = 4;
break;
case LevelInfo:
default:
realLevel = 1;
break;
}
return realLevel;
}
/**
* Set the log level of MMKV.
*
* @param level Defaults to {@link MMKVLogLevel#LevelInfo}.
*/
public static void setLogLevel(MMKVLogLevel level) {
int realLevel = logLevel2Int(level);
setLogLevel(realLevel);
}
/**
* Notify MMKV that App is about to exit. It's totally fine not calling it at all.
*/
public static native void onExit();
/**
* Single-process mode. The default mode on an MMKV instance.
*/
static public final int SINGLE_PROCESS_MODE = 1 << 0;
/**
* Multi-process mode.
* To enable multi-process accessing of an MMKV instance, you must set this mode whenever you getting that instance.
*/
static public final int MULTI_PROCESS_MODE = 1 << 1;
// in case someone mistakenly pass Context.MODE_MULTI_PROCESS
static private final int CONTEXT_MODE_MULTI_PROCESS = 1 << 2;
static private final int ASHMEM_MODE = 1 << 3;
static private final int BACKUP_MODE = 1 << 4;
/**
* Read-only mode.
*/
static public final int READ_ONLY_MODE = 1 << 5;
/**
* Create an MMKV instance with an unique ID (in single-process mode).
*
* @param mmapID The unique ID of the MMKV instance.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID) throws RuntimeException {
return mmkvWithID(mmapID, new MMKVConfig());
}
/**
* Create an MMKV instance with an unique ID (in single-process mode).
*
* @param mmapID The unique ID of the MMKV instance.
* @param config The all-in-one configuration for the MMKV instance.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID, MMKVConfig config) throws RuntimeException {
if (rootDir == null) {
throw new IllegalStateException("You should Call MMKV.initialize() first.");
}
int enableKeyExpire = (config.enableKeyExpire != null) ? (config.enableKeyExpire ? 1 : 0) : -1;
Integer value = recoverIndex.get(config.recover);
int recover = (value == null) ? -1 : value;
long handle = getMMKVWithID(mmapID, config.mode, config.cryptKey, config.rootPath, config.expectedCapacity,
config.aes256, enableKeyExpire, config.expiredInSeconds, config.enableCompareBeforeSet,
recover, config.itemSizeLimit);
return checkProcessMode(handle, mmapID, config.mode);
}
/**
* Create an MMKV instance in single-process or multi-process mode.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID, int mode) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance in single-process or multi-process mode.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param expectedCapacity The file size you expected when opening or creating file
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID, int mode, long expectedCapacity) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.expectedCapacity = expectedCapacity;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance in customize process mode, with an encryption key.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.cryptKey = cryptKey;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance in customize process mode, with an encryption key.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 32 bytes).
* @param aes256 Use AES 256 key length
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey, boolean aes256) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.aes256 = aes256;
config.cryptKey = cryptKey;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance in customize folder.
*
* @param mmapID The unique ID of the MMKV instance.
* @param rootPath The folder of the MMKV instance, defaults to $(FilesDir)/mmkv.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID, String rootPath) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.rootPath = rootPath;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance in customize folder.
*
* @param mmapID The unique ID of the MMKV instance.
* @param rootPath The folder of the MMKV instance, defaults to $(FilesDir)/mmkv.
* @param expectedCapacity The file size you expected when opening or creating file
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID, String rootPath, long expectedCapacity) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.rootPath = rootPath;
config.expectedCapacity = expectedCapacity;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance with customize settings all in one.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
* @param rootPath The folder of the MMKV instance, defaults to $(FilesDir)/mmkv.
* @param expectedCapacity The file size you expected when opening or creating file
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey, String rootPath, long expectedCapacity)
throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.cryptKey = cryptKey;
config.rootPath = rootPath;
config.expectedCapacity = expectedCapacity;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance with customize settings all in one.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 32 bytes).
* @param aes256 Use AES 256 key length
* @param rootPath The folder of the MMKV instance, defaults to $(FilesDir)/mmkv.
* @param expectedCapacity The file size you expected when opening or creating file
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey, boolean aes256, String rootPath,
long expectedCapacity) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.aes256 = aes256;
config.cryptKey = cryptKey;
config.rootPath = rootPath;
config.expectedCapacity = expectedCapacity;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance with customize settings all in one.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
* @param rootPath The folder of the MMKV instance, defaults to $(FilesDir)/mmkv.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey, String rootPath)
throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.cryptKey = cryptKey;
config.rootPath = rootPath;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance with customize settings all in one.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 32 bytes).
* @param aes256 Use AES 256 key length.
* @param rootPath The folder of the MMKV instance, defaults to $(FilesDir)/mmkv.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey, boolean aes256, String rootPath)
throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.aes256 = aes256;
config.cryptKey = cryptKey;
config.rootPath = rootPath;
return mmkvWithID(mmapID, config);
}
/**
* Get an backed-up MMKV instance with customize settings all in one.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
* @param rootPath The backup folder of the MMKV instance.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV backedUpMMKVWithID(String mmapID, int mode, @Nullable String cryptKey, String rootPath)
throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.cryptKey = cryptKey;
config.rootPath = rootPath;
return mmkvWithID(mmapID, config);
}
/**
* Get an backed-up MMKV instance with customize settings all in one.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 32 bytes).
* @param aes256 Use AES 256 key length.
* @param rootPath The backup folder of the MMKV instance.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV backedUpMMKVWithID(String mmapID, int mode, @Nullable String cryptKey, boolean aes256, String rootPath)
throws RuntimeException {
MMKVConfig config = new MMKVConfig();
mode |= BACKUP_MODE;
config.mode = mode;
config.cryptKey = cryptKey;
config.rootPath = rootPath;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance base on Anonymous Shared Memory, aka not synced to any disk files.
*
* @param context The context of Android App, usually from Application.
* @param mmapID The unique ID of the MMKV instance.
* @param size The maximum size of the underlying Anonymous Shared Memory.
* Anonymous Shared Memory on Android can't grow dynamically, must set an appropriate size on creation.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithAshmemID(Context context, String mmapID, int size, int mode, @Nullable String cryptKey)
throws RuntimeException {
return mmkvWithAshmemID(context, mmapID, size, mode, cryptKey, false);
}
/**
* Create an MMKV instance base on Anonymous Shared Memory, aka not synced to any disk files.
*
* @param context The context of Android App, usually from Application.
* @param mmapID The unique ID of the MMKV instance.
* @param size The maximum size of the underlying Anonymous Shared Memory.
* Anonymous Shared Memory on Android can't grow dynamically, must set an appropriate size on creation.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 32 bytes).
* @param aes256 Use AES 256 key length.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithAshmemID(Context context, String mmapID, int size, int mode, @Nullable String cryptKey,
boolean aes256) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode | ASHMEM_MODE;
config.expectedCapacity = size;
config.aes256 = aes256;
config.cryptKey = cryptKey;
return mmkvWithAshmemID(context, mmapID, config);
}
/**
* Create an MMKV instance base on Anonymous Shared Memory, aka not synced to any disk files.
*
* @param context The context of Android App, usually from Application.
* @param mmapID The unique ID of the MMKV instance.
* @param config The all-in-one configuration for the MMKV instance.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV mmkvWithAshmemID(Context context, String mmapID, MMKVConfig config) throws RuntimeException {
if (rootDir == null) {
throw new IllegalStateException("You should Call MMKV.initialize() first.");
}
String processName = MMKVContentProvider.getProcessNameByPID(context, Process.myPid());
if (processName == null || processName.isEmpty()) {
String message = "process name detect fail, try again later";
simpleLog(MMKVLogLevel.LevelError, message);
throw new IllegalStateException(message);
}
if (processName.contains(":")) {
Uri uri = MMKVContentProvider.contentUri(context);
if (uri == null) {
String message = "MMKVContentProvider has invalid authority";
simpleLog(MMKVLogLevel.LevelError, message);
throw new IllegalStateException(message);
}
simpleLog(MMKVLogLevel.LevelInfo, "getting parcelable mmkv in process, Uri = " + uri);
Bundle extras = new Bundle();
extras.putInt(MMKVContentProvider.KEY_SIZE, (int) config.expectedCapacity);
extras.putInt(MMKVContentProvider.KEY_MODE, config.mode);
if (config.cryptKey != null) {
extras.putString(MMKVContentProvider.KEY_CRYPT, config.cryptKey);
}
ContentResolver resolver = context.getContentResolver();
Bundle result = resolver.call(uri, MMKVContentProvider.FUNCTION_NAME, mmapID, extras);
if (result != null) {
result.setClassLoader(ParcelableMMKV.class.getClassLoader());
ParcelableMMKV parcelableMMKV = result.getParcelable(MMKVContentProvider.KEY);
if (parcelableMMKV != null) {
MMKV mmkv = parcelableMMKV.toMMKV();
if (mmkv != null) {
simpleLog(MMKVLogLevel.LevelInfo,
mmkv.mmapID() + " fd = " + mmkv.ashmemFD() + ", meta fd = " + mmkv.ashmemMetaFD());
return mmkv;
}
}
}
}
simpleLog(MMKVLogLevel.LevelInfo, "getting mmkv in main process");
config.mode = config.mode | ASHMEM_MODE;
return mmkvWithID(mmapID, config);
}
/**
* Create the default MMKV instance in single-process mode.
*
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV defaultMMKV() throws RuntimeException {
MMKVConfig config = new MMKVConfig();
return defaultMMKV(config);
}
/**
* Create the default MMKV instance in customize process mode, with an encryption key.
*
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV defaultMMKV(int mode, @Nullable String cryptKey) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.cryptKey = cryptKey;
return defaultMMKV(config);
}
/**
* Create the default MMKV instance in customize process mode, with an encryption key.
*
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 32 bytes).
* @param aes256 Use AES 256 key length.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV defaultMMKV(int mode, @Nullable String cryptKey, boolean aes256) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.aes256 = aes256;
config.cryptKey = cryptKey;
return defaultMMKV(config);
}
/**
* Create the default MMKV instance in customize configuration.
*
* @param config The all-in-one configuration for the MMKV instance.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public static MMKV defaultMMKV(MMKVConfig config) throws RuntimeException {
if (rootDir == null) {
throw new IllegalStateException("You should Call MMKV.initialize() first.");
}
int enableKeyExpire = (config.enableKeyExpire != null) ? (config.enableKeyExpire ? 1 : 0) : -1;
Integer value = recoverIndex.get(config.recover);
int recover = (value == null) ? -1 : value;
long handle = getDefaultMMKV(config.mode, config.cryptKey, config.expectedCapacity, config.aes256,
enableKeyExpire, config.expiredInSeconds, config.enableCompareBeforeSet, recover, config.itemSizeLimit);
return checkProcessMode(handle, "DefaultMMKV", config.mode);
}
@NonNull
@Contract("_, _, _ -> new")
static MMKV checkProcessMode(long handle, String mmapID, int mode) throws RuntimeException {
if (handle == 0) {
throw new RuntimeException("Fail to create an MMKV instance [" + mmapID + "] in JNI");
}
if (!isProcessModeCheckerEnabled) {
return new MMKV(handle);
}
synchronized (checkedHandleSet) {
if (!checkedHandleSet.contains(handle)) {
if (!checkProcessMode(handle)) {
String message;
if (mode == SINGLE_PROCESS_MODE) {
message = "Opening a multi-process MMKV instance [" + mmapID + "] with SINGLE_PROCESS_MODE!";
} else {
message = "Opening an MMKV instance [" + mmapID + "] with MULTI_PROCESS_MODE, ";
message += "while it's already been opened with SINGLE_PROCESS_MODE by someone somewhere else!";
}
throw new IllegalArgumentException(message);
}
checkedHandleSet.add(handle);
}
}
return new MMKV(handle);
}
// Enable checkProcessMode() when initializing an MMKV instance, it's automatically enabled on debug build.
private static boolean isProcessModeCheckerEnabled = true;
/**
* Manually enable the process mode checker.
* By default, it's automatically enabled in DEBUG build, and disabled in RELEASE build.
* If it's enabled, MMKV will throw exceptions when an MMKV instance is created with mismatch process mode.
*/
public static void enableProcessModeChecker() {
synchronized (checkedHandleSet) {
isProcessModeCheckerEnabled = true;
}
enableDisableProcessMode(true);
Log.i("MMKV", "Enable checkProcessMode()");
}
/**
* Manually disable the process mode checker.
* By default, it's automatically enabled in DEBUG build, and disabled in RELEASE build.
* If it's enabled, MMKV will throw exceptions when an MMKV instance is created with mismatch process mode.
*/
public static void disableProcessModeChecker() {
synchronized (checkedHandleSet) {
isProcessModeCheckerEnabled = false;
}
enableDisableProcessMode(false);
Log.i("MMKV", "Disable checkProcessMode()");
}
/**
* @return The encryption key (no more than 16 bytes).
*/
@Nullable
public native String cryptKey();
/**
* Transform plain text into encrypted text, or vice versa by passing a null encryption key.
* You can also change existing crypt key with a different cryptKey.
*
* @param cryptKey The new encryption key (no more than 16 bytes).
* @return True if success, otherwise False.
*/
public boolean reKey(@Nullable String cryptKey) {
return doReKey(cryptKey, false);
}
/**
* Transform plain text into encrypted text, or vice versa by passing a null encryption key.
* You can also change existing crypt key with a different cryptKey.
*
* @param cryptKey The new encryption key (no more than 32 bytes).
* @param aes256 Use AES 256 key length
* @return True if success, otherwise False.
*/
public boolean reKey(@Nullable String cryptKey, boolean aes256) {
return doReKey(cryptKey, aes256);
}
private native boolean doReKey(@Nullable String cryptKey, boolean aes256);
/**
* Just reset the encryption key (will not encrypt or decrypt anything).
* Usually you should call this method after another process has {@link #reKey(String)} the multi-process MMKV instance.
*
* @param cryptKey The new encryption key (no more than 16 bytes).
*/
public void checkReSetCryptKey(@Nullable String cryptKey) {
doCheckReSetCryptKey(cryptKey, false);
}
/**
* Just reset the encryption key (will not encrypt or decrypt anything).
* Usually you should call this method after another process has {@link #reKey(String)} the multi-process MMKV instance.
*
* @param cryptKey The new encryption key (no more than 16 bytes).
* @param aes256 Use AES 256 key length
*/
public void checkReSetCryptKey(@Nullable String cryptKey, boolean aes256) {
doCheckReSetCryptKey(cryptKey, aes256);
}
private native void doCheckReSetCryptKey(@Nullable String cryptKey, boolean aes256);
/**
* @return The device's memory page size.
*/
public static native int pageSize();
/**
* @return The version of MMKV.
*/
public static native String version();
/**
* @return The unique ID of the MMKV instance.
*/
public native String mmapID();
/**
* Exclusively inter-process lock the MMKV instance.
* It will block and wait until it successfully locks the file.
* It will make no effect if the MMKV instance is created with {@link #SINGLE_PROCESS_MODE}.
*/
public native void lock();
/**
* Exclusively inter-process unlock the MMKV instance.
* It will make no effect if the MMKV instance is created with {@link #SINGLE_PROCESS_MODE}.
*/
public native void unlock();
/**
* Try exclusively inter-process lock the MMKV instance.
* It will not block if the file has already been locked by another process.
* It will make no effect if the MMKV instance is created with {@link #SINGLE_PROCESS_MODE}.
*
* @return True if successfully locked, otherwise return immediately with False.
*/
public native boolean tryLock();
public boolean encode(String key, boolean value) {
return encodeBool(nativeHandle, key, value);
}
/**
* Set value with customize expiration in seconds.
*
* @param expireDurationInSecond override the default duration, {@link #ExpireNever} (0) means never expire.
*/
public boolean encode(String key, boolean value, int expireDurationInSecond) {
return encodeBool_2(nativeHandle, key, value, expireDurationInSecond);
}
public boolean decodeBool(String key) {
return decodeBool(nativeHandle, key, false);
}
public boolean decodeBool(String key, boolean defaultValue) {
return decodeBool(nativeHandle, key, defaultValue);
}
public boolean encode(String key, int value) {
return encodeInt(nativeHandle, key, value);
}
/**
* Set value with customize expiration in seconds.
*
* @param expireDurationInSecond override the default duration, {@link #ExpireNever} (0) means never expire.
*/
public boolean encode(String key, int value, int expireDurationInSecond) {
return encodeInt_2(nativeHandle, key, value, expireDurationInSecond);
}
public int decodeInt(String key) {
return decodeInt(nativeHandle, key, 0);
}
public int decodeInt(String key, int defaultValue) {
return decodeInt(nativeHandle, key, defaultValue);
}
public boolean encode(String key, long value) {
return encodeLong(nativeHandle, key, value);
}
/**
* Set value with customize expiration in seconds.
*
* @param expireDurationInSecond override the default duration, {@link #ExpireNever} (0) means never expire.
*/
public boolean encode(String key, long value, int expireDurationInSecond) {
return encodeLong_2(nativeHandle, key, value, expireDurationInSecond);
}
public long decodeLong(String key) {
return decodeLong(nativeHandle, key, 0);
}
public long decodeLong(String key, long defaultValue) {
return decodeLong(nativeHandle, key, defaultValue);
}
public boolean encode(String key, float value) {
return encodeFloat(nativeHandle, key, value);
}
/**
* Set value with customize expiration in seconds.
*
* @param expireDurationInSecond override the default duration, {@link #ExpireNever} (0) means never expire.
*/
public boolean encode(String key, float value, int expireDurationInSecond) {
return encodeFloat_2(nativeHandle, key, value, expireDurationInSecond);
}
public float decodeFloat(String key) {
return decodeFloat(nativeHandle, key, 0);
}
public float decodeFloat(String key, float defaultValue) {
return decodeFloat(nativeHandle, key, defaultValue);
}
public boolean encode(String key, double value) {
return encodeDouble(nativeHandle, key, value);
}
/**
* Set value with customize expiration in seconds.
*
* @param expireDurationInSecond override the default duration, {@link #ExpireNever} (0) means never expire.
*/
public boolean encode(String key, double value, int expireDurationInSecond) {
return encodeDouble_2(nativeHandle, key, value, expireDurationInSecond);
}
public double decodeDouble(String key) {
return decodeDouble(nativeHandle, key, 0);
}
public double decodeDouble(String key, double defaultValue) {
return decodeDouble(nativeHandle, key, defaultValue);
}
public boolean encode(String key, @Nullable String value) {
return encodeString(nativeHandle, key, value);
}
/**
* Set value with customize expiration in seconds.
*
* @param expireDurationInSecond override the default duration, {@link #ExpireNever} (0) means never expire.
*/
public boolean encode(String key, @Nullable String value, int expireDurationInSecond) {
return encodeString_2(nativeHandle, key, value, expireDurationInSecond);
}
@Nullable
public String decodeString(String key) {
return decodeString(nativeHandle, key, null);
}
@Nullable
public String decodeString(String key, @Nullable String defaultValue) {
return decodeString(nativeHandle, key, defaultValue);
}
public boolean encode(String key, @Nullable Set value) {
return encodeSet(nativeHandle, key, (value == null) ? null : value.toArray(new String[0]));
}
/**
* Set value with customize expiration in seconds.
*
* @param expireDurationInSecond override the default duration, {@link #ExpireNever} (0) means never expire.
*/
public boolean encode(String key, @Nullable Set value, int expireDurationInSecond) {
return encodeSet_2(nativeHandle, key, (value == null) ? null : value.toArray(new String[0]), expireDurationInSecond);
}
@Nullable
public Set decodeStringSet(String key) {
return decodeStringSet(key, null);
}
@Nullable
public Set decodeStringSet(String key, @Nullable Set defaultValue) {
return decodeStringSet(key, defaultValue, HashSet.class);
}
@SuppressWarnings("unchecked")
@Nullable
public Set decodeStringSet(String key, @Nullable Set defaultValue, Class extends Set> cls) {
String[] result = decodeStringSet(nativeHandle, key);
if (result == null) {
return defaultValue;
}
Set a;
try {
a = cls.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
return defaultValue;
}
a.addAll(Arrays.asList(result));
return a;
}
public boolean encode(String key, @Nullable byte[] value) {
return encodeBytes(nativeHandle, key, value);
}
/**
* Set value with customize expiration in seconds.
*
* @param expireDurationInSecond override the default duration, {@link #ExpireNever} (0) means never expire.
*/
public boolean encode(String key, @Nullable byte[] value, int expireDurationInSecond) {
return encodeBytes_2(nativeHandle, key, value, expireDurationInSecond);
}
@Nullable
public byte[] decodeBytes(String key) {
return decodeBytes(key, null);
}
@Nullable
public byte[] decodeBytes(String key, @Nullable byte[] defaultValue) {
byte[] ret = decodeBytes(nativeHandle, key);
return (ret != null) ? ret : defaultValue;
}
private static final HashMap> mCreators = new HashMap<>();
private byte[] getParcelableByte(@NonNull Parcelable value) {
Parcel source = Parcel.obtain();
value.writeToParcel(source, 0);
byte[] bytes = source.marshall();
source.recycle();
return bytes;
}
public boolean encode(String key, @Nullable Parcelable value) {
if (value == null) {
return encodeBytes(nativeHandle, key, null);
}
byte[] bytes = getParcelableByte(value);
return encodeBytes(nativeHandle, key, bytes);
}
/**
* Set value with customize expiration in seconds.
*
* @param expireDurationInSecond override the default duration, {@link #ExpireNever} (0) means never expire.
*/
public boolean encode(String key, @Nullable Parcelable value, int expireDurationInSecond) {
if (value == null) {
return encodeBytes_2(nativeHandle, key, null, expireDurationInSecond);
}
byte[] bytes = getParcelableByte(value);
return encodeBytes_2(nativeHandle, key, bytes, expireDurationInSecond);
}
@Nullable
public T decodeParcelable(String key, Class tClass) {
return decodeParcelable(key, tClass, null);
}
@SuppressWarnings("unchecked")
@Nullable
public T decodeParcelable(String key, Class tClass, @Nullable T defaultValue) {
if (tClass == null) {
return defaultValue;
}
byte[] bytes = decodeBytes(nativeHandle, key);
if (bytes == null) {
return defaultValue;
}
Parcel source = Parcel.obtain();
source.unmarshall(bytes, 0, bytes.length);
source.setDataPosition(0);
try {
String name = tClass.toString();
Parcelable.Creator creator;
synchronized (mCreators) {
creator = (Parcelable.Creator) mCreators.get(name);
if (creator == null) {
Field f = tClass.getField("CREATOR");
creator = (Parcelable.Creator) f.get(null);
if (creator != null) {
mCreators.put(name, creator);
}
}
}
if (creator != null) {
return creator.createFromParcel(source);
} else {
throw new Exception("Parcelable protocol requires a "
+ "non-null static Parcelable.Creator object called "
+ "CREATOR on class " + name);
}
} catch (Exception e) {
simpleLog(MMKVLogLevel.LevelError, e.toString());
} finally {
source.recycle();
}
return defaultValue;
}
/**
* Get the actual size consumption of the key's value.
* Note: might be a little bigger than value's length.
*
* @param key The key of the value.
*/
public int getValueSize(String key) {
return valueSize(nativeHandle, key, false);
}
/**
* Get the actual size of the key's value. String's length or byte[]'s length, etc.
*
* @param key The key of the value.
*/
public int getValueActualSize(String key) {
return valueSize(nativeHandle, key, true);
}
/**
* Check whether or not MMKV contains the key.
*
* @param key The key of the value.
*/
public boolean containsKey(String key) {
return containsKey(nativeHandle, key);
}
/**
* @return All the keys.
*/
@Nullable
public String[] allKeys() {
return allKeys(nativeHandle, false);
}
/**
* @return All non-expired keys. Note that this call has costs.
*/
@Nullable
public String[] allNonExpireKeys() {
return allKeys(nativeHandle, true);
}
/**
* @return The total count of all the keys.
*/
public long count() {
return count(nativeHandle, false);
}
/**
* @return The total count of all non-expired keys. Note that this call has costs.
*/
public long countNonExpiredKeys() {
return count(nativeHandle, true);
}
/**
* Get the size of the underlying file. Align to the disk block size, typically 4K for an Android device.
*/
public long totalSize() {
return totalSize(nativeHandle);
}
/**
* Get the actual used size of the MMKV instance.
* This size might increase and decrease as MMKV doing insertion and full write back.
*/
public long actualSize() {
return actualSize(nativeHandle);
}
public void removeValueForKey(String key) {
removeValueForKey(nativeHandle, key);
}
/**
* Batch remove some keys from the MMKV instance.
*
* @param arrKeys The keys to be removed.
*/
public native void removeValuesForKeys(String[] arrKeys);
/**
* Clear all the key-values inside the MMKV instance.
* The data file will be trimmed down to `pageSize`, and some sync operations will be called
* If you do not want to trim the file, use {@link #clearAllWithKeepingSpace()} instead for better performance
*/
public native void clearAll();
/**
* Faster {@link #clearAll()} implementation
* The file size is kept as previous for later use
*/
public native void clearAllWithKeepingSpace();
/**
* The {@link #totalSize()} of an MMKV instance won't reduce after deleting key-values,
* call this method after lots of deleting if you care about disk usage.
* Note that {@link #clearAll()} has a similar effect.
*/
public native void trim();
/**
* import all key-value items from src
* @return count of items imported
*/
public long importFrom(MMKV src) {
return importFrom(nativeHandle, src.nativeHandle);
}
/**
* Call this method if the MMKV instance is no longer needed in the near future.
* Any subsequent call to any MMKV instances with the same ID is undefined behavior.
*/
public native void close();
/**
* Clear memory cache of the MMKV instance.
* You can call it on memory warning.
* Any subsequent call to the MMKV instance will trigger all key-values loading from the file again.
*/
public native void clearMemoryCache();
/**
* Save all mmap memory to file synchronously.
* You don't need to call this, really, I mean it.
* Unless you worry about the device running out of battery.
*/
public void sync() {
sync(true);
}
/**
* Save all mmap memory to file asynchronously.
* No need to call this unless you worry about the device running out of battery.
*/
public void async() {
sync(false);
}
private native void sync(boolean sync);
/**
* Check whether the MMKV file is valid or not.
* Note: Don't use this to check the existence of the instance, the result is undefined on nonexistent files.
*/
public static boolean isFileValid(String mmapID) {
return isFileValid(mmapID, null);
}
/**
* Check whether the MMKV file is valid or not on customize folder.
*
* @param mmapID The unique ID of the MMKV instance.
* @param rootPath The folder of the MMKV instance, defaults to $(FilesDir)/mmkv.
*/
public static native boolean isFileValid(String mmapID, @Nullable String rootPath);
/**
* remove the storage of the MMKV, including the data file & meta file (.crc)
* Note: the existing instance (if any) will be closed & destroyed
*
* @param mmapID The unique ID of the MMKV instance.
*/
public static boolean removeStorage(String mmapID) {
return removeStorage(mmapID, null);
}
/**
* remove the storage of the MMKV, including the data file & meta file (.crc)
* Note: the existing instance (if any) will be closed & destroyed
*
* @param mmapID The unique ID of the MMKV instance.
* @param rootPath The folder of the MMKV instance, defaults to $(FilesDir)/mmkv.
*/
public static native boolean removeStorage(String mmapID, @Nullable String rootPath);
/**
* check existence of the MMKV file
* @param mmapID The unique ID of the MMKV instance.
*/
public static boolean checkExist(String mmapID) {
return checkExist(mmapID, null);
}
/**
* check existence of the MMKV file
* @param mmapID The unique ID of the MMKV instance.
* @param rootPath The folder of the MMKV instance, defaults to $(FilesDir)/mmkv.
*/
public static native boolean checkExist(String mmapID, @Nullable String rootPath);
/**
* Atomically migrate all key-values from an existent SharedPreferences to the MMKV instance.
*
* @param preferences The SharedPreferences to import from.
* @return The total count of key-values imported.
*/
@SuppressWarnings("unchecked")
public int importFromSharedPreferences(@NonNull SharedPreferences preferences) {
Map kvs = preferences.getAll();
if (kvs == null || kvs.size() <= 0) {
return 0;
}
for (Map.Entry entry : kvs.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key == null || value == null) {
continue;
}
if (value instanceof Boolean) {
encodeBool(nativeHandle, key, (boolean) value);
} else if (value instanceof Integer) {
encodeInt(nativeHandle, key, (int) value);
} else if (value instanceof Long) {
encodeLong(nativeHandle, key, (long) value);
} else if (value instanceof Float) {
encodeFloat(nativeHandle, key, (float) value);
} else if (value instanceof Double) {
encodeDouble(nativeHandle, key, (double) value);
} else if (value instanceof String) {
encodeString(nativeHandle, key, (String) value);
} else if (value instanceof Set) {
encode(key, (Set) value);
} else {
simpleLog(MMKVLogLevel.LevelError, "unknown type: " + value.getClass());
}
}
return kvs.size();
}
/**
* backup one MMKV instance to dstDir
*
* @param mmapID the MMKV ID to backup
* @param rootPath the customize root path of the MMKV, if null then backup from the root dir of MMKV
* @param dstDir the backup destination directory
*/
public static native boolean backupOneToDirectory(String mmapID, String dstDir, @Nullable String rootPath);
/**
* restore one MMKV instance from srcDir
*
* @param mmapID the MMKV ID to restore
* @param srcDir the restore source directory
* @param rootPath the customize root path of the MMKV, if null then restore to the root dir of MMKV
*/
public static native boolean restoreOneMMKVFromDirectory(String mmapID, String srcDir, @Nullable String rootPath);
/**
* backup all MMKV instance from default root dir to dstDir
*
* @param dstDir the backup destination directory
* @return count of MMKV successfully backuped
*/
public static native long backupAllToDirectory(String dstDir);
/**
* restore all MMKV instance from srcDir to default root dir
*
* @param srcDir the restore source directory
* @return count of MMKV successfully restored
*/
public static native long restoreAllFromDirectory(String srcDir);
// TODO: we can't have these functionality because we can't know for sure
// that each instance inside NameSpace has been upgraded successfully or not.
// The workaround is to manually call backup/restore on each instance of NameSpace.
// /**
// * backup all MMKV instance from srcDir to dstDir
// *
// * @param dstDir the backup destination directory
// * @param srcDir the backup source directory
// * @return count of MMKV successfully backuped
// */
// public static native long backupAllToDirectory(String dstDir, String srcDir);
//
// /**
// * restore all MMKV instance from srcDir to dstDir
// *
// * @param srcDir the restore source directory
// * @param dstDir the restore destination directory
// * @return count of MMKV successfully restored
// */
// public static native long restoreAllFromDirectory(String srcDir, String dstDir);
public static final int ExpireNever = 0;
public static final int ExpireInMinute = 60;
public static final int ExpireInHour = 60 * 60;
public static final int ExpireInDay = 24 * 60 * 60;
public static final int ExpireInMonth = 30 * 24 * 60 * 60;
public static final int ExpireInYear = 365 * 30 * 24 * 60 * 60;
/**
* Enable auto key expiration. This is a upgrade operation, the file format will change.
* And the file won't be accessed correctly by older version (v1.2.16) of MMKV.
* NOTICE: enableCompareBeforeSet will be invalid when Expiration is on
* @param expireDurationInSecond the expire duration for all keys, {@link #ExpireNever} (0) means no default duration (aka each key will have it's own expire date)
*/
public native boolean enableAutoKeyExpire(int expireDurationInSecond);
/**
* Disable auto key expiration. This is a downgrade operation.
*/
public native boolean disableAutoKeyExpire();
/**
* Enable data compare before set, for better performance.
* If data for key seldom changes, use it.
* When encryption or expiration is on, compare-before-set will be invalid.
* For encryption, compare operation must decrypt data which is time consuming.
* For expiration, compare is useless because in most cases the expiration time changes every time.
*/
public void enableCompareBeforeSet() {
if (isExpirationEnabled()) {
Log.e("MMKV", "enableCompareBeforeSet is invalid when Expiration is on");
if (BuildConfig.DEBUG) {
throw new RuntimeException("enableCompareBeforeSet is invalid when Expiration is on");
}
}
if (isEncryptionEnabled()) {
Log.e("MMKV", "enableCompareBeforeSet is invalid when key encryption is on");
if (BuildConfig.DEBUG) {
throw new RuntimeException("enableCompareBeforeSet is invalid when Expiration is on");
}
}
nativeEnableCompareBeforeSet();
}
@FastNative
private native void nativeEnableCompareBeforeSet();
/**
* Disable data compare before set
* disabled at default
*/
public native void disableCompareBeforeSet();
/**
* Intentionally Not Supported. Because MMKV does type-eraser inside to get better performance.
*/
@Override
public Map getAll() {
throw new UnsupportedOperationException(
"Intentionally Not Supported. Use allKeys() instead, getAll() not implement because type-erasure inside mmkv");
}
@Nullable
@Override
public String getString(String key, @Nullable String defValue) {
return decodeString(nativeHandle, key, defValue);
}
@Override
public Editor putString(String key, @Nullable String value) {
encodeString(nativeHandle, key, value);
return this;
}
public Editor putString(String key, @Nullable String value, int expireDurationInSecond) {
encodeString_2(nativeHandle, key, value, expireDurationInSecond);
return this;
}
@Nullable
@Override
public Set getStringSet(String key, @Nullable Set defValues) {
return decodeStringSet(key, defValues);
}
@Override
public Editor putStringSet(String key, @Nullable Set values) {
encode(key, values);
return this;
}
public Editor putStringSet(String key, @Nullable Set values, int expireDurationInSecond) {
encode(key, values, expireDurationInSecond);
return this;
}
public Editor putBytes(String key, @Nullable byte[] bytes) {
encode(key, bytes);
return this;
}
public Editor putBytes(String key, @Nullable byte[] bytes, int expireDurationInSecond) {
encode(key, bytes, expireDurationInSecond);
return this;
}
public byte[] getBytes(String key, @Nullable byte[] defValue) {
return decodeBytes(key, defValue);
}
@Override
public int getInt(String key, int defValue) {
return decodeInt(nativeHandle, key, defValue);
}
@Override
public Editor putInt(String key, int value) {
encodeInt(nativeHandle, key, value);
return this;
}
public Editor putInt(String key, int value, int expireDurationInSecond) {
encodeInt_2(nativeHandle, key, value, expireDurationInSecond);
return this;
}
@Override
public long getLong(String key, long defValue) {
return decodeLong(nativeHandle, key, defValue);
}
@Override
public Editor putLong(String key, long value) {
encodeLong(nativeHandle, key, value);
return this;
}
public Editor putLong(String key, long value, int expireDurationInSecond) {
encodeLong_2(nativeHandle, key, value, expireDurationInSecond);
return this;
}
@Override
public float getFloat(String key, float defValue) {
return decodeFloat(nativeHandle, key, defValue);
}
@Override
public Editor putFloat(String key, float value) {
encodeFloat(nativeHandle, key, value);
return this;
}
public Editor putFloat(String key, float value, int expireDurationInSecond) {
encodeFloat_2(nativeHandle, key, value, expireDurationInSecond);
return this;
}
@Override
public boolean getBoolean(String key, boolean defValue) {
return decodeBool(nativeHandle, key, defValue);
}
@Override
public Editor putBoolean(String key, boolean value) {
encodeBool(nativeHandle, key, value);
return this;
}
public Editor putBoolean(String key, boolean value, int expireDurationInSecond) {
encodeBool_2(nativeHandle, key, value, expireDurationInSecond);
return this;
}
@Override
public Editor remove(String key) {
removeValueForKey(key);
return this;
}
/**
* {@link #clearAll()}
*/
@Override
public Editor clear() {
clearAll();
return this;
}
/**
* @deprecated This method is only for compatibility purpose. You should remove all the calls after migration to MMKV.
* MMKV doesn't rely on commit() to save data to file.
* If you really worry about losing battery and data corruption, call {@link #async()} or {@link #sync()} instead.
*/
@Override
@Deprecated
public boolean commit() {
sync(true);
return true;
}
/**
* @deprecated This method is only for compatibility purpose. You should remove all the calls after migration to MMKV.
* MMKV doesn't rely on apply() to save data to file.
* If you really worry about losing battery and data corruption, call {@link #async()} instead.
*/
@Override
@Deprecated
public void apply() {
sync(false);
}
@Override
public boolean contains(String key) {
return containsKey(key);
}
@Override
public Editor edit() {
return this;
}
/**
* Intentionally Not Supported by MMKV. We believe it's better not for a storage framework to notify the change of data.
* Check {@link #registerContentChangeNotify} for a potential replacement on inter-process scene.
*/
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
throw new UnsupportedOperationException("Intentionally Not implement in MMKV");
}
/**
* Intentionally Not Supported by MMKV. We believe it's better not for a storage framework to notify the change of data.
*/
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
throw new UnsupportedOperationException("Intentionally Not implement in MMKV");
}
/**
* Get an ashmem MMKV instance that has been initiated by another process.
* Normally you should just call {@link #mmkvWithAshmemID(Context, String, int, int, String)} instead.
*
* @param mmapID The unique ID of the MMKV instance.
* @param fd The file descriptor of the ashmem of the MMKV file, transferred from another process by binder.
* @param metaFD The file descriptor of the ashmem of the MMKV crc file, transferred from another process by binder.
* @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
* @throws RuntimeException If any failure in JNI or runtime.
*/
// Parcelable
@NonNull
@Contract("_, _, _, _ -> new")
public static MMKV mmkvWithAshmemFD(String mmapID, int fd, int metaFD, String cryptKey) throws RuntimeException {
return mmkvWithAshmemFD(mmapID, fd, metaFD, cryptKey, false);
}
/**
* Get an ashmem MMKV instance that has been initiated by another process.
* Normally you should just call {@link #mmkvWithAshmemID(Context, String, int, int, String)} instead.
*
* @param mmapID The unique ID of the MMKV instance.
* @param fd The file descriptor of the ashmem of the MMKV file, transferred from another process by binder.
* @param metaFD The file descriptor of the ashmem of the MMKV crc file, transferred from another process by binder.
* @param cryptKey The encryption key of the MMKV instance (no more than 32 bytes).
* @param aes256 Use AES 256 key length.
* @throws RuntimeException If any failure in JNI or runtime.
*/
// Parcelable
@NonNull
@Contract("_, _, _, _, _ -> new")
public static MMKV mmkvWithAshmemFD(String mmapID, int fd, int metaFD, String cryptKey, boolean aes256) throws RuntimeException {
long handle = getMMKVWithAshmemFD(mmapID, fd, metaFD, cryptKey, aes256);
if (handle == 0) {
throw new RuntimeException("Fail to create an ashmem MMKV instance [" + mmapID + "] in JNI");
}
return new MMKV(handle);
}
/**
* @return The file descriptor of the ashmem of the MMKV file.
*/
public native int ashmemFD();
/**
* @return The file descriptor of the ashmem of the MMKV crc file.
*/
public native int ashmemMetaFD();
/**
* Create an native buffer, whose underlying memory can be directly transferred to another JNI method.
* Avoiding unnecessary JNI boxing and unboxing.
* An NativeBuffer must be manually {@link #destroyNativeBuffer} to avoid memory leak.
*
* @param size The size of the underlying memory.
*/
@Nullable
public static NativeBuffer createNativeBuffer(int size) {
long pointer = createNB(size);
if (pointer == 0) {
return null;
}
return new NativeBuffer(pointer, size);
}
/**
* Destroy the native buffer. An NativeBuffer must be manually destroy to avoid memory leak.
*/
public static void destroyNativeBuffer(@NonNull NativeBuffer buffer) {
destroyNB(buffer.pointer, buffer.size);
}
/**
* Write the value of the key to the native buffer.
*
* @return The size written. Return -1 on any error.
*/
public int writeValueToNativeBuffer(String key, @NonNull NativeBuffer buffer) {
return writeValueToNB(nativeHandle, key, buffer.pointer, buffer.size);
}
// callback handler
private static MMKVHandler gCallbackHandler = null;
private static boolean gWantLogReDirecting = false;
/**
* Register a handler for MMKV log redirecting, and error handling.
*
* @deprecated This method is deprecated.
* Use the {@link #initialize(Context, String, LibLoader, MMKVLogLevel, MMKVHandler)} method instead.
*/
public static void registerHandler(MMKVHandler handler) {
gCallbackHandler = handler;
gWantLogReDirecting = gCallbackHandler.wantLogRedirecting();
long nativeLogHandler = gCallbackHandler.getNativeLogHandler();
setCallbackHandler(gWantLogReDirecting, true, nativeLogHandler);
if (gCallbackHandler.wantContentChangeNotification()) {
setWantsContentChangeNotify(true);
}
}
/**
* Unregister the handler for MMKV.
*/
public static void unregisterHandler() {
gCallbackHandler = null;
setCallbackHandler(false, false, 0);
gWantLogReDirecting = false;
setWantsContentChangeNotify(gContentChangeNotify != null);
}
private static int onMMKVCRCCheckFail(String mmapID) {
MMKVRecoverStrategic strategic = MMKVRecoverStrategic.OnErrorDiscard;
MMKVHandler handler = gCallbackHandler;
if (handler != null) {
strategic = handler.onMMKVCRCCheckFail(mmapID);
}
simpleLog(MMKVLogLevel.LevelInfo, "Recover strategic for " + mmapID + " is " + strategic);
Integer value = recoverIndex.get(strategic);
return (value == null) ? 0 : value;
}
private static int onMMKVFileLengthError(String mmapID) {
MMKVRecoverStrategic strategic = MMKVRecoverStrategic.OnErrorDiscard;
MMKVHandler handler = gCallbackHandler;
if (handler != null) {
strategic = handler.onMMKVFileLengthError(mmapID);
}
simpleLog(MMKVLogLevel.LevelInfo, "Recover strategic for " + mmapID + " is " + strategic);
Integer value = recoverIndex.get(strategic);
return (value == null) ? 0 : value;
}
private static void mmkvLogImp(int level, String file, int line, String function, String message) {
MMKVHandler handler = gCallbackHandler;
if (handler != null && gWantLogReDirecting) {
handler.mmkvLog(index2LogLevel[level], file, line, function, message);
} else {
switch (index2LogLevel[level]) {
case LevelDebug:
Log.d("MMKV", message);
break;
case LevelInfo:
Log.i("MMKV", message);
break;
case LevelWarning:
Log.w("MMKV", message);
break;
case LevelError:
Log.e("MMKV", message);
break;
case LevelNone:
break;
}
}
}
private static void simpleLog(MMKVLogLevel level, String message) {
StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
StackTraceElement e = stacktrace[stacktrace.length - 1];
Integer i = logLevel2Index.get(level);
int intLevel = (i == null) ? 0 : i;
mmkvLogImp(intLevel, e.getFileName(), e.getLineNumber(), e.getMethodName(), message);
}
// content change notification of other process
// trigger by getXXX() or setXXX() or checkContentChangedByOuterProcess()
private static MMKVContentChangeNotification gContentChangeNotify;
/**
* Register for MMKV inter-process content change notification.
* The notification will trigger only when any method is manually called on the MMKV instance.
* For example {@link #checkContentChangedByOuterProcess()}.
*
* @param notify The notification handler.
* @deprecated Use {@link MMKVHandler#onContentChangedByOuterProcess(String)} instead.
*/
@Deprecated
public static void registerContentChangeNotify(MMKVContentChangeNotification notify) {
gContentChangeNotify = notify;
setWantsContentChangeNotify(gContentChangeNotify != null || (gCallbackHandler != null && gCallbackHandler.wantContentChangeNotification()));
}
/**
* Unregister for MMKV inter-process content change notification.
* @deprecated Use {@link MMKVHandler#onContentChangedByOuterProcess(String)} instead.
*/
@Deprecated
public static void unregisterContentChangeNotify() {
gContentChangeNotify = null;
setWantsContentChangeNotify(gCallbackHandler != null && gCallbackHandler.wantContentChangeNotification());
}
private static void onContentChangedByOuterProcess(String mmapID) {
MMKVHandler handler = gCallbackHandler;
if (handler != null && handler.wantContentChangeNotification()) {
handler.onContentChangedByOuterProcess(mmapID);
} else if (gContentChangeNotify != null) {
gContentChangeNotify.onContentChangedByOuterProcess(mmapID);
}
}
private static void onMMKVContentLoadSuccessfully(String mmapID) {
MMKVHandler handler = gCallbackHandler;
if (handler != null) {
handler.onMMKVContentLoadSuccessfully(mmapID);
}
}
private static native void setWantsContentChangeNotify(boolean needsNotify);
/**
* Check inter-process content change manually.
*/
public native void checkContentChangedByOuterProcess();
/**
* Check if this instance is in multi-process mode.
*/
public native boolean isMultiProcess();
/**
* Check if this instance is in read-only mode.
*/
public native boolean isReadOnly();
// jni
private final long nativeHandle;
private MMKV(long handle) {
nativeHandle = handle;
}
private static native void jniInitialize(String rootDir, String cacheDir, int level, boolean wantLogReDirecting, boolean hasCallback, long nativeHandler);
native static long
getMMKVWithID(String mmapID, int mode, @Nullable String cryptKey, @Nullable String rootPath,
long expectedCapacity, boolean aes256, int enableKeyExpire, int expiredInSeconds,
boolean enableCompareBeforeSet, int recover, int itemSizeLimit);
private native static long getDefaultMMKV(int mode, @Nullable String cryptKey, long expectedCapacity,
boolean aes256, int enableKeyExpire, int expiredInSeconds,
boolean enableCompareBeforeSet, int recover, int itemSizeLimit);
private native static long getMMKVWithAshmemFD(String mmapID, int fd, int metaFD, @Nullable String cryptKey, boolean aes256);
private native boolean encodeBool(long handle, String key, boolean value);
private native boolean encodeBool_2(long handle, String key, boolean value, int expireDurationInSecond);
private native boolean decodeBool(long handle, String key, boolean defaultValue);
private native boolean encodeInt(long handle, String key, int value);
private native boolean encodeInt_2(long handle, String key, int value, int expireDurationInSecond);
private native int decodeInt(long handle, String key, int defaultValue);
private native boolean encodeLong(long handle, String key, long value);
private native boolean encodeLong_2(long handle, String key, long value, int expireDurationInSecond);
private native long decodeLong(long handle, String key, long defaultValue);
private native boolean encodeFloat(long handle, String key, float value);
private native boolean encodeFloat_2(long handle, String key, float value, int expireDurationInSecond);
private native float decodeFloat(long handle, String key, float defaultValue);
private native boolean encodeDouble(long handle, String key, double value);
private native boolean encodeDouble_2(long handle, String key, double value, int expireDurationInSecond);
private native double decodeDouble(long handle, String key, double defaultValue);
private native boolean encodeString(long handle, String key, @Nullable String value);
private native boolean encodeString_2(long handle, String key, @Nullable String value, int expireDurationInSecond);
@Nullable
private native String decodeString(long handle, String key, @Nullable String defaultValue);
private native boolean encodeSet(long handle, String key, @Nullable String[] value);
private native boolean encodeSet_2(long handle, String key, @Nullable String[] value, int expireDurationInSecond);
@Nullable
private native String[] decodeStringSet(long handle, String key);
private native boolean encodeBytes(long handle, String key, @Nullable byte[] value);
private native boolean encodeBytes_2(long handle, String key, @Nullable byte[] value, int expireDurationInSecond);
@Nullable
private native byte[] decodeBytes(long handle, String key);
private native boolean containsKey(long handle, String key);
private native String[] allKeys(long handle, boolean filterExpire);
private native long count(long handle, boolean filterExpire);
private native long totalSize(long handle);
private native long actualSize(long handle);
private native void removeValueForKey(long handle, String key);
private native int valueSize(long handle, String key, boolean actualSize);
private static native void setLogLevel(int level);
private static native void setCallbackHandler(boolean logReDirecting, boolean hasCallback, long nativeHandle);
private static native long createNB(int size);
private static native void destroyNB(long pointer, int size);
private native int writeValueToNB(long handle, String key, long pointer, int size);
private native boolean isCompareBeforeSetEnabled();
@FastNative
private native boolean isEncryptionEnabled();
@FastNative
private native boolean isExpirationEnabled();
private static native void enableDisableProcessMode(boolean enable);
private static native boolean checkProcessMode(long handle);
private static native boolean getNameSpace(String rootPath);
private native long importFrom(long handle, long srcHandle);
}
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKVConfig.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
/**
* The all-in-one configuration for creating an MMKV instance.
*/
public class MMKVConfig {
public int mode = MMKV.SINGLE_PROCESS_MODE;
// using AES-256 key length
public boolean aes256 = false;
public String cryptKey = null;
public String rootPath = null;
public long expectedCapacity = 0; // the initial file size
public Boolean enableKeyExpire = null;
public int expiredInSeconds = 0; // ExpireNever = 0
public boolean enableCompareBeforeSet = false;
// if not set, use the old style callback
public MMKVRecoverStrategic recover = null;
// the size limit of a key-value pair, reject insert if pass limit
public int itemSizeLimit = 0;
}
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKVContentChangeNotification.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2025 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
/**
* Inter-process content change notification.
* Triggered by any method call, such as getXXX() or setXXX() or {@link MMKV#checkContentChangedByOuterProcess()}.
* @deprecated Use {@link MMKVHandler#onContentChangedByOuterProcess(String)} instead.
*/
@Deprecated
public interface MMKVContentChangeNotification {
/**
* Inter-process content change notification.
* Triggered by any method call, such as getXXX() or setXXX() or {@link MMKV#checkContentChangedByOuterProcess()}.
* @param mmapID The unique ID of the changed MMKV instance.
*/
void onContentChangedByOuterProcess(String mmapID);
}
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKVContentProvider.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* A helper class for MMKV based on Anonymous Shared Memory. {@link MMKV#mmkvWithAshmemID}
*/
public class MMKVContentProvider extends ContentProvider {
static protected final String KEY = "KEY";
static protected final String KEY_SIZE = "KEY_SIZE";
static protected final String KEY_MODE = "KEY_MODE";
static protected final String KEY_CRYPT = "KEY_CRYPT";
static protected final String FUNCTION_NAME = "mmkvFromAshmemID";
static private Uri gUri;
@Nullable
static protected Uri contentUri(Context context) {
if (MMKVContentProvider.gUri != null) {
return MMKVContentProvider.gUri;
}
if (context == null) {
return null;
}
String authority = queryAuthority(context);
if (authority == null) {
return null;
}
MMKVContentProvider.gUri = Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + authority);
return MMKVContentProvider.gUri;
}
@NonNull
private Bundle mmkvFromAshmemID(String ashmemID, int size, int mode, String cryptKey) throws RuntimeException {
MMKV mmkv = MMKV.mmkvWithAshmemID(getContext(), ashmemID, size, mode, cryptKey);
ParcelableMMKV parcelableMMKV = new ParcelableMMKV(mmkv);
Log.i("MMKV", ashmemID + " fd = " + mmkv.ashmemFD() + ", meta fd = " + mmkv.ashmemMetaFD());
Bundle result = new Bundle();
result.putParcelable(MMKVContentProvider.KEY, parcelableMMKV);
return result;
}
@Nullable
private static String queryAuthority(Context context) {
try {
ComponentName componentName = new ComponentName(context, MMKVContentProvider.class.getName());
PackageManager mgr = context.getPackageManager();
if (mgr != null) {
ProviderInfo providerInfo = mgr.getProviderInfo(componentName, 0);
if (providerInfo != null) {
return providerInfo.authority;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public boolean onCreate() {
Context context = getContext();
if (context == null) {
return false;
}
return true;
}
protected static String getProcessNameByPID(@NonNull Context context, int pid) {
if (pid == android.os.Process.myPid()) {
return MMKVProcessUtil.getCurrentProcessName(context);
}
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (manager != null) {
// clang-format off
for (ActivityManager.RunningAppProcessInfo processInfo
: manager.getRunningAppProcesses()) {
if (processInfo.pid == pid) {
return processInfo.processName;
}
}
// clang-format on
}
return "";
}
@Nullable
@Override
public Bundle call(@NonNull String method, @Nullable String mmapID, @Nullable Bundle extras) {
if (method.equals(MMKVContentProvider.FUNCTION_NAME)) {
if (extras != null) {
int size = extras.getInt(MMKVContentProvider.KEY_SIZE);
int mode = extras.getInt(MMKVContentProvider.KEY_MODE);
String cryptKey = extras.getString(MMKVContentProvider.KEY_CRYPT);
try {
return mmkvFromAshmemID(mmapID, size, mode, cryptKey);
} catch (Exception e) {
Log.e("MMKV", e.getMessage());
return null;
}
}
}
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri,
@Nullable String[] projection,
@Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder) {
throw new java.lang.UnsupportedOperationException("Not implement in MMKV");
}
@Override
public int update(@NonNull Uri uri,
@Nullable ContentValues values,
@Nullable String selection,
@Nullable String[] selectionArgs) {
throw new java.lang.UnsupportedOperationException("Not implement in MMKV");
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
throw new java.lang.UnsupportedOperationException("Not implement in MMKV");
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
throw new java.lang.UnsupportedOperationException("Not implement in MMKV");
}
}
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKVHandler.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
/**
* Unified callback handler for MMKV.
* Callback is called on the operating thread of the MMKV instance.
*/
public interface MMKVHandler {
/**
* By default MMKV will discard all data on crc32-check failure. {@link MMKVRecoverStrategic#OnErrorDiscard}
* @param mmapID The unique ID of the MMKV instance.
* @return Return {@link MMKVRecoverStrategic#OnErrorRecover} to recover any data on the file.
*/
MMKVRecoverStrategic onMMKVCRCCheckFail(String mmapID);
/**
* By default MMKV will discard all data on file length mismatch. {@link MMKVRecoverStrategic#OnErrorDiscard}
* @param mmapID The unique ID of the MMKV instance.
* @return Return {@link MMKVRecoverStrategic#OnErrorRecover} to recover any data on the file.
*/
MMKVRecoverStrategic onMMKVFileLengthError(String mmapID);
/**
* @return Return False if you don't want log redirecting.
*/
boolean wantLogRedirecting();
/**
* Log Redirecting.
* @param level The level of this log.
* @param file The file name of this log.
* @param line The line of code of this log.
* @param function The function name of this log.
* @param message The content of this log.
*/
void mmkvLog(MMKVLogLevel level, String file, int line, String function, String message);
/**
* handle the log in NDK native code
* @return a native log handler with signature of void log(int level, const char *file, int line, const char *function, const char *message)
* return 0 to indicate no native handler
*/
default long getNativeLogHandler() { return 0; }
/**
* @return Return true if you want inter-process content change notification.
*/
default boolean wantContentChangeNotification() { return false; }
/**
* Inter-process content change notification.
* Triggered by any method call, such as getXXX() or setXXX() or {@link MMKV#checkContentChangedByOuterProcess()}.
* @param mmapID The unique ID of the changed MMKV instance.
*/
default void onContentChangedByOuterProcess(String mmapID) {}
/**
* Called when an MMKV file is loaded successfully.
* @param mmapID The unique ID of the loaded MMKV instance.
*/
default void onMMKVContentLoadSuccessfully(String mmapID) {}
}
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKVLogLevel.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2025 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
/**
* The levels of MMKV log.
*/
public enum MMKVLogLevel {
/**
* Debug level. Not available for release/production build.
*/
LevelDebug,
/**
* Info level. The default level.
*/
LevelInfo,
/**
* Warning level.
*/
LevelWarning,
/**
* Error level.
*/
LevelError,
/**
* Special level for disabling all logging.
* It's highly NOT suggested to turn off logging. Makes it hard to diagnose online/production bugs.
*/
LevelNone
}
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKVProcessUtil.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2025 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import java.lang.reflect.Method;
import java.util.List;
/**
* Get current process name for AppStore review
*/
class MMKVProcessUtil {
private static String currentProcessName = "";
public static String getCurrentProcessName(@NonNull Context context) {
if (!TextUtils.isEmpty(currentProcessName)) {
return currentProcessName;
}
currentProcessName = getCurrentProcessNameByApplication();
if (!TextUtils.isEmpty(currentProcessName)) {
return currentProcessName;
}
currentProcessName = getCurrentProcessNameByActivityThread();
if (!TextUtils.isEmpty(currentProcessName)) {
return currentProcessName;
}
currentProcessName = getCurrentProcessNameByActivityManager(context);
return currentProcessName;
}
@NonNull
private static String getCurrentProcessNameByApplication() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return Application.getProcessName();
}
return "";
}
@NonNull
private static String getCurrentProcessNameByActivityThread() {
String processName = "";
try {
Method declaredMethod = Class.forName("android.app.ActivityThread").
getDeclaredMethod("currentProcessName");
declaredMethod.setAccessible(true);
final Object invoke = declaredMethod.invoke(null);
if (invoke instanceof String) {
processName = (String) invoke;
}
} catch (Throwable e) {
e.printStackTrace();
}
return processName;
}
private static String getCurrentProcessNameByActivityManager(@NonNull Context context) {
int pid = android.os.Process.myPid();
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am != null) {
List runningAppList = am.getRunningAppProcesses();
if (runningAppList != null) {
for (ActivityManager.RunningAppProcessInfo processInfo : runningAppList) {
if (processInfo.pid == pid) {
return processInfo.processName;
}
}
}
}
return "";
}
}
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKVRecoverStrategic.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
/**
* The recover strategic of MMKV on errors. {@link MMKV#registerHandler}
*/
public enum MMKVRecoverStrategic {
/**
* The default strategic is to discard everything on errors.
*/
OnErrorDiscard,
/**
* The recover strategic will try to recover as much data as possible.
*/
OnErrorRecover,
}
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/NameSpace.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2025 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
import static com.tencent.mmkv.MMKV.SINGLE_PROCESS_MODE;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* a facade wraps custom root directory
*/
public final class NameSpace {
private final String rootDir;
// make it internal
NameSpace(String dir) {
rootDir = dir;
}
/**
* @return The root folder of this NameSpace.
*/
public String getRootDir() {
return rootDir;
}
/**
* Create an MMKV instance with an unique ID (in single-process mode).
*
* @param mmapID The unique ID of the MMKV instance.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public MMKV mmkvWithID(String mmapID) throws RuntimeException {
return mmkvWithID(mmapID, new MMKVConfig());
}
/**
* Create an MMKV instance with an unique ID (in single-process mode).
*
* @param mmapID The unique ID of the MMKV instance.
* @param config The all-in-one configuration for the MMKV instance.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public MMKV mmkvWithID(String mmapID, MMKVConfig config) throws RuntimeException {
int enableKeyExpire = (config.enableKeyExpire != null) ? (config.enableKeyExpire ? 1 : 0) : -1;
int recover = (config.recover == null) ? -1 : ((config.recover == MMKVRecoverStrategic.OnErrorDiscard) ? 0 : 1);
long handle = MMKV.getMMKVWithID(mmapID, config.mode, config.cryptKey, rootDir, config.expectedCapacity,
config.aes256, enableKeyExpire, config.expiredInSeconds, config.enableCompareBeforeSet,
recover, config.itemSizeLimit);
return MMKV.checkProcessMode(handle, mmapID, config.mode);
}
/**
* Create an MMKV instance in single-process or multi-process mode.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public MMKV mmkvWithID(String mmapID, int mode) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance in single-process or multi-process mode.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param expectedCapacity The file size you expected when opening or creating file
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public MMKV mmkvWithID(String mmapID, int mode, long expectedCapacity) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.expectedCapacity = expectedCapacity;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance in customize process mode, with an encryption key.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.cryptKey = cryptKey;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance in customize process mode, with an encryption key.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 32 bytes).
* @param aes256 Use AES 256 key length.
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey, boolean aes256) throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.aes256 = aes256;
config.cryptKey = cryptKey;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance with customize settings all in one.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
* @param expectedCapacity The file size you expected when opening or creating file
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey, long expectedCapacity)
throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.cryptKey = cryptKey;
config.expectedCapacity = expectedCapacity;
return mmkvWithID(mmapID, config);
}
/**
* Create an MMKV instance with customize settings all in one.
*
* @param mmapID The unique ID of the MMKV instance.
* @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
* @param cryptKey The encryption key of the MMKV instance (no more than 32 bytes).
* @param aes256 Use AES 256 key length.
* @param expectedCapacity The file size you expected when opening or creating file
* @throws RuntimeException if there's an runtime error.
*/
@NonNull
public MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey, boolean aes256, long expectedCapacity)
throws RuntimeException {
MMKVConfig config = new MMKVConfig();
config.mode = mode;
config.aes256 = aes256;
config.cryptKey = cryptKey;
config.expectedCapacity = expectedCapacity;
return mmkvWithID(mmapID, config);
}
/**
* backup one MMKV instance to dstDir
*
* @param mmapID the MMKV ID to backup
* @param dstDir the backup destination directory
*/
public boolean backupOneToDirectory(String mmapID, String dstDir) {
return MMKV.backupOneToDirectory(mmapID, dstDir, rootDir);
}
/**
* restore one MMKV instance from srcDir
*
* @param mmapID the MMKV ID to restore
* @param srcDir the restore source directory
*/
public boolean restoreOneMMKVFromDirectory(String mmapID, String srcDir) {
return MMKV.restoreOneMMKVFromDirectory(mmapID, srcDir, rootDir);
}
// TODO: we can't have these functionality because we can't know for sure
// that each instance inside NameSpace has been upgraded successfully or not.
// The workaround is to manually call backup/restore on each instance of NameSpace.
// /**
// * backup all MMKV instance to dstDir
// *
// * @param dstDir the backup destination directory
// * @return count of MMKV successfully backuped
// */
// public long backupAllToDirectory(String dstDir) {
// return MMKV.backupAllToDirectory(dstDir, rootDir);
// }
//
// /**
// * restore all MMKV instance from srcDir
// *
// * @param srcDir the restore source directory
// * @return count of MMKV successfully restored
// */
// public long restoreAllFromDirectory(String srcDir) {
// return MMKV.restoreAllFromDirectory(srcDir, rootDir);
// }
/**
* Check whether the MMKV file is valid or not.
* Note: Don't use this to check the existence of the instance, the result is undefined on nonexistent files.
*/
public boolean isFileValid(String mmapID) {
return MMKV.isFileValid(mmapID, rootDir);
}
/**
* remove the storage of the MMKV, including the data file & meta file (.crc)
* Note: the existing instance (if any) will be closed & destroyed
*
* @param mmapID The unique ID of the MMKV instance.
*/
public boolean removeStorage(String mmapID) {
return MMKV.removeStorage(mmapID, rootDir);
}
/**
* check existence of the MMKV file
* @param mmapID The unique ID of the MMKV instance.
*/
public boolean checkExist(String mmapID) {
return MMKV.checkExist(mmapID, rootDir);
}
}
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/NativeBuffer.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2025 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
/**
* A native memory wrapper, whose underlying memory can be passed to another JNI method directly.
* Avoiding unnecessary JNI boxing and unboxing.
* Must be destroy manually {@link MMKV#destroyNativeBuffer}.
*/
public final class NativeBuffer {
public long pointer;
public int size;
public NativeBuffer(long ptr, int length) {
pointer = ptr;
size = length;
}
}
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/ParcelableMMKV.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.jetbrains.annotations.Contract;
import java.io.IOException;
/**
* A helper class for MMKV based on Anonymous Shared Memory. {@link MMKV#mmkvWithAshmemID}
*/
public final class ParcelableMMKV implements Parcelable {
private final String mmapID;
private int ashmemFD = -1;
private int ashmemMetaFD = -1;
private String cryptKey = null;
public ParcelableMMKV(@NonNull MMKV mmkv) {
mmapID = mmkv.mmapID();
ashmemFD = mmkv.ashmemFD();
ashmemMetaFD = mmkv.ashmemMetaFD();
cryptKey = mmkv.cryptKey();
}
private ParcelableMMKV(String id, int fd, int metaFD, String key) {
mmapID = id;
ashmemFD = fd;
ashmemMetaFD = metaFD;
cryptKey = key;
}
@Nullable
public MMKV toMMKV() {
if (ashmemFD >= 0 && ashmemMetaFD >= 0) {
return MMKV.mmkvWithAshmemFD(mmapID, ashmemFD, ashmemMetaFD, cryptKey);
}
return null;
}
@Override
public int describeContents() {
return CONTENTS_FILE_DESCRIPTOR;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
try {
dest.writeString(mmapID);
ParcelFileDescriptor fd = ParcelFileDescriptor.fromFd(ashmemFD);
ParcelFileDescriptor metaFD = ParcelFileDescriptor.fromFd(ashmemMetaFD);
flags = flags | Parcelable.PARCELABLE_WRITE_RETURN_VALUE;
fd.writeToParcel(dest, flags);
metaFD.writeToParcel(dest, flags);
if (cryptKey != null) {
dest.writeString(cryptKey);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
@Nullable
@Override
public ParcelableMMKV createFromParcel(@NonNull Parcel source) {
String mmapID = source.readString();
ParcelFileDescriptor fd = ParcelFileDescriptor.CREATOR.createFromParcel(source);
ParcelFileDescriptor metaFD = ParcelFileDescriptor.CREATOR.createFromParcel(source);
String cryptKey = source.readString();
if (fd != null && metaFD != null) {
return new ParcelableMMKV(mmapID, fd.detachFd(), metaFD.detachFd(), cryptKey);
}
return null;
}
@NonNull
@Contract(value = "_ -> new", pure = true)
@Override
public ParcelableMMKV[] newArray(int size) {
return new ParcelableMMKV[size];
}
};
}
================================================
FILE: Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/UnsupportedArchitectureException.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2025 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkv;
public class UnsupportedArchitectureException extends RuntimeException {
public UnsupportedArchitectureException(String message) {
super(message);
}
}
================================================
FILE: Android/MMKV/mmkvannotation/.gitignore
================================================
/build
================================================
FILE: Android/MMKV/mmkvannotation/build.gradle
================================================
apply plugin: 'com.android.library'
android {
namespace "com.tencent.mmkv"
compileSdk rootProject.ext.compileSdk
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
minSdkVersion rootProject.ext.minSdkVersion
}
buildTypes {
release {
minifyEnabled false
consumerProguardFiles 'proguard-rules.pro'
}
}
}
configurations {
javadocDeps
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.annotation:annotation:1.9.1'
javadocDeps 'androidx.annotation:annotation:1.9.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}
================================================
FILE: Android/MMKV/mmkvannotation/src/main/AndroidManifest.xml
================================================
================================================
FILE: Android/MMKV/mmkvannotation/src/main/java/dalvik/annotation/optimization/FastNative.java
================================================
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dalvik.annotation.optimization;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* An ART runtime built-in optimization for {@code native} methods to speed up JNI transitions:
* Compared to normal {@code native} methods, {@code native} methods that are annotated with
* {@literal @}{@code FastNative} use faster JNI transitions from managed code to the native code
* and back. Calls from a {@literal @}{@code FastNative} method implementation to JNI functions
* that access the managed heap or call managed code also have faster internal transitions.
*
*
* While executing a {@literal @}{@code FastNative} method, the garbage collection cannot
* suspend the thread for essential work and may become blocked. Use with caution. Do not use
* this annotation for long-running methods, including usually-fast, but generally unbounded,
* methods. In particular, the code should not perform significant I/O operations or acquire
* native locks that can be held for a long time. (Some logging or native allocations, which
* internally acquire native locks for a short time, are generally OK. However, as the cost
* of several such operations adds up, the {@literal @}{@code FastNative} performance gain
* can become insignificant and overshadowed by potential GC delays.)
* Acquiring managed locks is OK as it internally allows thread suspension.
*
*
*
* For performance critical methods that need this annotation, it is strongly recommended
* to explicitly register the method(s) with JNI {@code RegisterNatives} instead of relying
* on the built-in dynamic JNI linking.
*
*
*
* The {@literal @}{@code FastNative} optimization was implemented for system use since
* Android 8 and became CTS-tested public API in Android 14. Developers aiming for maximum
* compatibility should avoid calling {@literal @}{@code FastNative} methods on Android 13-.
* The optimization is likely to work also on Android 8-13 devices (after all, it was used
* in the system, albeit without the strong CTS guarantees), especially those that use
* unmodified versions of ART, such as Android 12+ devices with the official ART Module.
* The built-in dynamic JNI linking is working only in Android 12+, the explicit registration
* with JNI {@code RegisterNatives} is strictly required for running on Android versions 8-11.
* The annotation is ignored on Android 7-.
*
*
*
* Deadlock Warning: As a rule of thumb, any native locks acquired in a
* {@literal @}{@link FastNative} call (despite the above warning that this is an unbounded
* operation that can block GC for a long time) must be released before returning to managed code.
*
*
*
* Say some code does:
*
*
* fast_jni_call_to_grab_a_lock();
* does_some_java_work();
* fast_jni_call_to_release_a_lock();
*
*
*
* This code can lead to deadlocks. Say thread 1 just finishes
* {@code fast_jni_call_to_grab_a_lock()} and is in {@code does_some_java_work()}.
* GC kicks in and suspends thread 1. Thread 2 now is in {@code fast_jni_call_to_grab_a_lock()}
* but is blocked on grabbing the native lock since it's held by thread 1.
* Now thread suspension can't finish since thread 2 can't be suspended since it's doing
* FastNative JNI.
*
*
*
* Normal JNI doesn't have the issue since once it's in native code,
* it is considered suspended from java's point of view.
* FastNative JNI however doesn't do the state transition done by JNI.
*
*
*
* Note that even in FastNative methods you are allowed to
* allocate objects and make upcalls into Java code. A call from Java to
* a FastNative function and back to Java is equivalent to a call from one Java
* method to another. What's forbidden in a FastNative method is blocking
* the calling thread in some non-Java code and thereby preventing the thread
* from responding to requests from the garbage collector to enter the suspended
* state.
*
*
*
* Has no effect when used with non-native methods.
*
*/
@Retention(RetentionPolicy.CLASS) // Save memory, don't instantiate as an object at runtime.
@Target(ElementType.METHOD)
public @interface FastNative { }
================================================
FILE: Android/MMKV/mmkvdemo/.gitignore
================================================
/build
================================================
FILE: Android/MMKV/mmkvdemo/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace "com.tencent.mmkvdemo"
compileSdk rootProject.ext.compileSdk
signingConfigs {
config {
keyAlias 'key0'
keyPassword 'mmkv.wxg'
storeFile rootProject.file('debug.keystore')
storePassword 'mmkv.wxg'
}
}
defaultConfig {
applicationId "com.tencent.mmkvdemo"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
arguments = ["-DANDROID_STL=c++_shared"]
// use prefab or not: Gradle has bug handling local prefab modules, fails on linking
// arguments += ["-DHAS_PREFAB=1"]
cppFlags "-fvisibility=hidden", "-funwind-tables", "-fasynchronous-unwind-tables", "-O2",
"-Wl,-z,max-page-size=16384"
}
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.config
// debuggable true
// jniDebuggable true
}
debug {
signingConfig signingConfigs.config
jniDebuggable true
}
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
flavorDimensions = ["stl_mode"]
productFlavors {
DefaultCpp.dimension = "stl_mode"
StaticCpp.dimension = "stl_mode"
SharedCpp.dimension = "stl_mode"
}
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
}
}
buildFeatures {
aidl true
// prefab true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
/*splits {
// Configures multiple APKs based on ABI.
abi {
// Enables building multiple APKs per ABI.
enable true
// By default all ABIs are included, so use reset() and include to specify that we only want specific APKs
// Resets the list of ABIs that Gradle should create APKs for to none.
reset()
// Specifies a list of ABIs that Gradle should create APKs for.
include "armeabi-v7a"
// Specifies that we do not want to also generate a universal APK that includes all ABIs.
universalApk false
}
}*/
}
repositories {
mavenCentral()
mavenLocal()
}
// Define the mmkv version to use
def mmkvVersion = "${VERSION_NAME_PREFIX}${VERSION_NAME_SUFFIX}"
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':mmkv')
// implementation "com.tencent:mmkv:$mmkvVersion"
// implementation "com.tencent:mmkv-static:$mmkvVersion" // this is identical to 'com.tencent:mmkv'
// implementation "com.tencent:mmkv-shared:$mmkvVersion" // with prefab functionality
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'com.getkeepsafe.relinker:relinker:1.4.5'
}
================================================
FILE: Android/MMKV/mmkvdemo/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keepclasseswithmembers,includedescriptorclasses class com.tencent.mmkvdemo.** {
native ;
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/AndroidManifest.xml
================================================
================================================
FILE: Android/MMKV/mmkvdemo/src/main/aidl/com/tencent/mmkvdemo/IAshmemMMKV.aidl
================================================
// IAshmemMMKV.aidl
package com.tencent.mmkvdemo;
import com.tencent.mmkv.ParcelableMMKV;
interface IAshmemMMKV {
ParcelableMMKV GetAshmemMMKV();
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/cpp/CMakeLists.txt
================================================
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html.
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.10.0)
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("mmkvdemo")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
mmkvdemo.cpp)
# Find the mmkv lib in prefab
if (HAS_PREFAB)
find_package(mmkv REQUIRED CONFIG)
target_link_libraries(${CMAKE_PROJECT_NAME}
mmkv::mmkv
)
target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE
-DENABLE_MMKV_NATIVE=1
)
endif ()
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log
)
================================================
FILE: Android/MMKV/mmkvdemo/src/main/cpp/mmkvdemo.cpp
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// A demo for using MMKV C++ interface in native code
#include
#include
#include
#ifdef ENABLE_MMKV_NATIVE
#include
#endif
using namespace std;
constexpr auto APP_NAME = "MMKVNativeDemo";
void _MMKVLogWithLevel(android_LogPriority level, const char *filename, const char *func, int line, const char *format, ...) {
string message;
char buffer[16];
va_list args;
va_start(args, format);
auto length = std::vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
if (length < 0) { // something wrong
message = {};
} else if (length < sizeof(buffer)) {
message = string(buffer, static_cast(length));
} else {
message.resize(static_cast(length), '\0');
va_start(args, format);
std::vsnprintf(const_cast(message.data()), static_cast(length) + 1, format, args);
va_end(args);
}
__android_log_print(level, APP_NAME, "<%s:%d::%s> %s", filename, line, func, message.c_str());
}
#define MMKVError(format, ...) \
_MMKVLogWithLevel(ANDROID_LOG_ERROR, __FILE_NAME__, __func__, __LINE__, format, ##__VA_ARGS__)
#define MMKVWarning(format, ...) \
_MMKVLogWithLevel(ANDROID_LOG_WARN, __FILE_NAME__, __func__, __LINE__, format, ##__VA_ARGS__)
#define MMKVInfo(format, ...) \
_MMKVLogWithLevel(ANDROID_LOG_INFO, __FILE_NAME__, __func__, __LINE__, format, ##__VA_ARGS__)
#define MMKVLog MMKVInfo
static string jstring2string(JNIEnv *env, jstring str) {
if (str) {
const char *kstr = env->GetStringUTFChars(str, nullptr);
if (kstr) {
string result(kstr);
env->ReleaseStringUTFChars(str, kstr);
return result;
}
}
return "";
}
string to_string(const std::string& str) { return str; }
template
string to_string(const vector &arr, const char* sp = ", ") {
string str;
for (const auto &element : arr) {
str += to_string(element);
str += sp;
}
if (!str.empty()) {
str.erase(str.length() - strlen(sp));
}
return str;
}
#ifdef ENABLE_MMKV_NATIVE
void functionalTest(MMKV *mmkv, bool decodeOnly) {
if (!decodeOnly) {
mmkv->set(true, "bool");
}
MMKVLog("bool = %d\n", mmkv->getBool("bool"));
if (!decodeOnly) {
mmkv->set(1024, "int32");
}
MMKVLog("int32 = %d\n", mmkv->getInt32("int32"));
if (!decodeOnly) {
mmkv->set(std::numeric_limits::max(), "uint32");
}
MMKVLog("uint32 = %u\n", mmkv->getUInt32("uint32"));
if (!decodeOnly) {
mmkv->set(std::numeric_limits::min(), "int64");
}
MMKVLog("int64 = %lld\n", mmkv->getInt64("int64"));
if (!decodeOnly) {
mmkv->set(std::numeric_limits::max(), "uint64");
}
MMKVLog("uint64 = %llu\n", mmkv->getUInt64("uint64"));
if (!decodeOnly) {
mmkv->set(3.14f, "float");
}
MMKVLog("float = %f\n", mmkv->getFloat("float"));
if (!decodeOnly) {
mmkv->set(std::numeric_limits::max(), "double");
}
MMKVLog("double = %f\n", mmkv->getDouble("double"));
if (!decodeOnly) {
mmkv->set("Hello, MMKV-示例 for POSIX", "raw_string");
std::string str = "Hello, MMKV-示例 for POSIX string";
mmkv->set(str, "string");
mmkv->set(std::string_view(str).substr(7, 21), "string_view");
}
std::string result;
mmkv->getString("raw_string", result);
MMKVLog("raw_string = %s\n", result.c_str());
mmkv->getString("string", result);
MMKVLog("string = %s\n", result.c_str());
mmkv->getString("string_view", result);
MMKVLog("string_view = %s\n", result.c_str());
// containerTest(mmkv, decodeOnly);
MMKVLog("allKeys: %s\n", ::to_string(mmkv->allKeys()).c_str());
MMKVLog("count = %zu, totalSize = %zu\n", mmkv->count(), mmkv->totalSize());
MMKVLog("containsKey[string]: %d\n", mmkv->containsKey("string"));
mmkv->removeValueForKey("bool");
MMKVLog("bool: %d\n", mmkv->getBool("bool"));
mmkv->removeValuesForKeys({"int", "long"});
mmkv->set("some string", "null string");
result.erase();
mmkv->getString("null string", result);
MMKVLog("string before set null: %s\n", result.c_str());
mmkv->set((const char *) nullptr, "null string");
//mmkv->set("", "null string");
result.erase();
mmkv->getString("null string", result);
MMKVLog("string after set null: %s, containsKey: %d\n", result.c_str(), mmkv->containsKey("null string"));
//kv.sync();
//kv.async();
//kv.clearAll();
mmkv->clearMemoryCache();
MMKVLog("allKeys: %s\n", ::to_string(mmkv->allKeys()).c_str());
MMKVLog("isFileValid[%s]: %d\n", mmkv->mmapID().c_str(), MMKV::isFileValid(mmkv->mmapID()));
}
#endif
static void testNameSpaceInNative(JNIEnv *env, jobject obj, jstring rootDir, jstring mmapID) {
string root = jstring2string(env, rootDir);
string id = jstring2string(env, mmapID);
#ifdef ENABLE_MMKV_NATIVE
auto ns = MMKV::nameSpace(root);
auto kv = ns.mmkvWithID(id);
functionalTest(kv, false);
#else
MMKVWarning("ENABLE_MMKV_NATIVE not defined.");
#endif
}
#ifndef ENABLE_MMKV_NATIVE
enum MMKVLogLevel : int {
MMKVLogDebug = 0, // not available for release/product build
MMKVLogInfo = 1, // default level
MMKVLogWarning,
MMKVLogError,
MMKVLogNone, // special level used to disable all log messages
};
#endif
static android_LogPriority MMKVLogLevelDesc(MMKVLogLevel level) {
switch (level) {
case MMKVLogDebug:
return ANDROID_LOG_DEBUG;
case MMKVLogInfo:
return ANDROID_LOG_INFO;
case MMKVLogWarning:
return ANDROID_LOG_WARN;
case MMKVLogError:
return ANDROID_LOG_ERROR;
default:
return ANDROID_LOG_UNKNOWN;
}
}
static void mmkvLog(MMKVLogLevel level, const char *file, int line, const char *function, const char *message) {
auto desc = MMKVLogLevelDesc(level);
__android_log_print(desc, APP_NAME, "<%s:%d::%s> %s", file, line, function, message);
}
static jlong getNativeLogHandler(JNIEnv *env, jobject obj) {
return (jlong) mmkvLog;
}
static JNINativeMethod g_methods[] = {
{"testNameSpaceInNative", "(Ljava/lang/String;Ljava/lang/String;)V", (void *) testNameSpaceInNative},
{"getNativeLogHandler", "()J", (void *) getNativeLogHandler},
};
static int registerNativeMethods(JNIEnv *env, jclass cls) {
return env->RegisterNatives(cls, g_methods, sizeof(g_methods) / sizeof(g_methods[0]));
}
extern "C" JNIEXPORT JNICALL jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
static const char *clsName = "com/tencent/mmkvdemo/MyApplication";
jclass instance = env->FindClass(clsName);
if (!instance) {
MMKVError("fail to locate class: %s", clsName);
return -2;
}
int ret = registerNativeMethods(env, instance);
if (ret != 0) {
MMKVError("fail to register native methods for class %s, ret = %d", clsName, ret);
return -4;
}
return JNI_VERSION_1_6;
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/Baseline.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkvdemo;
import static android.content.Context.MODE_PRIVATE;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.tencent.mmkv.MMKV;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import java.util.Random;
public final class Baseline {
private String[] m_arrStrings;
private String[] m_arrKeys;
private String[] m_arrIntKeys;
private int m_loops = 1000;
private Context m_context;
private static final String MMKV_ID = "baseline3";
private static final String CryptKey = null;
//private static final String CryptKey = "baseline_key3";
private static final String TAG = "MMKV";
private DecimalFormat m_formatter;
Baseline(Context context, int loops) {
m_context = context;
m_loops = loops;
m_arrStrings = new String[loops];
m_arrKeys = new String[loops];
m_arrIntKeys = new String[loops];
Random r = new Random();
final String filename = "mmkv/Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/Baseline.java_";
for (int index = 0; index < loops; index++) {
//String str = "[MMKV] [Info]: protection on [/var/mobile/Containers/Data/Application/B93F2BD3-E0DB-49B3-9BB0-C662E2FC11D9/Documents/mmkv/cips_commoncache] is NSFileProtectionCompleteUntilFirstUserAuthentication_";
//m_arrStrings[index] = str + r.nextInt();
m_arrStrings[index] = filename + r.nextInt();
m_arrKeys[index] = "str_" + index;
m_arrIntKeys[index] = "int_" + index;
}
m_formatter = new DecimalFormat("0.0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
m_formatter.setRoundingMode(RoundingMode.DOWN);
}
public void mmkvBaselineTest() {
mmkvBatchWriteInt();
mmkvBatchReadInt();
mmkvBatchWriteString();
mmkvBatchReadString();
//mmkvBatchDeleteString();
MMKV mmkv = mmkvForTest();
//mmkv.trim();
mmkv.clearMemoryCache();
mmkv.totalSize();
}
private MMKV mmkvForTest() {
return MMKV.mmkvWithID(MMKV_ID, MMKV.SINGLE_PROCESS_MODE, CryptKey);
//return MMKV.mmkvWithAshmemID(m_context, MMKV_ID, 65536, MMKV.SINGLE_PROCESS_MODE, CryptKey);
}
private void mmkvBatchWriteInt() {
Random r = new Random();
long startTime = System.nanoTime();
MMKV mmkv = mmkvForTest();
for (int index = 0; index < m_loops; index++) {
int tmp = r.nextInt();
String key = m_arrIntKeys[index];
mmkv.encode(key, tmp);
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
Log.i(TAG, "MMKV write int: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
private void mmkvBatchReadInt() {
long startTime = System.nanoTime();
MMKV mmkv = mmkvForTest();
for (int index = 0; index < m_loops; index++) {
String key = m_arrIntKeys[index];
int tmp = mmkv.decodeInt(key);
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
Log.i(TAG, "MMKV read int: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
private void mmkvBatchWriteString() {
long startTime = System.nanoTime();
MMKV mmkv = mmkvForTest();
for (int index = 0; index < m_loops; index++) {
final String valueStr = m_arrStrings[index];
final String strKey = m_arrKeys[index];
mmkv.encode(strKey, valueStr);
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
Log.i(TAG, "MMKV write String: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
private void mmkvBatchReadString() {
long startTime = System.nanoTime();
MMKV mmkv = mmkvForTest();
for (int index = 0; index < m_loops; index++) {
String strKey = m_arrKeys[index];
String tmpStr = mmkv.decodeString(strKey);
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
Log.i(TAG, "MMKV read String: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
private void mmkvBatchDeleteString() {
long startTime = System.nanoTime();
MMKV mmkv = mmkvForTest();
for (int index = 0; index < m_loops; index++) {
String strKey = m_arrKeys[index];
mmkv.removeValueForKey(strKey);
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
Log.i(TAG, "MMKV delete String: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
public void sharedPreferencesBaselineTest() {
spBatchWriteInt();
spBatchReadInt();
spBatchWriteString();
spBatchReadString();
}
private void spBatchWriteInt() {
Random r = new Random();
long startTime = System.nanoTime();
SharedPreferences preferences = m_context.getSharedPreferences(MMKV_ID, MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
for (int index = 0; index < m_loops; index++) {
int tmp = r.nextInt();
String key = m_arrIntKeys[index];
editor.putInt(key, tmp);
// editor.commit();
editor.apply();
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
Log.i(TAG, "SharedPreferences write int: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
private void spBatchReadInt() {
long startTime = System.nanoTime();
SharedPreferences preferences = m_context.getSharedPreferences(MMKV_ID, MODE_PRIVATE);
for (int index = 0; index < m_loops; index++) {
String key = m_arrIntKeys[index];
int tmp = preferences.getInt(key, 0);
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
Log.i(TAG, "SharedPreferences read int: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
private void spBatchWriteString() {
long startTime = System.nanoTime();
SharedPreferences preferences = m_context.getSharedPreferences(MMKV_ID, MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
for (int index = 0; index < m_loops; index++) {
final String str = m_arrStrings[index];
final String key = m_arrKeys[index];
editor.putString(key, str);
// editor.commit();
editor.apply();
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
Log.i(TAG, "SharedPreferences write String: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
private void spBatchReadString() {
long startTime = System.nanoTime();
SharedPreferences preferences = m_context.getSharedPreferences(MMKV_ID, MODE_PRIVATE);
for (int index = 0; index < m_loops; index++) {
final String key = m_arrKeys[index];
final String tmp = preferences.getString(key, null);
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
Log.i(TAG, "SharedPreferences read String: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
public void sqliteBaselineTest(boolean useTransaction) {
sqliteWriteInt(useTransaction);
sqliteReadInt(useTransaction);
sqliteWriteString(useTransaction);
sqliteReadString(useTransaction);
}
private void sqliteWriteInt(boolean useTransaction) {
Random r = new Random();
long startTime = System.nanoTime();
SQLiteKV sqliteKV = new SQLiteKV(m_context);
if (useTransaction) {
sqliteKV.beginTransaction();
}
for (int index = 0; index < m_loops; index++) {
int tmp = r.nextInt();
String key = m_arrIntKeys[index];
sqliteKV.putInt(key, tmp);
}
if (useTransaction) {
sqliteKV.endTransaction();
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
final String msg = useTransaction ? "sqlite transaction" : "sqlite";
Log.i(TAG, msg + " write int: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
private void sqliteReadInt(boolean useTransaction) {
long startTime = System.nanoTime();
SQLiteKV sqliteKV = new SQLiteKV(m_context);
if (useTransaction) {
sqliteKV.beginTransaction();
}
for (int index = 0; index < m_loops; index++) {
String key = m_arrIntKeys[index];
int tmp = sqliteKV.getInt(key);
}
if (useTransaction) {
sqliteKV.endTransaction();
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
final String msg = useTransaction ? "sqlite transaction" : "sqlite";
Log.i(TAG, msg + " read int: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
private void sqliteWriteString(boolean useTransaction) {
long startTime = System.nanoTime();
SQLiteKV sqliteKV = new SQLiteKV(m_context);
if (useTransaction) {
sqliteKV.beginTransaction();
}
for (int index = 0; index < m_loops; index++) {
final String value = m_arrStrings[index];
final String key = m_arrKeys[index];
sqliteKV.putString(key, value);
}
if (useTransaction) {
sqliteKV.endTransaction();
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
final String msg = useTransaction ? "sqlite transaction" : "sqlite";
Log.i(TAG, msg + " write String: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
private void sqliteReadString(boolean useTransaction) {
long startTime = System.nanoTime();
SQLiteKV sqliteKV = new SQLiteKV(m_context);
if (useTransaction) {
sqliteKV.beginTransaction();
}
for (int index = 0; index < m_loops; index++) {
final String key = m_arrKeys[index];
final String tmp = sqliteKV.getString(key);
}
if (useTransaction) {
sqliteKV.endTransaction();
}
double endTime = (System.nanoTime() - startTime) / 1000000.0;
final String msg = useTransaction ? "sqlite transaction" : "sqlite";
Log.i(TAG, msg + " read String: loop[" + m_loops + "]: " + m_formatter.format(endTime) + " ms");
}
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/BenchMarkBaseService.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkvdemo;
import android.app.Service;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import com.tencent.mmkv.MMKV;
import com.tencent.mmkv.MMKVConfig;
import com.tencent.mmkv.ParcelableMMKV;
import java.util.Random;
public abstract class BenchMarkBaseService extends Service {
public static final String CMD_ID = "cmd_id";
public static final String CMD_READ_INT = "cmd_read_int";
public static final String CMD_WRITE_INT = "cmd_write_int";
public static final String CMD_READ_STRING = "cmd_read_string";
public static final String CMD_WRITE_STRING = "cmd_write_string";
public static final String CMD_PREPARE_ASHMEM_BY_CP = "cmd_prepare_ashmem_by_ContentProvider";
public static final String CMD_PREPARE_ASHMEM_KEY = "cmd_prepare_ashmem_key";
// 1M, ashmem cannot change size after opened
public static final int AshmemMMKV_Size = 1024 * 1024;
public static final String AshmemMMKV_ID = "testAshmemMMKVByCP";
private String[] m_arrStrings;
private String[] m_arrKeys;
private String[] m_arrIntKeys;
private static final int m_loops = 1000;
public static final String MMKV_ID = "benchmark_interprocess";
//public static final String MMKV_ID = "benchmark_interprocess_crypt1";
private static final String SP_ID = "benchmark_interprocess_sp";
public static final String CryptKey = null;
//public static final String CryptKey = "Tencent MMKV";
private static final boolean SQLite_Use_Transaction = false;
private static final String TAG = "MMKV";
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "onCreate BenchMarkBaseService");
MMKV.initialize(this);
MMKV.unregisterHandler();
MMKV.unregisterContentChangeNotify();
{
long startTime = System.currentTimeMillis();
MMKV mmkv = MMKV.mmkvWithID(MMKV_ID, MMKV.MULTI_PROCESS_MODE, CryptKey);
long endTime = System.currentTimeMillis();
Log.i(TAG, "load [" + MMKV_ID + "]: " + (endTime - startTime) + " ms");
}
m_arrStrings = new String[m_loops];
m_arrKeys = new String[m_loops];
m_arrIntKeys = new String[m_loops];
Random r = new Random();
final String filename =
"mmkv/Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/BenchMarkBaseService.java_";
for (int index = 0; index < m_loops; index++) {
//String str = "[MMKV] [Info]: protection on [/var/mobile/Containers/Data/Application/B93F2BD3-E0DB-49B3-9BB0-C662E2FC11D9/Documents/mmkv/cips_commoncache] is NSFileProtectionCompleteUntilFirstUserAuthentication_";
//m_arrStrings[index] = str + r.nextInt();
m_arrStrings[index] = filename + r.nextInt();
m_arrKeys[index] = "testStr_" + index;
m_arrIntKeys[index] = "int_" + index;
}
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy BenchMarkBaseService");
MMKV.onExit();
}
protected void batchWriteInt(String caller) {
mmkvBatchWriteInt(caller);
sqliteWriteInt(caller, SQLite_Use_Transaction);
spBatchWriteInt(caller);
}
private void mmkvBatchWriteInt(String caller) {
Random r = new Random();
Log.i(TAG, caller + " mmkv write int: loop[" + m_loops + "] star.");
long startTime = System.currentTimeMillis();
MMKV mmkv = GetMMKV();
for (int index = 0; index < m_loops; index++) {
int tmp = r.nextInt();
String key = m_arrIntKeys[index];
// MMKV mmkv = MMKV.mmkvWithID(MMKV_ID + key, MMKV.MULTI_PROCESS_MODE, CryptKey);
mmkv.encode(key, tmp);
}
long endTime = System.currentTimeMillis();
Log.i(TAG, caller + " mmkv write int: loop[" + m_loops + "]: " + (endTime - startTime) + " ms");
}
private void sqliteWriteInt(String caller, boolean useTransaction) {
Random r = new Random();
long startTime = System.currentTimeMillis();
SQLiteKV sqliteKV = new SQLiteKV(this);
if (useTransaction) {
sqliteKV.beginTransaction();
}
for (int index = 0; index < m_loops; index++) {
int tmp = r.nextInt();
String key = m_arrIntKeys[index];
sqliteKV.putInt(key, tmp);
}
if (useTransaction) {
sqliteKV.endTransaction();
}
long endTime = System.currentTimeMillis();
final String msg = useTransaction ? " sqlite transaction " : " sqlite ";
Log.i(TAG, caller + msg + "write int: loop[" + m_loops + "]: " + (endTime - startTime) + " ms");
}
private void spBatchWriteInt(String caller) {
Random r = new Random();
long startTime = System.currentTimeMillis();
SharedPreferences preferences = MultiProcessSharedPreferences.getSharedPreferences(this, SP_ID, MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
for (int index = 0; index < m_loops; index++) {
int tmp = r.nextInt();
String key = m_arrIntKeys[index];
editor.putInt(key, tmp);
//editor.commit();
editor.apply();
}
long endTime = System.currentTimeMillis();
Log.i(TAG, caller + " MultiProcessSharedPreferences write int: loop[" + m_loops + "]: " + (endTime - startTime)
+ " ms");
}
protected void batchReadInt(String caller) {
mmkvBatchReadInt(caller);
sqliteReadInt(caller, SQLite_Use_Transaction);
spBatchReadInt(caller);
}
private void mmkvBatchReadInt(String caller) {
Log.i(TAG, caller + " mmkv read int: loop[" + m_loops + "] star.");
long startTime = System.currentTimeMillis();
MMKV mmkv = GetMMKV();
for (int index = 0; index < m_loops; index++) {
String key = m_arrIntKeys[index];
int tmp = mmkv.decodeInt(key);
}
long endTime = System.currentTimeMillis();
Log.i(TAG, caller + " mmkv read int: loop[" + m_loops + "]: " + (endTime - startTime) + " ms");
}
private void sqliteReadInt(String caller, boolean useTransaction) {
long startTime = System.currentTimeMillis();
SQLiteKV sqliteKV = new SQLiteKV(this);
if (useTransaction) {
sqliteKV.beginTransaction();
}
for (int index = 0; index < m_loops; index++) {
String key = m_arrIntKeys[index];
int tmp = sqliteKV.getInt(key);
}
if (useTransaction) {
sqliteKV.endTransaction();
}
long endTime = System.currentTimeMillis();
final String msg = useTransaction ? " sqlite transaction " : " sqlite ";
Log.i(TAG, caller + msg + "read int: loop[" + m_loops + "]: " + (endTime - startTime) + " ms");
}
private void spBatchReadInt(String caller) {
long startTime = System.currentTimeMillis();
SharedPreferences preferences = MultiProcessSharedPreferences.getSharedPreferences(this, SP_ID, MODE_PRIVATE);
for (int index = 0; index < m_loops; index++) {
String key = m_arrIntKeys[index];
int tmp = preferences.getInt(key, 0);
}
long endTime = System.currentTimeMillis();
Log.i(TAG, caller + " MultiProcessSharedPreferences read int: loop[" + m_loops + "]: " + (endTime - startTime)
+ " ms");
}
protected void batchWriteString(String caller) {
mmkvBatchWriteString(caller);
sqliteWriteString(caller, SQLite_Use_Transaction);
spBatchWrieString(caller);
}
private void mmkvBatchWriteString(String caller) {
Log.i(TAG, caller + " mmkv write String: loop[" + m_loops + "] star.");
long startTime = System.currentTimeMillis();
MMKV mmkv = GetMMKV();
for (int index = 0; index < m_loops; index++) {
final String valueStr = m_arrStrings[index];
final String strKey = m_arrKeys[index];
mmkv.encode(strKey, valueStr);
}
long endTime = System.currentTimeMillis();
Log.i(TAG, caller + " mmkv write String: loop[" + m_loops + "]: " + (endTime - startTime) + " ms");
}
private void sqliteWriteString(String caller, boolean useTransaction) {
long startTime = System.currentTimeMillis();
SQLiteKV sqliteKV = new SQLiteKV(this);
if (useTransaction) {
sqliteKV.beginTransaction();
}
for (int index = 0; index < m_loops; index++) {
final String value = m_arrStrings[index];
final String key = m_arrKeys[index];
sqliteKV.putString(key, value);
}
if (useTransaction) {
sqliteKV.endTransaction();
}
long endTime = System.currentTimeMillis();
final String msg = useTransaction ? " sqlite transaction " : " sqlite ";
Log.i(TAG, caller + msg + "write String: loop[" + m_loops + "]: " + (endTime - startTime) + " ms");
}
private void spBatchWrieString(String caller) {
long startTime = System.currentTimeMillis();
SharedPreferences preferences = MultiProcessSharedPreferences.getSharedPreferences(this, SP_ID, MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
for (int index = 0; index < m_loops; index++) {
final String str = m_arrStrings[index];
final String key = m_arrKeys[index];
editor.putString(key, str);
//editor.commit();
editor.apply();
}
long endTime = System.currentTimeMillis();
Log.i(TAG, caller + " MultiProcessSharedPreferences write String: loop[" + m_loops
+ "]: " + (endTime - startTime) + " ms");
}
protected void batchReadString(String caller) {
mmkvBatchReadString(caller);
sqliteReadString(caller, SQLite_Use_Transaction);
spBatchReadStrinfg(caller);
}
private void mmkvBatchReadString(String caller) {
Log.i(TAG, caller + " mmkv read String: loop[" + m_loops + "] star.");
long startTime = System.currentTimeMillis();
MMKV mmkv = GetMMKV();
for (int index = 0; index < m_loops; index++) {
final String strKey = m_arrKeys[index];
final String tmpStr = mmkv.decodeString(strKey);
}
long endTime = System.currentTimeMillis();
Log.i(TAG, caller + " mmkv read String: loop[" + m_loops + "]: " + (endTime - startTime) + " ms");
}
private void sqliteReadString(String caller, boolean useTransaction) {
long startTime = System.currentTimeMillis();
SQLiteKV sqliteKV = new SQLiteKV(this);
if (useTransaction) {
sqliteKV.beginTransaction();
}
for (int index = 0; index < m_loops; index++) {
final String key = m_arrKeys[index];
final String tmp = sqliteKV.getString(key);
}
if (useTransaction) {
sqliteKV.endTransaction();
}
long endTime = System.currentTimeMillis();
final String msg = useTransaction ? " sqlite transaction " : " sqlite ";
Log.i(TAG, caller + msg + "read String: loop[" + m_loops + "]: " + (endTime - startTime) + " ms");
}
private void spBatchReadStrinfg(String caller) {
long startTime = System.currentTimeMillis();
SharedPreferences preferences = MultiProcessSharedPreferences.getSharedPreferences(this, SP_ID, MODE_PRIVATE);
for (int index = 0; index < m_loops; index++) {
final String key = m_arrKeys[index];
final String tmp = preferences.getString(key, null);
}
long endTime = System.currentTimeMillis();
Log.i(TAG, caller + " MultiProcessSharedPreferences read String: loop[" + m_loops
+ "]: " + (endTime - startTime) + " ms");
}
MMKV m_ashmemMMKV;
protected MMKV GetMMKV() {
if (m_ashmemMMKV != null) {
return m_ashmemMMKV;
} else {
return MMKV.mmkvWithID(MMKV_ID, MMKV.MULTI_PROCESS_MODE, CryptKey);
}
}
public class AshmemMMKVGetter extends IAshmemMMKV.Stub {
private AshmemMMKVGetter() {
// 1M, ashmem cannot change size after opened
final String id = "tetAshmemMMKV";
try {
MMKVConfig config = new MMKVConfig();
config.expectedCapacity = AshmemMMKV_Size;
config.mode = MMKV.MULTI_PROCESS_MODE;
config.cryptKey = CryptKey;
m_ashmemMMKV = MMKV.mmkvWithAshmemID(BenchMarkBaseService.this, id, config);
m_ashmemMMKV.encode("bool", true);
} catch (Exception e) {
Log.e("MMKV", e.getMessage());
}
}
public ParcelableMMKV GetAshmemMMKV() {
return new ParcelableMMKV(m_ashmemMMKV);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.i(TAG, "onBind, intent=" + intent);
return new AshmemMMKVGetter();
}
protected void prepareAshmemMMKVByCP() {
// it's ok for other process not knowing cryptKey
final String cryptKey = null;
try {
m_ashmemMMKV = MMKV.mmkvWithAshmemID(this, AshmemMMKV_ID, AshmemMMKV_Size, MMKV.MULTI_PROCESS_MODE, cryptKey);
} catch (Exception e) {
Log.e("MMKV", e.getMessage());
}
}
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/FakeInfo.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2025 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkvdemo;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class FakeInfo implements Parcelable {
public String channelId;
public int position;
public FakeInfo() {
}
@NonNull
@Override
public String toString() {
return "fake channelID = " + channelId + ", position = " + position;
}
protected FakeInfo(Parcel in) {
channelId = in.readString();
position = in.readInt();
}
public static final Creator CREATOR = new Creator() {
@Override
public FakeInfo createFromParcel(Parcel in) {
return new FakeInfo(in);
}
@Override
public FakeInfo[] newArray(int size) {
return new FakeInfo[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(channelId);
dest.writeInt(position);
}
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/Info.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2025 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkvdemo;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
class Info implements Parcelable {
public String channelId;
public int position;
public Info(String id, int pos) {
channelId = id;
position = pos;
}
@NonNull
@Override
public String toString() {
return "channelID = " + channelId + ", position = " + position;
}
protected Info(Parcel in) {
channelId = in.readString();
position = in.readInt();
}
public static final Creator CREATOR = new Creator() {
@Override
public Info createFromParcel(Parcel in) {
return new Info(in);
}
@Override
public Info[] newArray(int size) {
return new Info[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(channelId);
dest.writeInt(position);
}
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/MainActivity.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkvdemo;
import static com.tencent.mmkvdemo.BenchMarkBaseService.AshmemMMKV_ID;
import static com.tencent.mmkvdemo.BenchMarkBaseService.AshmemMMKV_Size;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import com.tencent.mmkv.MMKV;
import com.tencent.mmkv.MMKVConfig;
import com.tencent.mmkv.MMKVRecoverStrategic;
import com.tencent.mmkv.NameSpace;
import com.tencent.mmkv.NativeBuffer;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Objects;
public class MainActivity extends AppCompatActivity {
static private final String KEY_1 = "Ashmem_Key_1";
static private final String KEY_2 = "Ashmem_Key_2";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Enable edge-to-edge display
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
setContentView(R.layout.activity_main);
View mainLayout = findViewById(R.id.main_layout);
ViewCompat.setOnApplyWindowInsetsListener(mainLayout, (v, insets) -> {
int statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
int navigationBarHeight = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
v.setPadding(0, statusBarHeight, 0, navigationBarHeight);
return insets;
});
TextView tv = (TextView) findViewById(R.id.sample_text);
String rootDir = MMKV.getRootDir();
tv.setText(rootDir);
final Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
final Baseline baseline = new Baseline(getApplicationContext(), 1000);
public void onClick(View v) {
baseline.mmkvBaselineTest();
baseline.sharedPreferencesBaselineTest();
baseline.sqliteBaselineTest(false);
//testInterProcessReKey();
//testInterProcessLockPhase2();
}
});
//testHolderForMultiThread();
//prepareInterProcessAshmem();
//prepareInterProcessAshmemByContentProvider(KEY_1);
final Button button_read_int = findViewById(R.id.button_read_int);
button_read_int.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
interProcessBaselineTest(BenchMarkBaseService.CMD_READ_INT);
}
});
final Button button_write_int = findViewById(R.id.button_write_int);
button_write_int.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
interProcessBaselineTest(BenchMarkBaseService.CMD_WRITE_INT);
}
});
final Button button_read_string = findViewById(R.id.button_read_string);
button_read_string.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
interProcessBaselineTest(BenchMarkBaseService.CMD_READ_STRING);
}
});
final Button button_write_string = findViewById(R.id.button_write_string);
button_write_string.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
interProcessBaselineTest(BenchMarkBaseService.CMD_WRITE_STRING);
}
});
String otherDir = getFilesDir().getAbsolutePath() + "/mmkv_3";
MMKV kv = testMMKV("test/AES", "Tencent MMKV", false, false, otherDir);
kv.checkContentChangedByOuterProcess();
kv.close();
// prepare for backup customize root path
kv = testMMKV("test_backup", "MMKV Backup", false, false, otherDir);
kv.close();
testAshmem();
testReKey();
KotlinUsecaseKt.kotlinFunctionalTest();
testInterProcessLogic();
testImportSharedPreferences();
//testInterProcessLockPhase1();
//testCornerSize();
//testFastRemoveCornerSize();
//testTrimNonEmptyInterProcess();
//testItemSizeHolderOverride();
// testDiskFull();
testBackup();
testRestore();
testAutoExpire();
testExpectedCapacity();
testCompareBeforeSet();
testClearAllKeepSpace();
// testFastNativeSpeed();
testRemoveStorageAndCheckExist();
overrideTest();
testReadOnly();
testImport();
}
private void testCompareBeforeSet() {
MMKV mmkv = MMKV.mmkvWithID("testCompareBeforeSet");
mmkv.enableCompareBeforeSet();
mmkv.encode("key", "extra");
{
String key = "int";
int v = 12345;
mmkv.encode(key, v);
long actualSize = mmkv.actualSize();
Log.d("mmkv", "testCompareBeforeSet actualSize = " + actualSize);
Log.d("mmkv", "testCompareBeforeSet v = " + mmkv.getInt(key, -1));
mmkv.encode(key, v);
long actualSize2 = mmkv.actualSize();
Log.d("mmkv", "testCompareBeforeSet actualSize = " + actualSize2);
if (actualSize2 != actualSize) {
Log.e("mmkv", "testCompareBeforeSet fail");
}
mmkv.encode(key, v * 23);
Log.d("mmkv", "testCompareBeforeSet actualSize = " + mmkv.actualSize());
Log.d("mmkv", "testCompareBeforeSet v = " + mmkv.getInt(key, -1));
}
{
String key = "string";
String v = "w012A🏊🏻good";
mmkv.encode(key, v);
long actualSize = mmkv.actualSize();
Log.d("mmkv", "testCompareBeforeSet actualSize = " + actualSize);
Log.d("mmkv", "testCompareBeforeSet v = " + mmkv.getString(key, ""));
mmkv.encode(key, v);
long actualSize2 = mmkv.actualSize();
Log.d("mmkv", "testCompareBeforeSet actualSize = " + actualSize2);
if (actualSize2 != actualSize) {
Log.e("mmkv", "testCompareBeforeSet fail");
}
mmkv.encode(key, "temp data 👩🏻🏫");
Log.d("mmkv", "testCompareBeforeSet actualSize = " + mmkv.actualSize());
Log.d("mmkv", "testCompareBeforeSet v = " + mmkv.getString(key, ""));
}
}
/**
* FastNative
* Before Test, remove `MMKVInfo` log print in `enableCompareBeforeSet` function
*/
private void testFastNativeSpeed() {
int repeatCount = 5000000;
MMKV mmkv = MMKV.mmkvWithID("test_fastnative_speed");
long start, end;
start = System.currentTimeMillis();
for (int i = 0; i < repeatCount; i++) {
mmkv.enableCompareBeforeSet();
}
end = System.currentTimeMillis();
Log.e("MMKV", "testFastNativeSpeed: " + (end - start));
}
private void testInterProcessLogic() {
MMKV mmkv = MMKV.mmkvWithID(MyService.MMKV_ID, MMKV.MULTI_PROCESS_MODE, MyService.CryptKey);
mmkv.putInt(MyService.CMD_ID, 1024);
Log.d("mmkv in main", "" + mmkv.decodeInt(MyService.CMD_ID));
Intent intent = new Intent(this, MyService.class);
intent.putExtra(BenchMarkBaseService.CMD_ID, MyService.CMD_REMOVE);
startService(intent);
SystemClock.sleep(1000 * 3);
int value = mmkv.decodeInt(MyService.CMD_ID);
Log.d("mmkv", "" + value);
}
private MMKV testMMKV(String mmapID, String cryptKey, boolean aes256, boolean decodeOnly, String rootPath) {
//MMKV kv = MMKV.defaultMMKV();
MMKV kv = MMKV.mmkvWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, cryptKey, aes256, rootPath);
testMMKV(kv, decodeOnly);
Log.i("MMKV", "isFileValid[" + kv.mmapID() + "]: " + MMKV.isFileValid(kv.mmapID(), rootPath));
return kv;
}
static void testMMKV(MMKV kv, boolean decodeOnly) {
if (!decodeOnly) {
kv.encode("bool", true);
}
Log.i("MMKV", "bool: " + kv.decodeBool("bool"));
if (!decodeOnly) {
kv.encode("int", Integer.MIN_VALUE);
}
Log.i("MMKV", "int: " + kv.decodeInt("int"));
if (!decodeOnly) {
kv.encode("long", Long.MAX_VALUE);
}
Log.i("MMKV", "long: " + kv.decodeLong("long"));
if (!decodeOnly) {
kv.encode("float", -3.14f);
}
Log.i("MMKV", "float: " + kv.decodeFloat("float"));
if (!decodeOnly) {
kv.encode("double", Double.MIN_VALUE);
}
Log.i("MMKV", "double: " + kv.decodeDouble("double"));
if (!decodeOnly) {
kv.encode("string", "Hello from mmkv");
}
Log.i("MMKV", "string: " + kv.decodeString("string"));
if (!decodeOnly) {
byte[] bytes = {'m', 'm', 'k', 'v'};
kv.encode("bytes", bytes);
}
byte[] bytes = kv.decodeBytes("bytes");
Log.i("MMKV", "bytes: " + new String(bytes));
Log.i("MMKV", "bytes length = " + bytes.length + ", value size consumption = " + kv.getValueSize("bytes")
+ ", value size = " + kv.getValueActualSize("bytes"));
int sizeNeeded = kv.getValueActualSize("bytes");
NativeBuffer nativeBuffer = MMKV.createNativeBuffer(sizeNeeded);
if (nativeBuffer != null) {
int size = kv.writeValueToNativeBuffer("bytes", nativeBuffer);
Log.i("MMKV", "size Needed = " + sizeNeeded + " written size = " + size);
MMKV.destroyNativeBuffer(nativeBuffer);
}
if (!decodeOnly) {
TestParcelable testParcelable = new TestParcelable(1024, "Hi Parcelable");
kv.encode("parcel", testParcelable);
}
TestParcelable result = kv.decodeParcelable("parcel", TestParcelable.class);
if (result != null) {
Log.d("MMKV", "parcel: " + result.iValue + ", " + result.sValue + ", " + result.list);
} else {
Log.e("MMKV", "fail to decodeParcelable of key:parcel");
}
kv.encode("null string", "some string");
Log.i("MMKV", "string before set null: " + kv.decodeString("null string"));
kv.encode("null string", (String) null);
Log.i("MMKV", "string after set null: " + kv.decodeString("null string")
+ ", containsKey:" + kv.contains("null string"));
Log.i("MMKV", "allKeys: " + Arrays.toString(kv.allKeys()));
Log.i("MMKV",
"count = " + kv.count() + ", totalSize = " + kv.totalSize() + ", actualSize = " + kv.actualSize());
Log.i("MMKV", "containsKey[string]: " + kv.containsKey("string"));
kv.removeValueForKey("bool");
Log.i("MMKV", "bool: " + kv.decodeBool("bool"));
kv.removeValuesForKeys(new String[] {"int", "long"});
//kv.sync();
//kv.async();
//kv.clearAll();
kv.clearMemoryCache();
Log.i("MMKV", "allKeys: " + Arrays.toString(kv.allKeys()));
}
private void testImportSharedPreferences() {
SharedPreferences preferences = getSharedPreferences("imported", MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean("bool", true);
editor.putInt("int", Integer.MIN_VALUE);
editor.putLong("long", Long.MAX_VALUE);
editor.putFloat("float", -3.14f);
editor.putString("string", "hello, imported");
HashSet set = new HashSet();
set.add("W");
set.add("e");
set.add("C");
set.add("h");
set.add("a");
set.add("t");
editor.putStringSet("string-set", set);
editor.commit();
MMKV kv = MMKV.mmkvWithID("imported");
kv.clearAll();
kv.importFromSharedPreferences(preferences);
editor.clear().commit();
Log.i("MMKV", "allKeys: " + Arrays.toString(kv.allKeys()));
Log.i("MMKV", "bool: " + kv.getBoolean("bool", false));
Log.i("MMKV", "int: " + kv.getInt("int", 0));
Log.i("MMKV", "long: " + kv.getLong("long", 0));
Log.i("MMKV", "float: " + kv.getFloat("float", 0));
Log.i("MMKV", "double: " + kv.decodeDouble("double"));
Log.i("MMKV", "string: " + kv.getString("string", null));
Log.i("MMKV", "string-set: " + kv.getStringSet("string-set", null));
Log.i("MMKV", "linked-string-set: " + kv.decodeStringSet("string-set", null, LinkedHashSet.class));
// test @Nullable
kv.putStringSet("string-set", null);
Log.i("MMKV", "after set null, string-set: " + kv.getStringSet("string-set", null));
}
private void testReKey() {
final String mmapID = "test/AES_reKey1";
MMKV kv = testMMKV(mmapID, null, false, false, null);
kv.reKey("Key_seq_1");
kv.clearMemoryCache();
testMMKV(mmapID, "Key_seq_1", false, true, null);
kv.reKey("Key_Seq_Very_Looooooooong", true);
kv.clearMemoryCache();
testMMKV(mmapID, "Key_Seq_Very_Looooooooong", true, true, null);
kv.reKey(null);
kv.clearMemoryCache();
testMMKV(mmapID, null, false, true, null);
}
private void interProcessBaselineTest(String cmd) {
Intent intent = new Intent(this, MyService.class);
intent.putExtra(BenchMarkBaseService.CMD_ID, cmd);
startService(intent);
intent = new Intent(this, MyService_1.class);
intent.putExtra(BenchMarkBaseService.CMD_ID, cmd);
startService(intent);
}
private void testAshmem() {
String cryptKey = "Tencent MMKV";
MMKV kv = MMKV.mmkvWithAshmemID(this, "testAshmem", MMKV.pageSize(), MMKV.SINGLE_PROCESS_MODE, cryptKey);
kv.encode("bool", true);
Log.i("MMKV", "bool: " + kv.decodeBool("bool"));
kv.encode("int", Integer.MIN_VALUE);
Log.i("MMKV", "int: " + kv.decodeInt("int"));
kv.encode("long", Long.MAX_VALUE);
Log.i("MMKV", "long: " + kv.decodeLong("long"));
kv.encode("float", -3.14f);
Log.i("MMKV", "float: " + kv.decodeFloat("float"));
kv.encode("double", Double.MIN_VALUE);
Log.i("MMKV", "double: " + kv.decodeDouble("double"));
kv.encode("string", "Hello from mmkv");
Log.i("MMKV", "string: " + kv.decodeString("string"));
byte[] bytes = {'m', 'm', 'k', 'v'};
kv.encode("bytes", bytes);
Log.i("MMKV", "bytes: " + new String(kv.decodeBytes("bytes")));
Log.i("MMKV", "allKeys: " + Arrays.toString(kv.allKeys()));
Log.i("MMKV",
"count = " + kv.count() + ", totalSize = " + kv.totalSize() + ", actualSize = " + kv.actualSize());
Log.i("MMKV", "containsKey[string]: " + kv.containsKey("string"));
kv.removeValueForKey("bool");
Log.i("MMKV", "bool: " + kv.decodeBool("bool"));
kv.removeValueForKey("int");
kv.removeValueForKey("long");
//kv.removeValuesForKeys(new String[] {"int", "long"});
//kv.clearAll();
kv.clearMemoryCache();
Log.i("MMKV", "allKeys: " + Arrays.toString(kv.allKeys()));
Log.i("MMKV", "isFileValid[" + kv.mmapID() + "]: " + MMKV.isFileValid(kv.mmapID()));
}
private void prepareInterProcessAshmem() {
Intent intent = new Intent(this, MyService_1.class);
intent.putExtra(BenchMarkBaseService.CMD_ID, MyService_1.CMD_PREPARE_ASHMEM);
startService(intent);
}
private void prepareInterProcessAshmemByContentProvider(String cryptKey) {
// first of all, init ashmem mmkv in main process
MMKV.mmkvWithAshmemID(this, AshmemMMKV_ID, AshmemMMKV_Size, MMKV.MULTI_PROCESS_MODE, cryptKey);
// then other process can get by ContentProvider
Intent intent = new Intent(this, MyService.class);
intent.putExtra(BenchMarkBaseService.CMD_ID, BenchMarkBaseService.CMD_PREPARE_ASHMEM_BY_CP);
startService(intent);
intent = new Intent(this, MyService_1.class);
intent.putExtra(BenchMarkBaseService.CMD_ID, BenchMarkBaseService.CMD_PREPARE_ASHMEM_BY_CP);
startService(intent);
}
private void testInterProcessReKey() {
MMKV mmkv = MMKV.mmkvWithAshmemID(this, AshmemMMKV_ID, AshmemMMKV_Size, MMKV.MULTI_PROCESS_MODE, KEY_1);
mmkv.reKey(KEY_2);
prepareInterProcessAshmemByContentProvider(KEY_2);
}
private void testHolderForMultiThread() {
final int COUNT = 1;
final int THREAD_COUNT = 1;
final String ID = "Hotel";
final String KEY = "California";
final String VALUE = "You can checkout any time you like, but you can never leave.";
final MMKV mmkv = MMKV.mmkvWithID(ID);
Runnable task = new Runnable() {
public void run() {
for (int i = 0; i < COUNT; ++i) {
mmkv.putString(KEY, VALUE);
mmkv.getString(KEY, null);
mmkv.remove(KEY);
}
}
};
for (int i = 0; i < THREAD_COUNT; ++i) {
new Thread(task, "MMKV-" + i).start();
}
}
private void testInterProcessLockPhase1() {
MMKV mmkv1 = MMKV.mmkvWithID(MyService.LOCK_PHASE_1, MMKV.MULTI_PROCESS_MODE);
mmkv1.lock();
Log.d("locked in main", MyService.LOCK_PHASE_1);
Intent intent = new Intent(this, MyService.class);
intent.putExtra(BenchMarkBaseService.CMD_ID, MyService.CMD_LOCK);
startService(intent);
}
private void testInterProcessLockPhase2() {
MMKV mmkv2 = MMKV.mmkvWithID(MyService.LOCK_PHASE_2, MMKV.MULTI_PROCESS_MODE);
mmkv2.lock();
Log.d("locked in main", MyService.LOCK_PHASE_2);
}
private void testCornerSize() {
MMKV mmkv = MMKV.mmkvWithID("cornerSize", MMKV.MULTI_PROCESS_MODE, "aes");
mmkv.clearAll();
int size = MMKV.pageSize() - 2;
size -= 4;
String key = "key";
int keySize = 3 + 1;
size -= keySize;
int valueSize = 3;
size -= valueSize;
byte[] value = new byte[size];
mmkv.encode(key, value);
}
private void testFastRemoveCornerSize() {
MMKV mmkv = MMKV.mmkvWithID("fastRemoveCornerSize");
mmkv.clearAll();
int size = MMKV.pageSize() - 4;
size -= 4; // place holder size
String key = "key";
int keySize = 3 + 1;
size -= keySize;
int valueSize = 3;
size -= valueSize;
size -= (keySize + 1); // total size of fast remove
size /= 16;
byte[] value = new byte[size];
for (int i = 0; i < value.length; i++) {
value[i] = 'A';
}
for (int i = 0; i < 16; i++) {
mmkv.encode(key, value); // when a full write back is occur, here's corruption happens
mmkv.removeValueForKey(key);
}
}
private void testTrimNonEmptyInterProcess() {
MMKV mmkv = MMKV.mmkvWithID("trimNonEmptyInterProcess", MMKV.MULTI_PROCESS_MODE);
mmkv.clearAll();
mmkv.encode("NonEmptyKey", "Hello, world!");
byte[] value = new byte[MMKV.pageSize()];
mmkv.encode("largeKV", value);
mmkv.removeValueForKey("largeKV");
mmkv.trim();
Intent intent = new Intent(this, MyService.class);
intent.putExtra(BenchMarkBaseService.CMD_ID, MyService.CMD_TRIM_FINISH);
startService(intent);
SystemClock.sleep(1000 * 3);
Log.i("MMKV", "NonEmptyKey: " + mmkv.decodeString("NonEmptyKey"));
}
private void testItemSizeHolderOverride() {
// final String mmapID = "testItemSizeHolderOverride_crypted";
// final String encryptKey = "encrypeKey";
final String mmapID = "testItemSizeHolderOverride_plaintext";
final String encryptKey = null;
MMKV mmkv = MMKV.mmkvWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, encryptKey);
/* do this in v1.1.2
{
// mmkv.encode("b", true);
byte[] value = new byte[512];
mmkv.encode("data", value);
Log.i("MMKV", "allKeys: " + Arrays.toString(mmkv.allKeys()));
}*/
// do this in v1.2.0
{
long totalSize = mmkv.totalSize();
long bufferSize = totalSize - 512;
byte[] value = new byte[(int) bufferSize];
// force a fullwriteback()
mmkv.encode("bigData", value);
mmkv.clearMemoryCache();
Log.i("MMKV", "allKeys: " + Arrays.toString(mmkv.allKeys()));
}
}
private void testBackup() {
File f = new File(MMKV.getRootDir());
String backupRootDir = f.getParent() + "/mmkv_backup_3";
String mmapID = "test/AES";
String otherDir = getFilesDir().getAbsolutePath() + "/mmkv_3";
boolean ret = MMKV.backupOneToDirectory(mmapID, backupRootDir, otherDir);
Log.i("MMKV", "backup one [" + mmapID + "] ret = " + ret);
if (ret) {
MMKV mmkv = MMKV.backedUpMMKVWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, "Tencent MMKV", backupRootDir);
Log.i("MMKV", "check on backup file[" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
}
// test backup a normal mmkv from custom root path
mmapID = "test_backup";
ret = MMKV.backupOneToDirectory(mmapID, backupRootDir, otherDir);
Log.i("MMKV", "backup one [" + mmapID + "] ret = " + ret);
if (ret) {
MMKV mmkv = MMKV.backedUpMMKVWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, "MMKV Backup", backupRootDir);
Log.i("MMKV", "check on backup file[" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
}
/*{
MMKV mmkv = MMKV.mmkvWithID("imported");
mmkv.close();
mmkv = MMKV.mmkvWithID("test/AES_reKey1");
mmkv.close();
}*/
backupRootDir = f.getParent() + "/mmkv_backup";
long count = MMKV.backupAllToDirectory(backupRootDir);
Log.i("MMKV", "backup all count " + count);
if (count > 0) {
MMKV mmkv = MMKV.backedUpMMKVWithID("imported", MMKV.SINGLE_PROCESS_MODE, null, backupRootDir);
Log.i("MMKV", "check on backup file[" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
mmkv = MMKV.backedUpMMKVWithID("testKotlin", MMKV.SINGLE_PROCESS_MODE, null, backupRootDir);
Log.i("MMKV", "check on backup file[" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
mmkv = MMKV.backedUpMMKVWithID("test/AES_reKey1", MMKV.SINGLE_PROCESS_MODE, null, backupRootDir);
Log.i("MMKV", "check on backup file[" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
mmkv = MMKV.backedUpMMKVWithID("benchmark_interprocess", MMKV.MULTI_PROCESS_MODE, null, backupRootDir);
Log.i("MMKV", "check on backup file[" + mmkv.mmapID() + "] allKeys count: " + mmkv.count());
}
}
private void testRestore() {
File f = new File(MMKV.getRootDir());
String backupRootDir = f.getParent() + "/mmkv_backup_3";
String mmapID = "test/AES";
String otherDir = getFilesDir().getAbsolutePath() + "/mmkv_3";
MMKV mmkv = MMKV.mmkvWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, "Tencent MMKV", otherDir);
mmkv.encode("test_restore", true);
Log.i("MMKV", "before restore [" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
boolean ret = MMKV.restoreOneMMKVFromDirectory(mmapID, backupRootDir, otherDir);
Log.i("MMKV", "restore one [" + mmapID + "] ret = " + ret);
if (ret) {
Log.i("MMKV", "after restore [" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
}
// test backup a normal mmkv from custom root path
mmapID = "test_backup";
mmkv = MMKV.mmkvWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, "MMKV Backup", otherDir);
mmkv.encode("test_restore", 1024);
Log.i("MMKV", "before restore [" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
ret = MMKV.restoreOneMMKVFromDirectory(mmapID, backupRootDir, otherDir);
Log.i("MMKV", "backup one [" + mmapID + "] ret = " + ret);
if (ret) {
Log.i("MMKV", "check on backup file[" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
}
/*{
mmkv = MMKV.mmkvWithID("imported");
mmkv.close();
mmkv = MMKV.mmkvWithID("test/AES_reKey1");
mmkv.close();
}*/
backupRootDir = f.getParent() + "/mmkv_backup";
long count = MMKV.restoreAllFromDirectory(backupRootDir);
Log.i("MMKV", "restore all count " + count);
if (count > 0) {
mmkv = MMKV.mmkvWithID("imported");
Log.i("MMKV", "check on restore file[" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
mmkv = MMKV.mmkvWithID("testKotlin");
Log.i("MMKV", "check on restore file[" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
mmkv = MMKV.mmkvWithID("test/AES_reKey1");
Log.i("MMKV", "check on restore file[" + mmkv.mmapID() + "] allKeys: " + Arrays.toString(mmkv.allKeys()));
mmkv = MMKV.mmkvWithID("benchmark_interprocess", MMKV.MULTI_PROCESS_MODE);
Log.i("MMKV", "check on restore file[" + mmkv.mmapID() + "] allKeys count: " + mmkv.count());
}
}
private void testAutoExpire(MMKV kv, boolean decodeOnly, int expiration) {
if (!decodeOnly) {
kv.encode("bool", true, expiration);
}
Log.i("MMKV", "bool: " + kv.decodeBool("bool"));
if (!decodeOnly) {
kv.encode("int", Integer.MIN_VALUE, expiration);
}
Log.i("MMKV", "int: " + kv.decodeInt("int"));
if (!decodeOnly) {
kv.encode("long", Long.MAX_VALUE, expiration);
}
Log.i("MMKV", "long: " + kv.decodeLong("long"));
if (!decodeOnly) {
kv.encode("float", -3.14f, expiration);
}
Log.i("MMKV", "float: " + kv.decodeFloat("float"));
if (!decodeOnly) {
kv.encode("double", Double.MIN_VALUE, expiration);
}
Log.i("MMKV", "double: " + kv.decodeDouble("double"));
if (!decodeOnly) {
kv.encode("string", "Hello from mmkv", expiration);
}
Log.i("MMKV", "string: " + kv.decodeString("string"));
if (!decodeOnly) {
byte[] bytes = {'m', 'm', 'k', 'v'};
kv.encode("bytes", bytes, expiration);
}
byte[] bytes = kv.decodeBytes("bytes");
if (bytes != null) {
Log.i("MMKV", "bytes: " + new String(bytes));
Log.i("MMKV", "bytes length = " + bytes.length + ", value size consumption = " + kv.getValueSize("bytes")
+ ", value size = " + kv.getValueActualSize("bytes"));
int sizeNeeded = kv.getValueActualSize("bytes");
NativeBuffer nativeBuffer = MMKV.createNativeBuffer(sizeNeeded);
if (nativeBuffer != null) {
int size = kv.writeValueToNativeBuffer("bytes", nativeBuffer);
Log.i("MMKV", "size Needed = " + sizeNeeded + " written size = " + size);
MMKV.destroyNativeBuffer(nativeBuffer);
}
}
if (!decodeOnly) {
TestParcelable testParcelable = new TestParcelable(1024, "Hi Parcelable");
kv.encode("parcel", testParcelable, expiration);
}
TestParcelable result = kv.decodeParcelable("parcel", TestParcelable.class);
if (result != null) {
Log.i("MMKV", "parcel: " + result.iValue + ", " + result.sValue + ", " + result.list);
}
if (!decodeOnly) {
kv.encode("null string", "some string", expiration);
}
Log.i("MMKV", "string before set null: " + kv.decodeString("null string"));
if (!decodeOnly) {
kv.encode("null string", (String) null, expiration);
}
Log.i("MMKV", "string after set null: " + kv.decodeString("null string")
+ ", containsKey:" + kv.contains("null string"));
Log.i("MMKV", "allKeys: " + Arrays.toString(kv.allNonExpireKeys()));
Log.i("MMKV",
"count = " + kv.countNonExpiredKeys() + ", totalSize = " + kv.totalSize() + ", actualSize = " + kv.actualSize());
Log.i("MMKV", "containsKey[string]: " + kv.containsKey("string"));
}
private void testAutoExpire() {
// disable auto expire by configuration
MMKVConfig config = new MMKVConfig();
config.enableKeyExpire = false;
// config.recover = MMKVRecoverStrategic.OnErrorRecover;
// config.itemSizeLimit = 1;
MMKV mmkv = MMKV.mmkvWithID("test_auto_expire", config);
mmkv.clearAll();
mmkv.disableAutoKeyExpire(); // this become a no-op
// enable auto expire by configuration
mmkv.close();
config.enableKeyExpire = true;
config.expiredInSeconds = 1;
mmkv = MMKV.mmkvWithID("test_auto_expire", config);
mmkv.enableAutoKeyExpire(1); // this become a no-op
mmkv.encode("auto_expire_key_1", true);
mmkv.encode("never_expire_key_1", true, MMKV.ExpireNever);
// mmkv.enableCompareBeforeSet();
testAutoExpire(mmkv, false, 1);
SystemClock.sleep(1000 * 2);
testAutoExpire(mmkv, true, 1);
// mmkv.encode("string", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJFbHFTUjB");
// mmkv.clearMemoryCache();
// Log.i("MMKV", "space string = " + mmkv.decodeString("string", ""));
if (mmkv.contains("auto_expire_key_1")) {
Log.e("MMKV", "auto key expiration auto_expire_key_1");
} else {
Log.i("MMKV", "auto key expiration auto_expire_key_1");
}
if (mmkv.contains("never_expire_key_1")) {
Log.i("MMKV", "auto key expiration never_expire_key_1");
} else {
Log.e("MMKV", "auto key expiration never_expire_key_1");
}
}
private int addExtraRoundUp(int len) {
int pageSize = MMKV.pageSize();
int rest = len % pageSize;
return rest == 0 ? len + pageSize : (2 + len / pageSize) * pageSize;
}
private void testDiskFull() {
new Thread(new Runnable() {
@Override
public void run() {
MMKV mmkv = MMKV.mmkvWithID("disk_full");
long i = 0;
StringBuilder value = new StringBuilder();
for (int j = 0; j < 100000; j++) {
value.append("a");
}
while (true) {
boolean ret = mmkv.encode(i++ + "", value.toString());
if (!ret) {
break;
}
}
}
}).start();
}
private void testClearAllKeepSpace() {
MMKV mmkv = MMKV.mmkvWithID("testClearAllKeepSpace");
mmkv.encode("key", "value");
mmkv.encode("key2", "value2");
mmkv.clearAllWithKeepingSpace();
mmkv.encode("key3", "value3");
mmkv.clearAll();
mmkv.encode("key4", "value4");
}
private void testExpectedCapacity() {
String key = "key0";
String value = "🏊🏻®4️⃣🐅_";
int len = 10000;
for (int i = 0; i < len; i++) {
value += "0";
}
Log.i("MMKV", "value size = " + value.getBytes().length);
int expectedSize = key.getBytes().length + value.getBytes().length;
// if we know exactly the sizes of key and value, set expectedCapacity for performance improvement
// extra space can be added to round up
MMKV mmkv = MMKV.mmkvWithID("test_expected_capacity0", MMKV.SINGLE_PROCESS_MODE,
addExtraRoundUp(len));
// 0 times expand
mmkv.encode(key, value);
int count = 10;
expectedSize = expectedSize * count;
MMKV mmkv1 = MMKV.mmkvWithID("test_expected_capacity1", MMKV.SINGLE_PROCESS_MODE,
addExtraRoundUp(expectedSize));
for (int i = 0; i < count; i++) {
String k = "key" + i;
// 0 times expand
mmkv1.encode(k, value);
}
}
private void testRemoveStorageAndCheckExist() {
String mmapID = "test_remove";
{
MMKV mmkv = MMKV.mmkvWithID(mmapID, MMKV.MULTI_PROCESS_MODE);
mmkv.encode("bool", true);
}
Log.i("MMKV", "checkExist = " + MMKV.checkExist(mmapID));
MMKV.removeStorage(mmapID);
Log.i("MMKV", "after remove, checkExist = " + MMKV.checkExist(mmapID));
{
MMKV mmkv = MMKV.mmkvWithID(mmapID, MMKV.MULTI_PROCESS_MODE);
if (mmkv.count() != 0) {
Log.e("MMKV", "storage not successfully removed");
}
}
mmapID = "test_remove/sg";
String rootDir = getFilesDir().getAbsolutePath() + "/mmkv_sg";
MMKV mmkv = MMKV.mmkvWithID(mmapID, rootDir);
mmkv.encode("bool", true);
Log.i("MMKV", "checkExist = " + MMKV.checkExist(mmapID, rootDir));
MMKV.removeStorage(mmapID, rootDir);
Log.i("MMKV", "after remove, checkExist = " + MMKV.checkExist(mmapID, rootDir));
mmkv = MMKV.mmkvWithID(mmapID, rootDir);
if (mmkv.count() != 0) {
Log.e("MMKV", "storage not successfully removed");
}
}
private void overrideTest() {
MMKV mmkv0 = MMKV.mmkvWithID("overrideTest");
String key = "hello";
String key2 = "hello2";
String value = "world";
mmkv0.encode(key, value);
String v2 = mmkv0.decodeString(key);
if (!value.equals(v2)) {
System.out.println("value = " + v2);
System.exit(1);
}
mmkv0.removeValueForKey(key);
mmkv0.encode(key2, value);
v2 = mmkv0.decodeString(key2);
if (!value.equals(v2)) {
System.out.println("value = " + v2);
System.exit(1);
}
mmkv0.removeValueForKey(key2);
int len = 10000;
StringBuilder bigValue = new StringBuilder("🏊🏻®4️⃣🐅_");
for (int i = 0; i < len; i++) {
bigValue.append("0");
}
mmkv0.encode(key, bigValue.toString());
String v3 = mmkv0.decodeString(key);
if (!bigValue.toString().equals(v3)) {
System.exit(1);
}
// rewrite
mmkv0.encode(key, "OK");
String v4 = mmkv0.decodeString(key);
if (!"OK".equals(v4)) {
System.out.println("value = " + v2);
System.exit(1);
}
mmkv0.encode(key, 12345);
int v5 = mmkv0.decodeInt(key);
if (v5 != 12345) {
System.out.println("value = " + v5);
System.exit(1);
}
mmkv0.removeValueForKey(key);
mmkv0.clearAll();
overrideTestEncrypt();
}
private void overrideTestEncrypt() {
// test small value
encryptionTest("cryptworld");
// test medium value
encryptionTest("An efficient, small mobile key-value storage framework developed by WeChat. Works on Android, iOS, macOS, Windows, and POSIX.");
// test large value
encryptionTest("An efficient, small mobile key-value storage framework developed by WeChat. Works on Android, iOS, macOS, Windows, and POSIX. 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, Windows, POSIX and HarmonyOS NEXT.");
}
private void encryptionTest(String value) {
String key = "hello";
String key2 = "hello2";
encryptionTestKV(key, value);
encryptionTestKV(key2, value);
}
private void encryptionTestKV(String key, String value) {
String crypt = "fastestCrypt";
MMKV mmkv0 = MMKV.mmkvWithID("overrideCryptTest", MMKV.SINGLE_PROCESS_MODE, crypt);
mmkv0.encode(key, value);
String v2 = mmkv0.decodeString(key);
if (!value.equals(v2)) {
System.out.println("value = " + value + ", result = " + v2);
System.exit(1);
}
mmkv0.close();
mmkv0 = MMKV.mmkvWithID("overrideCryptTest", MMKV.SINGLE_PROCESS_MODE, crypt);
v2 = mmkv0.decodeString(key);
if (!value.equals(v2)) {
System.out.println("value = " + value + ", result = " + v2);
System.exit(1);
}
mmkv0.encode(key, value);
v2 = mmkv0.decodeString(key);
if (!value.equals(v2)) {
System.out.println("value = " + value + ", result = " + v2);
System.exit(1);
}
mmkv0.removeValueForKey(key);
}
private void testReadOnly() {
final String name = "testReadOnly";
final String key = "readonly+key";
{
MMKV kv = MMKV.mmkvWithID(name, MMKV.SINGLE_PROCESS_MODE, key);
testMMKV(kv, false);
kv.close();
}
String path = MMKV.getRootDir() + "/" + name;
File file = new File(path);
file.setReadOnly();
File crcFile = new File(path + ".crc");
crcFile.setReadOnly();
MMKV kv = MMKV.mmkvWithID(name, MMKV.SINGLE_PROCESS_MODE | MMKV.READ_ONLY_MODE, key);
testMMKV(kv, true);
// also check if it tolerate update operations without crash
testMMKV(kv, false);
file.setWritable(true);
crcFile.setWritable(true);
}
private void testImport() {
final String mmapID = "testImportSrc";
MMKV src = MMKV.mmkvWithID(mmapID);
src.encode("bool", true);
src.encode("int", Integer.MIN_VALUE);
src.encode("long", Long.MAX_VALUE);
src.encode("string", "test import");
MMKV dst = MMKV.mmkvWithID("testImportDst");
dst.clearAll();
dst.enableAutoKeyExpire(1);
dst.encode("bool", false);
dst.encode("int", -1);
dst.encode("long", 0);
dst.encode("string", mmapID);
long count = dst.importFrom(src);
if (count != 4 || dst.count() != 4) {
Log.e("MMKV", "import check count fail");
}
if (!dst.decodeBool("bool")) {
Log.e("MMKV", "import check bool fail");
}
if (dst.decodeInt("int") != Integer.MIN_VALUE) {
Log.e("MMKV", "import check int fail");
}
if (dst.decodeLong("long") != Long.MAX_VALUE) {
Log.e("MMKV", "import check long fail");
}
if (!Objects.equals(dst.decodeString("string"), "test import")) {
Log.e("MMKV", "import check string fail");
}
SystemClock.sleep(1000 * 2);
if (dst.countNonExpiredKeys() != 0) {
Log.e("MMKV", "import check expire fail");
}
}
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/MultiProcessSharedPreferences.java
================================================
package com.tencent.mmkvdemo;
import android.content.BroadcastReceiver;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
import java.lang.ref.SoftReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 使用ContentProvider实现多进程SharedPreferences读写;
* 1、ContentProvider天生支持多进程访问;
* 2、使用内部私有BroadcastReceiver实现多进程OnSharedPreferenceChangeListener监听;
*
* 使用方法:AndroidManifest.xml中添加provider申明:
*
*
* ContentProvider方式实现要注意:
* 1、当ContentProvider所在进程android.os.Process.killProcess(pid)时,会导致整个应用程序完全意外退出或者ContentProvider所在进程重启;
* 重启报错信息:Acquiring provider for user 0: existing object's process dead;
* 2、如果设备处在“安全模式”下,只有系统自带的ContentProvider才能被正常解析使用,因此put值时默认返回false,get值时默认返回null;
*
* 其他方式实现SharedPreferences的问题:
* 使用FileLock和FileObserver也可以实现多进程SharedPreferences读写,但是会有兼容性问题:
* 1、某些设备上卸载程序时锁文件无法删除导致卸载残留,进而导致无法重新安装该程序(报INSTALL_FAILED_UID_CHANGED错误);
* 2、某些设备上FileLock会导致僵尸进程出现进而导致耗电;
* 3、僵尸进程出现后,正常进程的FileLock会一直阻塞等待僵尸进程中的FileLock释放,导致进程一直阻塞;
*
* @author seven456@gmail.com
* @version 1.0
* @since JDK1.6
*
*/
public class MultiProcessSharedPreferences extends ContentProvider implements SharedPreferences {
private static final String TAG = "MicroMsg.MultiProcessSharedPreferences";
public static final boolean DEBUG = BuildConfig.DEBUG;
private Context mContext;
private String mName;
private int mMode;
private boolean mIsSafeMode;
private List> mListeners;
private BroadcastReceiver mReceiver;
private static String AUTHORITY;
private static volatile Uri AUTHORITY_URI;
private UriMatcher mUriMatcher;
private static final String KEY = "value";
private static final String KEY_NAME = "name";
private static final String PATH_WILDCARD = "*/";
private static final String PATH_GET_ALL = "getAll";
private static final String PATH_GET_STRING = "getString";
private static final String PATH_GET_INT = "getInt";
private static final String PATH_GET_LONG = "getLong";
private static final String PATH_GET_FLOAT = "getFloat";
private static final String PATH_GET_BOOLEAN = "getBoolean";
private static final String PATH_CONTAINS = "contains";
private static final String PATH_APPLY = "apply";
private static final String PATH_COMMIT = "commit";
private static final String PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER =
"registerOnSharedPreferenceChangeListener";
private static final String PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER =
"unregisterOnSharedPreferenceChangeListener";
private static final int GET_ALL = 1;
private static final int GET_STRING = 2;
private static final int GET_INT = 3;
private static final int GET_LONG = 4;
private static final int GET_FLOAT = 5;
private static final int GET_BOOLEAN = 6;
private static final int CONTAINS = 7;
private static final int APPLY = 8;
private static final int COMMIT = 9;
private static final int REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER = 10;
private static final int UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER = 11;
private Map mListenersCount;
private static class ReflectionUtil {
public static ContentValues contentValuesNewInstance(HashMap values) {
try {
Constructor c =
ContentValues.class.getDeclaredConstructor(new Class[] {HashMap.class}); // hide
c.setAccessible(true);
return c.newInstance(values);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
}
}
public static Editor editorPutStringSet(Editor editor, String key, Set values) {
try {
Method method = editor.getClass().getDeclaredMethod(
"putStringSet", new Class[] {String.class, Set.class}); // Android 3.0
return (Editor) method.invoke(editor, key, values);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
public static void editorApply(Editor editor) {
try {
Method method = editor.getClass().getDeclaredMethod("apply"); // Android 2.3
method.invoke(editor);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
private void checkInitAuthority(Context context) {
if (AUTHORITY_URI == null) {
String AUTHORITY = null;
Uri AUTHORITY_URI = MultiProcessSharedPreferences.AUTHORITY_URI;
synchronized (MultiProcessSharedPreferences.this) {
if (AUTHORITY_URI == null) {
AUTHORITY = queryAuthority(context);
AUTHORITY_URI = Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + AUTHORITY);
if (DEBUG) {
// Log.d(TAG, "checkInitAuthority.AUTHORITY = " + AUTHORITY);
}
}
if (AUTHORITY == null) {
throw new IllegalArgumentException("'AUTHORITY' initialize failed.");
}
}
MultiProcessSharedPreferences.AUTHORITY = AUTHORITY;
MultiProcessSharedPreferences.AUTHORITY_URI = AUTHORITY_URI;
}
}
private static String queryAuthority(Context context) {
PackageInfo packageInfos = null;
try {
PackageManager mgr = context.getPackageManager();
if (mgr != null) {
packageInfos = mgr.getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS);
}
} catch (NameNotFoundException e) {
if (DEBUG) {
// Log.printErrStackTrace(TAG, e, "");
}
}
if (packageInfos != null && packageInfos.providers != null) {
for (ProviderInfo providerInfo : packageInfos.providers) {
if (providerInfo.name.equals(MultiProcessSharedPreferences.class.getName())) {
return providerInfo.authority;
}
}
}
return null;
}
/**
* mode不使用{@link Context#MODE_MULTI_PROCESS}特可以支持多进程了;
*
* @param mode
*
* @see Context#MODE_PRIVATE
* @see Context#MODE_WORLD_READABLE
* @see Context#MODE_WORLD_WRITEABLE
*/
public static SharedPreferences getSharedPreferences(Context context, String name, int mode) {
return new MultiProcessSharedPreferences(context, name, mode);
}
public MultiProcessSharedPreferences() {
}
private MultiProcessSharedPreferences(Context context, String name, int mode) {
mContext = context;
mName = name;
mMode = mode;
PackageManager mgr = context.getPackageManager();
if (mgr != null) {
mIsSafeMode =
mgr.isSafeMode(); // 如果设备处在“安全模式”下,只有系统自带的ContentProvider才能被正常解析使用;
}
}
@SuppressWarnings("unchecked")
@Override
public Map getAll() {
return (Map) getValue(PATH_GET_ALL, null, null);
}
@Override
public String getString(String key, String defValue) {
String v = (String) getValue(PATH_GET_STRING, key, defValue);
return v != null ? v : defValue;
}
// @Override // Android 3.0
public Set getStringSet(String key, Set defValues) {
synchronized (this) {
@SuppressWarnings("unchecked")
Set v = (Set) getValue(PATH_GET_STRING, key, defValues);
return v != null ? v : defValues;
}
}
@Override
public int getInt(String key, int defValue) {
Integer v = (Integer) getValue(PATH_GET_INT, key, defValue);
return v != null ? v : defValue;
}
@Override
public long getLong(String key, long defValue) {
Long v = (Long) getValue(PATH_GET_LONG, key, defValue);
return v != null ? v : defValue;
}
@Override
public float getFloat(String key, float defValue) {
Float v = (Float) getValue(PATH_GET_FLOAT, key, defValue);
return v != null ? v : defValue;
}
@Override
public boolean getBoolean(String key, boolean defValue) {
Boolean v = (Boolean) getValue(PATH_GET_BOOLEAN, key, defValue);
return v != null ? v : defValue;
}
@Override
public boolean contains(String key) {
Boolean v = (Boolean) getValue(PATH_CONTAINS, key, null);
return v != null ? v : false;
}
@Override
public Editor edit() {
return new EditorImpl();
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
synchronized (this) {
if (mListeners == null) {
mListeners = new ArrayList>();
}
Boolean result = (Boolean) getValue(PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER, null, false);
if (result != null && result) {
mListeners.add(new SoftReference(listener));
if (mReceiver == null) {
mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String name = intent.getStringExtra(KEY_NAME);
@SuppressWarnings("unchecked")
List keysModified = (List) intent.getSerializableExtra(KEY);
if (mName.equals(name) && keysModified != null) {
List arrayListeners;
synchronized (MultiProcessSharedPreferences.this) {
arrayListeners = mListeners;
}
List> listeners =
new ArrayList>(arrayListeners);
for (int i = keysModified.size() - 1; i >= 0; i--) {
final String key = keysModified.get(i);
for (SoftReference srlistener : listeners) {
OnSharedPreferenceChangeListener listener = srlistener.get();
if (listener != null) {
listener.onSharedPreferenceChanged(MultiProcessSharedPreferences.this, key);
}
}
}
}
}
};
mContext.registerReceiver(mReceiver, new IntentFilter(makeAction(mName)));
}
}
}
}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
synchronized (this) {
getValue(PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER, null, false);
if (mListeners != null) {
List> removing = new ArrayList<>();
for (SoftReference srlistener : mListeners) {
OnSharedPreferenceChangeListener listenerFromSR = srlistener.get();
if (listenerFromSR != null && listenerFromSR.equals(listener)) {
removing.add(srlistener);
}
}
for (SoftReference srlistener : removing) {
mListeners.remove(srlistener);
}
if (mListeners.isEmpty() && mReceiver != null) {
mContext.unregisterReceiver(mReceiver);
mReceiver = null;
mListeners = null;
}
}
}
}
public final class EditorImpl implements Editor {
private final Map mModified = new HashMap();
private boolean mClear = false;
@Override
public Editor putString(String key, String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
// @Override // Android 3.0
public Editor putStringSet(String key, Set values) {
synchronized (this) {
mModified.put(key, (values == null) ? null : new HashSet(values));
return this;
}
}
@Override
public Editor putInt(String key, int value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor putLong(String key, long value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor putFloat(String key, float value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor putBoolean(String key, boolean value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor remove(String key) {
synchronized (this) {
mModified.put(key, null);
return this;
}
}
@Override
public Editor clear() {
synchronized (this) {
mClear = true;
return this;
}
}
@Override
public void apply() {
setValue(PATH_APPLY);
}
@Override
public boolean commit() {
return setValue(PATH_COMMIT);
}
private boolean setValue(String pathSegment) {
if (mIsSafeMode) { // 如果设备处在“安全模式”,返回false;
return false;
} else {
synchronized (MultiProcessSharedPreferences.this) {
checkInitAuthority(mContext);
String[] selectionArgs = new String[] {String.valueOf(mMode), String.valueOf(mClear)};
synchronized (this) {
Uri uri = Uri.withAppendedPath(Uri.withAppendedPath(AUTHORITY_URI, mName), pathSegment);
ContentValues values =
ReflectionUtil.contentValuesNewInstance((HashMap) mModified);
return mContext.getContentResolver().update(uri, values, null, selectionArgs) > 0;
}
}
}
}
}
private Object getValue(String pathSegment, String key, Object defValue) {
if (mIsSafeMode) { // 如果设备处在“安全模式”,返回null;
return null;
} else {
checkInitAuthority(mContext);
Object v = null;
Uri uri = Uri.withAppendedPath(Uri.withAppendedPath(AUTHORITY_URI, mName), pathSegment);
String[] selectionArgs =
new String[] {String.valueOf(mMode), key, defValue == null ? null : String.valueOf(defValue)};
Cursor cursor = mContext.getContentResolver().query(uri, null, null, selectionArgs, null);
if (cursor != null) {
try {
Bundle bundle = cursor.getExtras();
if (bundle != null) {
v = bundle.get(KEY);
bundle.clear();
}
} catch (Exception e) {
}
cursor.close();
}
return v != null ? v : defValue;
}
}
private String makeAction(String name) {
return String.format("%1$s_%2$s", MultiProcessSharedPreferences.class.getName(), name);
}
@Override
public boolean onCreate() {
checkInitAuthority(getContext());
mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_ALL, GET_ALL);
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_STRING, GET_STRING);
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_INT, GET_INT);
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_LONG, GET_LONG);
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_FLOAT, GET_FLOAT);
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_BOOLEAN, GET_BOOLEAN);
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_CONTAINS, CONTAINS);
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_APPLY, APPLY);
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_COMMIT, COMMIT);
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER,
REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER);
mUriMatcher.addURI(AUTHORITY, PATH_WILDCARD + PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER,
UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String name = uri.getPathSegments().get(0);
int mode = Integer.parseInt(selectionArgs[0]);
String key = selectionArgs[1];
String defValue = selectionArgs[2];
Bundle bundle = new Bundle();
switch (mUriMatcher.match(uri)) {
case GET_ALL:
bundle.putSerializable(KEY,
(HashMap) getContext().getSharedPreferences(name, mode).getAll());
break;
case GET_STRING:
bundle.putString(KEY, getContext().getSharedPreferences(name, mode).getString(key, defValue));
break;
case GET_INT:
bundle.putInt(KEY,
getContext().getSharedPreferences(name, mode).getInt(key, Integer.parseInt(defValue)));
break;
case GET_LONG:
bundle.putLong(KEY,
getContext().getSharedPreferences(name, mode).getLong(key, Long.parseLong(defValue)));
break;
case GET_FLOAT:
bundle.putFloat(
KEY, getContext().getSharedPreferences(name, mode).getFloat(key, Float.parseFloat(defValue)));
break;
case GET_BOOLEAN:
bundle.putBoolean(
KEY, getContext().getSharedPreferences(name, mode).getBoolean(key, Boolean.parseBoolean(defValue)));
break;
case CONTAINS:
bundle.putBoolean(KEY, getContext().getSharedPreferences(name, mode).contains(key));
break;
case REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER: {
checkInitListenersCount();
Integer countInteger = mListenersCount.get(name);
int count = (countInteger == null ? 0 : countInteger) + 1;
mListenersCount.put(name, count);
countInteger = mListenersCount.get(name);
bundle.putBoolean(KEY, count == (countInteger == null ? 0 : countInteger));
} break;
case UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER: {
checkInitListenersCount();
Integer countInteger = mListenersCount.get(name);
int count = (countInteger == null ? 0 : countInteger) - 1;
if (count <= 0) {
mListenersCount.remove(name);
bundle.putBoolean(KEY, !mListenersCount.containsKey(name));
} else {
mListenersCount.put(name, count);
countInteger = mListenersCount.get(name);
bundle.putBoolean(KEY, count == (countInteger == null ? 0 : countInteger));
}
} break;
default:
throw new IllegalArgumentException("This is Unknown Uri:" + uri);
}
return new BundleCursor(bundle);
}
@Override
public String getType(Uri uri) {
throw new UnsupportedOperationException("No external call");
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("No external insert");
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("No external delete");
}
@SuppressWarnings("unchecked")
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int result = 0;
String name = uri.getPathSegments().get(0);
int mode = Integer.parseInt(selectionArgs[0]);
SharedPreferences preferences = getContext().getSharedPreferences(name, mode);
int match = mUriMatcher.match(uri);
switch (match) {
case APPLY:
case COMMIT:
boolean hasListeners =
mListenersCount != null && mListenersCount.get(name) != null && mListenersCount.get(name) > 0;
ArrayList keysModified = null;
Map map = new HashMap();
if (hasListeners) {
keysModified = new ArrayList();
map = (Map) preferences.getAll();
}
Editor editor = preferences.edit();
boolean clear = Boolean.parseBoolean(selectionArgs[1]);
if (clear) {
if (hasListeners && map != null && !map.isEmpty()) {
for (Map.Entry entry : map.entrySet()) {
keysModified.add(entry.getKey());
}
}
editor.clear();
}
for (Map.Entry entry : values.valueSet()) {
String k = entry.getKey();
Object v = entry.getValue();
// Android 5.L_preview : "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
if (v instanceof EditorImpl || v == null) {
editor.remove(k);
if (hasListeners && map != null && map.containsKey(k)) {
keysModified.add(k);
}
} else {
if (hasListeners && map != null
&& (!map.containsKey(k) || (map.containsKey(k) && !v.equals(map.get(k))))) {
keysModified.add(k);
}
}
if (v instanceof String) {
editor.putString(k, (String) v);
} else if (v instanceof Set) {
ReflectionUtil.editorPutStringSet(editor, k,
(Set) v); // Android 3.0
} else if (v instanceof Integer) {
editor.putInt(k, (Integer) v);
} else if (v instanceof Long) {
editor.putLong(k, (Long) v);
} else if (v instanceof Float) {
editor.putFloat(k, (Float) v);
} else if (v instanceof Boolean) {
editor.putBoolean(k, (Boolean) v);
}
}
if (hasListeners && keysModified.isEmpty()) {
result = 1;
} else {
switch (match) {
case APPLY:
ReflectionUtil.editorApply(editor); // Android 2.3
result = 1;
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(name, keysModified);
break;
case COMMIT:
if (editor.commit()) {
result = 1;
notifyListeners(name, keysModified);
}
break;
}
}
values.clear();
break;
default:
throw new IllegalArgumentException("This is Unknown Uri:" + uri);
}
return result;
}
@Override
public void onLowMemory() {
if (mListenersCount != null) {
mListenersCount.clear();
}
super.onLowMemory();
}
@Override
public void onTrimMemory(int level) {
if (mListenersCount != null) {
mListenersCount.clear();
}
super.onTrimMemory(level);
}
private void checkInitListenersCount() {
if (mListenersCount == null) {
mListenersCount = new HashMap();
}
}
private void notifyListeners(String name, ArrayList keysModified) {
if (keysModified != null && !keysModified.isEmpty()) {
Intent intent = new Intent();
intent.setAction(makeAction(name));
intent.setPackage(getContext().getPackageName());
intent.putExtra(KEY_NAME, name);
intent.putExtra(KEY, keysModified);
getContext().sendBroadcast(intent);
}
}
private static final class BundleCursor extends MatrixCursor {
private Bundle mBundle;
public BundleCursor(Bundle extras) {
super(new String[] {}, 0);
mBundle = extras;
}
@Override
public Bundle getExtras() {
return mBundle;
}
@Override
public Bundle respond(Bundle extras) {
mBundle = extras;
return mBundle;
}
}
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/MyApplication.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2025 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkvdemo;
import android.app.Application;
import android.util.Log;
import com.getkeepsafe.relinker.ReLinker;
import com.tencent.mmkv.MMKV;
import com.tencent.mmkv.MMKVHandler;
import com.tencent.mmkv.MMKVLogLevel;
import com.tencent.mmkv.MMKVRecoverStrategic;
import com.tencent.mmkv.NameSpace;
public class MyApplication extends Application implements MMKVHandler {
@Override
public void onCreate() {
super.onCreate();
// NameSpace can access an MMKV instance regardless of MMKV.initialized() been called or not
testNameSpace();
// set root dir
//String rootDir = MMKV.initialize(this);
String dir = getFilesDir().getAbsolutePath() + "/mmkv";
String rootDir = MMKV.initialize(this, dir, new MMKV.LibLoader() {
@Override
public void loadLibrary(String libName) {
ReLinker.loadLibrary(MyApplication.this, libName);
}
}, MMKVLogLevel.LevelInfo, this);
Log.i("MMKV", "mmkv root: " + rootDir);
// set log level
// MMKV.setLogLevel(MMKVLogLevel.LevelInfo);
// you can turn off logging
//MMKV.setLogLevel(MMKVLogLevel.LevelNone);
// register log redirecting & recover handler is moved into MMKV.initialize()
// MMKV.registerHandler(this);
// deprecated: content change notification
// MMKV.registerContentChangeNotify(this);
}
@Override
public void onTerminate() {
MMKV.onExit();
super.onTerminate();
}
@Override
public MMKVRecoverStrategic onMMKVCRCCheckFail(String mmapID) {
return MMKVRecoverStrategic.OnErrorRecover;
}
@Override
public MMKVRecoverStrategic onMMKVFileLengthError(String mmapID) {
return MMKVRecoverStrategic.OnErrorRecover;
}
@Override
public boolean wantLogRedirecting() {
return true;
}
@Override
public void mmkvLog(MMKVLogLevel level, String file, int line, String func, String message) {
String log = "<" + file + ":" + line + "::" + func + "> " + message;
switch (level) {
case LevelDebug:
Log.d("redirect logging MMKV", log);
break;
case LevelNone:
case LevelInfo:
Log.i("redirect logging MMKV", log);
break;
case LevelWarning:
Log.w("redirect logging MMKV", log);
break;
case LevelError:
Log.e("redirect logging MMKV", log);
break;
}
}
@Override
public native long getNativeLogHandler();
@Override
public void onContentChangedByOuterProcess(String mmapID) {
Log.i("MMKV", "other process has changed content of : " + mmapID);
}
@Override
public boolean wantContentChangeNotification() {
return true;
}
@Override
public void onMMKVContentLoadSuccessfully(String mmapID) {
Log.i("MMKV", "content load successfully : " + mmapID);
}
private void testNameSpace() {
final String nsRoot = getFilesDir().getAbsolutePath() + "/namespace";
final NameSpace ns = MMKV.nameSpace(nsRoot);
MMKV mmkv = ns.mmkvWithID("test/namespace/crypt", MMKV.MULTI_PROCESS_MODE, "crypt_key", 4096 * 2);
MainActivity.testMMKV(mmkv, false);
System.loadLibrary("mmkvdemo");
long nativeLogger = getNativeLogHandler();
Log.i("MMKVDemo", "native log handler: " + nativeLogger);
testNameSpaceInNative(nsRoot, "testNameSpaceInNative");
}
private native void testNameSpaceInNative(String nameSpaceRoot, String mmapID);
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/MyService.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkvdemo;
import android.content.Intent;
import android.util.Log;
import com.tencent.mmkv.MMKV;
public class MyService extends BenchMarkBaseService {
private static final String CALLER = "MyService";
public static final String CMD_REMOVE = "cmd_remove";
public static final String CMD_LOCK = "cmd_lock";
public static final String LOCK_PHASE_1 = "lock_phase_1";
public static final String LOCK_PHASE_2 = "lock_phase_2";
public static final String CMD_TRIM_FINISH = "trim_finish";
@Override
public void onCreate() {
super.onCreate();
Log.i("MMKV", "onCreate MyService");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i("MMKV", "onDestroy MyService");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("MMKV", "MyService onStartCommand");
if (intent != null) {
String cmd = intent.getStringExtra(CMD_ID);
if (cmd != null) {
if (cmd.equals(CMD_READ_INT)) {
super.batchReadInt(CALLER);
} else if (cmd.equals(CMD_READ_STRING)) {
super.batchReadString(CALLER);
} else if (cmd.equals(CMD_WRITE_INT)) {
super.batchWriteInt(CALLER);
} else if (cmd.equals(CMD_WRITE_STRING)) {
super.batchWriteString(CALLER);
} else if (cmd.equals(CMD_PREPARE_ASHMEM_BY_CP)) {
super.prepareAshmemMMKVByCP();
} else if (cmd.equals(CMD_REMOVE)) {
testRemove();
} else if (cmd.equals(CMD_LOCK)) {
testLock();
} else if (cmd.equals(CMD_TRIM_FINISH)) {
testTrimNonEmpty();
}
}
}
return super.onStartCommand(intent, flags, startId);
}
private void testRemove() {
MMKV mmkv = GetMMKV();
Log.d("mmkv in child", "" + mmkv.decodeInt(CMD_ID));
mmkv.remove(CMD_ID);
}
private void testLock() {
// get the lock immediately
MMKV mmkv2 = MMKV.mmkvWithID(LOCK_PHASE_2, MMKV.MULTI_PROCESS_MODE);
mmkv2.lock();
Log.d("locked in child", LOCK_PHASE_2);
Runnable waiter = new Runnable() {
@Override
public void run() {
MMKV mmkv1 = MMKV.mmkvWithID(LOCK_PHASE_1, MMKV.MULTI_PROCESS_MODE);
mmkv1.lock();
Log.d("locked in child", LOCK_PHASE_1);
}
};
// wait infinitely
new Thread(waiter).start();
}
private void testTrimNonEmpty() {
MMKV mmkv = MMKV.mmkvWithID("trimNonEmptyInterProcess", MMKV.MULTI_PROCESS_MODE);
byte[] value = new byte[64];
mmkv.encode("SomeKey", value);
}
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/MyService_1.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkvdemo;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.tencent.mmkv.ParcelableMMKV;
public class MyService_1 extends BenchMarkBaseService implements ServiceConnection {
public static final String CMD_PREPARE_ASHMEM = "cmd_prepare_ashmem";
private static final String CALLER = "MyService_1";
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("MMKV", "MyService_1 onStartCommand:");
if (intent != null) {
String cmd = intent.getStringExtra(CMD_ID);
Log.i("MMKV", "----MyService_1 onStartCommand:" + cmd);
if (cmd != null) {
if (cmd.equals(CMD_READ_INT)) {
super.batchReadInt(CALLER);
} else if (cmd.equals(CMD_READ_STRING)) {
super.batchReadString(CALLER);
} else if (cmd.equals(CMD_WRITE_INT)) {
super.batchWriteInt(CALLER);
} else if (cmd.equals(CMD_WRITE_STRING)) {
super.batchWriteString(CALLER);
} else if (cmd.equals(CMD_PREPARE_ASHMEM)) {
Intent i = new Intent("com.tencent.mmkvdemo.MyService").setPackage("com.tencent.mmkvdemo");
bindService(i, this, BIND_AUTO_CREATE);
} else if (cmd.equals(CMD_PREPARE_ASHMEM_BY_CP)) {
super.prepareAshmemMMKVByCP();
}
}
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onCreate() {
super.onCreate();
Log.i("MMKV", "onCreate MyService_1");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.i("MMKV", "onDestroy MyService_1");
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IAshmemMMKV ashmemMMKV = IAshmemMMKV.Stub.asInterface(service);
try {
ParcelableMMKV parcelableMMKV = ashmemMMKV.GetAshmemMMKV();
if (parcelableMMKV != null) {
m_ashmemMMKV = parcelableMMKV.toMMKV();
if (m_ashmemMMKV != null) {
Log.i("MMKV", "ashmem bool: " + m_ashmemMMKV.decodeBool("bool"));
}
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/SQLiteKV.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkvdemo;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public final class SQLiteKV {
private static class SQLiteKVDBHelper extends SQLiteOpenHelper {
private static final int DB_VERSION = 1;
private static final String DB_NAME = "kv.db";
public static final String TABLE_NAME_STR = "kv_str";
public static final String TABLE_NAME_INT = "kv_int";
public SQLiteKVDBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
String sql =
"create table if not exists " + TABLE_NAME_STR + " (k text UNIQUE on conflict replace, v text)";
sqLiteDatabase.execSQL(sql);
sql = "create table if not exists " + TABLE_NAME_INT + " (k text UNIQUE on conflict replace, v integer)";
sqLiteDatabase.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
String sql = "DROP TABLE IF EXISTS " + TABLE_NAME_STR;
sqLiteDatabase.execSQL(sql);
sql = "DROP TABLE IF EXISTS " + TABLE_NAME_INT;
sqLiteDatabase.execSQL(sql);
onCreate(sqLiteDatabase);
}
}
private SQLiteKVDBHelper m_dbHelper;
private SQLiteDatabase m_writetableDB;
private SQLiteDatabase m_readableDB;
public SQLiteKV(Context context) {
m_dbHelper = new SQLiteKVDBHelper(context);
m_dbHelper.setWriteAheadLoggingEnabled(true);
}
public void beginTransaction() {
getWritetableDB().beginTransaction();
}
public void endTransaction() {
if (m_writetableDB != null) {
try {
m_writetableDB.setTransactionSuccessful();
} finally {
m_writetableDB.endTransaction();
}
}
}
private SQLiteDatabase getWritetableDB() {
if (m_writetableDB == null) {
m_writetableDB = m_dbHelper.getWritableDatabase();
}
return m_writetableDB;
}
private SQLiteDatabase getReadableDatabase() {
if (m_readableDB == null) {
m_readableDB = m_dbHelper.getReadableDatabase();
}
return m_readableDB;
}
@Override
protected void finalize() throws Throwable {
if (m_readableDB != null) {
m_readableDB.close();
}
if (m_writetableDB != null) {
m_writetableDB.close();
}
super.finalize();
}
public boolean putInt(String key, int value) {
ContentValues contentValues = new ContentValues();
contentValues.put("k", key);
contentValues.put("v", value);
long rowID = getWritetableDB().insert(SQLiteKVDBHelper.TABLE_NAME_INT, null, contentValues);
return rowID != -1;
}
public int getInt(String key) {
int value = 0;
Cursor cursor = getReadableDatabase().rawQuery(
"select v from " + SQLiteKVDBHelper.TABLE_NAME_INT + " where k=?", new String[] {key});
if (cursor.moveToFirst()) {
value = cursor.getInt(cursor.getColumnIndexOrThrow("v"));
}
cursor.close();
return value;
}
public boolean putString(String key, String value) {
ContentValues contentValues = new ContentValues();
contentValues.put("k", key);
contentValues.put("v", value);
long rowID = getWritetableDB().insert(SQLiteKVDBHelper.TABLE_NAME_STR, null, contentValues);
return rowID != -1;
}
public String getString(String key) {
String value = null;
Cursor cursor = getReadableDatabase().rawQuery(
"select v from " + SQLiteKVDBHelper.TABLE_NAME_STR + " where k=?", new String[] {key});
if (cursor.moveToFirst()) {
value = cursor.getString(cursor.getColumnIndexOrThrow("v"));
}
cursor.close();
return value;
}
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/java/com/tencent/mmkvdemo/TestParcelable.java
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2025 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkvdemo;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.LinkedList;
import java.util.List;
class TestParcelable implements Parcelable {
int iValue;
String sValue;
List list;
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeInt(iValue);
parcel.writeString(sValue);
// parcel.writeTypedList(list);
}
@Override
public int describeContents() {
return 0;
}
TestParcelable(int i, String s) {
iValue = i;
sValue = s;
// list = new LinkedList<>();
// list.add(new Info("channel", 1));
// list.add(new Info("oppo", 2));
}
private TestParcelable(Parcel in) {
iValue = in.readInt();
sValue = in.readString();
list = in.createTypedArrayList(FakeInfo.CREATOR);
}
public static final Creator CREATOR = new Creator() {
@Override
public TestParcelable createFromParcel(Parcel parcel) {
return new TestParcelable(parcel);
}
@Override
public TestParcelable[] newArray(int i) {
return new TestParcelable[i];
}
};
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/kotlin/KotlinUsecase.kt
================================================
/*
* Tencent is pleased to support the open source community by making
* MMKV available.
*
* Copyright (C) 2018 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use
* this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tencent.mmkvdemo
import com.tencent.mmkv.MMKV
import java.util.*
fun kotlinFunctionalTest() {
val mmkv = MMKV.mmkvWithID("testKotlin")
mmkv.encode("bool", true)
println("bool = " + mmkv.decodeBool("bool"))
mmkv.encode("int", Integer.MIN_VALUE)
println("int: " + mmkv.decodeInt("int"))
mmkv.encode("long", java.lang.Long.MAX_VALUE)
println("long: " + mmkv.decodeLong("long"))
mmkv.encode("float", -3.14f)
println("float: " + mmkv.decodeFloat("float"))
mmkv.encode("double", java.lang.Double.MIN_VALUE)
println("double: " + mmkv.decodeDouble("double"))
mmkv.encode("string", "Hello from mmkv")
println("string: " + mmkv.decodeString("string"))
val bytes = byteArrayOf('m'.code.toByte(), 'm'.code.toByte(), 'k'.code.toByte(), 'v'.code.toByte())
mmkv.encode("bytes", bytes)
println("bytes: " + mmkv.decodeBytes("bytes")?.let { String(it) })
mmkv.encode("empty_bytes", "".toByteArray());
println("empty_bytes: " + mmkv.decodeBytes("empty_bytes")?.let { String(it) })
mmkv.encode("stringSet", HashSet())
println("empty string set: " + mmkv.decodeStringSet("stringSet"))
println("allKeys: " + Arrays.toString(mmkv.allKeys()))
println("count = " + mmkv.count() + ", totalSize = " + mmkv.totalSize())
println("containsKey[string]: " + mmkv.containsKey("string"))
mmkv.removeValueForKey("bool")
println("bool: " + mmkv.decodeBool("bool"))
mmkv.removeValuesForKeys(arrayOf("int", "long"))
//mmkv.clearAll();
mmkv.clearMemoryCache()
println("allKeys: " + Arrays.toString(mmkv.allKeys()))
println("isFileValid[" + mmkv.mmapID() + "]: " + MMKV.isFileValid(mmkv.mmapID()))
}
================================================
FILE: Android/MMKV/mmkvdemo/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: Android/MMKV/mmkvdemo/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
================================================
FILE: Android/MMKV/mmkvdemo/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: Android/MMKV/mmkvdemo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: Android/MMKV/mmkvdemo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: Android/MMKV/mmkvdemo/src/main/res/values/colors.xml
================================================
#3F51B5#303F9F#FF4081
================================================
FILE: Android/MMKV/mmkvdemo/src/main/res/values/strings.xml
================================================
MMKVDemo
================================================
FILE: Android/MMKV/mmkvdemo/src/main/res/values/styles.xml
================================================
================================================
FILE: Android/MMKV/pmd-ruleset.xml
================================================
This ruleset was created from PMD.rul
================================================
FILE: Android/MMKV/proguard-rules/proguard-rules-android-lib.pro
================================================
# OSS_ANDROID_TEMPLATE_FILE_HEADER
#
# Proguard config for publish an private aar module
# NOTE: It's different from libraries' consumer proguard config
#
-renamesourcefileattribute SourceFile
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,EnclosingMethod
# Preserve all annotations.
-keepattributes *Annotation*
# Preserve all public classes, and their public and protected fields and
# methods.
-keep public class * {
public protected *;
}
# Preserve all .class method names.
-keepclassmembernames class * {
java.lang.Class class$(java.lang.String);
java.lang.Class class$(java.lang.String, boolean);
}
# Preserve all native method names and the names of their classes.
-keepclasseswithmembernames class * {
native ;
}
# Preserve the special static methods that are required in all enumeration
# classes.
-keepclassmembers class * extends java.lang.Enum {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# Explicitly preserve all serialization members. The Serializable interface
# is only a marker interface, so it wouldn't save them.
# You can comment this out if your library doesn't use serialization.
# If your code contains serializable classes that have to be backward
# compatible, please refer to the manual.
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
================================================
FILE: Android/MMKV/proguard-rules/proguard-rules-test.pro
================================================
# OSS_ANDROID_TEMPLATE_FILE_HEADER
-dontnote junit.**
-dontnote org.xmlpull.v1.**
-dontwarn junit.**
-dontwarn org.junit.**
-dontwarn android.content.**
================================================
FILE: Android/MMKV/settings.gradle
================================================
rootProject.name = "MMKV-Android"
include ':mmkv', ':mmkvdemo', 'mmkvannotation'
================================================
FILE: CHANGELOG.md
================================================
# MMKV Change Log
## v2.4.0 / 2026-03-18
This release introduces a **unified `MMKVHandler` callback interface** across all platforms, replacing scattered per-callback registration with a single, OO-style handler. It also adds the `MMKVConfig` all-in-one configuration and several bug fixes.
### Changes for All Platforms
* **Feature:** Refactored the callback system into a **unified `MMKVHandler`** interface. All callbacks (log redirecting, error handling, content change notification, content loaded notification) are now grouped in a single handler registered via `registerHandler()`. The old per-callback `registerLogHandler()` / `registerErrorHandler()` / `registerContentChangeHandler()` / `registerContentLoadedHandler()` APIs have been removed.
* **Feature:** Added `onMMKVContentLoadSuccessfully` callback, triggered when an MMKV file is loaded/mapped successfully.
* **Feature:** Added `MMKVConfig` for all-in-one instance configuration, supporting all options (`mode`, `cryptKey`, `aes256`, `expectedCapacity`, `enableKeyExpire`, `expiredInSeconds`, `enableCompareBeforeSet`, `recover`, `itemSizeLimit`) in a single struct/class.
* **Feature:** Added `defaultMMKV(config)` variant for creating the default instance with full configuration.
* **Fix:** Robust check on encryption mode ([#1642](https://github.com/nicksunday/mmkv/issues/1642)).
* **Fix:** Protect from `delete` file failure on corrupted files.
* **Fix:** Protect from `m_file` not valid for `isDiskOfMMAPFileCorrupted()`.
* **Fix:** Reduce `absolutePath()` calls as much as possible.
* Drop old-style actual size downgrade support.
* MMKV must be compiled with **C++17**.
### Android
* **Change:** Merged `MMKVContentChangeNotification` into `MMKVHandler`. The old `MMKVContentChangeNotification` interface is now `@Deprecated`.
* **Fix:** Fix `fcntl()` OFD lock failure on ashmem ([#1637](https://github.com/nicksunday/mmkv/issues/1637)).
* Merge ashmem size with `expectedCapacity`.
* Correctly collect so-symbols.
### iOS/macOS
* **Feature:** Added `getBytes()` with `string_view` key for ObjC++.
* **Fix:** Fixed a memory leak on getting `NameSpace` instance.
* Support **Swift Package Manager** ([#535](https://github.com/nicksunday/mmkv/issues/535)).
* Fix compile error on `tryAtomicRename()`.
### HarmonyOS NEXT
* Merge ashmem size with `expectedCapacity`.
### Flutter
* Updated to use the unified `MMKVHandler` callback interface.
### Win32
* Fix: Convert log message to UTF-8 for Win32 client.
* Verify all pages for corrupted file detection.
## v2.3.0 / 2025-12-03 (Breaking Change)
This release is a **breaking change** and introduces **AES-256 encryption** for enhanced security.
### All Platforms
* **Feature:** Added **AES-256 encryption** functionality. To upgrade an existing encrypted MMKV instance to AES-256, first load it using the old key. Then, call the `reKey()` method with the new key and set the `aes256` parameter to `true`, e.g., `reKey(newKey, aes256=true)`. After this, you should use the new key for all future loads of this instance.
* **Fix:** Resolved a crash that occurred when loading an empty file in `ReadOnly` mode.
* **Fix:** Added protection against the `weakly_canonical()` exception caused by an invalid file path.
* **Fix:** Fixed an issue where the file size could change during multi-process loading.
* **Fix:** Corrected a bug where a single key could be overridden incorrectly when upgrading from a v1.1.x version.
### iOS/macOS/watchOS
* **Change:** Dropped support for the **armv7k** architecture on Apple Watch. To adapt, you should add `armv7k` to the **"Excluded Architectures"** setting for your Apple Watch App or Extension target.
* **Important:** Do not upgrade to this version if you need to maintain `armv7k` support.
* **Change:** Dropped deprecated methods that use the `relativePath:` parameter. If you were using these methods, please migrate to those using the `rootPath:` parameter instead.
### POSIX
* **Change:** Dropped support for **armv7** architecture. **Do not** upgrade to this version if you need to maintain `armv7` support.
## v2.2.4 / 2025-09-25
This is a hotfix release mainly for iOS/macOS CocoaPods users.
### Changes for All platforms
* Improve the performance of MMBuffer a little bit in some cases.
### iOS/macOS
* Make MMKV and MMKVCore podspec define modules.
## v2.2.3 / 2025-08-20
This is a feature release that brings **full desktop support to Flutter**. It also includes key bug fixes and enhancements for Android and other platforms.
### Flutter
* **Added Full Desktop Support**: MMKV for Flutter now officially supports **Windows**, **macOS**, and **Linux**.
* **Example App**: The example application has been updated to run on all new desktop platforms.
### Android
* **Feature**: Added `MMKVHandler.getNativeLogHandler()` to allow native handling of MMKV logs from C++ code, improving performance.
* **Fix**: Corrected an integer overflow bug where a native memory address exceeding `Long.MAX_VALUE` was misinterpreted as a negative number.
* **Fix**: Prevented potential memory overflow by ensuring JNI local references are deleted after use.
* **Fix**: Fixed an issue where directories for special relative paths were not being created correctly.
* **Enhancement**: Improved the thread-safety of the callback handler to prevent crashes.
### iOS/macOS
* **Fix**: Resolved a build error on Apple platforms that occurred when the `MMKV_APPLE` flag was not set.
* **Fix**: Addressed a naming conflict in the Swift implementation.
### Windows
* **Fix**: Corrected character encoding issues in the Win32 demo by converting files to UTF-8.
## v2.2.2 / 2025-05-08
This is a hot fix version mainly **for Android/Linux platforms**. It’s highly recommended for v2.2.0~v2.2.1 users.
### Changes for All platforms
* Improve file lock consistency for Mayfly FD MMKV instances.
### Android
* Fix a potential ANR on multi-process mode MMKV init/creation.
### POSIX
* Fix a potential ANR on multi-process mode MMKV init/creation on Linux.
### iOS/macOS
* Retain the callback handler to prevent a potential short-lived callback handler from crashing.
### Win32
* Improve efficiency on MMKV instance init/creation.
## v2.2.1 / 2025-04-25
### Changes for All platforms
* Add `importFrom()`.
### Android
* Fix a bug on initialization.
## v2.2.0 / 2025-04-24
We introduce the **Mayfly FD** (short-lived file descriptor) enhancement, reducing MMKV's fd footprint by half and more. For a single-process mode MMKV instance, the fd footprint is reduced to zero (except Android/OHOS, details below). For a multi-process mode MMKV instance, the fd footprint is reduced by half, for we still need a long-lived fd to inter-process lock the shared memory.
### Changes for All platforms
* Add **Mayfly FD** (short-lived file descriptor) enhancement.
* Improve multi-process access efficiency by about 20%.
* Add `checkExist()` to check if a MMKV instance exists on disk.
* Drop deprecated armv7 AES hardware acceleration.
### Android
* Reduce the fd footprint by half. For a single-process mode MMKV instance, we still need a long-lived fd to support the **process-mode checker** and **legacy name upgrading**. In the far future, when all legacy names have been upgraded, we might drop the long-lived fd like other platforms.
* Upgrade Android compileSdk/targetSdk to 35, NDK to 28.1, Java to 11, Gradle to 8.11.1, and AGP to 8.9.2.
### HarmonyOS NEXT
* Reduce the fd footprint by half. For a single-process mode MMKV instance, we still need a long-lived fd to support the **legacy name upgrading**. In the far future, when all legacy names have been upgraded, we might drop the long-lived fd like other platforms.
* Drop `checkProcessMode()`, it’s never been used.
* Improve obfuscation configuration with relative path.
### iOS
* Add `+mmkvGroupPath` to get the group folder for MMKV multi-process storage.
### Flutter
* Add `isFileValid()`.
* Add `groupPath()` on iOS.
* Fix `checkContentChangedByOuterProcess()` not working bug.
## v2.1.0 / 2025-02-18
Happy Chinese New Year!
This is a **breaking change version for the Android/OHOS** platform. Read the change log bellow and upgrade carefully.
### Changes for All platforms
* Add the `NameSpace` feature that easily supports customizing a root directory.
* Add protection from bad disk records of MMKV files.
* Fix FileLock not being unlocked on destruction.
* Improve directory creation on `ReadOnly` mode.
### Android
* **Breaking change**: Migrate legacy MMKV in a custom directory to normal MMKV. Historically Android/OHOS mistakenly use mmapKey as mmapID, which will be problematic with the `NameSpace` feature. Starting from v2.1.0, MMKV will try to migrate them back to normal when possible.
It's highly recommended that you **upgrade to v2.0.2/v1.3.11 first** with **forward support** of normal MMKV in a custom directory.
* Supports using MMKV directly in C++ code.
* Improve inter-process locking by using `F_OFD_SETLK` instead of `F_SETLK`.
* Add *experimental* protection from bad disk records of MMKV files.
### HarmonyOS NEXT
* **Breaking change**: Migrate legacy MMKV in a custom directory to normal MMKV. Historically Android/OHOS mistakenly use mmapKey as mmapID, which will be problematic with the `NameSpace` feature. Starting from v2.1.0, MMKV will try to migrate them back to normal when possible.
It's highly recommended that you **upgrade to v2.0.2/v1.3.11 first** with **forward support** of normal MMKV in a custom directory.
* Supports using MMKV directly in C++ code.
* Improve inter-process locking by using `F_OFD_SETLK` instead of `F_SETLK`.
* Add *experimental* protection from bad disk records of MMKV files.
### iOS/macOS
* Upgrade to iOS 13, macOS 10.15, and watchOS 6 to support the `NameSpace` functionality.
* Supports using MMKV directly in C++ code.
* Drop the background `mlock()` protection given that we are iOS 13+.
* Add *tested* protection from bad disk records of MMKV files.
* Fix a package error when using MMKV by submodule.
### Flutter
* Remove unused imports and fix deprecated implementations.
* Support flutter 3.29.
### Win32
* Add *tested* protection from bad disk records of MMKV files.
### POSIX
* Improve inter-process locking by using `F_OFD_SETLK` instead of `F_SETLK`.
* Add *experimental* protection from bad disk records of MMKV files.
* Fix a compile error on the GNU toolchain.
### Golang
* Fix a link error in the armv8 arch.
* Improve multi-platform building.
## v2.0.2 / 2024-12-27
Mary holiday and a happy new year!
### Changes for All platforms
* Fix a bug that MMKV might fail to backup/restore across different filesystems.
* Add protection from invalid value size of auto-key-expire mmkv.
### Android
* If the running App is 32-bit only, warn about it (by throwing `UnsupportedArchitectureException`) before trying to load native lib.
* Add forward support for the correct filename with a custom root path.
### HarmonyOS NEXT
* Obfuscation fully supported.
* Use atomic file rename on OHOS.
* Add forward support for the correct filename with a custom root path.
### Win32
* Only `mmap()` on `ftruncate()`/`zeroFillFile()` failure iff we have a valid file mapping before.
## v2.0.1 / 2024-11-12
**This is a hotfix release.**
### Changes for All platforms
* Fix a bug that might cause MMKV to become dead-locked for other threads after decoding container-type values. The affected platforms and value types are listed below. So don't be surprised if you find no update on the unaffected platforms.
### HarmonyOS NEXT
* Fix a bug that MMKV might become dead-locked for other threads after `decodeStringSet()` / `decodeNumberSet` / `decodeBoolSet` or decoding `TypedArray`.
### Flutter
* Fix the bug on HarmonyOS NEXT listed above. A version named v2.0.1 was added to fix the Android version conflict between the LTS series & v2.0. Thanks to the federated plugins framework, only the underlying `mmkv_ohos` plugin is upgraded, the `mmkv` plugin stays the same.
### POSIX
* Fix a bug that MMKV might become dead-locked for other threads after decoding `std::vector` or `std::span` values.
## v2.0.0 / 2024-10-21
**This release is a breaking change release, especially for Android.**
### Changes for All platforms
* Add **readonly mode** support.
* Fix a compile error when `MMKV_DISABLE_CRYPT` is defined and MMKV is built by source in DEBUG.
### Android
* Support 16K page size for Android 15.
* Drop the support of **32-bit ABI**.
* Bump the mini **SDK level to API 23**.
### iOS / macOS
* Add Mac Catalyst support
### Flutter
* Add add log/error/content-change callback.
### HarmonyOS NEXT
* Add add log/error/content-change callback.
* Support obfuscation. For the time being, you will have to manually copy the content of MMKV's [consumer-rules.txt](https://github.com/Tencent/MMKV/blob/master/OpenHarmony/MMKV/consumer-rules.txt) into your App's obfuscation-rules.txt.
### Win32
* Replace `random()` with `rand()` to fix a compile error.
## v1.3.9 / 2024-07-26
**This is a Long Term Support (LTS) release.**
### Changes for All platforms
* Fix a data corruption bug on an encrypted MMKV with only one key value stored.
* Make encryption more resilient from brute force cracking.
* Fix a bug that `pthread_mutex` is not being destroyed correctly.
### Android
* Use an alternative way to get the process name to avoid potential App review issues.
* Upgrade to NDK 26.3.11579264.
### Flutter
* Add support for HarmonyOS NEXT. In fact, a temp version named v1.3.8 adds this support with the native lib of v1.3.7. To avoid potential confusion, bump both versions to v1.3.9.
## v1.3.7 / 2024-07-08
### Android & Flutter
**This Long Term Support (LTS) release** primarily reintroduces support for the ARMv7 architecture and lowers the minimum SDK version requirement to 21. Please note that only critical bug fixes will be applied to the 1.3.x series. New features will be introduced in version 2.0 and later, which will discontinue support for 32-bit architectures and raise the minimum SDK version requirement to 23.
### For other platforms
This is exactly the same as v1.3.6, so don't be surprised if you don't see this version in CocoaPods or OHPM.
## v1.3.6 / 2024-07-05
### Changes for All platforms
* The Core library now upgrades the C++ standard from C++17 to C++20 to make the most of morden C++ feature such as `Concept` & unordered containers with `std::string_view` as key. From now on, if you build MMKV by source (iOS, Windows, POSIX, etc), you will need a C++ compiler that supports the C++20 standard. If you only use MMKV in binary (Android, OHOS, etc), this upgrade of the C++ compiler is not required.
* The key type changes from `std::string` to `std::string_view`, to avoid unnecessary string construction and destruction when the key is a raw string.
### Android
* Use the latest ashmem API if possible.
* Use the latest API to get the device API level.
### Flutter
* MMKV will try to load libmmkv.so before Dart code, to reduce the error of loading library in Android.
### HarmonyOS NEXT
* Fix a bug that a `String` value might get truncated on encoding.
* MMKV returns `undefined` when a key does not exist, previously a default value of the type (`false` for `boolean`, `0` for `number`, etc) is returned.
* Add the feature to encode/decode a `float` value.
* Add the feature to encode/decode a `TypedArray` value.
* Support encoding a part of an `ArrayBuffer`.
### iOS/macOS
* Hide the default `NSObject.initialize()` from Swift/ObjC to prevent potential misuse.
### POSIX
* Support encode/decode `std::vector` or `std::span` values, with T as primitive types.
* Fix a compile error on Linux env.
* Fix a compile error on the GNU compiler.
* Fix a compile error with some old version of zlib (on CentOS).
### Python
* Python now runs on Windows. Check out the latest [wiki](https://github.com/Tencent/MMKV/wiki/python_setup) for instructions.
### Windows
* Support encode/decode `std::vector` or `std::span` values, with T as primitive types.
* Python now runs on Windows. Check out the latest [wiki](https://github.com/Tencent/MMKV/wiki/python_setup) for instructions.
## v1.3.5 / 2024-04-24
### HarmonyOS NEXT
* This is the first official support of HarmonyOS NEXT.
* Most things actually work!
* Checkout the [wiki](https://github.com/Tencent/MMKV/wiki/ohos_setup) for more.
### Flutter
* Migrate to federated plugins to avoid the iOS rename headache. From now on, no more renaming from `mmkv` to `mmkvflutter` is needed.
* Bump iOS Deployment Target to iOS 12.
* Bump Android minSdkVersion to 23.
### iOS & macOS
* Avoid using so-called privacy APIs (`lstat()`, `fstat()`, `NSUserDefaults`).
* Bump iOS Deployment Target to iOS 12.
### Android
* Bump minSdkVersion to 23.
* Drop armv7 & x86 support.
### POSIX
* Use the embedded libz when libz can not be found.
* Fix compile error when building with gcc.
### Windows
* Support x64 architecture.
## v1.3.4 / 2024-03-15
### Changes for All platforms
* Make `trim()` more robust in multi-process mode.
### iOS & macOS
* Support visionOS.
### POSIX
* Fix a compile error on `::unlink()`.
## v1.3.3 / 2024-01-25
### Changes for All platforms
* Add `removeStorage()` static method to safely delete underlying files of an MMKV instance.
* Add protection from a potential crash of a multi-process MMKV loading due to the MMKV file not being valid.
* Add back the lazy load feature. It was first introduced in v1.2.16. But it was rollbacked in v1.3.0 of potential ANR & file corruption. Now it's clear that the bug was caused by something else, it's time to bring it back.
* **Optimize loading speed** by using shared inter-process lock unless there's a need to truncate the file size, which is rare.
* Make these two lately added features **more robust**: customizing the initial file size & optimizing write speed when there's only one key inside MMKV.
### Android
* Fix a bug that `null` is returned when the value is in fact an empty ByteArray.
* Fix AGP >= 8 package namespace error.
* Fix the `FastNative` naming conflict.
* Upgrade to SDK 34.
* Upgrade `androidx.annotation` to v1.7.1.
### iOS & macOS
* On the Xcode 15 build, an App will crash on iOS 14 and below. Previously we have recommended some workarounds (check the v1.3.2 release note for details). Now you can use Xcode 15.1 to fix this issue.
* Fix a bug that the multi-process mode won't configure correctly. It was introduced in v1.3.2.
* Fix a macro naming conflict.
* Avoid using a so-called privacy API when creating temp files.
### POSIX
* Fix a compile error on `memcpy()`.
### Golang
* Fix a compile error when `MMKV_DISABLE_CRYPT` is on.
## v1.3.2 / 2023-11-20
Among most of the features added in this version, the credit goes to @kaitian521.
### Changes for All platforms
* Add the feature of customizing the **initial file size** of an MMKV instance.
* **Optimize write speed** when there's only one key inside MMKV, the new key is the same as the old one, and MMKV is in `SINGLE_PROCESS_MODE`.
* **Optimize write speed** by overriding from the beginning of the file instead of append in the back, when there's zero key inside MMKV, and MMKV is in `SINGLE_PROCESS_MODE`.
* Add the feature of `clearAll()` with keeping file disk space unchanged, **reducing the need to expand file size** on later insert & update operations. This feature is off by default, you will have to call it with relative params or newly added methods.
* Add the feature of **comparing values before setting/encoding** on the same key.
* Fix a potential bug that the MMKV file will be invalid state after a successful expansion but a failure `zeroFill()`, will lead to a crash.
* Fix a potential crash due to other module/static lib turn-off **RTTI**, which will cause MMKV to fail to catch `std::exception`.
* Fix several potential crash due to the MMKV file not being valid.
### Android
* Use the `-O2` optimization level by default, which will **reduce native lib size** and improve read/write speed a little bit.
* Experimantal use `@fastNative` annotation on `enableCompareBeforeCompare()` to speed up JNI call.
### iOS & macOS
* Optimize auto-clean logic to **reduce lock waiting time**.
* Turn-off mlock() protection in background on iOS 13+. We have **verified it on WeChat** that the protection is no longer needed from at least iOS 13. Maybe iOS 12 or older is also not needed, but we don't have the chance to verify that because WeChat no longer supports iOS 12.
#### Known Issue
* On Xcode 15 build, App will crash on iOS 14 and below. The bug is introduced by Apple's new linker. The official solutions provided by Apple are either:
* Drop the support of iOS 14.
* Add `-Wl,-weak_reference_mismatches,weak` or `-Wl,-ld_classic` options to the `OTHER_LDFLAGS` build setting of Xcode 15. Note that these options are **not recognized** by older versions of Xcode.
* Use older versions of Xcode, or **wait for Xcode 15.2**.
## v1.3.1 / 2023-8-11
This is a hotfix version. It's **highly recommended** that v1.2.16 & v1.3.0 users upgrade as soon as possible.
### Changes for All platforms
* Fix a critical bug that might cause multi-process MMKV corrupt. This bug was introduced in v1.2.16.
* Add the ability to filter expired keys on `count()` & `allKeys()` methods when auto key expiration is turn on.
* Reduce the `msync()` call on newly created MMKV instances.
### iOS & macOS
* Fix a bug that NSKeyedArchive object might fail to decode when auto key expiration is turn on.
## v1.3.0 / 2023-06-14
### Changes for All platforms
* Add auto key expiration feature. Note that this is a breaking change, once upgrade to auto expiration, the MMKV file is not valid for older versions of MMKV (v1.2.16 and below) to correctly operate.
* Roll back the lazy load optimization due to reported ANR issues. It was introduced in v1.2.16.
### iOS & macOS
* Fix a potential memory leak on setting a new value for an existing key.
* Upgrade min support target to iOS 11 / macOS 10.13 / tvOS 13 / watchOS 4.
### Windows
* Fix a bug that might fail to truncate the file size to a smaller size in some cases.
### Flutter
* The version of MMKV for Flutter is now the same as the MMKV native library.
* Starting from v1.3.0, Flutter for Android will use `com.tencent:mmkv`. Previously it's `com.tencent:mmkv-static`. It's the same as `com.tencent:mmkv` starting from v1.2.11.
## v1.2.16 / 2023-04-20
### Changes for All platforms
* Optimization: The actual file content is lazy loaded now, saving time on MMKV instance creation, and avoiding lock waiting when a lot of instances are created at the same time.
* Fix a bug when restoring a loaded MMKV instance the meta file might mistakenly report corrupted.
### Android
* Optimization: Remove unnecessary binder call on main process instantiation.
### Flutter
* Fix a crash on decoding an empty list.
* Remove deprecated dependence.
* Make the script more robust to fix the iOS Flutter plugin name.
### Windows
* Fix a string format bug on the MinGW64 environment.
### golang
* Fix a build error on 32-bit OS.
## v1.2.15 / 2023-01-12
### Changes for All platforms
* Log handler now handles all logs from the very beginning, especially the logs in initialization.
* Log handler register method is now deprecated. It's integrated with `initialize()`.
* Fix a bug that `lock()`/`unlock()`/`try_lock()` is not thread-safe.
### Flutter
* Reduce the privacy info needed to obtain android `sdkInt`, avoid unnecessary risk on Android App Review.
### iOS & macOS
* Fix a compile error on macOS.
* Fix a bug that some ObjC exceptions are not being caught.
* Add assert on nil MMKV base path, protect from mis-using MMKV in global variable initialization.
* Starting from v1.2.15, one must call `+[MMKV initializeMMKV:]` manually before calling any MMKV methods.
### golang
* Fix a compile error on GCC.
### Windows
* Support CMake project on Windows.
## v1.2.14 / 2022-08-10
### Changes for All platforms
* Fix a bug that `MMKV.getXXX()` may return invalid results in multi-process mode.
### Android
* Return `[]` instead of `null` on empty `StringSet` from `MMKV.decodeStringSet()` methods.
* Upgrade Android Compile & Target SDK to `32`.
### iOS
* Protect from the crash in `-[MMKV getObject:forKey:]` method when the key-value doesn't exist.
## v1.2.13 / 2022-03-30
### Android
* Fix crash on using Ashmem while `MMKV_DISABLE_CRYPT` macro is defined.
### iOS
* Add ability to retrieve key existece while getting value, aka `-[MMKV getXXX:forKey:hasValue:]` methods.
### POSIX
* Add ability to retrieve key existece while getting value, aka `MMKV::getXXX(key, defaultValue, hasValue)` methods.
### Windows
* Add ability to retrieve key existece while getting value, aka `MMKV::getXXX(key, defaultValue, hasValue)` methods.
## v1.2.12 / 2022-01-17
### Changes for All platforms
* Fix a bug that a subsequential `clearAll()` call may fail to take effect in multi-process mode.
* Hide some OpenSSL symbols to prevent link-time symbol conflict, when an App somehow also static linking OpenSSL.
### Android
* Upgrade `compileSdkVersion` & `targetSdkVersion` from `30` to `31`.
## v1.2.11 / 2021-10-26
### Android
* Due to increasing report about crash inside STL, we have decided to make MMKV **static linking** `libc++` **by default**. Starting from v1.2.11, `com.tencent:mmkv-static` is the same as `com.tencent:mmkv`.
* For those still in need of MMKV with shared linking of `libc++_shared`, you could use `com.tencent:mmkv-shared` instead.
* Add backup & restore ability.
### iOS / macOS
* Add backup & restore ability.
* Support tvOS.
* Fix a compile error on some old Xcode.
### Flutter (v1.2.12)
* Add backup & restore ability.
### POSIX / golang / Python
* Add backup & restore ability.
* Fix a compile error on Gentoo.
### Windows
* Add backup & restore ability.
## v1.2.10 / 2021-06-25
This version is mainly for Android & Flutter.
### Android
* Complete **JavaDoc documentation** for all public methods, classes, and interfaces. From now on, you can find the [API reference online](https://javadoc.io/doc/com.tencent/mmkv).
* Drop the support of **armeabi** arch. Due to some local build cache mistake, the last version (v1.2.9) of MMKV still has an unstripped armeabi arch inside. This is fixed.
* Change `MMKV.mmkvWithID()` from returning `null` to throwing exceptions on any error.
* Add `MMKV.actualSize()` to get the actual used size of the file.
* Mark `MMKV.commit()` & `MMKV.apply()` as deprecated, to avoid some misuse after migration from SharedPreferences to MMKV.
### Flutter (v1.2.11)
* Bug Fixed: When building on iOS, occasionally it will fail on symbol conflict with other libs. We have renamed all public native methods to avoid potential conflict.
* Keep up with MMKV native lib v1.2.10.
## v1.2.9 / 2021-05-26
This version is mainly for Android & Flutter.
### Android
* Drop the support of **armeabi** arch. As has been mention in the last release, to avoid some crashes on the old NDK (r16b), and make the most of a more stable `libc++`, we have decided to upgrade MMKV's building NDK in this release. That means we can't support **armeabi** anymore. Those who still in need of armeabi can **build from sources** by following the [instruction in the wiki](https://github.com/Tencent/MMKV/wiki/android_setup).
We really appreciate your understanding.
### Flutter (v1.2.10)
* Bug Fixed: When calling `MMKV.encodeString()` with an empty string value on Android, `MMKV.decodeString()` will return `null`.
* Bug Fixed: After **upgrading** from Flutter 1.20+ to 2.0+, calling `MMKV.defaultMMKV()` on Android might fail to load, you can try calling `MMKV.defaultMMKV(cryptKey: '\u{2}U')` with an **encrytion key** '\u{2}U' instead.
* Keep up with MMKV native lib v1.2.9.
## v1.2.8 / 2021-05-06
This will be the last version that supports **armeabi arch** on Android. To avoid some crashes on the old NDK (r16b), and make the most of a more stable `libc++`, we have decided to upgrade MMKV's building NDK in the next release. That means we can't support **armeabi** anymore.
We really appreciate your understanding.
### Android
* Migrate MMKV to Maven Central Repository. For versions older than v1.2.7 (including), they are still available on JCenter.
* Add `MMKV.disableProcessModeChecker()`. There are some native crash reports due to the process mode checker. You can disable it manually.
* For the same reason described above (native crashed), MMKV will now turn off the process mode checker on a non-debuggable app (aka, a release build).
* For MMKV to detect whether the app is debuggable or not, when calling `MMKV.initialize()` to customize the root directory, a `context` parameter is required now.
### iOS / macOS
* Min iOS support has been **upgrade to iOS 9**.
* Support building by Xcode 12.
### Flutter (v1.2.9)
* Support null-safety.
* Upgrade to flutter 2.0.
* Fix a crash on the iOS when calling `encodeString()` with an empty string value.
**Known Issue on Flutter**
* When calling `encodeString()` with an empty string value on Android, `decodeString()` will return `null`. This bug will be fixed in the next version of Android Native Lib. iOS does not have such a bug.
### Windows
* Fix a compile error on Visual Studio 2019.
## v1.2.7 / 2020-12-25
Happy holidays everyone!
### Changes for All platforms
* Fix a bug when calling `sync()` with `false ` won't do `msync()` asynchronous and won't return immediately.
### Android
* Fix an null pointer exception when calling `putStringSet()` with `null`.
* Complete review of all MMKV methods about Java nullable/nonnull annotation.
* Add API for `MMKV.initialize()` with both `Context` and `LibLoader` parammeters.
### Flutter (v1.2.8)
* Fix a crash on the iOS simulator when accessing the default MMKV instance.
* Fix a bug on iOS when initing the default MMKV instance with a crypt key, the instance is still in plaintext.
### Golang
Add golang for POSIX platforms. Most things actually work!. Check out the [wiki](https://github.com/Tencent/MMKV/wiki/golang_setup) for information.
## v1.2.6 / 2020-11-27
### Changes for All platforms
* Fix a file corruption when calling `reKey()` after `removeKeys()` has just been called.
### Android
* Fix compile error when `MMKV_DISABLE_CRYPT` is set.
* Add a preprocess directive `MMKV_DISABLE_FLUTTER` to disable flutter plugin features. If you integrate MMKV by source code, and if you are pretty sure the flutter plugin is not needed, you can turn that off to save some binary size.
### Flutter (v1.2.7)
Add MMKV support for **Flutter** on iOS & Android platform. Most things actually work!
Check out the [wiki](https://github.com/Tencent/MMKV/wiki/flutter_setup) for more info.
## v1.2.5 / 2020-11-13
This is a pre-version for Flutter. The official Flutter plugin of MMKV will come out soon. Stay Tune!
### iOS / macOS
* Fix an assert error of encrypted MMKV when encoding some `` objects.
* Fix a potential leak when decoding duplicated keys from the file.
* Add `+[MMKV pageSize]`, `+[MMKV version]` methods.
* Add `+[MMKV defaultMMKVWithCryptKey:]`, you can encrypt the default MMKV instance now, just like the Android users who already enjoy this for a long time.
* Rename `-[MMKV getValueSizeForKey:]` to `-[MMKV getValueSizeForKey: actualSize:]` to align with Android interface.
### Android
* Fix a potential crash when getting MMKV instances in multi-thread at the same time.
* Add `MMKV.version()` method.
## v1.2.4 / 2020-10-21
This is a hotfix mainly for iOS.
### iOS / macOS
* Fix a decode error of encrypted MMKV on some devices.
### Android
* Fix a potential issue on checking `rootDir` in multi-thread while MMKV initialization is not finished.
## v1.2.3 / 2020-10-16
### Changes for All platforms
* Fix a potential crash on 32-bit devices due to pointer alignment issue.
* Fix a decode error of encrypted MMKV on some 32-bit devices.
### iOS / macOS
* Fix a potential `crc32()` crash on some kind of arm64 devices.
* Fix a potential crash after calling `+[MMKV onAppTerminate]`.
### Android
* Fix a rare lock conflict on `checkProcessMode()`.
### POSIX
Add MMKV support for **Python** on POSIX platforms. Most things actually work!
Check out the [wiki](https://github.com/Tencent/MMKV/wiki/python_setup) for more info.
## v1.2.2 / 2020-07-30
### iOS / macOS
* Add auto clean up feature. Call `+[MMKV enableAutoCleanUp:]` to enable auto cleanup MMKV instances that not been accessed recently.
* Fix a potential crash on devices under iPhone X.
### Android
* Add multi-process mode check. After so many issues had been created due to mistakenly using MMKV in multi-process mode in Android, this check is added. If an MMKV instance is accessed with `SINGLE_PROCESS_MODE` in multi-process, an `IllegalArgumentException` will be thrown.
### POSIX
* Add support for armv7 & arm64 arch on Linux.
## v1.2.1 / 2020-07-03
This is a hotfix release. Anyone who has upgraded to v1.2.0 should upgrade to this version **immediately**.
* Fix a potential file corruption bug when writing a file that was created in versions older than v1.2.0. This bug was introduced in v1.2.0.
* Add a preprocess directive `MMKV_DISABLE_CRYPT` to turn off MMKV encryption ability once and for all. If you integrate MMKV by source code, and if you are pretty sure encryption is not needed, you can turn that off to save some binary size.
* The parameter `relativePath` (customizing a separate folder for an MMKV instance), has been renamed to `rootPath`. Making it clear that an absolute path is expected for that parameter.
### iOS / macOS
* `-[MMKV mmkvWithID: relativePath:]` is deprecated. Use `-[MMKV mmkvWithID: rootPath:]` instead.
* Likewise, `-[MMKV mmkvWithID: cryptKey: relativePath:]` is deprecated. Use `-[MMKV mmkvWithID: cryptKey: rootPath:]` instead.
## v1.2.0 / 2020-06-30
This is the second **major version** of MMKV. Everything you call is the same as the last version, while almost everything underneath has been improved.
* **Reduce Memory Footprint**. We used to cache all key-values in a dictionary for efficiency. From now on we store the offset of each key-value inside the mmap-memory instead, **reducing memory footprint by almost half** for (non-encrypt) MMKV. And the accessing efficiency is almost the same as before. As for encrypted MMKV, we can't simply store the offset of each key-value, the relative encrypt info needs to be stored as well. That will be too much for small key-values. We only store such info for large key-values (larger than 256B).
* **Improve Writeback Efficiency**. Thanks to the optimization above, we now can implement writeback by simply calling **memmove()** multiple times. Efficiency is increased and memory consumption is down.
* **Optimize Small Key-Values**. Small key-values of encrypted MMKV are still cached in memory, as all the old versions did. From now on, the struct `MMBuffer` will try to **store small values in the stack** instead of in the heap, saving a lot of time from `malloc()` & `free()`. In fact, all primitive types will be store in the stack memory.
All of the improvements above are available to all supported platforms. Here are the additional changes for each platform.
### iOS / macOS
* **Optimize insert & delete**. Especially for inserting new values to **existing keys**, or deleting keys. We now use the UTF-8 encoded keys in the mmap-memory instead of live encoding from keys, cutting the cost of string encoding conversion.
* Fix Xcode compile error on some projects.
* Drop the support of iOS 8. `thread_local` is not available on iOS 8. We choose to drop support instead of working around because iOS 8's market share is considerably small.
### POSIX
* It's known that GCC before 5.0 doesn't support C++17 standard very well. You should upgrade to the latest version of GCC to compile MMKV.
## v1.1.2 / 2020-05-29
### Android / iOS & macOS / Windows / POSIX
* Fix a potential crash after `trim()` a multi-process MMKV instance.
* Improve `clearAll()` a bit.
## v1.1.1 / 2020-04-13
### iOS / macOS
* Support WatchOS.
* Rename `+[MMKV onExit]` to `+[MMKV onAppTerminate]`, to avoid naming conflict with some other OpenSource projects.
* Make background write protection much more robust, fix a potential crash when writing meta info in background.
* Fix a potential data corruption bug when writing a UTF-8 (non-ASCII) key.
### Android
* Fix a crash in the demo project when the App is hot reloaded.
* Improve wiki & readme to recommend users to init & destruct MMKV in the `Application` class instead of the `MainActivity` class.
### POSIX
* Fix two compile errors with some compilers.
## v1.1.0 / 2020-03-24
This is the first **major breaking version** ever since MMKV was made public in September 2018, introducing bunches of improvement. Due to the Covid-19, it has been delayed for about a month. Now it's finally here!
* **Improved File Recovery Strategic**. We store the CRC checksum & actual file size on each sync operation & full write back, plus storing the actual file size in the same file(aka the .crc meta file) as the CRC checksum. Base on our usage inside WeChat on the iOS platform, it cuts the file **corruption rate down by almost half**.
* **Unified Core Library**. We refactor the whole MMKV project and unify the cross-platform Core library. From now on, MMKV on iOS/macOS, Android, Windows all **share the same core logic code**. It brings many benefits such as reducing the work to fix common bugs, improvements on one platform are available to other platforms immediately, and much more.
* **Supports POSIX Platforms**. Thanks to the unified Core library, we port MMKV to POSIX platforms easily.
* **Multi-Process Access on iOS/macOS**. Thanks to the unified Core library, we add multi-process access to iOS/macOS platforms easily.
* **Efficiency Improvement**. We make the most of armv8 ability including the AES & CRC32 instructions to tune **encryption & error checking speed up by one order higher** than before on armv8 devices. There are bunches of other speed tuning all around the whole project.
Here are the old-style change logs of each platform.
### iOS / macOS
* Adds **multi-process access** support. You should initialize MMKV by calling `+[MMKV initializeMMKV: groupDir: logLevel:]`, passing your **app group id**. Then you can get a multi-process instance by calling `+[MMKV mmkvWithID: mode:]` or `+[MMKV mmkvWithID: cryptKey: mode:]`, accessing it cross your app & your app extensions.
* Add **inter-process content change notification**. You can get MMKV changes notification of other processes by implementing `- onMMKVContentChange:` of `` protocol.
* **Improved File Recovery Strategic**. Cuts the file corruption rate down by almost half. Details are above.
* **Efficiency Improvement**. Encryption & error checking speed are up by one order higher on armv8 devices(aka iDevice including iPhone 5S and above). Encryption on armv7 devices is improved as well. Details are ahead.
* Other speed improvements. Refactor core logic using **MRC**, improve std::vector `push_back()` speed by using **move constructors** & move assignments.
* `+[MMKV setMMKVBasePath:]` & `+[MMKV setLogLevel:]` are marked **deprecated**. You should use `+[MMKV initializeMMKV:]` or `+[MMKV initializeMMKV: logLevel:]` instead.
* The `MMKVLogLevel` enum has been improved in Swift. It can be used like `MMKVLogLevel.info` and so on.
### Android
* **Improved File Recovery Strategic**. Cuts the file corruption rate down by almost half. Details are above.
* **Efficiency Improvement**. Encryption & error checking speed are up by one order higher on armv8 devices with the `arm64-v8a` abi. Encryption on `armeabi` & `armeabi-v7a` is improved as well. Details are ahead.
* Add exception inside core encode & decode logic, making MMKV much more robust.
* Other speed improvements. Improve std::vector `push_back()` speed by using **move constructors** & move assignments.
### Windows
* **Improved File Recovery Strategic**. Cuts the file corruption rate down by almost half. Details are above.
* Add exception inside core encode & decode logic, making MMKV much more robust.
* Other speed improvements. Improve std::vector `push_back()` speed by using **move constructors** & move assignments.
### POSIX
* Most things actually work! We have tested MMKV on the latest version of Linux(Ubuntu, Arch Linux, CentOS, Gentoo), and Unix(macOS, FreeBSD, OpenBSD) on the time v1.1.0 is released.
## v1.0.24 / 2020-01-16
### iOS / macOS
What's new
* Fix a bug that MMKV will fail to save any key-values after calling `-[MMKV clearMemoryCache]` and then `-[MMKV clearAll]`.
* Add `+[MMKV initializeMMKV:]` for users to init MMKV in the main thread, to avoid an iOS 13 potential crash when accessing `UIApplicationState` in child threads.
* Fix a potential crash when writing a uniquely constructed string.
* Fix a performance slow down when acquiring MMKV instances too often.
* Make the baseline test in MMKVDemo more robust to NSUserDefaults' caches.
### Android
What's new
* Fix `flock()` bug on ashmem files in Android.
* Fix a potential crash when writing a uniquely constructed string.
* Fix a bug that the MMKVDemo might crash when running in a simulator.
### Windows
* Fix a potential crash when writing a uniquely constructed string or data.
## v1.0.23 / 2019-09-03
### iOS / macOS
What's new
* Fix a potential security leak on encrypted MMKV.
### Android
What's new
* Fix a potential security leak on encrypted MMKV.
* Fix filename bug when compiled on Windows environment.
* Add option for decoding String Set into other `Set<>` classes other than the default `HashSet`, check `decodeStringSet()` for details.
* Add `putBytes()` & `getBytes()`, to make function names more clear and consistent.
* Add notification of content changed by other process, check the new `MMKVContentChangeNotification<>` interface & `checkContentChangedByOuterProcess()` for details.
### Windows
What's new
* Fix a potential security leak on encrypted MMKV.
* Fix `CriticalSection` init bug.
## v1.0.22 / 2019-06-10
### iOS / macOS
What's new
* Fix a bug that MMKV will corrupt while adding just one key-value, and reboot or clear memory cache. This bug was introduced in v1.0.21.
### Android
What's new
* Fix a bug that MMKV will corrupt while adding just one key-value, and reboot or clear memory cache. This bug was introduced in v1.0.21.
### Windows
What's new
* Fix a bug that MMKV will corrupt while adding just one key-value, and reboot or clear memory cache. This bug was introduced in v1.0.21.
## v1.0.21 / 2019-06-06
### iOS / macOS
What's new
* Fix a bug that MMKV might corrupt while repeatedly adding & removing key-value with specific length. This bug was introduced in v1.0.20.
### Android
What's new
* Fix a bug that MMKV might corrupt while repeatedly adding & removing key-value with specific length. This bug was introduced in v1.0.20.
### Windows
What's new
* Fix a bug that MMKV might corrupt while repeatedly adding & removing key-value with specific length. This bug was introduced in v1.0.20.
## v1.0.20 / 2019-06-05
### iOS / macOS
What's new
* Fix a bug that MMKV might crash while storing key-value with specific length.
* Fix a bug that `-[MMKV trim]` might not work properly.
### Android
What's new
* Migrate to AndroidX library.
* Fix a bug that MMKV might crash while storing key-value with specific length.
* Fix a bug that `trim()` might not work properly.
* Fix a bug that dead-lock might be reported by Android mistakenly.
* Using `RegisterNatives()` to simplify native method naming.
### Windows
* Fix a bug that MMKV might crash while storing key-value with specific length.
* Fix a bug that `trim()` might not work properly.
* Fix a bug that `clearAll()` might not work properly.
## v1.0.19 / 2019-04-22
### iOS / macOS
What's new
* Support Swift 5.
* Add method to get all keys `-[MMKV allKeys]`;
* Add method to synchronize to file asynchronously `-[MMKV async]`.
* Fix a pod configuration bug that might override target project's C++ setting on `CLANG_CXX_LANGUAGE_STANDARD`.
* Fix a bug that `DEFAULT_MMAP_SIZE` might not be initialized before getting any MMKV instance.
* Fix a bug that openssl's header files included inside MMKV might mess with target project's own openssl implementation.
### Android
What's new
* Support Android Q.
* Add method to synchronize to file asynchronously `void sync()`, or `void apply()` that comes with `SharedPreferences.Editor` interface.
* Fix a bug that a buffer with length of zero might be returned when the key is not existed.
* Fix a bug that `DEFAULT_MMAP_SIZE` might not be initialized before getting any MMKV instance.
## v1.0.18 / 2019-03-14
### iOS / macOS
What's new
* Fix a bug that defaultValue was not returned while decoding a `NSCoding` value.
* Fix a compile error on static linking MMKV while openssl is static linked too.
### Android
What's new
* Introducing **Native Buffer**. Checkout [wiki](https://github.com/Tencent/MMKV/wiki/android_advance#native-buffer) for details.
* Fix a potential crash when trying to recover data from file length error.
* Protect from mistakenly passing `Context.MODE_MULTI_PROCESS` to init MMKV.
### Windows
* Fix a potential crash when trying to recover data from file length error.
## v1.0.17 / 2019-01-25
### iOS / macOS
What's new
* Redirect logging of MMKV is supported now.
* Dynamically disable logging of MMKV is supported now.
* Add method `migrateFromUserDefaults ` to import from NSUserDefaults.
### Android
What's new
* Redirect logging of MMKV is supported now.
* Dynamically disable logging of MMKV is supported now.
Note: These two are breaking changes for interface `MMKVHandler`, update your implementation with `wantLogRedirecting()` & `mmkvLog()` for v1.0.17. (Interface with default method requires API level 24, sigh...)
* Add option to use custom library loader `initialize(String rootDir, LibLoader loader)`. If you're facing `System.loadLibrary()` crash on some low API level device, consider using **ReLinker** to load MMKV. Example can be found in **mmkvdemo**.
* Fix a potential corruption of meta file on multi-process mode.
* Fix a potential crash when the meta file is not valid on multi-process mode.
### Windows
* Redirect logging of MMKV is supported now.
* Dynamically disable logging of MMKV is supported now.
* Fix a potential corruption of meta file on multi-process mode.
## v1.0.16 / 2019-01-04
### iOS / macOS
What's new
* Customizing root folder of MMKV is supported now.
* Customizing folder for specific MMKV is supported now.
* Add method `getValueSizeForKey:` to get value's size of a key.
### Android
What's new
* Customizing root folder of MMKV is supported now.
* Customizing folder for specific MMKV is supported now.
* Add method `getValueSizeForKey()` to get value's size of a key.
* Fix a potential crash when the meta file is not valid.
### Windows
MMKV for Windows is released now. Most things actually work!
## v1.0.15 / 2018-12-13
### iOS / macOS
What's new
* Storing **NSString/NSData/NSDate** directly by calling `setString`/`getSring`, `setData`/`getData`, `setDate`/`getDate`.
* Fix a potential crash due to divided by zero.
### Android
What's new
* Fix a stack overflow crash due to the **callback** feature introduced by v1.0.13.
* Fix a potential crash due to divided by zero.
### Windows
MMKV for Windows in under construction. Hopefully will come out in next release. For those who are interested, check out branch `dev_Windows` for the latest development.
## v1.0.14 / 2018-11-30
### iOS / macOS
What's new
* Setting `nil` value to reset a key is supported now.
* Rename `boolValue(forKey:)` to `bool(forKey:)` for Swift.
### Android
What's new
* `Parcelable` objects can be stored directly into MMKV now.
* Setting `null` value to reset a key is supported now.
* Fix an issue that MMKV's file size might expand unexpectly large in some case.
* Fix an issue that MMKV might crash on multi-thread removing and getting on the same key.
## v1.0.13 / 2018-11-09
### iOS / macOS
What's new
* Special chars like `/` are supported in MMKV now. The file name of MMKV with special mmapID will be encoded with md5 and stored in seperate folder.
* Add **callback** for MMKV error handling. You can make MMKV to recover instead of discard when crc32 check fail happens.
* Add `trim` and `close` operation. Generally speaking they are not necessary in daily usage. Use them if you worry about disk / memory / fd usage.
* Fix an issue that MMKV's file size might expand unexpectly large in some case.
Known Issues
* Setting `nil` value to reset a key will be ignored. Use `remove` instead.
### Android
What's new
* Add static linked of libc++ to trim AAR size. Use it when there's no other lib in your App embeds `libc++_shared.so`. Or if you already have an older version of `libc++_shared.so` that doesn't agree with MMKV.
Add `implementation 'com.tencent:mmkv-static:1.0.13'` to your App's gradle setting to integrate.
* Special chars like `/` are supported in MMKV now. The file name of MMKV with special mmapID will be encoded with md5 and stored in seperate folder.
* Add **callback** for MMKV error handling. You can make MMKV to recover instead of discard when crc32 check fail happens.
* Add `trim` and `close` operation. Generally speaking they are not necessary in daily usage. Use them if you worry about disk / memory / fd usage.
Known Issues
* Setting `null` value to reset a key will be ignored. Use `remove` instead.
* MMKV's file size might expand unexpectly large in some case.
## v1.0.12 / 2018-10-18
### iOS / macOS
What's new
* Fix `mlock` fail on some devices
* Fix a performance issue caused by mistakenly merge of test code
* Fix CocoaPods integration error of **macOS**
### Android / 2018-10-24
What's new
* Fix `remove()` causing data inconsistency on `MULTI_PROCESS_MODE`
## v1.0.11 / 2018-10-12
### iOS / macOS
What's new
* Port to **macOS**
* Support **NSCoding**
You can store NSArray/NSDictionary or any object what implements `` protocol.
* Redesign Swift interface
* Some performance improvement
Known Issues
* MMKV use mmapID as its filename, so don't contain any `/` inside mmapID.
* Storing a value of `type A` and getting by `type B` may not work. MMKV does type erasure while storing values. That means it's hard for MMKV to do value-type-checking, if not impossible.
### Android
What's new
* Some performance improvement
Known Issues
* Getting an MMKV instance with mmapID that contains `/` may fail.
MMKV uses mmapID as its filename, so don't contain any `/` inside mmapID.
* Storing a value of `type A` and getting by `type B` may not work.
MMKV does type erasure while storing values. That means it's hard for MMKV to do value-type-checking, if not impossible.
* `registerOnSharedPreferenceChangeListener` not supported.
This is intended. We believe doing data-change-listener inside a storage framework smells really bad to us. We suggest using something like event-bus to notify any interesting clients.
## v1.0.10 / 2018-09-21
* Initial Release
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at guoling@tencent.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to MMKV
Welcome to [report Issues](https://github.com/Tencent/MMKV/issues) or [pull requests](https://github.com/Tencent/MMKV/pulls). It's recommended to read the following Contributing Guide first before contributing.
## Issues
We use issues to track public bugs and feature requests.
### Search Known Issues First
Please search the existing issues to see if any similar issue or feature request has already been filed. You should make sure your issue isn't redundant.
### Reporting New Issues
If you open an issue, the more information the better. Such as detailed description, screenshot or video of your problem, logcat or code blocks for your crash.
## Pull Requests
We strongly welcome your pull request to make MMKV better.
### Branch Management
There are three main branches here:
1. `master` branch.
1. It is the latest (pre-)release branch. We use `master` for tags, with version number `1.1.0`, `1.2.0`, `1.3.0`...
2. **Don't submit any PR on `master` branch.**
2. `dev` branch.
1. It is our stable developing branch. After full testing, `dev` will be merged to `master` branch for the next release.
2. **You are recommended to submit bugfix or feature PR on `dev` branch.**
3. `hotfix` branch.
1. It is the latest tag version for hot fix. If we accept your pull request, we may just tag with version number `1.1.1`, `1.2.3`.
2. **Only submit urgent PR on `hotfix` branch for next specific release.**
Normal bugfix or feature request should be submitted to `dev` branch. After full testing, we will merge them to `master` branch for the next release.
If you have some urgent bugfixes on a published version, but the `master` branch have already far away with the latest tag version, you can submit a PR on hotfix. And it will be cherry picked to `dev` branch if it is possible.
```
master
↑
dev <--- hotfix PR
↑
feature/bugfix PR
```
### Make Pull Requests
The code team will monitor all pull request, we run some code check and test on it. After all tests passed, we will accecpt this PR. But it won't merge to `master` branch at once, which have some delay.
Before submitting a pull request, please make sure the followings are done:
1. Fork the repo and create your branch from `master` or `hotfix`.
2. Update code or documentation if you have changed APIs.
3. Add the copyright notice to the top of any new files you've added.
4. Check your code lints and checkstyles.
5. Test and test again your code.
6. Now, you can submit your pull request on `dev` or `hotfix` branch.
## Code Style Guide
We choose the `LLVM code style` for MMKV project, with the specialization that using 4 space width for indent, and using tab for ObjC indentation. To make things simple, we have already defined our code style inside [clang-format](./.clang-format).
You can just run `make format_code` on top directory to format all your changes before committing them.
Additionly, check out [Code Style](./Android/MMKV/checkstyle.xml) for Java and Android.
## License
By contributing to MMKV, you agree that your contributions will be licensed
under its [BSD LICENSE](./LICENSE.txt)
================================================
FILE: Core/.gitignore
================================================
include/
================================================
FILE: Core/CMakeLists.txt
================================================
#
# Tencent is pleased to support the open source community by making
# MMKV available.
#
# Copyright (C) 2019 THL A29 Limited, a Tencent company.
# All rights reserved.
#
# Licensed under the BSD 3-Clause License (the "License"); you may not use
# this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://opensource.org/licenses/BSD-3-Clause
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.0)
IF(APPLE)
# tell ranlib to ignore empty compilation units
SET(CMAKE_C_ARCHIVE_FINISH " -no_warning_for_no_symbols -c ")
SET(CMAKE_CXX_ARCHIVE_FINISH " -no_warning_for_no_symbols -c ")
# prevents ar from invoking ranlib, let CMake do it
SET(CMAKE_C_ARCHIVE_CREATE " qc -S ")
SET(CMAKE_CXX_ARCHIVE_CREATE " qc -S