从iOS的角度看KotlinNative内存管理

一、前言

转载请注明出处

本文主要是在iOS端的角度来看Kotlin-Native是如何进行内存管理的,需要捋清楚OC实例和kotlin实例的对应关系,以及在两个不同的Runtime中是适配引用计数和GC的内存回收的。

1.1 kotlin-native编译

我们基于kotlin 2.0.21进行编译和调试(需要使用Xcode15编译),可能会遇到编译报错:

* What went wrong:Could not determine the dependencies of task ':kotlin-stdlib:compileJava9KotlinJvm'.> No matching toolchains found for requested specification: {languageVersion=9, vendor=any, implementation=vendor-specific} for MAC_OS on aarch64.   > No locally installed toolchains match and the configured toolchain download repositories aren't able to provide a match either.

我们需要按照官方文档提示执行如下两步操作(新版本已经集成到一个sh脚本中):

$ sed -i '' -e '/<components>/,/<\/components>/d' gradle/verification-metadata.xml$ ./gradlew -i --write-verification-metadata sha256,md5 -Pkotlin.native.enabled=true resolveDependencies

依然会报错,可以看到是因为没有java9的工具链,我们可以在stdlib这个module的build.gradle.kts里面修改对应Java9的配置即可。

具体的编译指令按照官方文档里面./gradlew对应的指令即可(我编译的完整版的bundle版本),如果要编译带debug信息需要加上debug编译参数:

./gradlew -Pshims=true :kotlin-native:bundle -Pkotlin.native.debug=true

当我们编译成功之后,可以在demo工程的gradle.properties里面配置我们的kotlin-native的路径:

kotlin.native.home=/Users/wandawwang/workspace/kmm/kotlin/kotlin-native/dist

1.2 基于CLion代码编辑

这部分官方的在kotlin-native/HACKING.md中说明了如何进行编译的预设置,生成编译数据库:

./gradlew :kotlin-native:compdb

然后使用Clion打开kotlin-native打开项目并配置一下compile-command.json的路径(我们编译完成之后会在kotlin-native路径下生成)。使用Clion的目的只是为了能够在看代码时更好跳转(编译和运行还是用上面的gradlew)。

1.3 kotlin-native调试

我们成功编译kotlin-native之后,可以手动建一个Demo并把kn的路径设置为我们自己编译的路径(上一节中已经提到)。
AS中我们编译自己需要导出的代码:

interface InteroperatAble {
}

/**
 * export
 */
object Interoperator {
    private var bridge: InteroperatAble? = null
    // oc to kotlin
    fun inject(bridge: InteroperatAble?) {
        println("inject")
        this.bridge = bridge
    }
}

并在Xcode中写调用代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    [SharedInteroperator.shared injectBridge:self];
}

这时候我们就可以运行自己的代码,并查看对应的执行情况。
如果使用Android Studio创建的kmp工程,需要在toml里面把kotlin的版本号改成2.0.21版本。这样就能和我们自己编译的kotlin-native版本保持一致:

kotlin = "2.0.21"

二、Runtime能力注入

从kn的源码可以看到一个与objc相关的很基础的类KotlinBase:

struct ObjHeader;
@interface KotlinBase : NSObject <NSCopying>
+ (instancetype)createRetainedWrapper:(struct ObjHeader *)obj;
- (instancetype)initWithExternalRCRef:(uintptr_t)ref NS_REFINED_FOR_SWIFT;
// Return kotlin.native.ref.ExternalRCRef stored in this class
- (uintptr_t)externalRCRef NS_REFINED_FOR_SWIFT;
@end

在该类中重写了两个比较关键的方法load和initialize,我们分别来看看分别都做了什么。

2.1 load

load方法会在应用程序启动期间由系统自动进行调用,因此kn在load方法内部调用了injectToRuntimeImpl函数(使用dispatch_once保证只调用一次)。

static void injectToRuntimeImpl() {
  Kotlin_ObjCExport_toKotlinSelector = @selector(toKotlin:);
  Kotlin_ObjCExport_releaseAsAssociatedObjectSelector = @selector(releaseAsAssociatedObject);
}
  • 1、将toKotlin方法绑定到Kotlin_ObjCExport_toKotlinSelector全局变量中(类型在iOS中为SEL);
  • 2、将releaseAsAssociatedObject方法绑定到Kotlin_ObjCExport_releaseAsAssociatedObjectSelector全局变量中。

2.2 initialize

这部分会做三个事情:

  • 1.尝试性再一次调用injectToRuntime方法去绑定toKolitn和release的selector;
  • 2.为Block、Bool以及swift类添加toKotlin和release方法;
  • 3.将Kotlin中类型定义的方法和接口绑定到对应的原生类中;
    首先在initialize方法的内部如果当前类型是KotlinBase也会去调用上面提到injectToRuntime方法。

我们在编译Kotlin代码时会为类添加@ObjCName注解或者iosmain中定义的类/接口,编译器会将这部分内容进行导出(这部分源码在ObjCExportCodeGenerator.kt中),并将类/接口保存在如下的弱符号变量中(这是编译属性,也就是):

__attribute__((weak)) const ObjCTypeAdapter** Kotlin_ObjCExport_sortedClassAdapters = nullptr;
__attribute__((weak)) int Kotlin_ObjCExport_sortedClassAdaptersNum = 0;

__attribute__((weak)) const ObjCTypeAdapter** Kotlin_ObjCExport_sortedProtocolAdapters = nullptr;
__attribute__((weak)) int Kotlin_ObjCExport_sortedProtocolAdaptersNum = 0;

这里的 attribute((weak)) 表示这是一个弱符号,初始值为 0。弱符号的特点是如果在其他地方有同名的强符号定义,则会使用强符号的值;如果没有,则使用这里的默认值。导出的ObjCTypeAdapter类型。
因此initTypeAdapters会读取由编译器在编译时写入的Kotlin_ObjCExport_sortedClassAdapters(class适配器)和Kotlin_ObjCExport_sortedProtocolAdapters(protocol适配器)写入到TypeInfo中缓存起来,以便在按需转换OC实例和Kotlin实例时能找到对应的适配器:

typeInfo->writableInfo_->objCExport.typeAdapter = adapter;

然后为 NSBlock、布尔类和 Swift 对象添加了 toKotlin: 方法,使这些对象可以转换为 Kotlin 对象:

BOOL added = class_addMethod(nsBlockClass, toKotlinSelector, (IMP)blockToKotlinImp, toKotlinTypeEncoding);
added = class_addMethod(booleanClass, toKotlinSelector, (IMP)boxedBooleanToKotlinImp, toKotlinTypeEncoding);
added = class_addMethod(swiftRootClass, toKotlinSelector, (IMP)SwiftObject_toKotlinImp, toKotlinTypeEncoding);

以及为 Swift 对象添加了 releaseAsAssociatedObject 方法,用于管理对象的生命周期:

added = class_addMethod(
        swiftRootClass, releaseAsAssociatedObjectSelector,
        (IMP)SwiftObject_releaseAsAssociatedObjectImp, releaseAsAssociatedObjectTypeEncoding
      )

最后就是第3点提到的内容,这部分在Kotlin_ObjCExport_initializeClass函数中实现。先将ObjCTypeAdapter的kotlinTypeInfo和Class类型关联起来:

objc_setAssociatedObject(clazz, &associatedTypeInfoKey, value, OBJC_ASSOCIATION_ASSIGN);

在第3步的最后将kotlin定义的方法实现指针绑定到该class中,以便于后续所有针对该Class或者Class实例的方法调用都能找到方法的实现:

// 给class添加实例方法
class_addMethod(clazz, selector, adapter->imp, adapter->encoding);
// 给class添加类方法
class_addMethod(object_getClass(clazz), selector, adapter->imp, adapter->encoding);
// 给class添加协议
addProtocolForInterface(clazz, typeInfo->implementedInterfaces_[i]);

这都是一些准备工作,下面我们来详细看一下是如何进行跨Runtime内存内管里。

三、跨Runtime的内存管理

我们从上面提到的例子入手来看一下跨Runtime的内存管理是如何进行的。
这里shared的framework命名是通过在build.gradle.kts中配置的:

cocoapods {
    name = "shared"
    podfile = project.file("../iosApp/Podfile")
    framework {
        baseName = "shared"
        isStatic = true
    }
}

为了搞清楚跨Runtime的内存管理,我们需要明确如下目标:

目标:观察objc实例时候如何传递kotlin侧使用,并且查看objc实例和对应的kotlin实例是关联的?

把目标拆分一下:

  • 1.纯OC的实例传递到Kotlin侧;
  • 2.Kotlin导出到OC的类,在OC侧基于该类生成的实例并回传给Kotlin;
  • 3.Kotlin侧生成类的实例并导出到OC;

3.1 纯OC的实例传递到Kotlin侧

我们在objc侧调用的injectBridge方法实参self指针是如何传递到kotlin的呢?kotlin-native透露的内容很少,那如何查看其内部的真相是个问题:

  • 我们可以正向的去阅读konan的源码看看编译器是如何插入的,但那内容太多了。
  • 通过编译出来的MachO文件来看汇编吗?没问题,但那读起来晦涩难懂。
  • 那再往上一层,我们去看经过LLVM编译之后IR代码。那我们尝试一下。

3.1.1 LLVM IR

我在文章最开头部分有列出当前的kotlin版本是2.0.21,我们要能够看到编译过程中的IR,需要配置一下build.gradle.kts的编译选项:

iosSimulatorArm64() {
    compilations.all {
        kotlinOptions {
            // 打印gc的信息
            freeCompilerArgs += "-Xruntime-logs=gc=info"
            freeCompilerArgs += "-Xbinary=stripDebugInfoFromNativeLibs=false"
            freeCompilerArgs += "-g"
            freeCompilerArgs += "-verbose"
        }
    }
}

添加了verbose选项之后,我们在编译的时候能够在输出栏看到如下内容输出:

/Users/wandawwang/.konan/dependencies/llvm-12.0.1-macos-aarch64-20250512/bin/clang++ -cc1 -emit-obj -disable-llvm-passes -x ir -triple arm64-apple-ios14.0-simulator -O0 -mllvm -fast-isel=false -mllvm -global-isel=false /var/folders/0g/29bl_1hj56ddl7fw5_l8ylvh0000gn/T/konan_temp3482722465042693681/out.bc -o 

会将中间的临时IR输出到形如"/var/folders/0g/xxxxxx/T/konan_temp3...1/out.bc"的路径中。然后使用llvm-dis将bc转换为ll文件即可。
然后我们在kotlin侧的inject方法内加一个断点,运行起来能看到类似这样的调用栈:

#0  0x0000000102454144 in kfun:com.example.kotlinnativedebugdemo.Interoperator#inject(com.example.kotlinnativedebugdemo.InteroperatAble?){}
#1  0x0000000102455104 in objc2kotlin_kfun:com.example.kotlinnativedebugdemo.Interoperator#inject(com.example.kotlinnativedebugdemo.InteroperatAble?){} at /<compiler-generated>:1
#2  0x0000000102451908 in -[MainViewController viewDidLoad]

很明显我们要搞清楚objc的指针如何传递到kotlin侧,objc2kotlin_kfun:com.example.kotlinnativedebugdemo.Interoperator#inject方法是我们必须要去弄清楚的。我们去上面得到的ll文件中进行搜索,可以看到类似的方法定义:

define internal void @"objc2kotlin_kfun:com.example.kotlinnativedebugdemo.Interoperator#inject(com.example.kotlinnativedebugdemo.InteroperatAble?){}"(i8* %0, i8* %1, i8* %2) #9 personality i32 (...)* @__gxx_personality_v0 !dbg !2292 {
 ...
}

有三个参数:

  • %0(对应的SharedInteroperator.shared);
  • %1(对应injectBridge这个selector)
  • %2(对应self指针),类型在 LLVM IR 中被类型擦除为 i8*

关于SharedInteroperator.shared和self的变量处理主要是这部分代码:

entry: ; preds = %stack_locals_init
  %9 = invoke %struct.ObjHeader* @Kotlin_ObjCExport_refFromObjC(i8* %0, %struct.ObjHeader** %6)
          to label %call_success unwind label %fatal_landingpad, !dbg !2293

call_success: ; preds = %entry
  %10 = invoke %struct.ObjHeader* @Kotlin_ObjCExport_refFromObjC(i8* %2, %struct.ObjHeader** %7)
          to label %call_success1 unwind label %fatal_landingpad, !dbg !2293

可以看到entry对应的内容主要是处理SharedInteroperator.shared的,它的第一个实参是%0。而我们关心的self是call_success对应的内容,它的第一个实参是%2。很明显它调用了一个Kotlin_ObjCExport_refFromObjC函数!
call_success之后接着是调用call_success1:

call_success1: ; preds = %call_success
  invoke void @"kfun:com.example.kotlinnativedebugdemo.Interoperator#inject(com.example.kotlinnativedebugdemo.InteroperatAble?){}"(%struct.ObjHeader* %9, %struct.ObjHeader* %10)
          to label %call_success3 unwind label %kotlinExceptionHandler, !dbg !2293

注意这个函数和我们上面提到的inject方法有点类似,但并不一样!它是kfun的前缀,也就是它是纯kotlin侧的inject方法;而上面的是objc2kotlin_kfun,它是由编译器生成由objc到kotlin的桥接方法。
从这里我们能知道从Native调用到Kotlin侧,首先会调用编译器生成的objc2kotlin_kfun的方法,然后在该方法内部去调用kotlin侧,也就是以kfun为前缀的方法。

我们继续看看主流程,也就是oc的实例是如何转换为kotlin侧使用的实例的。

3.1.2 Kotlin_ObjCExport_refFromObjC函数

从函数的命名我们能够大概猜到它是生成一个基于objc实例的引用,其源代码如下:

extern "C" ObjHeader* Kotlin_ObjCExport_refFromObjC(id obj, ObjHeader** __result__) {
  kotlin::AssertThreadState(kotlin::ThreadState::kRunnable);
  if (obj == nullptr)  {
      ObjHeader* __obj = nullptr;
      UpdateReturnRef(__result__, __obj);
      return __obj; 
  };
  auto msgSend = reinterpret_cast<ObjHeader* (*)(id self, SEL cmd, ObjHeader** slot)>(&objc_msgSend);
  ObjHeader* __result = msgSend(obj, Kotlin_Kotlin_ObjCExport_toKotlinSelector_toKotlinSelector, __result__);
  return __result;
}

我们在Xcode中下一个符号断点验证一下是否会执行到对应的函数。通过输出的结果来看我们前面的分析是正确的:

(lldb) register read x0
      x0 = 0x000000010640e200
(lldb) po 0x000000010640e200
<MainViewController: 0x10640e200>

回到源码,obj明显不是nullptr。因此会执行到Kotlin_ObjCExport_toKotlinSelector,我们在上一节的load有提到该selector会绑定到toKotlin方法。那我们下一个符号断点"-[KotlinBase toKotlin:]"来看看,但是发现该断点并不会生效!
那怎么办呢?只能在Kotlin_ObjCExport_refFromObjC进行单步调试,停留在objc_msgSend之前:

    0x1061fa9d4 <+76>:  ldur   x2, [x29, #-0x10]
    0x1061fa9d8 <+80>:  ldur   x0, [x29, #-0x8]
    0x1061fa9dc <+84>:  adrp   x8, 147
    0x1061fa9e0 <+88>:  ldr    x1, [x8, #0x7b0]
->  0x1061fa9e4 <+92>:  bl     0x10620d654               ; symbol stub for: objc_msgSend

对于objc_msgSend来说,其第二个参数x1对应到selector。我们打印看看情况:

(lldb) register read x1
      x1 = 0x000000010620ef58  "Shared_toKotlin:"

通过搜索源码可以看到拼接规则:

fun addPrivateSelector(name: String) {
    literalPatches += LiteralPatch(ObjCDataGenerator.selectorGenerator, name, "${privatePrefix}_$name")
}

从ll文件中也能搜到Shared_toKotlin关键字样:

@OBJC_METH_VAR_NAME_.24 = private unnamed_addr constant [17 x i8] c"Shared_toKotlin:\00", section "__TEXT,__objc_methname,cstring_literals", align 1

理论上来说,我们应该能够在ll文件中看到三个基于Shared_toKotlin的变量,他们分别用于:

  • 1.Kotlin中基础数据类型。比如Int、Long、Boolean、Double、Byte等等添加的toKotlin方法;
  • 2.为Kotlin中的集合类型:Map、List、Set等等。添加toKotlin方法;
  • 3.OC中的封装类型:NSObject、NSString、NSNumber等等添加toKotlin方法,还有就是KotlinBase的toKotlin方法;

我们这里要看的self的对应的toKotlin方法,也就是上面的第三种。那他实现是什么呢?来看一下其内容:

@"_OBJC_$_CATEGORY_INSTANCE_METHODS_NSObject_$_NSObjectToKotlin" = internal global 
{ i32, i32, [2 x %_objc_method] } 
{ i32 24, i32 2, [2 x %_objc_method] 
[
%_objc_method { 
    i8* getelementptr inbounds ([17 x i8], [17 x i8]* @OBJC_METH_VAR_NAME_.24, i32 0, i32 0), 
    i8* getelementptr inbounds ([46 x i8], [46 x i8]* @OBJC_METH_VAR_TYPE_.56, i32 0, i32 0), 
    i8* bitcast (%struct.ObjHeader* (%27*, i8*, %struct.ObjHeader**)* @"\01-[NSObject(NSObjectToKotlin) toKotlin:]" to i8*)
}, 
...
] 
}, section "__DATA, __objc_const", align 8

这里定义了toKotlin方法:

%_objc_method { 
// 方法名称
i8* getelementptr inbounds ([17 x i8], [17 x i8]* @OBJC_METH_VAR_NAME_.24, i32 0, i32 0), 
// 方法签名,也就是方法的类型
i8* getelementptr inbounds ([46 x i8], [46 x i8]* @OBJC_METH_VAR_TYPE_.56, i32 0, i32 0), 
// 方法实现
i8* bitcast (%struct.ObjHeader* (%27*, i8*, %struct.ObjHeader**)* @"\01-[NSObject(NSObjectToKotlin) toKotlin:]" to i8*)
}

变量“@OBJC_METH_VAR_NAME_.24”(往前第二个代码块,可以搜索关键字)就是我们上面已经贴出来的内容,他表示的这个objc_method的方法名称是“Shared_toKotlin:”。

方法签名@OBJC_METH_VAR_TYPE_.56(在上一个代码框的第5行,方法签名中)的定义:

@OBJC_METH_VAR_TYPE_.56 = private unnamed_addr constant [46 x i8] c"^{ObjHeader=^{TypeInfo}}24@0:8^^{ObjHeader}16\00", section "__TEXT,__objc_methtype,cstring_literals", align 1

其中"^{ObjHeader=^{TypeInfo}}"表示返回的返回值是ObjHeader结构体,在该结构体内部包含有TypeInfo类型的成员变量。
24表示进入这个方法需要申请24字节的栈大小。

  • @ 符号表示的对象类型,
  • :符号表示的selector类型;
  • ^^{ObjHeader}表示的是指针的指针;

他们后面数字表示偏移量,分别是0,8,16;
对应的方法签名类似于:

- (ObjHeader *)xxxxx:(ObjHeader **)param;

最后我们可以看到具体的方法实现,指向了"-[NSObject(NSObjectToKotlin) toKotlin:]"。从源码中能够看到其定义如下:

@implementation NSObject (NSObjectToKotlin)
-(ObjHeader*)toKotlin:(ObjHeader**)OBJ_RESULT {
  ObjHeader* __result =     Kotlin_ObjCExport_convertUnmappedObjCObject(self, OBJ_RESULT);
  return __result;
}
@end

可以看到其内部调用了Kotlin_ObjCExport_convertUnmappedObjCObject函数。我们在Xcode添加该函数的符号断点,看到其参数的确是MainViewController的实例:

总结一下这个函数的作用:

调用对应的类型基于NSObject的扩展toKotlin方法,生成与OC对象对应的ObjHeader作为Kotlin使用的实例

3.1.3 Kotlin_ObjCExport_convertUnmappedObjCObject函数

源码如下:

extern "C" ObjHeader* Kotlin_ObjCExport_convertUnmappedObjCObject(id obj, ObjHeader** OBJ_RESULT) {
  const TypeInfo* typeInfo = getOrCreateTypeInfo(object_getClass(obj));
  ObjHeader* __result = AllocInstanceWithAssociatedObject(typeInfo, objc_retain(obj), OBJ_RESULT);
  return __result;
}

static const TypeInfo* getOrCreateTypeInfo(Class clazz) {
  const TypeInfo* result = Kotlin_ObjCExport_getAssociatedTypeInfo(clazz);
  if (result != nullptr) {
    return result;
  }
  Class superClass = class_getSuperclass(clazz);
  const TypeInfo* superType = superClass == nullptr ?
    theForeignObjCObjectTypeInfo :
    getOrCreateTypeInfo(superClass);
    
  std::lock_guard lockGuard(typeInfoCreationMutex);

  result = Kotlin_ObjCExport_getAssociatedTypeInfo(clazz); // double-checking.
  if (result == nullptr) {
    result = createTypeInfo(clazz, superType, nullptr);
    setAssociatedTypeInfo(clazz, result);
  }
  return result;
}

首先获取当前class对应的kotlin的TypeInfo。
由于我们传入的是MainViewController的实例,是无法获取到已有的TypeInfo的,所以他会尝试去查找其父类的TypeInfo(调试看依次查找了UIViewController、UIResponder和NSObject)。
可以看到MainViewController自身是没有TypeInfo的:

const TypeInfo* result = Kotlin_ObjCExport_getAssociatedTypeInfo(clazz);

最后在运行期间会通过createTypeInfo给OC的实例创建与之对应的TypeInfo:

result = createTypeInfo(clazz, superType, nullptr);

并将typeInfo添加到类的关联对象中,以便于下一次class可以直接找到对应的TypeInfo而不用去重复创建TypeInfo:

static void setAssociatedTypeInfo(Class clazz, const TypeInfo* typeInfo) {
  kotlin::NativeOrUnregisteredThreadGuard threadStateGuard(/* reentrant = */ true);
  NSValue* value = [[NSValue alloc] initWithBytes:&typeInfo objCType:@encode(void*)];
  objc_setAssociatedObject(clazz, &associatedTypeInfoKey, value, OBJC_ASSOCIATION_ASSIGN);
}

然后基于上面得到的TypeInfo创建对应的实例:

inline static ObjHeader* AllocInstanceWithAssociatedObject(const TypeInfo* typeInfo, id associatedObject, ObjHeader** OBJ_RESULT) {
  ObjHeader* result = AllocInstance(typeInfo, OBJ_RESULT);
  SetAssociatedObject(result, associatedObject);
  return result;
}

在调用AllocInstanceWithAssociatedObject传入的参数,是调用objc_retain(obj)的返回值。也就是说这里会对OC的实例引用计数+1:

ObjHeader* __result = AllocInstanceWithAssociatedObject(typeInfo, objc_retain(obj), OBJ_RESULT);

我们可以自行验证一下:

object KotlinInnerOperator {
    internal var bridge: InteroperatAble? = null
}

object Interoperator {
    private var bridge: InteroperatAble? = null
    private var bridge_bk: InteroperatAble? = null
    fun inject(bridge: InteroperatAble?) {
        this.bridge = bridge
        this.bridge_bk = bridge
        KotlinInnerOperator.bridge = bridge
    }
}

生成逻辑:纯OC的实例会调用NSObject的toKolitn扩展方法,在该方法内部调用Kotlin_ObjCExport_convertUnmappedObjCObject生成Kotlin侧的ObjHeader!
引用计数:纯OC的实例传递到kotlin侧会使其引用计数+1,在kotlin侧无论增加了多少次引用,均不会继续增加该实例的引用计数。

可以简单一下TypeInfo的内存分布情况:

(lldb) x/16gx 0x000000010631adb0
0x10631adb0: 0x000000010631adb0 0x0000000106327330
0x10631adc0: 0x0000000c00000000 0x0000000106310f10
0x10631add0: 0x0000000000000000 0x0000000000000000
0x10631ade0: 0x00000001063272f8 0x0000000000000001
0x10631adf0: 0x0000000106327310 0x00000001063270f0
0x10631ae00: 0x0000000106327360 0x0000000000000302
0x10631ae10: 0x00000001063445b0 0x0000000000000000
0x10631ae20: 0x00000001062ccd8c 0x0000000000000008

3.2 OC侧生成Kotlin类的实例并传递到Kotlin侧

我们上面观察到了纯OC类的实例传递给Kotlin侧的执行流程。现在我们将一个kotlin类导出到OC侧并在OC侧生成该类的实例,最终将该实例回传给kotlin。
修改一下我们的demo:

class Kotlin2Objc:InteroperatAble {
}

OC的代码修改如下:

SharedKotlin2Objc *k2o = [[SharedKotlin2Objc alloc] init];
[SharedInteroperator.shared injectBridge:k2o];

我们debug能够看到alloc会走到KotlinBase的allocWithZone方法。我们把着重看一下其内部关于内存管理部分的代码:

KotlinBase* result = [super allocWithZone:zone];
ObjHolder holder;
AllocInstanceWithAssociatedObject(typeInfo, result, holder.slot());
result->refHolder.initAndAddRef(holder.obj());

result是native侧调用生成的OC对象(我们在MainViewController中调用了alloc生成的),holder.slot()是调用AllocInstanceWithAssociatedObject返回的结果,我们可以把它理解为Kotlin侧的实例。最后result会被添加kotlin侧的实例的AssociatedObject内部保留起来。
这里AllocInstanceWithAssociatedObject调用传入的参数明显和3.1.3节调用时传递的不一样,这里没有执行objc_retain来增加引用计数。

上面提到的的refHolder是KotlinBase内部的一个成员变量:

@implementation KotlinBase {
  BackRefFromAssociatedObject refHolder;
}

所以我们先来看BackRefFromAssociatedObject的内容。

3.2.1 BackRefFromAssociatedObject

它主要作用是维护从OC对象到 Kotlin 对象的反向引用(back reference),提供了引用计数机制,确保只要关联对象存在,Kotlin 对象就不会被垃圾回收。
这样做的目的是保证了只要OC对象没有被销毁,那么该Kotlin对象就不会被回收;反过来只要Kotlin对象不销毁,OC对象也能够一直存在。这保证了生命周期的同步。

BackRefFromAssociatedObject定义如下(移除了部分我们此时不关注的内容):

class BackRefFromAssociatedObject {
 private:
  union {
    struct {
      // ....
      volatile int refCount;
    }; // Legacy MM
    struct {
      kotlin::mm::RawSpecialRef* ref_;
      // ...
    }; // New MM. Regular object.
  };
};

调用的initAndAddRef和createObjCBackRef都是针对ref_成员变量的操作,其实现为:

void BackRefFromAssociatedObject::initAndAddRef(ObjHeader* obj) {
    ref_ = static_cast<mm::RawSpecialRef*>(mm::ObjCBackRef::create(obj));
    deallocMutex_.construct();
}
mm::ObjCBackRef mm::SpecialRefRegistry::ThreadQueue::createObjCBackRef(ObjHeader* object) noexcept {
    // 默认的引用计数为1(非OC侧的引用计数)
    return mm::ObjCBackRef(registerNode(object, 1, false).asRaw());
}
Node& registerNode(ObjHeader* obj, Node::Rc rc, bool allowFastDeletion) noexcept {
    RuntimeAssert(obj != nullptr, "Creating node for null object");
    queue_.emplace_back(owner_, obj, rc);
    auto& node = queue_.back();
    return node;
}

这里生成了Node实例,并将其放入到queue_中。注意这里传入了ObjHeader并被Node实例所持有,后面在toKotlin中返回的实例就是该obj!

3.2.2 ObjCBackRef和Node

上面提到了ObjCBackRef的类型,它主要用于桥接 Objective-C 和 Kotlin/Native 的对象生命周期管理。让 ObjC 的对象可以安全地持有 Kotlin 对象的引用(作为“反向引用”/back reference),通过引用计数(refcount)机制,配合 Kotlin 的 GC,确保Kotlin的对象不会被过早回收。
GC 会自动跟踪引用计数大于 0 的对象作为根,并在 Kotlin 对象被收集时使引用计数为 0 的引用失效。

实现了retain/release/tryRetain 等接口,模拟 ObjC 的引用计数内存管理。

class ObjCBackRef : private MoveOnly {
private:
    raw_ptr<SpecialRefRegistry::Node> node_;
}

node_成员的赋值是其在显示构造方法中:

explicit ObjCBackRef(RawSpecialRef* raw) noexcept : node_(SpecialRefRegistry::Node::fromRaw(raw)) {}

Node的类型定义如下:

class Node : private Pinned {
private:
    ObjHeader* obj_; // kotlin侧实例的指针
    std::atomic<Rc> rc_; // 引用计数
    std::atomic<Node*> nextRoot_ = nullptr; 
}

因此后续的retain和release都是基于Node的rc_成员进行操作的:

void retain() noexcept {
    if (!node_) return;
    node_->retainRef();
}
void release() noexcept {
    if (!node_) return;
    node_->releaseRef();
}

3.2.3 获取Kotlin实例

这里需要特别说明一下:并不是说Kotlin侧定义并导出到OC侧的类都是KotlinBase的子类,依次查看superclass可以看到其父类链路中并不包含KotlinBase类,编译器会基于自己的Framework前缀生成一个XXXBase!

比如咱们例子中的SharedBase,看一下SharedBase在llvm中间码中的定义如下:

@"OBJC_CLASS_$_SharedBase" = global %_class_t { 
    %_class_t* @"OBJC_METACLASS_$_SharedBase", // isa指针
    %_class_t* @"OBJC_CLASS_$_NSObject",  // superclass指针
    %_objc_cache* @_objc_empty_cache,  // cache指针
    i8* (i8*, i8*)** null, // vtable指针
     %_class_ro_t* @"_OBJC_CLASS_RO_$_KotlinBase"  // class_ro_t的data指针
}, section "__DATA, __objc_data", align 8

每一个属性的含义如下:

  • isa指针:指向该类的元类(metaclass)。元类用于存储类方法。
  • superclass指针:指向父类 NSObject,表明 SharedBase 继承自 NSObject。
  • cache指针:指向方法缓存,初始为空缓存。
  • vtable指针:这里设为 null。
  • class_ro_t数据指针:指向类的只读数据,注意这里指向的是 KotlinBase 而不是 SharedBase。

说明编译器帮忙生成的SharedBase包括方法、成员变量、属性和协议等等信息都是指向了KotlinBase类。

我们在3.2.1知道在Native侧调用alloc_init会走到KotlinBase的allocWithZone生成一个Native实例,但后面我们需要将该实例传递到Native侧。
这里的转换用到了KotlinBase的toKotlin方法,这里可以从生成ll代码来看(用llvm-dis从bc转换过来的,我们3.1节有详细讲):

define internal void @"objc2kotlin_kfun:com.example.kn_runtime_debug_demo.Interoperator#inject(com.example.kn_runtime_debug_demo.InteroperatAble?){}"(i8* %0, i8* %1, i8* %2) #5 personality i32 (...)* @__gxx_personality_v0 !dbg !17589 {
    // ...
    %9 = invoke %struct.ObjHeader* @Kotlin_ObjCExport_refFromObjC(i8* %0, %struct.ObjHeader** %6)
          to label %call_success unwind label %fatal_landingpad, !dbg !17590
    // ...
}

这个和我们3.1看到的情况一样,都是调用Kotlin_ObjCExport_refFromObjC函数,那我们添加对应函数的符号断点,并在调用objc_msgSend之前查看x1的情况:

我们使用类似3.1.2节拆出KotlinBase的实例方法定义列表:

@"_OBJC_$_INSTANCE_METHODS_KotlinBase" = internal global { i32, i32, [9 x %_objc_method] } 
{ i32 24, i32 9, [9 x %_objc_method] 
[
%_objc_method { 
    i8* getelementptr inbounds ([17 x i8], [17 x i8]* @OBJC_METH_VAR_NAME_.49.916, i32 0, i32 0), 
    i8* getelementptr inbounds ([46 x i8], [46 x i8]* @OBJC_METH_VAR_TYPE_.84, i32 0, i32 0), 
    i8* bitcast (%struct.ObjHeader* (%31*, i8*, %struct.ObjHeader**)* @"\01-[KotlinBase toKotlin:]" to i8*) 
}, 
// ....
] 
}, section "__DATA, __objc_const", align 8

在第7行能看到其实现是调用"-[KotlinBase toKotlin:]"。来到kotlin-native的runtime源码,由于我们生成的非持久性的实例,因此在toKotlin中执行代码如下:

-(ObjHeader *)toKotlin:(ObjHeader **)__result__ {
    // ...
    ObjHeader* __obj = refHolder.ref<ErrorPolicy::kTerminate>();
    UpdateReturnRef(__result__, __obj);
    return __obj;
}

ObjHeader* BackRefFromAssociatedObject::ref() const {
    return *mm::ObjCBackRef::reinterpret(ref_);
}

可以看到这里的ref函数是调用了ObjCBackRef的解引用操作符:

ObjHeader* operator*() const noexcept {
    if (!node_) return nullptr;
    return node_->ref();
}

这里node_的ref函数最终返回的是Node中所持有的obj_成员,所以在这里我们就得到了Kotlin侧的ObjHeader实例。
再回头看一下内存管理,由于KotlinBase重写了retain和release方法,因此所有针对KotlinBase实例的引用计数操作均时下面的实现:

-(instancetype)retain {
  if (permanent) {
    [super retain];
  } else {
    refHolder.addRef<ErrorPolicy::kTerminate>();
  }
  return self;
}

template <ErrorPolicy errorPolicy>
void BackRefFromAssociatedObject::addRef() {
    mm::ObjCBackRef::reinterpret(ref_).retain();
}

看到非permanent对象在retain和release的时候是直接调用的refHolder对应的函数。这是kn-runtime自己管理的引用计数(非iOS原生的引用计数,该引用计数并不会影响到OC的对象释放)。

无论如何增加其引用关系,我们通过

️️️CFGetRetainCount得到的引用计数一直为1(并未增加引用计数),这和我们在3.1.3节观察到纯OC实例的引用计数表现完全不一致。

这里我们看一下OC实例是如何反向引用Kotlin的ObjHeader的(ObjHeader正向引用OC实例,我们在3.3节再详细看看)

其中nextRoot_管理所有需要作为 GC 根的特殊引用指针,这是单向链表。基本上所有Kotlin类型的实例在OC侧创建时所生成Node均会被添加到这个单向链表中:

Node(SpecialRefRegistry& registry, ObjHeader* obj, Rc rc) noexcept : obj_(obj), rc_(rc) {
    // 在3.2.1节中调用registerNode(object, 1, false).asRaw()
    // 传入的rc都为1
    if (rc > 0) { 
        registry.insertIntoRootsHead(*this);
    }
}

3.3 Kotlin侧生成类的实例导出到OC并回传到Kotlin侧

同样的我们改造一下我们的Demo,新增一个Kt2OC2Kt的类(实际上我们使用的Interoperator单例也能作为这个case的例子,但我们要观察release的情况)。

class Kt2OC2Kt: InteroperatAble {
}
object Interoperator {
    private var k2o2k: Kt2OC2Kt? = Kt2OC2Kt()
    fun getKotlinInstance(): Kt2OC2Kt? {
        return k2o2k
    }
    fun inject(bridge: InteroperatAble?) {}
}

这个case和上面两种case最大的差异在于:上面两种case都是在OC侧生成的实例,该case是在Kotlin侧生成的实例,并传递到OC侧。在3.1和3.2我们基本弄清楚了OC的实例是如何转换为Kotlin侧能够使用ObjHeader指针的。
本节我们需要弄清楚的问题是:

Kotlin侧生成的实例是如何转换为OC侧可以使用的实例呢?

用llvm-dis翻译一下 ir代码"objc2kotlin_kfun:com.example.kn_runtime_debug_demo.Interoperator#getKotlinInstance()
",注意这里是objc2kotlin_kfun!!!表明当前还处于oc侧的一个函数。
我摘出了比较关键的代码:

entry: ; preds = %stack_locals_init
  %8 = invoke %struct.ObjHeader* @Kotlin_ObjCExport_refFromObjC(i8* %0, %struct.ObjHeader** %5)
          to label %call_success unwind label %fatal_landingpad, !dbg !17588
call_success: ; preds = %entry
  %9 = invoke %struct.ObjHeader* @"kfun:com.example.kn_runtime_debug_demo.Interoperator#getKotlinInstance(){}com.example.kn_runtime_debug_demo.Kt2OC2Kt?"(%struct.ObjHeader* %8, %struct.ObjHeader** %7)
          to label %call_success2 unwind label %kotlinExceptionHandler, !dbg !17588
call_success2: ; preds = %call_success
  %10 = invoke i8* @Kotlin_ObjCExport_refToRetainedObjC(%struct.ObjHeader* %9)
          to label %call_success3 unwind label %fatal_landingpad, !dbg !17588
call_success3: ; preds = %call_success2
  // ...
  %11 = tail call i8* @llvm.objc.autoreleaseReturnValue(i8* %10) #9, !dbg !17588
  ret i8* %11, !dbg !17588

首先是entry块(preds表示当前块的前驱,这里stack_locals_init块我忽略了,可以理解为会先执行本地栈初始化工作)调用Kotlin_ObjCExport_refFromObjC(我们3.1.2节已经详细说了)获取oc对象对应的kotlin实例(ObjHeader);
接着call_success去调用对应kfun,其返回值保存在%9变量中。最后在call_success2中调用Kotlin_ObjCExport_refToRetainedObjC函数的返回值保存在%10变量中。最后的尾调用autoreleaseReturnValue将OC的实例放入自动释放池中。

3.3.1 Kotlin_ObjCExport_refToRetainedObjC

其函数定义:

extern "C" id Kotlin_ObjCExport_refToRetainedObjC(ObjHeader* obj) {
  return Kotlin_ObjCExport_refToObjCImpl<true>(obj);
}

接收一个ObjHeader参数,返回一个id类型的值。
其内部实现为:

convertReferenceToRetainedObjC convertToRetained = (convertReferenceToRetainedObjC)obj->type_info()->writableInfo_->objCExport.convertToRetained;
id retainedResult;
if (convertToRetained != nullptr) {
    retainedResult = convertToRetained(obj);
} else {
    retainedResult = Kotlin_ObjCExport_refToRetainedObjC_slowpath(obj);
}

其中"obj->type_info()->writableInfo_->objCExport.convertToRetained"对应的汇编代码:

ldr    x8, [x8]
and    x8, x8, #0xfffffffffffffffc
ldr    x8, [x8]
ldr    x8, [x8, #0x60] // writableInfo_在TypeInfo中的偏移量为12*8
ldr    x8, [x8]

执行到第5行之后,register read x8的值为0。因此会执行else分支中的Kotlin_ObjCExport_refToRetainedObjC_slowpath函数:

static id Kotlin_ObjCExport_refToRetainedObjC_slowpath(ObjHeader* obj) {
  const TypeInfo* typeInfo = obj->type_info();
  convertReferenceToRetainedObjC convertToRetained = findConvertToRetainedFromInterfaces(typeInfo);
  if (convertToRetained == nullptr) {
    getOrCreateClass(typeInfo);
    convertToRetained = (typeInfo == theUnitTypeInfo) ? &Kotlin_ObjCExport_convertUnitToRetained : &convertKotlinObjectToRetained;
  }
  typeInfo->writableInfo_->objCExport.convertToRetained = (void*)convertToRetained;
  return convertToRetained(obj);
}

通过debug能够知道其执行的是第6行(x8保留的是convertToRetained变量的结果,由于其为nullptr,因此执行的第6行的逻辑):

接着执行到0x101e27310,subs指令会将x10减去x11,并设置cpsr寄存器的标志位。如果相减为0则会设置z标记位,否则z标记位为0:

可以看到cpsr的值为0x20001000:

回头看一下上面的源代码,能够知道当typeInfo是Unit类型会执行Kotlin_ObjCExport_convertUnitToRetained,否则执行convertKotlinObjectToRetained:

static id convertKotlinObjectToRetained(ObjHeader* obj) {
  Class clazz = obj->type_info()->writableInfo_->objCExport.objCClass;
  RuntimeAssert(clazz != nullptr, "");
  return [clazz createRetainedWrapper:obj];
}

最后在Kotlin_ObjCExport_refToRetainedObjC_slowpath函数中会把convert的函数指针赋值给"typeInfo->writableInfo_->objCExport.convertToRetained"缓存起来,下一次Kotlin实例的转换时,就不需要走slowpath直接读取缓存中的函数指针即可。

3.3.2 createRetainedWrapper方法

上一节我们看到kotlin侧的ObjHeader会调用createRetainedWrapper方法来创建一个OC的实例。该方法针对持久化对象和非持久化对象有两种不同的处理方式,由于我们现在看的是Kt2OC2Kt的实例,该实例为非持久化对象。简化之后的源码为:

+ (instancetype)createRetainedWrapper:(ObjHeader*)obj {
    KotlinBase* candidate = [super allocWithZone:nil];
    bool permanent = obj->permanent();
    candidate->permanent = permanent;
    candidate->refHolder.initAndAddRef(obj);
    if (!isShareable(obj)) {
      SetAssociatedObject(obj, candidate);
    } else {
      id old = AtomicCompareAndSwapAssociatedObject(obj, nullptr, candidate);
      if (old != nullptr) {
        {
          kotlin::ThreadStateGuard guard(kotlin::ThreadState::kNative);
          candidate->refHolder.releaseRef();
          [candidate releaseAsAssociatedObject];
        }
        return objc_retain(old);
      }
    }
    return candidate;
}

第5行会获取refHolder并将BackRef设置为当前传入obj指针,接着由于isSharedable在当前版本是固定返回true的:

ALWAYS_INLINE bool isShareable(const ObjHeader* obj) {
    return true;
}

因此会执行到第9行,AtomicCompareAndSwapAssociatedObject函数的作用是原子性的交换obj的关联对象,如果其关联对象为nullptr,则会把candidate写入到关联对象中;如果关联对象不为nullptr,则会返回该关联对象,并把此次生成的candidate释放掉。并且会增加一次引用计数。

对于Kotlin中声明的类实例无论如何调用objc_retain都不会增加OC定义中的引用计数。这是因为KotlinBase重写了retain方法,该实现中均只会增加kotlin-native自己维护的引用计数。

这是因为objc_retain最终都会调用类自身的retain方法:

id  objc_retain(id obj) {
    if (obj->isTaggedPointerOrNil()) return obj;
    return obj->retain();
}

四、WeakReference实现

我们使用弱引用的目的是:

  • 新对象不会影响到原对象的释放时机;
  • 原对象释放之后新对象能够被正常置空;
    带着这两个目的先来看一下WeakReference在kotlin的定义:
@GCUnsafeCall("Konan_getWeakReferenceImpl")
@Escapes(0b01) // referent escapes.
external internal fun getWeakReferenceImpl(referent: Any): WeakReferenceImpl

public class WeakReference<T : Any> {
    public constructor(referred: T) {
        pointer = getWeakReferenceImpl(referred)
    }
}

从下面的代码我们能知道创建弱引用实例是调用的Konan_getWeakReferenceImpl函数:

RUNTIME_NOTHROW extern "C" OBJ_GETTER(Konan_getWeakReferenceImpl, ObjHeader* referred) {
    if (referred->permanent()) {
        RETURN_RESULT_OF(makePermanentWeakReferenceImpl, referred);
    }
#if KONAN_OBJC_INTEROP
    if (IsInstanceInternal(referred, theObjCObjectWrapperTypeInfo)) {
        RETURN_RESULT_OF(makeObjCWeakReferenceImpl, referred->GetAssociatedObject());
    }
#endif // KONAN_OBJC_INTEROP
    RETURN_RESULT_OF(mm::createRegularWeakReferenceImpl, referred);
}

因此这里分为了三类不同的Weak实现:

  • 1.用于长生命(permanent)周期的对象;
  • 2.ObjC对象的弱引用;
  • 3.Kotlin对象的弱引用;

因此我们分别来看一下这三种不同的Weak实现。

4.1 Permanent对象

从字面意思来看,Permanent对象就是拥有长生命周期的对象,对于这个概念我的认识比较浅薄。目前测试来看Kotlin中定义的单例并不是permanent对象,但是字符串常量和全局常量。
回到其WeakReference的实现:

internal class PermanentWeakReferenceImpl(val referred: Any): kotlin.native.ref.WeakReferenceImpl() {
    override fun get(): Any? = referred
}

@ExportForCppRuntime
internal fun makePermanentWeakReferenceImpl(referred: Any) = PermanentWeakReferenceImpl(referred)

可以看出:

对于permanent对象,其WeakReference就是该对象自身!
1、是否会影响原对象的生命周期:该对象本身就是长生命周期的对象,不存在销毁的场景;
2、新对象是否能够被自动置空:该对象本身就是长生命周期的对象,不存在销毁的场景;

4.2 ObjC对象

所有NSObject的子类,无论是在iosmain中通过kotlin创建的、还是OC代码中创建的实例均是被认为是ObjC对象。这部分实例的WeakReference源码:

@kotlin.native.internal.ExportForCppRuntime internal fun makeObjCWeakReferenceImpl(objcPtr: NativePtr): ObjCWeakReferenceImpl {
    val result = ObjCWeakReferenceImpl()
    result.init(objcPtr)
    return result
}

返回一个ObjCWeakReferenceImpl实例。真正的实现是在init方法里面:

@kotlin.native.internal.ExportForCppRuntime internal fun makeObjCWeakReferenceImpl(objcPtr: NativePtr): ObjCWeakReferenceImpl {
    val result = ObjCWeakReferenceImpl()
    result.init(objcPtr)
    return result
}

@GCUnsafeCall("Konan_ObjCInterop_initWeakReference")
private external fun ObjCWeakReferenceImpl.init(objcPtr: NativePtr)

static void Konan_ObjCInterop_initWeakReference(KRef ref, id objcPtr) {
  KotlinObjCWeakReference* objcRef = [KotlinObjCWeakReference new];
  objc_storeWeak(&objcRef->referred, objcPtr);
  ref->SetAssociatedObject(objcRef);
}

其完全是基于oc中__weak的实现方式,将objcRef->referred指向了objcPtr。从本节最开始的代码objcPtr就是“referred->GetAssociatedObject())”。这里会生成一个临时KotlinObjCWeakReference实例:

@implementation KotlinObjCWeakReference {
  @public id referred;
}

最终内存的引用关系为:

读取逻辑:

static OBJ_GETTER(Konan_ObjCInterop_getWeakReference, KRef ref) {
  KotlinObjCWeakReference* objcRef = (KotlinObjCWeakReference*)ref->GetAssociatedObject();
  id objcReferred = kotlin::CallWithThreadState<kotlin::ThreadState::kNative>(objc_loadWeakRetained, &objcRef->referred);
  KRef result = Kotlin_Interop_refFromObjC(objcReferred, OBJ_RESULT);
  kotlin::CallWithThreadState<kotlin::ThreadState::kNative>(objc_release, objcReferred);
  return result;
}

在通过get读取WeakReference的原始值时,首先会获取extraData持有的KotlinObjCWeakReference,接着调用objc_loadWeakRetained从OC的weak表中读取出原对象的指针。
该对象是一个OC对象,我们需要调用Kotlin_Interop_refFromObjC转换为最终的kotlin的实例(这部分流程见3.1节)。由于该函数会增加OC对象的引用计数,因此在该实现的最后调用了objc_release将其引用计数-1。

1、是否会影响原对象的生命周期:基于OC的weak原生实现,该引用并不会增加原OC实例的引用计数;
2、新对象是否能够被自动置空:基于OC的weak原生实现,当原对象被释放时,在对象的dealloc过程中调用_object_remove_assocations函数将保留的对应weak指针进行置空;

4.3 Kotlin对象

Kotlin对象的Weak实现调用了createRegularWeakReferenceImpl函数,会先尝试一下调用GetRegularWeakReferenceImpl来获取已经存在weakRef:

auto& extraObject = mm::ExtraObjectData::GetOrInstall(object);
auto* weakRef = extraObject.GetRegularWeakReferenceImpl();

ObjHeader* GetRegularWeakReferenceImpl() noexcept {
    auto* pointer = weakReferenceOrBaseObject_.load();
    if (hasPointerBits(pointer, WEAK_REF_TAG)) return clearPointerBits(pointer, WEAK_REF_TAG);
    return nullptr;
}

可以看到弱引用是保存在weakReferenceOrBaseObject_成员中,WEAK_REF_TAG是在指针的最低位设置为1来标记当前是一个弱引用(由于是字节对齐的,默认情况下至少低8位都是0)。

如果当前的weakReferenceOrBaseObject_不存在弱引用值,就会新创建一个weakRef:

auto* weakRef = makeRegularWeakReferenceImpl(
    static_cast<mm::RawSpecialRef*>(mm::WeakRef::create(object)),
    object,
    holder.slot()
);
// kotlin代码
internal class RegularWeakReferenceImpl(
    val weakRef: COpaquePointer,
    val referred: COpaquePointer, // TODO: This exists only for the ExtraObjectData's sake. Refactor and remove.
) : WeakReferenceImpl() {
    @GCUnsafeCall("Konan_RegularWeakReferenceImpl_get")
    external override fun get(): Any?
}
internal fun makeRegularWeakReferenceImpl(weakRef: COpaquePointer, referred: COpaquePointer) = RegularWeakReferenceImpl(weakRef, referred)

并调用GetOrSetRegularWeakReferenceImpl函数将上面得到的weakRef保留起来:

ObjHeader* GetOrSetRegularWeakReferenceImpl(ObjHeader* object, ObjHeader* weakRef) noexcept {
    if (weakReferenceOrBaseObject_.compare_exchange_strong(object, setPointerBits(weakRef, WEAK_REF_TAG))) {
        return weakRef;
    } else {
        return clearPointerBits(object, WEAK_REF_TAG); // on fail current value of weakRef is stored to object
    }
}
  • 如果weakReferenceOrBaseObject_和object相等,则将 weakReferenceOrBaseObject_ 设置为 setPointerBits(weakRef, WEAK_REF_TAG),即带有弱引用标记的新弱引用实现;
  • 如果不相等object 参数会被 compare_exchange_strong 修改为 weakReferenceOrBaseObject_ 的实际值。并将object返回;

换句话说就是首次被引用的实例没有生成weakRef则会新创建一个,如果已经存在则直接获取weakRef。在demo中验证了这一点:

val srcObj = Card()
val weakObj = WeakReference(srcObj)
val weakObj2 = WeakReference(srcObj)

接着来看一下get是如何实现的。从上面面是Kotlin侧RegularWeakReferenceImpl的详细定义
可以看到其get方法最终是调用Konan_RegularWeakReferenceImpl_get函数,且该函数内部会调用derefRegularWeakReferenceImpl函数:

// 这里我把宏展开了一下
OBJ_GETTER(mm::derefRegularWeakReferenceImpl, ObjHeader* weakRef) noexcept {
    ObjHeader* __obj = asRegularWeakReferenceImpl(weakRef)->weakRef.tryRef(OBJ_RESULT);
    return __obj;
}

最终的引用关系如下:

无论生成多少个弱引用实例,均只会生成一个impl实例!
1、是否会影响原对象的生命周期:在垃圾回收过程中,如果系统认为对象已经"死亡"(没有其他强引用指向它),那么 RegularWeakReferenceImpl 内部指向该对象的引用会被置为 null。
2、新对象是否能够被自动置空:由于RegularWeakReferenceImpl内部已经被设置为null,我们通过弱引用尝试访问该对象时,将会得到 null,表示原对象已经被回收。

五、后记

我们从上面的内容可以大致的了解到在iOS中kotlin-native管理内存的方式,不过就像官方的说法“Unfortunately, no special instruments are currently available to automatically detect retain cycles in Kotlin/Native code”,目前并没有一个有效的工具能够帮我们找到跨Runtime的内存引用链条。

我们能做的就是如果发现存在内存泄漏,就需要在Native使用weak或者在Kotlin使用WeakReference来断开引用链条。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容