Blog Infos
Author
Published
Topics
, , , ,
Author
Published

This article is the second part of my series on how the garbage collector (GC) functions in Kotlin Multiplatform (KMP). If you haven’t yet, I recommend starting with Part 1 to build a solid foundation.

Now that you have a grasp of how the GC works in Android apps built with KMP, it’s time to delve into a more intricate aspect: Kotlin’s GC implementation in iOS.

However, before jumping into how KMP’s GC operates on iOS, it’s crucial to understand how garbage collection works in native iOS apps written in Swift or Objective-C. While this may be second nature to iOS developers, it’s valuable knowledge for Android developers stepping into the iOS world — after all, “to know the road ahead, ask those coming back.”

Understanding Garbage Collection in iOS: Swift

In Swift, memory management is handled differently compared to other languages like Java or Kotlin. Swift doesn’t use a traditional garbage collector. Instead, it relies on a system called Automatic Reference Counting (ARC). This method ensures efficient memory usage while providing a seamless experience for developers.

How ARC Works

In ARC, every time you create an instance of a class, Swift automatically tracks how many references are pointing to it. Each time a new reference is created, the count increases, and when a reference is removed, the count decreases. As soon as the reference count reaches zero, Swift deallocates the memory, effectively cleaning up unused instances.

This differs from a traditional garbage collector, which periodically checks for unused objects in memory and clears them. With ARC, deallocation happens immediately when an object is no longer needed, making memory management predictable.

Reference Cycles and Weak References

One challenge with ARC is the possibility of strong reference cycles, where two objects hold strong references to each other, preventing memory deallocation. To solve this, Swift provides weak and unowned references. A weak reference doesn’t increase the reference count and can be set to nil once the object it points to is deallocated. An unowned reference, on the other hand, assumes that the object it refers to will always exist for the lifetime of the reference.

ARC in Action

Swift’s ARC handles memory automatically for most scenarios, reducing the need for developers to manually manage memory. However, understanding how ARC works can help in optimizing memory usage and preventing issues like memory leaks.

You should definitely learn more about this in detail and there are lot of articles available on this so I won’t dive further.

Understanding Garbage Collection in iOS: Kotlin

The garbage collection (GC) in iOS apps operates through a combination of Kotlin Native’s GC and Automatic Reference Counting (ARC). This leads to a key difference depending on your development approach: whether you’re building the UI in SwiftUI while sharing business logic in Kotlin, or you’re also sharing the UI layer using Compose Multiplatform. Let’s dive into both scenarios to understand how they work.

Shared Business Logic in Kotlin, UI in SwiftUI: How ARC and Kotlin Native GC Coexist

When building an iOS app with Kotlin Multiplatform, a common pattern is to share business logic in Kotlin while creating the UI in SwiftUI. This introduces an interaction between two memory management systems: iOS’s ARC (Automatic Reference Counting) and Kotlin Native’s garbage collector.

  • Object Management: When Kotlin objects are passed from the shared Kotlin code to SwiftUI, they are wrapped in a DisposableHandle. ARC takes over from here and manages the lifecycle of these wrapped objects on the Swift side. Each time the object is referenced, ARC increments its reference count, and when it’s no longer needed, ARC decrements the count. Once the count hits zero, the object is deallocated.
  • Reference Tracking by Kotlin Native GC: Even though ARC manages these Kotlin objects on the Swift side, Kotlin Native’s garbage collector is still involved because the actual memory allocation and lifecycle of the Kotlin object remain under the control of Kotlin Native. Kotlin Native’s garbage collector tracks references internally and ensures that objects in circular references or other complex scenarios are properly deallocated.
  • Passing Swift Objects to Kotlin: The reverse situation is also possible: when Swift objects are passed to the Kotlin code, ARC continues to manage them. Kotlin Native, on the other hand, keeps track of these references on the Kotlin side. This ensures that even when objects cross language boundaries, neither ARC nor Kotlin Native deallocates them prematurely.
  • Memory Overhead: While this approach works well in many cases, there’s an overhead when objects need to be wrapped or unwrapped to transition between SwiftUI and Kotlin Native. Every time an object is transferred across the boundary, there is an additional layer of wrapping that could introduce performance costs, particularly when complex or frequent object transfers are involved.
Extending UI Sharing with Compose Multiplatform

Instead of keeping the UI in SwiftUI, another option is to share the UI code using Compose Multiplatform. This approach allows you to create the UI in Kotlin and share it across platforms, meaning the entire app (business logic and UI) runs on Kotlin. In this case, memory management shifts almost entirely to Kotlin Native’s garbage collector, with little interaction with ARC.

  • UI Rendering in Kotlin Native: Compose Multiplatform relies entirely on Kotlin’s memory management system. Because the UI code is now in Kotlin, ARC doesn’t play a role in managing the UI components. Kotlin Native’s garbage collector becomes responsible for managing all objects, including UI components, which would have been handled by ARC if SwiftUI were used.
  • Performance Considerations: Kotlin Native’s garbage collection is efficient but isn’t optimized for high-demand tasks like rendering complex or dynamic UI components. SwiftUI benefits from ARC’s fast, immediate deallocation of unused objects. However, in Kotlin Native, garbage collection cycles are less predictable and might introduce temporary memory spikes if not handled carefully.
  • In essence, while Compose Multiplatform simplifies the codebase by unifying the UI layer, the cost is less control over memory deallocation and potentially slower cleanup of UI components compared to SwiftUI with ARC.
  • Resource Management in UI: Since Kotlin Native doesn’t free up objects as quickly as ARC, using Compose Multiplatform for UI might result in larger memory footprints, especially when dealing with animations or high-frequency UI updates. This can affect the performance of your app in terms of responsiveness and memory usage, particularly in scenarios involving dynamic UIs.
Potential Pitfalls and Performance Considerations
  1. Retain Cycles: ARC struggles with circular references (or retain cycles), where two objects hold strong references to each other and neither can be deallocated. Kotlin Native GC, on the other hand, can detect and clean up circular references during garbage collection. However, if retain cycles occur at the boundary between Kotlin Native and Swift, managing these cycles becomes more complicated. You may need to manually intervene by using weak references in Swift or by ensuring proper nullability and lifecycle management in Kotlin.
  2. GC Delays: One of the major performance differences between ARC and Kotlin Native GC is how they handle memory deallocation. ARC deallocates objects as soon as the reference count hits zero. This is beneficial for performance-critical areas like UI rendering because memory is freed up promptly. Conversely, Kotlin Native’s garbage collector runs on a separate cycle. This means that even if objects are no longer in use, they may not be deallocated immediately, potentially causing memory bloat in situations that demand real-time memory management.
  3. Object Transfer Overhead: The overhead of transferring objects between Kotlin and Swift can lead to performance bottlenecks, especially when the app has frequent interactions between the UI and business logic layers. This becomes more noticeable in scenarios where large or complex objects are passed between Kotlin and Swift repeatedly, as both ARC and Kotlin Native need to wrap and unwrap these objects.
  4. UI Complexity with Compose Multiplatform: When sharing the UI with Compose Multiplatform, you rely entirely on Kotlin Native’s garbage collection. This can introduce memory management inefficiencies compared to SwiftUI, especially for apps with complex UI hierarchies or heavy animations. Compose is not yet as optimized for iOS as SwiftUI, which could result in slower performance in UI-heavy applications.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Writing Kotlin Multiplatform libraries that your iOS teammates are gonna love

Kotlin Multiplatform Mobile (KMM) is awesome for us Android Developers. Writing multiplatform code with it doesn’t diverge much from our usual routine, and now with Compose Multiplaform, we can write an entire iOS app without…
Watch Video

Writing Kotlin Multiplatform libraries that your iOS teammates are gonna love

André Oriani
Principal Software Engineer
Walmart

Writing Kotlin Multiplatform libraries that your iOS teammates are gonna love

André Oriani
Principal Software E ...
Walmart

Writing Kotlin Multiplatform libraries that your iOS teammates are gonna love

André Oriani
Principal Software Engine ...
Walmart

Jobs

Best Practices for Efficient Memory Management in KMP Projects
  1. Careful Object Lifecycle Management: In cases where objects cross the Kotlin-Swift boundary, it’s crucial to monitor how they are referenced. Both Swift and Kotlin developers should be aware of how strong references are managed and where potential retain cycles might form. Using weak references or explicitly nullifying references on the Kotlin side can help mitigate memory leaks or unnecessary memory retention.
  2. Optimize Cross-Language Calls: To minimize the overhead caused by frequent object transfers between Kotlin and Swift, optimize how often objects need to cross the language boundary. Each time you pass an object from Kotlin Native to Swift (or vice versa), the system incurs the cost of wrapping and unwrapping objects. Reducing the number of cross-language calls by grouping operations together, batching data transfers, or simplifying the interface between Kotlin and Swift can improve performance. For example, instead of calling Kotlin business logic repeatedly from Swift for small operations, consider aggregating tasks on the Kotlin side, returning the result once all calculations are complete. This reduces the interaction frequency between Kotlin GC and ARC, thereby lowering the performance overhead.
  3. Reduce Object Creation in UI Rendering: When using Compose Multiplatform, be mindful of how often objects are created, particularly within UI rendering loops. Excessive object creation can lead to high memory churn and put pressure on the Kotlin Native GC, which may not deallocate memory as quickly as ARC. Reuse objects wherever possible and structure your code to minimize unnecessary object instantiation, especially for short-lived UI components.
  4. Understand SwiftUI vs. Compose Memory Characteristics: SwiftUI with ARC offers instantaneous memory deallocation, whereas Compose relies on Kotlin Native’s GC, which might introduce delays. If your app is UI-heavy and performance-critical (e.g., animation or real-time data), SwiftUI’s ARC will likely provide better memory performance. On the other hand, Compose Multiplatform simplifies code sharing but requires extra attention to memory usage and efficient UI rendering.
  5. Manual Garbage Collection Control in Kotlin Native: Although Kotlin Native manages memory automatically, there are ways to exert more control when necessary. Kotlin Native provides mechanisms for manually triggering garbage collection through the GC.collect() function. This can be helpful in memory-sensitive areas where you want to ensure that unused objects are deallocated promptly.
  6. Monitor Memory Usage with Tools: Memory management issues, such as leaks, retain cycles, or excessive garbage collection delays, can be hard to catch without proper tools. Regularly profile your app’s memory behavior using tools like Xcode Instruments (for Swift/ARC) and built-in memory profilers. Identifying memory leaks, redundant allocations, or unexpected memory growth early will help you tune your app for optimal performance.
  7. Balance Between Shared and Native UI: If memory management performance becomes a bottleneck, consider whether you need to share the UI via Compose Multiplatform or if it makes more sense to use SwiftUI for the UI and share only the business logic. Native UIs will always be more efficient in managing platform-specific resources. For projects where UI performance is critical, sticking to SwiftUI while sharing Kotlin business logic can provide the best memory performance.
Conclusion

When building iOS apps using Kotlin Multiplatform, understanding how ARC and Kotlin Native’s garbage collector interact is key to optimizing performance and ensuring smooth memory management. Sharing business logic from Kotlin while building the UI in SwiftUI ensures native performance benefits but comes with some GC overhead. On the other hand, sharing the UI via Compose Multiplatform can simplify development but may introduce memory management challenges unique to Kotlin Native’s garbage collection system.

Ultimately, the choice between SwiftUI and Compose Multiplatform for your UI layer should balance memory management efficiency with code maintainability, based on your project’s requirements. Both approaches have their pros and cons, and understanding how ARC and Kotlin Native GC work together will help you make informed decisions as you navigate the complexities of Kotlin Multiplatform development.

Bonus

I’m sure you might have plenty of questions about GC after reading this, and I’m here to help. If there’s enough interest, I’ll create a demo app to showcase the GC in action across different scenarios: an Android app in Kotlin, an iOS app with SwiftUI, and an iOS app using Compose Multiplatform. The demo will compare performance, memory overhead, and GC behavior. Let me know in the comments if you’d like to see it!

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE
blog
With JCenter sunsetted, distributing public Kotlin Multiplatform libraries now often relies on Maven Central…
READ MORE
Menu