Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GC You Later, Allocator

Jesse Wilson
September 17, 2024

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

September 17, 2024
Tweet

More Decks by Jesse Wilson

Other Decks in Programming

Transcript

  1. Kitchen Counters You allocate space You do stuff in that

    space You clean it up and start again
  2. Counter Space is Precious More counter space lets you do

    more things! When counter space is tight, you thrash
  3. /** * 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>) } }
  4. 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 } }
  5. class NearbyGatorsContent: ObservableObject, GatorsServiceListener { @Published var array: [NearbyGator] =

    [] init(gatorsService: GatorsService) { gatorsService.addListener(listener: self) } func onGators(nearbyGators: [NearbyGator]) { self.array = nearbyGators } }
  6. @Composable fun Content() { if (showNearbyGators) { NearbyGatorsScreen( content =

    NearbyGatorsContent(service), onDismiss = { showNearbyGators = false }, ).Show() } ... }
  7. struct ContentView: View { ... var body: some View {

    Button( ... ) .sheet(isPresented: $showNearbyGators) { NearbyGatorsView( NearbyGatorsContent(service), $showNearbyGators ) } } }
  8. val chompy = NearbyGator( name = "Chompy", distance = 5.5,

    ) NearbyGator name "Chompy" distance 5.5
  9. val chompy = NearbyGator( name = "Chompy", distance = 5.5,

    ) String data "Chompy" NearbyGator name "Chompy" distance 5.5
  10. val chompy = NearbyGator( name = "Chompy", distance = 5.5,

    ) String data "Chompy" NearbyGator name "Chompy" distance 5.5
  11. 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
  12. 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
  13. 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
  14. 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 ...
  15. 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 ...
  16. 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 ...
  17. 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 ...
  18. 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 ...
  19. 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 ...
  20. 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 ...
  21. 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 ...
  22. 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 ...
  23. 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 ...
  24. 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 ...
  25. val chompy = NearbyGator( name = "Chompy", distance = 5.5,

    ) 6 C h o m p y 5.5 ... null ... ...
  26. val chompy = NearbyGator( name = "Chompy", distance = 5.5,

    ) 0x06 0x43 0x68 0x6f 0x6d 0x70 0x79 0x40160000000000 ... 0x00 ... ...
  27. 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
  28. RAM: Random Access Memory Like a ByteArray(4294967296) Keeps state for

    the OS and all running apps Physical Memory is a Big ByteArray!
  29. 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") } } }
  30. 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06

    0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x
  31. var nextOffset = 160000 0x99 0xfe 0xb0b15c00 0x0465 0x00 0x09

    0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x
  32. var nextOffset = 160000 val referenceSize = 4 0x99 0xfe

    0xb0b15c00 0x0465 0x00 0x09 0xcafebabe 0x08675309 0x43 0x06 0x68 0x6d 0x6f 0x79 0x70 0xc0 4016000000000000 0x
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. List size 1 0 NearbyGator name "Lyle" distance 4.57 NearbyGators

    Content list HomeActivity scope ... model
  53. 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
  54. 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
  55. 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
  56. 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 ✔ ✔ ✔ ✔ ✔
  57. NearbyGators Content list NearbyGator name "Smiley" distance 7.64 NearbyGator name

    "Chompy" distance 9.58 List size 2 0 1 HomeActivity scope ... model ✔ ✔ ✔ ✔ ✔
  58. 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>
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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 } }
  67. 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 } }
  68. class NearbyGatorsContent: ObservableObject, GatorsServiceListener { @Published var array: [NearbyGator] =

    [] init(gatorsService: GatorsService) { gatorsService.addListener(listener: self) } func onGators(array: [NearbyGator]) { self.array = array } }
  69. class NearbyGatorsContent: ObservableObject, GatorsServiceListener { @Published var array: [NearbyGator] =

    [] init(gatorsService: GatorsService) { gatorsService.addListener(listener: self) } func onGators(array: [NearbyGator]) { self.array = array } }
  70. 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") } } } }
  71. 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
  72. /** * 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
  73. XCODE TIPS 5 FLATTEN KOTLIN/NATIVE ALLOCATIONS PAY ATTENTION TO ZOOM

    PRINT DESCRIPTION 1. 2. 3. 4. 5. STACK LOGGING FORCE GC
  74. 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
  75. YourKit Too Export an .hprof file from Android Studio Open

    that in YourKit Much more capable than Android Studio’s heap viewer
  76. 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) } } }
  77. 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) } }
  78. 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) } }
  79. 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) } }
  80. Just Use LeakSwift Just joking! iOS doesn’t have a P-Y

    There’s a couple of libraries but nothing as easy or capable
  81. 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 } } }
  82. 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 } }
  83. 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 }
  84. 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) } }
  85. List size 0 0 GatorsService listeners ... NearbyGators Content ref

    count 1 service ... HomeView ref count 1 model
  86. List size 0 0 GatorsService listeners ... NearbyGators Content ref

    count 1 service ... HomeView ref count 1 model 1
  87. List size 0 0 GatorsService listeners ... NearbyGators Content ref

    count 1 service ... 2 HomeView ref count 1 model 1
  88. List size 0 0 GatorsService listeners ... NearbyGators Content ref

    count 1 service ... 2 HomeView ref count 1 model 1 1
  89. List size 0 0 GatorsService listeners ... NearbyGators Content ref

    count 1 service ... 2 HomeView ref count 1 model 1 1 ✔ ✔
  90. 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 ,
  91. 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
  92. 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
  93. 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
  94. 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
  95. 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
  96. 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
  97. 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
  98. 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
  99. NearbyGators Content ref count 1 service listener ... List size

    0 0 GatorsService listeners ... HomeView ref count 1 model
  100. NearbyGators Content ref count 1 service listener ... List size

    0 0 GatorsService listeners ... HomeView ref count 1 model Listener ref count 1 content
  101. 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
  102. 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
  103. 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
  104. 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
  105. 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
  106. List size 0 0 GatorsService listeners ... HomeView ref count

    1 model 1 Listener ref count 1 content 2 1
  107. It’s Difficult One big graph of objects with different rules

    for different objects: • Swift does reference counting • Kotlin does garbage collection
  108. 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
  109. 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
  110. 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 ... }
  111. 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
  112. class DisplayLinkTarget( private var displayLink: CADisplayLink?, ) { ... fun

    close() { displayLink?.invalidate() displayLink = null // Break a reference cycle. } }
  113. Did ya get the latest Kotlin alligators library? Yep! But

    Xcode is mad ’cause it’s leaking memory
  114. 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
  115. 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()
  116. 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", )
  117. 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
  118. 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!
  119. 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
  120. 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
  121. Valhalla Started in 2014 as a research project You can

    download a build of JDK 23 that has value classes today