Skip to the content.

Swift Object로 Associated Object 써보기

Objective-C에서 Associated Object는 Object에 Key-Value로 값을 저장하고 읽을 수 있게 해줍니다.

이는 NSObject만 지원하며, 아래처럼 Swift Object에서는 지원하지 않습니다.

import Foundation

actor MyObject {
    
}

let object: MyObject = .init()
let p: UnsafeMutablePointer<UInt8> = .allocate(capacity: 1)

// Runtime ERROR: objc[75001]: objc_setAssociatedObject called on instance (0x600002834000) of class Ass.MyObject which does not allow associated objects
objc_setAssociatedObject(object, p, nil, .OBJC_ASSOCIATION_ASSIGN)

p.deallocate()

이 글에서는 NSObject를 subclassing하지 않으면서 위 코드가 되게 하는 방법을 소개합니다.

arm64e assembly로 설명합니다.

왜 크래시가 나는지

이런 과정으로 크래시가 납니다.

참고로 Swift class가 Objective-C Runtime에 넘어가게 되면 Swift class는 SwiftObject라는 Objective-C class 객체로 wrap됩니다. 하지만 이 객체는 NSObject를 subclassing하지 않고 있습니다. 또한 객체가 아닌건 아마 __SwiftValue였던 걸로 기억하는데 가물가물하고 이 글이랑 관련 없는 내용

크래시 피하기 (1)

이렇게 하니 잘 되네요.

크래시 피하기 (2)

아까 위에서 보여드렸던 objc_setAssociatedObject의 코드 중

if (object->getIsa()->forbidsAssociatedObjects())
    _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

위 코드를 무력화시키면 됩니다. 위 코드는 assembly에서는 아래와 같기에

0x1825f56d4 <+312>:  tbnz   w8, #0x4, 0x1825f5b14     ; <+1400>

# <+1400>으로 jump

0x1825f5b14 <+1400>: mov    x0, x19
0x1825f5b18 <+1404>: bl     0x1825f4060               ; object_getClassName
0x1825f5b1c <+1408>: stp    x19, x0, [sp]
0x1825f5b20 <+1412>: adrp   x0, 54
0x1825f5b24 <+1416>: add    x0, x0, #0xea             ; "objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects"
0x1825f5b28 <+1420>: bl     0x18261d398               ; _objc_fatal(char const*, ...)

<+312>에서 w8 register에 0x0을 주입해주면 마찬가지로 잘 되나… 이건 objc_setAssociatedObject이 호출될 때마다 register 값을 바꾸는거라 성능에 안 좋아요.

처음에 소개드린 방법이 dyld load 시점이 되는거라 최초 한 번만 수행되기에 성능에 더 좋습니다.

Memory Leak

NSObject에서는?

NSObject에서는 -[NSObject dealloc]이 불릴 때 모든 associated objects들을 release 시킵니다. 이 원리는

따라서 NSObject에서는 -dealloc만 잘 호출된다면 associated objects들은 leak을 발생시키지 않습니다.

SwiftObject에서는

SwiftObject는 associated object를 지원하지 않기에 위 NSObject와 같은 로직이 없습니다. 따라서 위 방법대로 SwiftObject에서 associated object를 강제로 설정한다면 leak이 발생합니다.

‘SwiftObject의 dealloc을 swizzling하면 되는거 아니야?’ 라는 생각을 할 수 있는데, dealloc은 Objective-C class에 종속된게 아닌, -[NSObject dealloc]의 기능입니다. 따라서 SwiftObject에는 -dealloc이 존재하지 않습니다.

대신 swift::swift_unknownObjectRelease_nswift::swift_unknownObjectRelease이 존재합니다. 내부에 objc_release를 호출하는 것을 보실 수 있습니다. 아마 objc_release에서 retain count가 0일 경우 메모리를 비워줄 것 같네요.

따라서 memory leak을 해결하려면

이런 방법들이 있을 것 같은데 해보진 않음… 성능에 너무 안 졸은 방법이라 ㅠ

애플이 SwiftObject에서 associated object를 허용하지 않는 이유?

Swift class가 Objective-C Runtime에 넘어갈 떄 하나의 SwiftObject가 생성되고, 그렇게 만들어진 Swift class와 SwiftObject는 일대일 관계를 이룹니다.

하나의 Swift class가 Objective-C Runtime에 여러번 넘어간다고 해서 SwiftObject가 여러개 생성되지 않습니다.

만약 여러개 생성된다면 여러개의 SwiftObject들끼리 associated object이 동기화되지 않을 것이기에 문제가 될 것 같네요.

하지만 여러개 생성되지 않으므로 문제될건 없어 보이는데… 모르겠네요.

이런 이상한 짓을 하는 이유

SwiftData의 내부 버그를 고치기 위해… ㅠ

SwiftData의 내부에서 SwiftObjectobjc_setAssociatedObject에 넣어서 크래시나는 이슈가 있었고 이를 해결하기 위해 이짓거리를 했어요.

참고 : SwiftData에서 ModelActor 사용하기