Kotlin中objc_msgSend调用异常:iOS侧BaseId实例属性赋值失败排查
我最近在做Kotlin/Native框架向iOS项目暴露接口的开发,想通过动态创建BaseId子类实例的方式提供给iOS调用,但遇到了一个奇怪的问题:动态创建的实例属性没有被正确初始化,直接调用OC的初始化方法却能正常工作。以下是相关代码和现象:
我的Kotlin实现代码
actual fun create(typeName: String, id: String): BaseId { val objCClass = NSClassFromString(typeName) var selector = NSSelectorFromString("alloc") val instance = objc_msgSend(objCClass, selector) selector = NSSelectorFromString("initWithId:") // ???????? this msgSend not work ! objc_msgSend(instance, selector, id) // -------------------------------- return instance as BaseId }
iOS端调用代码
// UserId extends BaseId let typeName = NSStringFromClass(UserId.classForCoder()) let userId = framework.create(typeName: typeName, id: "testId") as! UserId // ???????? userId创建成功但userId.id为空,不符合预期的"testId" let userId2 = UserId.init(id: "testId2") // userId2正常,userId2.id == "testId2"
UserId对应的OC头文件
__attribute__((objc_subclassing_restricted)) __attribute__((swift_name("UserId"))) @interface CommonUserId : CommonBaseId - (instancetype)initWithId:(NSString *)id __attribute__((swift_name("init(id:)"))) __attribute__((objc_designated_initializer)); - (NSString *)testFun __attribute__((swift_name("testFun()"))); @property NSString *testProperty __attribute__((swift_name("testProperty"))); @end;
问题根源
你这里的核心问题是忽略了objc_msgSend调用初始化方法的返回值。在Objective-C中,init系列方法(包括initWithId:)是会返回初始化后的实例对象的——虽然大部分情况下它和alloc出来的实例是同一个,但某些初始化逻辑可能会返回不同的实例(比如单例、缓存实例等),而且Kotlin/Native的objc_msgSend不会自动帮你处理这个返回值,你必须手动接收并使用它。
你的代码里调用objc_msgSend(instance, selector, id)后,直接用了原来的instance变量,但实际上这个初始化方法的返回值才是真正完成初始化的实例,原来的instance可能并没有被正确赋值。
修复后的代码
修改Kotlin代码,接收初始化方法的返回值即可解决问题:
actual fun create(typeName: String, id: String): BaseId { val objCClass = NSClassFromString(typeName) var selector = NSSelectorFromString("alloc") var instance = objc_msgSend(objCClass, selector) selector = NSSelectorFromString("initWithId:") // 接收初始化后的实例,替换原变量 instance = objc_msgSend(instance, selector, id) return instance as BaseId }
如果想要更严谨,可以指定objc_msgSend的返回类型,避免潜在的类型转换问题:
import kotlinx.cinterop.* actual fun create(typeName: String, id: String): BaseId { val objCClass = NSClassFromString(typeName) ?: error("Class $typeName not found") val allocSelector = NSSelectorFromString("alloc") val instance = objc_msgSend(objCClass, allocSelector) as CPointer<ObjCObjectVar> val initSelector = NSSelectorFromString("initWithId:") val initializedInstance = objc_msgSend(instance, initSelector, id.cstr) as CPointer<ObjCObjectVar> return initializedInstance.asStableRef<BaseId>().get() }
补充说明
为什么直接在iOS端调用UserId.init(id: "testId2")正常?因为Swift会自动处理init方法的返回值,它底层会正确接收objc_msgSend的返回结果并赋值给userId2,而你的Kotlin代码没有做这一步,导致使用的是alloc出来但未完成初始化的实例,所以属性是空的。
内容的提问来源于stack exchange,提问作者Lei Wu

