Share
GC You Later, Allocator
Video: https://www.droidcon.com/2024/10/17/gc-you-later-allocator/
Memory management on Android is easy: the garbage collector does almost everything and LeakCanary handles the rest! But Kotlin/Multiplatform brings new challenges. Your new features could be blocked if Kotlin/Native leaks memory.
In this talk you’ll get a refresher on Android's garbage collector, and you’ll learn how Swift works without one. Once that groundwork is laid you’ll see how Kotlin/Native integrates these two models.
We’ll cover some memory management tools & techniques:
- Using Xcode to see what’s leaking
- Avoiding leaks when mixing Kotlin and Swift
- Using PhantomReference to test memory bugs
- How Kotlin inline classes compare to Valhalla’s primitive classes.
If you’d like to improve memory management in a multiplatform app, this talk is for you.
Jesse Wilson
Transcript
209 slides
-
Kitchen Counters You allocate space You do stuff in that
space You clean it up and start again -
Counter Space is Precious More counter space lets you do
more things! When counter space is tight, you thrash -
/** * Detects green toothy gators using Bluetooth GATT ERS.
*/ interface GatorsService : AutoCloseable { fun addListener(listener: Listener) fun removeListener(listener: Listener) interface Listener { fun onGators(nearbyGators: List<NearbyGator>) } } -
class NearbyGatorsContent( gatorsService: GatorsService, ) : GatorsService.Listener { var list
by mutableStateOf<List<NearbyGator>>(listOf()) init { gatorsService.addListener(this) } override fun onGators(nearbyGators: List<NearbyGator>) { list = nearbyGators } } -
class NearbyGatorsContent: ObservableObject, GatorsServiceListener { @Published var array: [NearbyGator] =
[] init(gatorsService: GatorsService) { gatorsService.addListener(listener: self) } func onGators(nearbyGators: [NearbyGator]) { self.array = nearbyGators } } -
@Composable fun Content() { if (showNearbyGators) { NearbyGatorsScreen( content =
NearbyGatorsContent(service), onDismiss = { showNearbyGators = false }, ).Show() } ... } -
struct ContentView: View { ... var body: some View {
Button( ... ) .sheet(isPresented: $showNearbyGators) { NearbyGatorsView( NearbyGatorsContent(service), $showNearbyGators ) } } } -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) NearbyGator name "Chompy" distance 5.5 -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) String data "Chompy" NearbyGator name "Chompy" distance 5.5 -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) String data "Chompy" NearbyGator name "Chompy" distance 5.5 -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" NearbyGator name "Chompy" distance 5.5 -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" NearbyGator name "Chompy" distance 5.5 -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" class NearbyGator name "Chompy" distance 5.5 -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" class NearbyGator name "Chompy" distance 5.5 Class name "NearbyGator" superclass Any ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" class NearbyGator name "Chompy" distance 5.5 Class name "NearbyGator" superclass Any ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" class NearbyGator name "Chompy" distance 5.5 Class name "NearbyGator" superclass Any ... Class name "Any" superclass null ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" class NearbyGator name "Chompy" distance 5.5 Class name "NearbyGator" superclass Any ... Class name "Any" superclass null ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" class NearbyGator name "Chompy" distance 5.5 class Class name "NearbyGator" superclass Any ... class Class name "Any" superclass null ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" class NearbyGator name "Chompy" distance 5.5 class Class name "NearbyGator" superclass Any ... class Class name "Any" superclass null ... class Class name "Class" superclass Any ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" class NearbyGator name "Chompy" distance 5.5 class Class name "NearbyGator" superclass Any ... class Class name "Any" superclass null ... class Class name "Class" superclass Any ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" class NearbyGator name "Chompy" distance 5.5 class Class name "NearbyGator" superclass Any ... class Class name "Any" superclass null ... class Class name "Class" superclass Any ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y String data "Chompy" class NearbyGator name "Chompy" distance 5.5 class Class name "NearbyGator" superclass Any ... class Class name "Any" superclass null ... class Class name "Class" superclass Any ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) class CharArray length 6 0 C 1 h 2 o 3 m 4 p 5 y class String data "Chompy" class NearbyGator name "Chompy" distance 5.5 class Class name "NearbyGator" superclass Any ... class Class name "Any" superclass null ... class Class name "Class" superclass Any ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) CharArray 6 C h o m p y String "Chompy" NearbyGator "Chompy" 5.5 Class "NearbyGator" Any ... Class "Any" null ... Class "Class" Any ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) 6 C h o m p y 5.5 ... null ... ... -
val chompy = NearbyGator( name = "Chompy", distance = 5.5,
) 0x06 0x43 0x68 0x6f 0x6d 0x70 0x79 0x40160000000000 ... 0x00 ... ... -
Logical Memory is a Big Graph! Each object is just
a combination of: • Values like Doubles and Chars • References to other objects Your program works by manipulating this graph -
RAM: Random Access Memory Like a ByteArray(4294967296) Keeps state for
the OS and all running apps Physical Memory is a Big ByteArray! -
The allocator’s job is to bridge the gap between ‘Big
Graph’ and ‘Big ByteArray’ -
The Simplest Allocator Allocate memory in sequence, as requested When
you run out of memory, crash! targets.withType<KotlinNativeTarget>().all { compilations.configureEach { compileTaskProvider.configure { compilerOptions.freeCompilerArgs.add("-Xgc=noop") } } } -
0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06
0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x -
var nextOffset = 160000 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09
0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x -
var nextOffset = 160000 val referenceSize = 4 0x99 0xfe
0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x -
var nextOffset = 160000 val referenceSize = 4 0x99 0xfe
0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 -
var nextOffset = 160000 val referenceSize = 4 10 0x99
0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 -
var nextOffset = 160000 val referenceSize = 4 10 0x99
0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 160010 -
var nextOffset = 160000 val referenceSize = 4 10 21
0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 160010 -
var nextOffset = 160000 val referenceSize = 4 10 21
0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 160021 160010 -
var nextOffset = 160000 val referenceSize = 4 10 21
34 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 160021 160010 -
var nextOffset = 160000 val referenceSize = 4 10 21
34 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 160021 160034 160010 -
var nextOffset = 160000 val referenceSize = 4 10 21
34 39 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 160021 160034 160010 -
var nextOffset = 160000 val referenceSize = 4 10 21
34 39 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 160021 160034 160039 160010 -
var nextOffset = 160000 val referenceSize = 4 10 21
34 39 45 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 160021 160034 160039 160010 -
var nextOffset = 160000 val referenceSize = 4 10 21
34 39 45 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 160021 160034 160039 160045 160010 -
var nextOffset = 160000 val referenceSize = 4 10 21
34 39 45 56 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 160000 160021 160034 160039 160045 160010 -
var nextOffset = 160000 val referenceSize = 4 10 21
34 39 45 56 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 0x00027100 0x00027115 0x00027122 0x00027127 0x0002712d 0x000271a0 -
var nextOffset = 160056 val referenceSize = 4 0x00027122 0x0002712d
0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x 0x000271a0 0x00027115 0x00027115 0x0002712d 0x00027127 0x00027127 0x00027100 0x00027100 0x00027100 0x00027100 0x00027100 0x000271a0 0x000271a0 -
0x000271a0 0x00027115 0x00027122 0x00027127 0x0002712d 0x00027100 0x00027100 0x00027100 0x000271a0 0x000271a0
0x00027115 0x0002712d 0x00027127 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x var nextOffset = 160056 val referenceSize = 4 0x00027100 0x00027100 -
0x00027100 0x000271a0 0x00027115 0x00027122 0x00027127 0x0002712d 00027100 00027100 00027100 000271a0
000271a0 00027115 0002712d 00027127 99 fe b0b15c00 0465 00 09 cafebabe 4016000000000000 08675309 43 06 68 6d 6f 79 70 c0 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x 0x var nextOffset = 160056 val referenceSize = 4 -
00027100 00027100 00027100 000271a0 000271a0 00027115 0002712d 00027127 99 fe
b0b15c00 0465 00 09 cafebabe 4016000000000000 08675309 43 06 68 6d 6f 79 70 c0 var nextOffset = 160056 val referenceSize = 4 -
Real Allocators They don’t keep everything in the big graph!
• Some objects in the graph have no incoming references • Nobody will know if we stop tracking them! You can create temporary objects without running out of memory -
Garbage Collection A batch process to find & free unnecessary
objects 1. Mark every object you can reach 2. Free the rest Used by the JVM, ART, Kotlin/Native, JavaScript, and Golang -
class NearbyGatorsContent { var list = listOf<NearbyGator>() override fun onGators(nearbyGators:
List<NearbyGator>) { list = nearbyGators } } -
List size 1 0 NearbyGator name "Lyle" distance 4.57 NearbyGators
Content list HomeActivity scope ... model -
List size 1 0 NearbyGator name "Lyle" distance 4.57 NearbyGators
Content list NearbyGator name "Smiley" distance 7.64 NearbyGator name "Chompy" distance 9.58 List size 2 0 1 HomeActivity scope ... model -
List size 1 0 NearbyGator name "Lyle" distance 4.57 NearbyGators
Content list NearbyGator name "Smiley" distance 7.64 NearbyGator name "Chompy" distance 9.58 List size 2 0 1 HomeActivity scope ... model -
List size 1 0 NearbyGator name "Lyle" distance 4.57 NearbyGators
Content list NearbyGator name "Smiley" distance 7.64 NearbyGator name "Chompy" distance 9.58 List size 2 0 1 HomeActivity scope ... model -
List size 1 0 NearbyGator name "Lyle" distance 4.57 NearbyGators
Content list NearbyGator name "Smiley" distance 7.64 NearbyGator name "Chompy" distance 9.58 List size 2 0 1 HomeActivity scope ... model ✔ ✔ ✔ ✔ ✔ -
NearbyGators Content list NearbyGator name "Smiley" distance 7.64 NearbyGator name
"Chompy" distance 9.58 List size 2 0 1 HomeActivity scope ... model ✔ ✔ ✔ ✔ ✔ -
Reference Counting Each object tracks how many other objects reference
it When the count reaches zero, it self-destructs Used by Swift, Python, C++’s std::shared_ptr, and Rust’s Rc<T> -
class NearbyGatorsContent { var array: [NearbyGator] = [] func onGators(array:
[NearbyGator]) { self.array = array } } -
NearbyGators Content ref count 1 array NearbyGator ref count 1
name "Lyle" distance 4.57 HomeView ref count 1 model Array ref count 1 size 1 0 -
NearbyGators Content ref count 1 array NearbyGator ref count 1
name "Chompy" distance 9.58 NearbyGator ref count 1 name "Smiley" distance 7.64 NearbyGator ref count 1 name "Lyle" distance 4.57 HomeView ref count 1 model Array ref count 1 size 1 0 Array ref count 1 size 1 0 1 -
NearbyGators Content ref count 1 array NearbyGator ref count 1
name "Chompy" distance 9.58 NearbyGator ref count 1 name "Smiley" distance 7.64 NearbyGator ref count 1 name "Lyle" distance 4.57 HomeView ref count 1 model Array ref count 1 size 1 0 Array ref count 1 size 1 0 1 0 -
NearbyGators Content ref count 1 array NearbyGator ref count 1
name "Chompy" distance 9.58 NearbyGator ref count 1 name "Smiley" distance 7.64 NearbyGator ref count 1 name "Lyle" distance 4.57 HomeView ref count 1 model Array ref count 1 size 1 0 1 0 -
NearbyGators Content ref count 1 array NearbyGator ref count 1
name "Chompy" distance 9.58 NearbyGator ref count 1 name "Smiley" distance 7.64 HomeView ref count 1 model Array ref count 1 size 1 0 1 -
NearbyGators Content ref count 1 array NearbyGator ref count 1
name "Chompy" distance 9.58 NearbyGator ref count 1 name "Smiley" distance 7.64 HomeView ref count 1 model Array ref count 1 size 1 0 1 2 -
Leaks Wasted counter space wrecks your cooking performance Wasted memory
can wreck your app’s performance Impact is most severe for long-running processes: • Servers • Point-of-sale apps -
class NearbyGatorsContent( gatorsService: GatorsService, ) : GatorsService.Listener { var list
by mutableStateOf<List<NearbyGator>>(listOf()) init { gatorsService.addListener(this) } override fun onGators(nearbyGators: List<NearbyGator>) { list = nearbyGators } } -
class NearbyGatorsContent( gatorsService: GatorsService, ) : GatorsService.Listener { var list
by mutableStateOf<List<NearbyGator>>(listOf()) init { gatorsService.addListener(this) } override fun onGators(nearbyGators: List<NearbyGator>) { list = nearbyGators } } -
class NearbyGatorsContent: ObservableObject, GatorsServiceListener { @Published var array: [NearbyGator] =
[] init(gatorsService: GatorsService) { gatorsService.addListener(listener: self) } func onGators(array: [NearbyGator]) { self.array = array } } -
class NearbyGatorsContent: ObservableObject, GatorsServiceListener { @Published var array: [NearbyGator] =
[] init(gatorsService: GatorsService) { gatorsService.addListener(listener: self) } func onGators(array: [NearbyGator]) { self.array = array } } -
FLATTEN KOTLIN/NATIVE ALLOCATIONS #1 kotlin { targets.withType<KotlinNativeTarget>().all { compilations.configureEach {
compileTaskProvider.configure { // This makes the Xcode memory analyzer graphs easier to // understand. Be careful when using this in production; // it's not as stable as the default allocator. // (See KT-68769) compilerOptions.freeCompilerArgs.add("-Xallocator=std") // Work around an issue with the above option! compilerOptions.freeCompilerArgs.add("-opt") } } } } -
FLATTEN KOTLIN/NATIVE ALLOCATIONS #1 Task :shared:linkDebugFrameworkIosSimulatorArm64 FAILED /Users/jwilson/.gradle/.../kotlinx-coroutines-core is cached
(in ~/.konan/kotlin-native-prebuilt-macos-aarch64-2.0.0/...), but its dependency isn't: /Users/jwilson/.konan/.../stdlib $ rm -rf ~/.konan/kotlin-native-prebuilt-macos-aarch64-2.0.0 -
/** * Call this in debug builds to collect Kotlin
objects and chains * of Kotlin + Swift objects. This isn't necessary for correct * application behavior, but it is helpful when doing memory * analysis because it removes unreachable objects. * * This runs a Kotlin GC which may have the side effect of * queueing some Swift objects to be eligible for `deinit()`. * And running that `deinit()` on the main thread will potentially * make some Kotlin objects eligible for collection! */ @OptIn(NativeRuntimeApi::class) object KotlinMemory { fun blockingCollect() { GC.collect() } } FORCE GC #5 -
XCODE TIPS 5 FLATTEN KOTLIN/NATIVE ALLOCATIONS PAY ATTENTION TO ZOOM
PRINT DESCRIPTION 1. 2. 3. 4. 5. STACK LOGGING FORCE GC -
Just Use LeakCanary It detects leaks automatically: • It knows
the lifecycles for common Android objects • If an object is still reachable in the Big Graph after its lifecycle is done, it probably leaked The leak trace shows you what to fix -
YourKit Too Export an .hprof file from Android Studio Open
that in YourKit Much more capable than Android Studio’s heap viewer -
class NearbyGatorsContent( private val gatorsService: GatorsService, ) : GatorsService.Listener {
val list = mutableStateListOf<NearbyGator>() init { gatorsService.addListener(this) } override fun onGators(nearbyGators: List<NearbyGator>) { list.clear() list.addAll(nearbyGators) } } } -
class NearbyGatorsContent( private val gatorsService: GatorsService, ) : GatorsService.Listener {
val list = mutableStateListOf<NearbyGator>() init { gatorsService.addListener(this) } override fun onGators(nearbyGators: List<NearbyGator>) { list.clear() list.addAll(nearbyGators) } } -
class NearbyGatorsContent( private val gatorsService: GatorsService, ) : GatorsService.Listener {
val list = mutableStateListOf<NearbyGator>() init { gatorsService.addListener(this) } override fun onGators(nearbyGators: List<NearbyGator>) { list.clear() list.addAll(nearbyGators) } fun close() { gatorsService.removeListener(this) } } -
class NearbyGatorsContent( private val gatorsService: GatorsService, ) : GatorsService.Listener {
val list = mutableStateListOf<NearbyGator>() init { gatorsService.addListener(this) } override fun onGators(nearbyGators: List<NearbyGator>) { list.clear() list.addAll(nearbyGators) } fun close() { gatorsService.removeListener(this) } } -
Just Use LeakSwift Just joking! iOS doesn’t have a P-Y
There’s a couple of libraries but nothing as easy or capable -
class NearbyGatorsContent: ObservableObject, GatorsServiceListener { private let service: GatorsService @Published
var array: [NearbyGator] = [] init(service: GatorsService) { self.service = service service.addListener(listener: self) } func onGators(array: [NearbyGator]) { self.array = array } } } -
class NearbyGatorsContent: ObservableObject, GatorsServiceListener { private let service: GatorsService @Published
var array: [NearbyGator] = [] init(service: GatorsService) { self.service = service service.addListener(listener: self) } func onGators(array: [NearbyGator]) { self.array = array } } -
deinit { service.removeListener(listener: self) } } class NearbyGatorsContent: ObservableObject, GatorsServiceListener
{ private let service: GatorsService @Published var array: [NearbyGator] = [] init(service: GatorsService) { self.service = service service.addListener(listener: self) } func onGators(array: [NearbyGator]) { self.array = array } -
class NearbyGatorsContent: ObservableObject, GatorsServiceListener { private let service: GatorsService @Published
var array: [NearbyGator] = [] init(service: GatorsService) { self.service = service service.addListener(listener: self) } func onGators(array: [NearbyGator]) { self.array = array } deinit { service.removeListener(listener: self) } } -
List size 0 0 GatorsService listeners ... NearbyGators Content ref
count 1 service ... HomeView ref count 1 model -
List size 0 0 GatorsService listeners ... NearbyGators Content ref
count 1 service ... HomeView ref count 1 model 1 -
List size 0 0 GatorsService listeners ... NearbyGators Content ref
count 1 service ... 2 HomeView ref count 1 model 1 -
List size 0 0 GatorsService listeners ... NearbyGators Content ref
count 1 service ... 2 HomeView ref count 1 model 1 1 -
List size 0 0 GatorsService listeners ... NearbyGators Content ref
count 1 service ... 2 HomeView ref count 1 model 1 1 ✔ ✔ -
func onGators(array: [NearbyGator]) { } .array = array private let
service: GatorsService @Published var array: [NearbyGator] = [] {abc class NearbyGatorsContent: ObservableObject init(service: GatorsService) { self.service = service service.addListener( listener: )def deinit { service.removeListener( } listener: )ghi GatorsServiceListener }nop self. self self }xxz , -
private let service: GatorsService @Published var array: [NearbyGator] = []
{abc class NearbyGatorsContent: ObservableObject init(service: GatorsService) { self.service = service service.addListener( )def deinit { service.removeListener( } )ghi }nop }xxz , func onGators(array: [NearbyGator]) { } .array = array self. GatorsServiceListener listener: listener: self self -
func onGators(array: [NearbyGator]) { } .array = array content? private
let service: GatorsService @Published var array: [NearbyGator] = [] {abc class NearbyGatorsContent: ObservableObject init(service: GatorsService) { self.service = service service.addListener( )def }xxz )ghi GatorsServiceListener {hij }klm }nop private class Listener : var content: NearbyGatorsContent? = nil deinit { service.removeListener( } listener: listener: self self -
func onGators(array: [NearbyGator]) { } .array = array content? private
let service: GatorsService @Published var array: [NearbyGator] = [] {abc class NearbyGatorsContent: ObservableObject init(service: GatorsService) { self.service = service service.addListener( )def }xxz )ghi GatorsServiceListener {hij }klm }nop private class Listener : var content: NearbyGatorsContent? = nil deinit { service.removeListener( } listener: listener listener: listener -
deinit { service.removeListener( } func onGators(array: [NearbyGator]) { } .array
= array content? private let service: GatorsService @Published var array: [NearbyGator] = [] {abc class NearbyGatorsContent: ObservableObject private let listener: Listener init(service: GatorsService) { self.service = service service.addListener(listener: listener )def self.listener = Listener() self.listener.content = self }xxz listener: listener )ghi GatorsServiceListener {hij }klm }nop private class Listener : var content: NearbyGatorsContent? = nil -
deinit { service.removeListener( } func onGators(array: [NearbyGator]) { } .array
= array content? private let service: GatorsService @Published var array: [NearbyGator] = [] {abc class NearbyGatorsContent: ObservableObject private let listener: Listener init(service: GatorsService) { self.service = service service.addListener(listener: listener )def self.listener = Listener() self.listener.content = self }xxz listener: listener )ghi GatorsServiceListener {hij }klm }nop private class Listener : var content: NearbyGatorsContent? = nil -
deinit { service.removeListener( } func onGators(array: [NearbyGator]) { } .array
= array content? private let service: GatorsService @Published var array: [NearbyGator] = [] {abc class NearbyGatorsContent: ObservableObject private let listener: Listener init(service: GatorsService) { self.service = service service.addListener(listener: listener )def self.listener = Listener() self.listener.content = self }xxz listener: listener )ghi GatorsServiceListener {hij }klm }nop private class Listener : var content: NearbyGatorsContent? = nil -
deinit { service.removeListener( } func onGators(array: [NearbyGator]) { } .array
= array content? private let service: GatorsService @Published var array: [NearbyGator] = [] {abc class NearbyGatorsContent: ObservableObject private let listener: Listener init(service: GatorsService) { self.service = service service.addListener(listener: listener )def self.listener = Listener() self.listener.content = self }xxz listener: listener )ghi weak GatorsServiceListener {hij }klm }nop private class Listener : var content: NearbyGatorsContent? = nil -
deinit { service.removeListener( } func onGators(array: [NearbyGator]) { } .array
= array content? private let service: GatorsService @Published var array: [NearbyGator] = [] {abc class NearbyGatorsContent: ObservableObject private let listener: Listener init(service: GatorsService) { self.service = service service.addListener(listener: listener )def self.listener = Listener() self.listener.content = self }xxz listener: listener )ghi weak GatorsServiceListener {hij }klm }nop private class Listener : var content: NearbyGatorsContent? = nil -
NearbyGators Content ref count 1 service listener ... List size
0 0 GatorsService listeners ... HomeView ref count 1 model -
NearbyGators Content ref count 1 service listener ... List size
0 0 GatorsService listeners ... HomeView ref count 1 model Listener ref count 1 content -
NearbyGators Content ref count 1 service listener ... List size
0 0 GatorsService listeners ... HomeView ref count 1 model Listener ref count 1 content + 1 weak -
NearbyGators Content ref count 1 service listener ... List size
0 0 GatorsService listeners ... HomeView ref count 1 model 1 Listener ref count 1 content + 1 weak -
NearbyGators Content ref count 1 service listener ... List size
0 0 GatorsService listeners ... HomeView ref count 1 model 1 Listener ref count 1 content 2 + 1 weak -
NearbyGators Content ref count 1 service listener ... List size
0 0 GatorsService listeners ... HomeView ref count 1 model 1 Listener ref count 1 content 2 + 1 weak -
NearbyGators Content ref count 1 service listener ... List size
0 0 GatorsService listeners ... HomeView ref count 1 model 1 Listener ref count 1 content 2 0 + 1 weak -
List size 0 0 GatorsService listeners ... HomeView ref count
1 model 1 Listener ref count 1 content 2 1 -
It’s Difficult One big graph of objects with different rules
for different objects: • Swift does reference counting • Kotlin does garbage collection -
class Holder { var target: Any? = null } fun
createSmallCycle() { val holder = Holder() val array = NSMutableArray() array.addObject(holder) holder.target = array } Retain Cycles without any Swift NSMutableArray ref count 1 size 1 0 Holder target -
It’s not a problem while the objects are still in
use The garbage collector can collect cycles as long as every property is declared in Kotlin A cycle* in the Big Graph with at least one NSObject** * ** Anatomy of a Leak -
How Swift Does It Every property is either an owned
reference (default) or one of two other kinds, weak and unowned iOS developers are always thinking about this! It’s as bad as Java developers worrying about null DispatchQueue.main.async { [weak self] in ... } -
How I Avoid Leaks in Kotlin/Native Never reference a Swift
object from a Kotlin object If I must break Rule #1: A. Give the referencing Kotlin class a close() function B. That function sets properties to null C. Make sure it gets called RULE #1 RULE #2 -
class DisplayLinkTarget( private var displayLink: CADisplayLink?, ) { ... fun
close() { displayLink?.invalidate() displayLink = null // Break a reference cycle. } } -
Why Break Cycles From Kotlin? Did ya get the latest
Kotlin alligators library? -
Did ya get the latest Kotlin alligators library? Yep! But
Xcode is mad ’cause it’s leaking memory -
Oh shit It’s equally valid to break cycles from Swift,
or a mix of both! Kotlin that doesn’t leak helps to earn iOS engineers' trust -
Detecting Leaks in Tests We built a LeakWatcher test helper
in Redwood Currently JVM-only It uses PhantomReference to check whether an object is referenced val leakWatcher = LeakWatcher { content.listener } content.close() leakWatcher.assertNotLeaked() -
Detecting Leaks in Production We also built a LeakDetector API
in Redwood Supports the JVM, Kotlin/Native, and Kotlin/JS It uses WeakReference to check whether an object is referenced leakDetector.watchReference( reference = listener, note = "content listener removed", ) -
Life Comes At You Swiftly Garbage Collection is great LeakCanary
is great iOS has neither Every Swift object is a resource you might need to release Xcode can be coerced into helping you -
Java’s Epic Refactor Kotlin already has a limited version with
@JvmInline value classes GOAL: HOW: PLUS: Increase the information density in the JVM’s memory By unifying primitives and references in the type system Better null handling! -
0 1 2 3 size Array distance name NearbyGator 7.64
String data "Lyle" distance name NearbyGator 9.58 String data "Smiley" 5.31 distance name NearbyGator String data "Chompy" 0 1 2 -
0 1 2 3 size Array distance name NearbyGator 7.64
String data "Lyle" distance name 9.58 String data "Smiley" 5.31 distance name String data "Chompy" 0 1 2 -
Valhalla Started in 2014 as a research project You can
download a build of JDK 23 that has value classes today