If you are building an iOS app with Kotlin Multiplatform (KMP) or Compose Multiplatform, you might have opened your generated Shared.h header at some point and wondered why it is 20,000+ lines long.
I ran into this recently while optimizing one of my personal KMP apps. I kept seeing Objective-C classes generated for every single theme color, dimension constant, and internal state model, even though my Swift code never touched any of them.
To get a clear picture of what was actually going on, I built a small Gradle plugin called kmprofiler. It parses the generated Objective-C header, scans your Swift source files, and highlights which exported declarations have zero call sites in Swift.
The numbers on my app caught me off guard, but cleaning it up took just a few minutes.
Why does Kotlin/Native export so much?
In Kotlin, declarations are public by default.
When targeting iOS, the Kotlin/Native compiler looks at every public class, top-level function, and property in your shared module and creates an Objective-C class interface and runtime method trampolines in the framework binary.
The compiler cannot dead-strip these automatically because Objective-C relies on dynamic dispatch. It has to assume Swift or Objective-C could call them at runtime.
If your UI is built with Compose Multiplatform or your Swift app only interacts with a couple of high-level bridge interfaces, most of those exported Objective-C wrappers end up being dead weight.
The Audit: 459 Exports, 282 Unused
When I ran kmprofiler on my app (Framed), it gave me this breakdown:
### 📊 KMP iOS Export Profile
Export surface: 459 Kotlin declarations exported to Objective-C.
No direct Swift call site found for 282 of them (61.4% uncalled).
The unused exports mostly fell into three buckets:
-
File Facades (
*Ktclasses): Top-level properties in files likeDimens.kt(38 spacing constants) orColor.ktgenerated synthetic Objective-C classes likeDimensKtwith static getters for every single constant. -
Internal UI State and Callbacks: Internal data classes like
HomeScreenCallbacks(23 callbacks) and state models were leftpublic, creating full Objective-C class descriptors. - Leaked Library Types: Third-party classes from Compose and Ktor leaked into the bridging header because of exposed function signatures.
Cleaning It Up
I went through 5 files in my shared module and adjusted their visibility:
1. Making theme files internal:
// Dimens.kt / Color.kt / Type.kt
package com.framed.app.ui.theme
// Before: val spacing16 = 16.dp
internal val spacing16 = 16.dp
internal val beige = Color(0xFFF1E5D7)
2. Making UI models internal:
// Before: data class HomeScreenCallbacks(...)
internal data class HomeScreenCallbacks(
val onRefresh: () -> Unit = {},
// ...
)
(If you have classes that need to stay public for Android modules, you can use @OptIn(ExperimentalObjCRefinement::class) @HiddenFromObjC to hide them only from iOS).
The Numbers
After making those 5 files internal, I recompiled the release framework (./gradlew linkReleaseFrameworkIosArm64) and checked the results:
| Metric | Before | After (5 files fixed) | Delta |
|---|---|---|---|
Shared.h Header Lines |
23,622 lines | 13,327 lines | -10,295 lines (-43.6%) |
| Header File Size | 1.30 MB | 0.71 MB | -587.5 KB |
Shared.framework.o (Mach-O Object) |
80.1 MB | 74.4 MB | -5.70 MB saved |
| Total Framework Archive | 249.6 MB | 243.9 MB | -5.72 MB saved |
| Uncalled Exports | 282 declarations | 163 declarations | 119 dead exports removed |
Cleaning up those 5 files removed over 10,000 lines from the header and shaved 5.7 MB off the compiled object binary.
Trying It on Your Own Project
I published kmprofiler (v0.1.0) to the Gradle Plugin Portal if you want to inspect your own KMP project.
1. Apply the plugin in your shared module's build.gradle.kts:
plugins {
id("io.github.siddhantpanhalkar.kmprofiler") version "0.1.0"
}
kmprofiler {
headerFile.set(
layout.buildDirectory.file("bin/iosArm64/releaseFramework/Shared.framework/Headers/Shared.h")
)
swiftSourceDirs.setFrom(layout.projectDirectory.dir("../iosApp"))
isStatic.set(true)
}
2. Run the task:
./gradlew analyzeKmprofiler
It prints a summary to your terminal and saves a report to build/reports/kmprofiler-report.md.
Next Steps
Right now I'm working on v0.2.0 to parse Xcode Link Maps (-Xlinker -map), which will calculate the exact linked byte attribution per Kotlin library in your final iOS binary. After that, I plan to add baseline diffing for CI so you can catch export bloat in pull requests.
Repo is here: https://github.com/SiddhantPanhalkar/kmprofiler
Feel free to try it out and let me know if you run into any issues or have suggestions!
Top comments (0)