diff --git a/Android/MMKV/mmkv/src/androidTest/java/com/tencent/mmkv/MMKVTest.java b/Android/MMKV/mmkv/src/androidTest/java/com/tencent/mmkv/MMKVTest.java index 8a9d4a31..e23a766b 100644 --- a/Android/MMKV/mmkv/src/androidTest/java/com/tencent/mmkv/MMKVTest.java +++ b/Android/MMKV/mmkv/src/androidTest/java/com/tencent/mmkv/MMKVTest.java @@ -281,6 +281,22 @@ public class MMKVTest { kv.clearAll(); } + @Test + public void testCloseIsIdempotentAndAllowsReopen() { + String mmapID = "closeLifecycleTest"; + MMKV kv = MMKV.mmkvWithID(mmapID); + assertTrue(kv.encode("value", true)); + + kv.close(); + kv.close(); + assertFalse(kv.encode("afterClose", true)); + + MMKV reopened = MMKV.mmkvWithID(mmapID); + assertTrue(reopened.decodeBool("value")); + reopened.clearAll(); + reopened.close(); + } + @Test public void testIPCUpdateInt() { MMKV mmkv = MMKV.mmkvWithID(MMKVTestService.SharedMMKVID, MMKV.MULTI_PROCESS_MODE); diff --git a/Android/MMKV/mmkv/src/main/cpp/native-bridge.cpp b/Android/MMKV/mmkv/src/main/cpp/native-bridge.cpp index 5c1de549..af1710b0 100644 --- a/Android/MMKV/mmkv/src/main/cpp/native-bridge.cpp +++ b/Android/MMKV/mmkv/src/main/cpp/native-bridge.cpp @@ -976,11 +976,10 @@ MMKV_JNI void trim(JNIEnv *env, jobject instance) { } } -MMKV_JNI void close(JNIEnv *env, jobject instance) { - MMKV *kv = getMMKV(env, instance); +MMKV_JNI void close(JNIEnv *env, jclass, jlong handle) { + MMKV *kv = reinterpret_cast(handle); if (kv) { kv->close(); - env->SetLongField(instance, g_fileID, 0); } } @@ -1240,7 +1239,7 @@ static JNINativeMethod g_methods[] = { {"removeValuesForKeys", "([Ljava/lang/String;)V", (void *) mmkv::removeValuesForKeys}, {"clearAll", "()V", (void *) mmkv::clearAll}, {"trim", "()V", (void *) mmkv::trim}, - {"close", "()V", (void *) mmkv::close}, + {"close", "(J)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}, diff --git a/Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKV.java b/Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKV.java index 74bee096..8f40439e 100644 --- a/Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKV.java +++ b/Android/MMKV/mmkv/src/main/java/com/tencent/mmkv/MMKV.java @@ -50,7 +50,7 @@ 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 { +public class MMKV implements SharedPreferences, SharedPreferences.Editor, AutoCloseable { private static final EnumMap recoverIndex; private static final EnumMap logLevel2Index; @@ -1329,10 +1329,27 @@ public class MMKV implements SharedPreferences, SharedPreferences.Editor { } /** - * 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. + * Permanently close the underlying native MMKV instance. + *

+ * This is a terminal, idempotent operation on this wrapper. It invalidates every Java + * wrapper/reference backed by the same native instance. Make sure no operation is running, + * discard all aliases, and only then reopen the same ID. Calls on this wrapper after close + * have no effect or return the method's empty/default value. */ - public native void close(); + @Override + public void close() { + synchronized (this) { + long handle = nativeHandle; + if (handle == 0) { + return; + } + close(handle); + nativeHandle = 0; + synchronized (checkedHandleSet) { + checkedHandleSet.remove(handle); + } + } + } /** * Clear memory cache of the MMKV instance. @@ -1997,7 +2014,7 @@ public class MMKV implements SharedPreferences, SharedPreferences.Editor { public native boolean isReadOnly(); // jni - private final long nativeHandle; + private volatile long nativeHandle; private MMKV(long handle) { nativeHandle = handle; @@ -2005,6 +2022,8 @@ public class MMKV implements SharedPreferences, SharedPreferences.Editor { private static native void jniInitialize(String rootDir, String cacheDir, int level, boolean wantLogReDirecting, boolean hasCallback, long nativeHandler); + private static native void close(long handle); + native static long getMMKVWithID(String mmapID, int mode, @Nullable String cryptKey, @Nullable String rootPath, long expectedCapacity, boolean aes256, int enableKeyExpire, int expiredInSeconds, diff --git a/CHANGELOG.md b/CHANGELOG.md index 4239a1af..aa211c22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ This is a bugfix-oriented release based on v2.4.0. It focuses on data-safety fixes, encrypted-file compatibility, packaging/build cleanup, and ready-to-use Kotlin Multiplatform support for Android and iOS. ### Changes for All Platforms +* **Fix:** Fixed an iOS/macOS Objective-C ownership handoff race where auto cleanup could release an MMKV wrapper before an ARC caller retained the returned instance. +* **Change:** Hardened and clarified `close()` as a terminal lifecycle operation across public APIs. Managed raw-handle wrappers now make repeated close safe and clear their handle; closing invalidates every alias backed by the same native instance, while raw C++/C/Python/Go handles explicitly require exclusive ownership. Android and KMP MMKV wrappers now implement `AutoCloseable`. * **Fix:** Fixed an oversized-key corruption bug where keys longer than the internal `uint16_t` holder limit could overflow key/value metadata and corrupt subsequent reads. * **Fix:** Fixed an `expireDuration` overflow bug ([#1665](https://github.com/tencent/mmkv/issues/1665)). Very large expiration durations are now clamped safely instead of wrapping around. * **Fix:** Improved encrypted MMKV random-IV upgrade behavior. Existing encrypted files using the older IV format now trigger a full writeback when possible so they are upgraded to the random-IV format. diff --git a/Core/MMKV.h b/Core/MMKV.h index c9ee847d..b21ef085 100755 --- a/Core/MMKV.h +++ b/Core/MMKV.h @@ -608,8 +608,9 @@ public: // return count of items imported size_t importFrom(MMKV *src); - // call this method if the instance is no longer needed in the near future - // any subsequent call to the instance is undefined behavior + // Permanently close and destroy this instance. This is a terminal operation. + // All aliases to the pointer become invalid immediately after this call. + // The caller must ensure close() does not race with any other operation. void close(); // call this method if you are facing memory-warning diff --git a/Core/cbridge/MMKVBridge.cpp b/Core/cbridge/MMKVBridge.cpp index 2410631e..b0346a01 100644 --- a/Core/cbridge/MMKVBridge.cpp +++ b/Core/cbridge/MMKVBridge.cpp @@ -179,7 +179,18 @@ MMKV_EXPORT const char *mmkv_mmap_id(MMKVHandle_t handle) { MMKV_EXPORT void mmkv_close(MMKVHandle_t handle) { MMKV *kv = kvFromHandle(handle); - if (kv) { kv->close(); } + if (kv) { + kv->close(); + } +} + +MMKV_EXPORT void mmkv_close_handle(MMKVHandle_t *handle) { + if (!handle || !*handle) { + return; + } + MMKV *kv = kvFromHandle(*handle); + *handle = nullptr; + kv->close(); } /* ── Encode: bool ──────────────────────────────────────────────────── */ diff --git a/Core/cbridge/MMKVBridge.h b/Core/cbridge/MMKVBridge.h index 62036733..86d95bc0 100644 --- a/Core/cbridge/MMKVBridge.h +++ b/Core/cbridge/MMKVBridge.h @@ -87,8 +87,12 @@ MMKVHandle_t mmkv_default(MMKVConfig_t config); MMKVHandle_t mmkv_with_id(const char *mmapID, MMKVConfig_t config); /* Returns internal pointer — valid for the lifetime of the instance. Do NOT free. */ const char *mmkv_mmap_id(MMKVHandle_t handle); -/* Close an MMKV instance. Any subsequent call to the handle is undefined behavior. */ +/* Permanently close an MMKV instance. All aliases to this handle become invalid. + This terminal operation must not race with any other operation. */ void mmkv_close(MMKVHandle_t handle); +/* Close an MMKV instance and clear the caller's handle. + This is the preferred idempotent form for managed bindings. */ +void mmkv_close_handle(MMKVHandle_t *handle); /* ── Encode ────────────────────────────────────────────────────────── */ diff --git a/KMP/README.md b/KMP/README.md index 56db5cdd..ee95faee 100644 --- a/KMP/README.md +++ b/KMP/README.md @@ -85,6 +85,17 @@ kv.encodeString("name", "MMKV") val name = kv.decodeString("name") ``` +`MMKV` implements `AutoCloseable`, so scoped ownership can use Kotlin's +`use {}` helper. `close()` is terminal and idempotent on that wrapper, but +it invalidates all wrappers backed by the same native instance. Do not race +it with another operation, and discard all aliases before reopening the ID. + +```kotlin +MMKV.mmkvWithID("scoped").use { kv -> + kv.encodeString("name", "MMKV") +} +``` + Android delegates to the native `com.tencent:mmkv:2.4.1` AAR. iOS embeds MMKV Core through the C bridge in the published native KLIBs, so consumers do not need CocoaPods, Swift Package Manager, or a source build. diff --git a/KMP/mmkv/src/androidMain/kotlin/com/tencent/mmkv/kmp/MMKV.android.kt b/KMP/mmkv/src/androidMain/kotlin/com/tencent/mmkv/kmp/MMKV.android.kt index 78de5184..1c1df3b9 100644 --- a/KMP/mmkv/src/androidMain/kotlin/com/tencent/mmkv/kmp/MMKV.android.kt +++ b/KMP/mmkv/src/androidMain/kotlin/com/tencent/mmkv/kmp/MMKV.android.kt @@ -71,7 +71,7 @@ fun MMKV.Companion.initialize( * Since the KMP class is now in `com.tencent.mmkv.kmp`, we can reference the Java * `com.tencent.mmkv.MMKV` directly via import alias without any naming collision. */ -actual class MMKV internal constructor(private val impl: AndroidMMKV) { +actual class MMKV internal constructor(private val impl: AndroidMMKV) : AutoCloseable { actual companion object { actual fun onExit() = AndroidMMKV.onExit() @@ -182,7 +182,7 @@ actual class MMKV internal constructor(private val impl: AndroidMMKV) { actual fun sync() = impl.sync() actual fun async() = impl.async() actual fun trim() = impl.trim() - actual fun close() = impl.close() + actual override fun close() = impl.close() actual fun clearMemoryCache() = impl.clearMemoryCache() actual fun importFrom(source: MMKV): Long = impl.importFrom(source.impl) diff --git a/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKV.kt b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKV.kt index bc5f8fcb..677c269c 100644 --- a/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKV.kt +++ b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKV.kt @@ -28,7 +28,7 @@ package com.tencent.mmkv.kmp * on [MMKV.Companion] in each platform source set, since Android requires [Context] * while other platforms do not. */ -expect class MMKV { +expect class MMKV : AutoCloseable { companion object { /** @@ -274,10 +274,12 @@ expect class MMKV { fun trim() /** - * Call this method if the MMKV instance is no longer needed in the near future. - * Any subsequent call to the instance is undefined behavior. + * Permanently close the underlying native MMKV instance. + * This is terminal and idempotent on this wrapper, and it invalidates every + * wrapper backed by the same native instance. Do not race close with another + * operation. Discard all aliases before reopening the same ID. */ - fun close() + override fun close() /** * Clear memory cache of the MMKV instance. diff --git a/KMP/mmkv/src/commonTest/kotlin/com/tencent/mmkv/kmp/MMKVSmokeTest.kt b/KMP/mmkv/src/commonTest/kotlin/com/tencent/mmkv/kmp/MMKVSmokeTest.kt index ec7d6e02..eaf2207b 100644 --- a/KMP/mmkv/src/commonTest/kotlin/com/tencent/mmkv/kmp/MMKVSmokeTest.kt +++ b/KMP/mmkv/src/commonTest/kotlin/com/tencent/mmkv/kmp/MMKVSmokeTest.kt @@ -25,6 +25,7 @@ import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNull +import kotlin.test.assertFailsWith import kotlin.test.assertTrue class MMKVSmokeTest { @@ -117,4 +118,27 @@ class MMKVSmokeTest { namespace.close() } } + + @Test + fun closeIsIdempotentAndTerminal() { + MMKVTestEnv.initialize() + val id = MMKVTestEnv.uniqueID("close") + val kv = MMKV.mmkvWithID(id) + kv.clearAll() + assertTrue(kv.encodeString("key", "value")) + + kv.close() + kv.close() + assertFailsWith { + kv.decodeString("key") + } + + val reopened = MMKV.mmkvWithID(id) + try { + assertEquals("value", reopened.decodeString("key")) + } finally { + reopened.clearAll() + reopened.close() + } + } } diff --git a/KMP/mmkv/src/darwinMain/kotlin/com/tencent/mmkv/kmp/MMKV.darwin.kt b/KMP/mmkv/src/darwinMain/kotlin/com/tencent/mmkv/kmp/MMKV.darwin.kt index 9f9a5921..1ac017c1 100644 --- a/KMP/mmkv/src/darwinMain/kotlin/com/tencent/mmkv/kmp/MMKV.darwin.kt +++ b/KMP/mmkv/src/darwinMain/kotlin/com/tencent/mmkv/kmp/MMKV.darwin.kt @@ -90,7 +90,11 @@ fun MMKV.Companion.initialize( // endregion @OptIn(ExperimentalForeignApi::class) -actual class MMKV internal constructor(private val handle: COpaquePointer) { +actual class MMKV internal constructor(handle: COpaquePointer) : AutoCloseable { + + private var nativeHandle: COpaquePointer? = handle + private val handle: COpaquePointer + get() = checkNotNull(nativeHandle) { "MMKV instance has been closed" } actual companion object { actual fun onExit() { @@ -359,7 +363,15 @@ actual class MMKV internal constructor(private val handle: COpaquePointer) { actual fun sync() = mmkv_sync(handle, true) actual fun async() = mmkv_sync(handle, false) actual fun trim() = mmkv_trim(handle) - actual fun close() = mmkv_close(handle) + actual override fun close() { + val current = nativeHandle ?: return + memScoped { + val handleVar = alloc() + handleVar.value = current + mmkv_close_handle(handleVar.ptr) + } + nativeHandle = null + } actual fun clearMemoryCache() = mmkv_clear_memory_cache(handle, false) actual fun importFrom(source: MMKV): Long = mmkv_import_from(handle, source.handle).toLong() diff --git a/KMP/mmkv/src/desktopMain/kotlin/com/tencent/mmkv/kmp/MMKV.desktop.kt b/KMP/mmkv/src/desktopMain/kotlin/com/tencent/mmkv/kmp/MMKV.desktop.kt index 2b0e8440..51e87bfe 100644 --- a/KMP/mmkv/src/desktopMain/kotlin/com/tencent/mmkv/kmp/MMKV.desktop.kt +++ b/KMP/mmkv/src/desktopMain/kotlin/com/tencent/mmkv/kmp/MMKV.desktop.kt @@ -23,6 +23,7 @@ package com.tencent.mmkv.kmp import com.sun.jna.* import com.sun.jna.ptr.IntByReference import com.sun.jna.ptr.LongByReference +import com.sun.jna.ptr.PointerByReference // region JNA interface mapping the C bridge @@ -34,7 +35,7 @@ internal interface MMKVLib : Library { fun mmkv_default(config: JnaMMKVConfig.ByValue): Pointer? fun mmkv_with_id(mmapID: String, config: JnaMMKVConfig.ByValue): Pointer? fun mmkv_mmap_id(handle: Pointer): String? - fun mmkv_close(handle: Pointer) + fun mmkv_close_handle(handle: PointerByReference) fun mmkv_encode_bool(handle: Pointer, key: String, value: Boolean): Boolean fun mmkv_encode_bool_v2(handle: Pointer, key: String, value: Boolean, expireDuration: Int): Boolean @@ -174,7 +175,11 @@ fun MMKV.Companion.initialize( // endregion -actual class MMKV internal constructor(private val handle: Pointer) { +actual class MMKV internal constructor(handle: Pointer) : AutoCloseable { + + private var nativeHandle: Pointer? = handle + private val handle: Pointer + get() = checkNotNull(nativeHandle) { "MMKV instance has been closed" } actual companion object { private val lib = MMKVLib.INSTANCE @@ -346,7 +351,12 @@ actual class MMKV internal constructor(private val handle: Pointer) { actual fun sync() = lib.mmkv_sync(handle, true) actual fun async() = lib.mmkv_sync(handle, false) actual fun trim() = lib.mmkv_trim(handle) - actual fun close() = lib.mmkv_close(handle) + actual override fun close() { + val current = nativeHandle ?: return + val handleRef = PointerByReference(current) + lib.mmkv_close_handle(handleRef) + nativeHandle = null + } actual fun clearMemoryCache() = lib.mmkv_clear_memory_cache(handle, false) actual fun importFrom(source: MMKV): Long = lib.mmkv_import_from(handle, source.handle) actual fun enableAutoKeyExpire(expiredInSeconds: UInt): Boolean = lib.mmkv_enable_auto_expire(handle, expiredInSeconds.toInt()) diff --git a/KMP/mmkv/src/nativeDesktopMain/kotlin/com/tencent/mmkv/kmp/MMKV.nativeDesktop.kt b/KMP/mmkv/src/nativeDesktopMain/kotlin/com/tencent/mmkv/kmp/MMKV.nativeDesktop.kt index 0f05cd97..16e770a6 100644 --- a/KMP/mmkv/src/nativeDesktopMain/kotlin/com/tencent/mmkv/kmp/MMKV.nativeDesktop.kt +++ b/KMP/mmkv/src/nativeDesktopMain/kotlin/com/tencent/mmkv/kmp/MMKV.nativeDesktop.kt @@ -58,7 +58,11 @@ fun MMKV.Companion.initialize( // endregion @OptIn(ExperimentalForeignApi::class) -actual class MMKV internal constructor(private val handle: COpaquePointer) { +actual class MMKV internal constructor(handle: COpaquePointer) : AutoCloseable { + + private var nativeHandle: COpaquePointer? = handle + private val handle: COpaquePointer + get() = checkNotNull(nativeHandle) { "MMKV instance has been closed" } actual companion object { actual fun onExit() { @@ -325,7 +329,15 @@ actual class MMKV internal constructor(private val handle: COpaquePointer) { actual fun sync() = mmkv_sync(handle, true) actual fun async() = mmkv_sync(handle, false) actual fun trim() = mmkv_trim(handle) - actual fun close() = mmkv_close(handle) + actual override fun close() { + val current = nativeHandle ?: return + memScoped { + val handleVar = alloc() + handleVar.value = current + mmkv_close_handle(handleVar.ptr) + } + nativeHandle = null + } actual fun clearMemoryCache() = mmkv_clear_memory_cache(handle, false) actual fun importFrom(source: MMKV): Long = mmkv_import_from(handle, source.handle).toLong() diff --git a/OpenHarmony/MMKV/src/main/ets/utils/MMKV.ets b/OpenHarmony/MMKV/src/main/ets/utils/MMKV.ets index 245bfa6f..b4b7d59d 100644 --- a/OpenHarmony/MMKV/src/main/ets/utils/MMKV.ets +++ b/OpenHarmony/MMKV/src/main/ets/utils/MMKV.ets @@ -130,7 +130,7 @@ export class MMKV { return MMKV.g_rootDir; } - private readonly nativeHandle: bigint; + private nativeHandle: bigint; /** * Never use this constructor directly. It's visible only because ArkTS has no package-internal access control. @@ -681,11 +681,18 @@ export class MMKV { } /** - * 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. + * Permanently close the underlying native MMKV instance. + * This is terminal and idempotent on this wrapper, and it invalidates every + * wrapper backed by the same native instance. Do not race close with another + * operation. Discard all aliases before reopening the same ID. */ public close(): void { - return native.close(this.nativeHandle); + const handle = this.nativeHandle; + if (handle === 0n) { + return; + } + native.close(handle); + this.nativeHandle = 0n; } /** @@ -1134,4 +1141,4 @@ class NameSpace { public checkExist(mmapID: string): boolean { return native.checkExist(mmapID, this._rootDir); } -} \ No newline at end of file +} diff --git a/POSIX/demo/demo_c.c b/POSIX/demo/demo_c.c index b25acfd9..4fd454e5 100644 --- a/POSIX/demo/demo_c.c +++ b/POSIX/demo/demo_c.c @@ -301,7 +301,8 @@ static void namespaceTest(void) { } /* removeStorage within namespace */ - mmkv_close(kv); + mmkv_close_handle(&kv); + mmkv_close_handle(&kv); bool removeRet = mmkv_namespace_remove_storage(ns, "ns_test"); printf("namespace removeStorage = %d\n", removeRet); printf("namespace checkExist after remove = %d\n", mmkv_namespace_check_exist(ns, "ns_test")); diff --git a/POSIX/golang/mmkv.go b/POSIX/golang/mmkv.go index 45d6dab1..902789cb 100644 --- a/POSIX/golang/mmkv.go +++ b/POSIX/golang/mmkv.go @@ -280,8 +280,9 @@ type MMKV interface { */ Trim() - // Close the instance when it's no longer needed in the near future. - // Any subsequent call to the instance is undefined behavior. + // Close permanently destroys the native instance. + // The copied Go handle and all of its aliases become invalid immediately. + // Close must not race with another operation on the same instance. Close() /* ReKey Change encryption key for the MMKV instance. diff --git a/Python/libmmkv_python.cpp b/Python/libmmkv_python.cpp index 9e4e56c7..ed93dc09 100644 --- a/Python/libmmkv_python.cpp +++ b/Python/libmmkv_python.cpp @@ -397,7 +397,10 @@ PYBIND11_MODULE(mmkv, m) { clsMMKV.def("isMultiProcess", &MMKV::isMultiProcess, "check multi-process mode"); clsMMKV.def("isReadOnly", &MMKV::isReadOnly, "check read-only mode"); - clsMMKV.def("close", &MMKV::close, "close the instance"); + clsMMKV.def("close", &MMKV::close, + "Permanently close the instance. This is terminal: all Python aliases " + "to the same native instance become invalid, and close must not race " + "with another operation."); clsMMKV.def_static("rootDir", &MMKV::getRootDir, "get the root directory of MMKV"); diff --git a/flutter/mmkv/lib/mmkv.dart b/flutter/mmkv/lib/mmkv.dart index 8f62a0d1..6835e940 100644 --- a/flutter/mmkv/lib/mmkv.dart +++ b/flutter/mmkv/lib/mmkv.dart @@ -721,10 +721,18 @@ class MMKV { return _importFrom(_handle, src._handle); } - /// Close the instance when it's no longer needed in the near future. - /// Any subsequent call to the instance is **undefined behavior**. + /// Permanently close the underlying native MMKV instance. + /// + /// This is terminal and idempotent on this wrapper, and it invalidates every + /// wrapper backed by the same native instance. Do not race close with another + /// operation. Discard all aliases before reopening the same ID. void close() { - _mmkvClose(_handle); + final handle = _handle; + if (handle == nullptr) { + return; + } + _mmkvClose(handle); + _handle = nullptr; } /// backup one MMKV instance to [dstDir] @@ -1077,4 +1085,4 @@ final Pointer Function() _groupPath = _mmkvPlatform.groupPathFunc(); final int Function(Pointer, Pointer) _importFrom = _mmkvPlatform.importFromFunc(); -final void Function(Pointer) _freePtr = _mmkvPlatform.freePtrFunc(); \ No newline at end of file +final void Function(Pointer) _freePtr = _mmkvPlatform.freePtrFunc(); diff --git a/flutter/mmkv/test/mmkv_test.dart b/flutter/mmkv/test/mmkv_test.dart index fd209bd9..0cf91ba6 100644 --- a/flutter/mmkv/test/mmkv_test.dart +++ b/flutter/mmkv/test/mmkv_test.dart @@ -45,4 +45,13 @@ void main() { expect(mmkv.decodeBool("bool"), true); }); + + test("close is idempotent and clears the Dart handle", () { + final mmkv = MMKV.defaultMMKV(); + + mmkv.close(); + mmkv.close(); + + expect(mmkv.encodeBool("afterClose", true), false); + }); } diff --git a/iOS/MMKV/MMKV/MMKV.h b/iOS/MMKV/MMKV/MMKV.h index ab1547b6..2bafea6c 100644 --- a/iOS/MMKV/MMKV/MMKV.h +++ b/iOS/MMKV/MMKV/MMKV.h @@ -401,8 +401,11 @@ class MMKV; // return count of items imported - (size_t)importFrom:(MMKV *)src; -/// call this method if the instance is no longer needed in the near future -/// any subsequent call to the instance is undefined behavior +/// Release MMKV's cache ownership of this wrapper. +/// This is a terminal, idempotent operation on this wrapper. It must not race +/// with another operation. Discard all strong references to this wrapper before +/// reopening the same ID; reopening while the old wrapper is alive is unsupported. +/// Any use of this wrapper after close is a programming error. - (void)close; /// call this method if you are facing memory-warning diff --git a/iOS/MMKV/MMKV/libMMKV.mm b/iOS/MMKV/MMKV/libMMKV.mm index 146acf19..0f3002e3 100644 --- a/iOS/MMKV/MMKV/libMMKV.mm +++ b/iOS/MMKV/MMKV/libMMKV.mm @@ -333,7 +333,10 @@ static BOOL g_hasCalledInitializeMMKV = NO; [kv release]; } kv->m_lastAccessTime = llround([NSDate timeIntervalSinceReferenceDate] * 1000); - return kv; + // g_instanceDic owns kv while g_lock is held. Establish an autoreleased + // ownership handoff before releasing the lock so auto cleanup cannot + // deallocate the wrapper before an ARC caller retains the return value. + return [[kv retain] autorelease]; } - (instancetype)initWithMMapID:(NSString *)mmapID config:(const MMKVConfig&)config { @@ -442,14 +445,21 @@ static BOOL g_hasCalledInitializeMMKV = NO; } - (void)close { - SCOPED_LOCK(g_lock); - MMKVInfo("closing %@", m_mmapID); + // Keep the wrapper alive while removing the dictionary's retain. + [self retain]; + { + SCOPED_LOCK(g_lock); + MMKVInfo("closing %@", m_mmapID); - [g_instanceDic removeObjectForKey:m_mmapKey]; + if ([g_instanceDic objectForKey:m_mmapKey] == self) { + [g_instanceDic removeObjectForKey:m_mmapKey]; + } - if (self.retainCount > 1) { - MMKVWarning("There's still reference on this kv: %@", m_mmapID); + // Actual Core destruction remains tied to wrapper deallocation so + // existing external references are not turned into nullptr wrappers. + // A second close on this wrapper is therefore harmless. } + [self release]; } - (void)trim { diff --git a/iOS/MMKVDemo/MMKVDemoTests/MMKVDemoTests.mm b/iOS/MMKVDemo/MMKVDemoTests/MMKVDemoTests.mm index 63bee0c9..4cb78968 100644 --- a/iOS/MMKVDemo/MMKVDemoTests/MMKVDemoTests.mm +++ b/iOS/MMKVDemo/MMKVDemoTests/MMKVDemoTests.mm @@ -441,6 +441,21 @@ using namespace std; [kv clearAll]; } +- (void)testCloseIsIdempotentAndAllowsReopen { + NSString *mmapID = @"close_lifecycle_test"; + MMKV *kv = [MMKV mmkvWithID:mmapID]; + XCTAssertTrue([kv setBool:YES forKey:@"value"]); + + [kv close]; + [kv close]; + kv = nil; + + MMKV *reopened = [MMKV mmkvWithID:mmapID]; + XCTAssertTrue([reopened getBoolForKey:@"value"]); + [reopened clearAll]; + [reopened close]; +} + - (void)compareDate:(NSDate *)date withDate:(NSDate *)other { XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, other.timeIntervalSince1970, 0.001); }