The Hidden Bloat in KMP iOS Headers

Developers building iOS applications with Kotlin Multiplatform (KMP) or Compose Multiplatform often encounter a surprisingly large Shared.h header file, sometimes exceeding 20,000 lines. This isn't just a cosmetic issue; it can indicate significant overhead. In a recent optimization effort for a personal KMP app, I observed that the generated header included Objective-C classes for every theme color, dimension constant, and internal state model, none of which were directly used by the Swift codebase. This generated code represents substantial dead weight, increasing build times and potentially complicating the integration process without providing any functional benefit.

To quantify and address this problem, I developed a Gradle plugin called kmprofiler. This tool parses the generated Objective-C header file and cross-references it with the Swift source files in the project. It then highlights any exported declarations from the Kotlin side that have zero call sites in the Swift code. The results were striking: 61% of the exported declarations in my app's header were entirely unused by Swift. While initially surprising, cleaning up this bloat proved to be a quick process.

Example of kmprofiler identifying unused KMP declarations in an iOS project

Why Kotlin/Native Exports So Much

The extensive generation of Objective-C code from Kotlin Multiplatform stems from Kotlin/Native's interoperability model. When you expose Kotlin code to Swift or Objective-C, Kotlin/Native generates Objective-C equivalents for many Kotlin constructs to ensure seamless communication. This includes not just public functions and classes, but also properties, constants, enums, and even internal data structures that might be part of the generated Objective-C API surface, regardless of whether they are directly consumed by the Swift consumer.

The default behavior aims for maximum discoverability and potential use, assuming that anything exposed might eventually be needed. However, in practice, many of these generated elements are internal implementation details or boilerplate that Swift developers never interact with. This over-generation leads to the bloated headers. Consider the analogy of a comprehensive instruction manual for a product that includes detailed schematics for every single screw and washer, even if the end-user only ever needs to plug it in. The manual is complete, but a significant portion is irrelevant to the user's immediate task and adds bulk.

Introducing kmprofiler: The Solution

The kmprofiler Gradle plugin tackles this problem head-on. It automates the process of identifying unused exported declarations. After a KMP project is built, the plugin performs the following steps:

  • Parses the Generated Header: It reads the Shared.h file (or equivalent) produced by Kotlin/Native.
  • Analyzes Swift Sources: It scans all the Swift files within the iOS application target.
  • Identifies Call Sites: It determines which Objective-C declarations generated from Kotlin are actually referenced in the Swift code.
  • Reports Unused Declarations: It outputs a list of all Kotlin-exported elements that have no corresponding call sites in Swift, along with their percentage of total exported declarations.

The plugin is designed to be simple to integrate into an existing KMP project via the Gradle build system. By providing a clear report of dead weight, it empowers developers to make informed decisions about what truly needs to be exported and to clean up unnecessary code, thereby improving build performance and maintainability.

The Impact of Cleaning Up

Reducing the size of the Shared.h header file can have several positive effects on the KMP development workflow for iOS applications:

  • Faster Compile Times: A smaller header file means less work for the Swift compiler during incremental builds, leading to quicker iteration cycles.
  • Reduced Build Complexity: Less generated code can simplify the build process and reduce the chances of unexpected issues arising from complex Objective-C bridging.
  • Improved Codebase Clarity: By removing unused exports, the public API surface presented to Swift developers becomes cleaner and more focused on essential functionality.

The process of cleaning up the dead weight identified by kmprofiler often involves adjusting the @ExportObjCDeclaration annotations or simply removing unnecessary Kotlin code that was being exposed. In my case, the 61% reduction in unused declarations was achieved with minimal effort, demonstrating that this is a practical and worthwhile optimization for KMP projects.

What remains unaddressed, however, is whether Kotlin/Native's default export behavior will evolve to become more intelligent over time, perhaps by inferring usage or providing more granular control over what gets exposed without manual annotation. For now, tools like kmprofiler provide a necessary safeguard.

Conclusion

The discovery of significant dead weight in KMP iOS export headers is a critical insight for developers using Kotlin Multiplatform. The kmprofiler tool offers a practical solution to identify and eliminate this bloat, leading to tangible improvements in build performance and code clarity. By proactively managing the generated Objective-C interface, developers can ensure a more efficient and maintainable cross-platform development experience.