Are there any advanced steps possible to reduce th...
# multiplatform
p
Are there any advanced steps possible to reduce the size of the ios / watchos application? What we tried so far: • Move everything into a single gradle module and make everything internal that’s possible • Switching from dynamic to static linking (-1MB) • Not exporting any libraries Maybe some compiler flags that further reduce the size? We are currently blocked by this because the size of the watch (81mb) is larger than what apple allows (75mb)
😮 1
😬 2
p
Is it 81mb for code or the assets as well?
p
Approx 52mb for the framework alone
h
That's huge... Did you try it with Xcode 14? Maybe! this could reduce the code as well, because Xcode contains clang and framework build tools, or does Kotlin use its own tools?
p
Xcode 14, beta 6.
h
Do you disable bitcode: embedBitcode = BitcodeEmbeddingMode.DISABLE? For my playground app: ComposeTodo this results from 10 MB release iosArm64 to 7 MB with disabled bitcode... And bitmode is deprecated with iOS 16:https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes https://stackoverflow.com/questions/72543728/xcode-14-deprecates-bitcode-but-why. https://github.com/hfhbd/ComposeTodo/blob/ae72ecf3c8a3c69dfcaa5307181ad1619e48a44c/clients/build.gradle.kts#L32
p
Made our framework way smaller but the final one that you get back from the appstore has the same size
t
Hey, I am not sure if it will help, but this issue contains some compiler options that help with app size. Too big code size was an issue for watchOS arm32 so lots of tricks mentioned there too reduce it. I guess they could work for the other watchOS targets, too, that you are using? https://youtrack.jetbrains.com/issue/KT-37368/Native-compiler-fails-to-compile-big-projects
p
Lol, lots of comments by me 😄
t
Oh, did not notice 🙂
But that did not work or havent you tried it yet?
p
Are you sure that deceases the complete size?
t
I think so but I am not sure.
Sergey mentioned:
Clang might fail to compile files bigger than 16Mb (that’s Mach-O format limitation). By passing
-Os
instead of
-O0
we force Clang to generate smaller code which should help in some cases.
h
Ah you actually can change clang and its optimization, good to know
t
I mean I guess you can give it a try
p
Also iOS 16 is about to be released. Maybe we can get rid of arm32 and the fat framework alltogether
h
Wait, what kind of framework do you use? No XCFramework?
p
An xcframework, but that still only contains other frameworks
t
Removing arm32 would remove support for the watch series 3 so that’s up to you
p
Yep if that unblocks us we have to bear with that
h
Hm, Xcode only uses the actually framework in the xcframework. so I don't expect a size reduction
p
You’re sure about that for the fat framework?
Copy code
//see: <https://developer.apple.com/forums/thread/666335>
private fun Project.registerAssembleFatForXCFrameworkTask(
    xcFrameworkName: String,
    buildType: NativeBuildType,
    appleTarget: AppleTarget
): TaskProvider<FatFrameworkTask> {
    val taskName = lowerCamelCaseName(
        "assemble",
        buildType.getName(),
        appleTarget.targetName,
        "FatFrameworkFor",
        xcFrameworkName,
        "XCFramework"
    )

    return registerTask(taskName) { task ->
        task.destinationDir = XCFrameworkTask.fatFrameworkDir(project, xcFrameworkName, buildType, appleTarget)
        task.onlyIf {
            task.frameworks.size > 1
        }
    }
}
h
Hmm, let me try it. At the moment I have iosArm64 and its simulator counterpart, but I will add a watchos target. But what is the purpose of xcframework and its support for different targets if "you" still use a big fat framework? 🤔
p
I don’t know, ask 🍎 ^^
k
Fat frameworks shouldn't matter. App store slices things up before shipping them to users.
p
But the architecture of ie watch 4 is arm32_64 That reads like it contains both?
h
apple watch 4 uses the S4 an arm64 chip but yes, the chip supports arm32 as well as arm64 instructions, maybe to support older watchos apps? 🤔
I used my app ComposeTodo and played with some iOS/watchOS and static variants. I have two modules, shared and client. targets: iosArm64, iosSimulatorArm64, watchosArm32, watchosArm64, watchosSimulatorArm64 The code used by iOS and watchOS is the same, both stored in darwinMain. I only measure the release frameworks. shared (iOS only) + client (iOS only) & isStatic = false => iosArm64: 9,6 MB shared (watchOS + iOS) + client (iOS only) & isStatic = false => iosArm64: 9,6 MB shared (watchOS + iOS) + client (watchOS & iOS) & isStatic = false => iosArm64: 9,6 MB + watchos 14,9 MB shared (watchOS + iOS) + client (watchOS & iOS) & isStatic = true => iosArm64: 19,2 MB + watchos 30,2 MB I always used a clean build:
./gradlew clean assembleXCFramework
=> ios is always about 9,6 MB, enabling watchos target does not matter. => watchos is almost +50% in size, for unknown reasons => isStatic results into a huge size Link to branch: https://github.com/hfhbd/ComposeTodo/compare/main...watchos
So why does isStatic increase my module size, but decrease @Paul Woitascheks? 🤔
p
You need to measure what you get from AppStore connect
h
Measuring the internal framework (the binary) is not enough? I am glad I didn't delete the build artifacts 😄
p
Yes, apple does it's 🪄 first
s
It should be able possible to generate an App Thinning Size Report from Xcode that lists all the possible variants that get sliced & delivered to different devices from the App Store – I’d benchmark with those (https://medium.com/@gauravborole/measure-your-ios-app-ipa-size-2b0135daca2f)
s
• Move everything into a single gradle module and make everything internal that’s possible
Maybe split your logic into modules instead and use only relevant modules on the watchOS app?
h
This is exactly what Paul mean
p
That's not really possible because else, the watch and App can't have a shared layer
h
@Paul Woitaschek But yeah, you are right: The watchos framework watchos-arm64_32_armv7k contains both architecture in one framework, which causes the huge size. Maybe it is possible to split the target into watchos-arm32 and watchos-arm64
p
@Sebastian Sellmair [JB] do you have an idea if that's possible?
h
As far as I understand @sergey.bogolepov it also needs a proper arm64 watchos target? 🤔
s
it also needs a proper arm64 watchos target?
Why?
the watch and App can’t have a shared layer
Why? Take one set of modules, compile it into watchOS framework. Take its superset, compile in for iOS.
s
That’s another thread and another problem. Why do you think that arm64 watchOS target is relevant here? :)
p
The watch and App have a package that contains common code for the shared Framework. If we now have two frameworks - iOS and watch, there can't be a shared layer no more. In Gradle, you could create three modules, but you can't depend have multiple kotlin frameworks accessing each other.
Is it true that the fat frameworks have the double size and if it's true is there a way to prevent that?
h
Yeah, they are overlapping 😄 I guess, the main reason for this huge watchos binary is the fat binary containing arm32 as well as arm64 shared libary: left iosArm64 binary, right arm64_32_armv7k. So maybe you could switching to arm64 only to drop 32 bit support and its code but then you cant support watchos 32 bit devices anymore, if it is possible at all.
s
If we now have two frameworks - iOS and watch, there can’t be a shared layer no more.
I have a feeling that it still should be possible in some form, but need to check first. Anyway, thanks! A proper solution would require multiple namespaces in the generated frameworks, so you could have
shared.h
and
ios_specific.h
. Good news: we are working on it. Bad news: the feature is big and amount of pitfalls is enormous 🙃
h
Regarding splitting the fat binaries into arm32 and arm64. I created a sample with 6 kotlin targets: iosarm32, iosarm64, iossimulatorarm64, watchosarm32, watchosarm64, and watchossimulatorarm64. my initial expectation was to get one xcframework with 6 inner frameworks. but there are only 4: ios-arm64_armv7 and watchos-arm64_32_armv7k and its simulator frameworks. arm64 is 64-bit (iosarm64), armv7 (iosarm32) and armv7k (watchosarm32) are 32-bit, and arm64_32 is watchosarm64 with 32-bit length pointer. So the xcframework contains 4 platforms, which include several fat binaries for each architecture. iosarm32 is deprecated, the last iPhone with 32-bit was the iPhone 5, last os iOS 10. So it is not used anymore. If you remove the iosarm32 kotlin target, you get an xcframework which contains not an ios fat binary with 1 arch, but a sharedlibrary for iosarm64 only, "at the top" of xcframework. The problem here is a watchos module using watchos32 and watchos64 kotlin targets to support 32-bit (Apple Watch Series 3, still sold today) and the newer 64-bit watches Apple Watch Series 4 and up. Using the two targets, you get one framework with a fat binary containing 2 shared libs. This fat binary is too big. So one option would be to split this fat binary and move the two shared libs directly into the xcframework, like iosarm64. I tried this with a sample project: first use only watchosarm32 target, then watchosarm64 and copy the resulting sharedlibraries into a xcframework. Unfortunately, it looks like the xcframework only supports 1 platform framework at the top, I get this error in Xcode: Both 'watchos-arm64' and 'watchos-armv7k' represent two equivalent library definitions. And according to this thread it is not supported: https://developer.apple.com/forums/thread/666335?answerId=685927022#685927022 I attached my project, which also contains my manually created XCframework with 6 archs at the top, which does not work. So I think, splitting the fat binary is not possible and you have to use other options to reduce the binary size, maybe with clang: https://clang.llvm.org/docs/CommandGuide/clang.html#cmdoption-o0
p
Shitty solution that we applied: drop watch arm 32 support
h
I am curios what will happen with the new watchos arm 64 target? https://youtrack.jetbrains.com/issue/KT-53107/Add-arm64-support-for-watchOS-targets-Xcode-14
t
I expect that to be the same issue. So obviously dropping arm32 support is a temporary solution until we are forced to support the new arm64 target.
Once we need to include the new arm64 target there are two frameworks again, same issue I guess.
It just doesn’t make sense that Apple requires the total app size to be under 75MB.
k
It’s odd that they don’t count the architecture-specific size. Their size estimate should only consider arm32 or arm64 on it’s own, as that’s the size that the installed app should take up on the device. I’m not as clear on how the watch architectures work, though.
t
message has been deleted
As you can see from the screenshots Apple is counting the total size of the app, so for all architectures combined.
447 Views