Why is the Swift Reference Type adversely affecting app launch time?

Tram Ho

The first impression users experience is the app launch experience. Every millisecond they wait for your application to start is valuable time they can spend elsewhere. If your application has high access and is used more than once a day then the user has to wait for launch several times. Apple recommends that the first frame be drawn below 400ms. This ensures your app is ready to be used when the Springboard animation of your app ends. With just 400ms, developers need to be careful not to accidentally increase app launch time. However, launching an app is a complex process with so many parts it’s difficult to know exactly what it contributes. We began to dig deeper into the relationship between binary size and startup time while working with Emerge , the app size profiler. In this post we’ll shed some more light on one of the more esoteric aspects of the application and show you how reference types contribute to the binary size and make the app boot slower.

Dyld

Your App starts when the Macho-O executable is loaded by dyld. Dyld is Apple’s program is responsible for getting an app ready for use. It runs in the same process as the code you write and starts by loading all the frameworks, including the system frameworks.

Part of the job of dyld’s is to “rebasing” the pointer in the binary metadata which describes the types in your source code. This metadata enables dynamic runtime features, but can be a common source of binary size scaling. Here’s the layout of an Obj-C class found in our compiled app binary:

Each UInt64 is m, an address of another piece of metadata. This is in app binary, so everyone around the world downloaded the same data from the App Store. However, each time your app is started it is placed in a different location in memory (as opposed to always starting at 0) because of address space layout randomization (ASLR). This is a security feature designed to make it difficult to predict a specific function in memory.

So the problem with ASLR is that the hardcoded address in your app is now wrong, offset by a random start location. Dyld is responsible for fixing this by rebasing all pointers up to the single starting position. This process is done for every pointer in your executable and all dependent frameworks, including recursive dependencies. There are other types of metadata settings implemented by DYLD that affect boot time, such as “binding”, but for this article, we will only focus on rebasing.

All of these pointer settings increase application start-up time, so reducing it in app binary will make the start time faster. Let’s see where it comes from and what exactly it could impact.

Swift and Obj-C

We see that rebase time is due to Obj-C metadata in app, but what exactly is causing metadata in Swift app? Swift has @objc attribute to display declarations from Objective-C code, but metadata is generated even when Swift type is not visible with Objective-C code. This is because all of the class types that contain Objective-C metadata in Apple platforms Watch this in action with the following code:

This is pure Swift code, it does not inherit from NSObject and does not use @objc . However, it will generate an Obj-C class metadata in the binary and add 9 pointers that it needs rabasing. To demonstrate it, look at the binary with a tool like Hopper and look at the objc_class entry for your “pure Swift” class:

Obj-C metadata in the app binary

You can see the exact number of pointers needed to run an application by setting the environment variable of DYLD_PRINT_STATISTICS_DETAILS to 1. It will print out the total number of rebae fixups on the console after the app has started. We can even figure out exactly where these 9 pointers are

Not all swift types need some rebase. If you expose methods to Obj-C by overriding the superclass or confirm with an Obj-C protocol you will need to add more rebases. In addition, every property in the Swift class will create an ivar in the Objective-C metadata.

Measure

App launch times affected by rebase will vary based on device type and others running on the phone. I have measured it on one of the oldest devices iPhone 5S.

Starting iOS can be classified as warm or cool. Warm when the system has launched the app and stores some DYLD setting information. Since the first boot I experimented with a cool start it was a bit slower.

In this case, we should see ~ 1ms increase over 2000 rebase activity. This would not be an absolute number for boot time since some operations can be performed in parallel, but it gives us a lower limit and with 400k rebases we used half of the limit. Apple’s recommended term is 400ms.

For example

Try measuring the amount of rebase activity in a few popular apps.

Tiktok has more than 2 million rebases, this result in the entire boot time! Tiktok uses Objective-C, but I’ve also tested a few of the biggest Swift applications that use the monolithic binary architecture (as opposed to the framworks) and found between 685k and 1.8m rebase.

So what should we do?

Although each class increases Rebase operations, I would not recommend replacing every Swift class with a struct. Large structs can also increase the binary size and in some cases you only need reference semantics. As with any performance improvement, you should avoid early optimization and start with measurements. Emerge can determine how many rebases are in your app, which modules are coming from, and which types of those modules are the biggest contributor. Once you’ve measured the problem you can look for improvement areas in your own application. Here are a few common ones:

Composition vs Inheritance

Let’s say you have:

This will generate a lot of metadata, but you could denote the same idea with value types preferred for a data class and end up with a rebase less than 22%. This involves replacing object inheritance with value compositions, such as enum with associated values ​​or generic types.

Categories in Swift

Although Swift uses extension and does not use categorues, you can still generate category binary metadata by defining an extension using the Objective-C function. For example:

Both functions include binary metadata, but when they are declared in the extension they are referenced by a synthesized category in TestClass . Move these functions into the original class declaration to avoid adding metadata overhead in the binary. This type of metadata can be automatically flagged with Emerge’s binary analysis tools.

Going one step further, you can avoid ojbc entirely by using closure-based callbacks – introduced in iOS 14 .

Many Properties

Each property in a Swift class adds 3-6 rebasing fixups, depending on if the class is final . They can actually be incremented for larger classes with more than 20 properties. For example:

Putting it in the struct will reduce the rebase fixups by up to 60%:

Codegen

One of the highest ROI changes you can make is improving the codegen. A common use of CodeGen is to create data models that are shared across codebases. If you are doing this with multiple types, you should be wary of the amount of OBJ-C metadata it can add. However, even the value types have an overhead in code and rebase fixups. The best solution would be to minimize the number of cidegebed types, even replace custom types with generated functions.

These examples are just a few of the ways that binary size can lead to increased boot time. Another reason is that the time it takes to load code from disk into memory, the more code you have, the longer it will take.

This article is over. Source: https://medium.com/geekculture/why-swift-reference-types-are-bad-for-app-startup-time-90fbb25237fc

Share the news now

Source : Viblo