Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Size of JRE.xcframework exceeds 75MB limit of appstoreconnect #2241

Open
herbertvuijk opened this issue Nov 23, 2023 · 2 comments
Open

Size of JRE.xcframework exceeds 75MB limit of appstoreconnect #2241

herbertvuijk opened this issue Nov 23, 2023 · 2 comments

Comments

@herbertvuijk
Copy link

herbertvuijk commented Nov 23, 2023

I'm using the JRE.xcframework in my own framework. I've built the JRE.xcframework for arm64_32 and armv7k. I can see that that all worked well, because there is a corresponding folder. So my full set up is: My project -> links my framework -> links JRE.xcframework

When uploading my app, is not exceeds the 75MB treshold by a tiny margin:
"ITMS-90389: Size limit exceeded - The size of the watchOS variant at "/Payload/Surfr..app/Watch/Surfr..app" is 75MB, which exceeds the maximum allowable size of 75MB after app thinning. For details about reducing your app's size, view: https://developer.apple.com/documentation/xcode/reducing-your-app-s-size"

I followed the instructions and indeed found out that in some of the thinned IPA's there is a Surfr..app in the Payload/Watch folder of 75.8 mb. This is mainly because of the linked binary of the JRE.xcramework, which is already 68MB (approx 32mb per architecture). If I remove one of the architectures (and build JRE.xcframework, my framework , and my app again) than the size of the Payload/Watch/Surfr..app drops to 43MB.

It seems like my thinned IPA's aren't really thinned/splitted per architecture. I would expect that one of my thinned IPA's would never contain more than one architecture.

What is expected here and how can I resolve this issue so that in my thinned IPA's only one architecture is included and I'm not exceeding the appstore limit?

Here a picture of the linked JRE.xcframework, which make my watch IPA increase with 68MB, because the JRE.xcframework contains two frameworks.

Screen Shot 2023-11-23 at 08 39 09

Here the actual problem, a 75.8MB watch IPA within my app archive bundle (thinned ipa's). If I built the JRE.xcframework for, for example only ARM64_32, then the size drops to +- 40MB. But appstore requires both ARM64_32 and ARMv7k so dropping an architecture is not an option. My main question is though, why does including an extra architecture lead to lineair increase in IPA size. I would expect that the app splitting/thinning proces would take care of that and the IPA would be splitted per architecture.

Screen Shot 2023-11-23 at 08 41 06
@herbertvuijk
Copy link
Author

herbertvuijk commented Nov 23, 2023

The problem becomes more clear when I just built my framework. If I build my framework with architecture arm64_32 my build product becomes 37MB
Screen Shot 2023-11-23 at 08 48 54

If I build my framework with architectures arm64_32 and armv7k my built product become 72mb. My app that bundles this framework adds a few more MB's and exceeds then the 75mb
Screen Shot 2023-11-23 at 08 49 54

I would expect that the app thinning process would only select the relevant slice of my framework / the JRE framework but it doesnt seem to do that since I'm hitting the 75MB limit.

ChatCPT:
"When you build a framework for multiple architectures, the resulting binary can contain slices for each supported architecture. These slices include code specifically compiled for each architecture, allowing the framework to be used on a variety of devices. When you create a thinned IPA for your app, the App Store will only include the necessary slices for the target device.

However, the size of the thinned IPA can still be affected by the inclusion of additional architectures in your framework. This is because the thinned IPA needs to include all the necessary slices for the target architecture, even if some of them are not used.

In your case, when you build the framework for both arm64_32 and armv7k, the resulting binary includes slices for both architectures. When you use this framework in your app and create a thinned IPA for a specific device, the IPA needs to include the slices for that device's architecture. If the device supports both arm64_32 and armv7k, the thinned IPA will include both sets of slices, leading to a larger IPA size.

On the other hand, when you build the framework only for arm64_32, the resulting binary only includes slices for that architecture. When you create a thinned IPA for a specific device, it will only include the slices for that architecture, resulting in a smaller IPA size.

To reduce the size of the thinned IPA, you may consider building the framework for the specific architectures you intend to support in your app. If you don't need to support older devices that use armv7k, you can exclude that architecture from your build to reduce the size of the resulting thinned IPA."

So if this is expected, then the JRE.xcframework for arm64_32 and armv7k already takes almost 70MB of my watch app leaving only 5MB for the rest..

I realize now that I also have the option, instead of using the JRE xcframework to use the .a files once again, and only linking in jre_core.. but still that will also result in significant size of the IPA's when building for more arechitectures.

@tomball
Copy link
Collaborator

tomball commented Nov 27, 2023

Most iOS projects build with the -ObjC flag (the default in new Xcode projects), which tells the linker to include every .o file in every static library whether any are used or not. That flag makes it easier to build apps that dynamically load classes and/or define Objective-C categories. The cost is that apps that are built with static libraries, like j2objc, can be bigger due to unused code being included.

Compounding this problem is that the JRE is huge, including the JRE framework makes apps much bigger. So linking with the JRE.framework is really just for development and testing. Published apps need to be trimmed down somehow.

The easiest thing to try is to remove the -ObjC flag and test the app thoroughly to verify that any code using dynamically loaded classes still works (look for Class.forName(<class-name>) calls). If you define any Objective-C categories, those need to be tested as well. Unfortunately, not all apps have rigorous integration tests, so those teams want to keep that flag even though it causes bloat.

That's why the JRE subset libraries exist. We broke the JRE into chunks where possible, consisting of a jre_core (the minimum all j2objc apps need), plus subsets for JRE specifics like networking, concurrency, XML, NIO, etc. You'll see them all by listing j2objc/dist/lib/iphone/libjre_*.a. The list of which JRE classes are in which subsets is defined in j2objc/jre_emul/jre_sources.mk, but it's often easier to start with core and inspect the "missing symbol" linker errors. Your app uses XML? Then it probably needs to link in libjre_xml.a. java.util.concurrent references? That's libjre_concurrent.a. And so on...

This isn't a perfect solution, though, as some JRE subsets reference other subsets. The subset for the java.time package is the worst, because the Android team implemented it by porting a large subset of ICU4J, causing the libjre_time.a to add ~45Mb to an app. Many teams therefore have switched to JodaTime, which though it's also big, at least just contains the code needed for time and calendar support.

If you need JRE subset frameworks instead of libraries, in the j2objc/jre_emul directory run build_subset_frameworks.sh.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants