Static VS dynamic frameworks in Swift: an in-depth analysis
There has been quiet some discussion lately in the Swift community about the benefits of using static instead of dynamic frameworks (see 1, 2, 3). I was specially interested in this post, that explains how switching from dynamic to static frameworks can cut your app’s launch time in half.
I decided to test this by myself. The app I am working on at the moment depends on a total of 27 dynamic frameworks, 6 consumed using Carthage and 21 through CocoaPods.
The first two sections of this post explain how I generated static frameworks with Carthage and CocoaPods. The third one shows the results I got, in terms of app size and launch times. If you are just interested in the conclusions, scroll to the bottom.
Generate static frameworks: Carthage
In order to generate static frameworks using Carthage, the Xcode projects under the Carthage/Checkouts
folder have to set the MACH_O_TYPE
build setting to staticlib
. Using this and this as a reference, I came up with the following script:
xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX)
echo "MACH_O_TYPE = staticlib" >> $xcconfig
export XCODE_XCCONFIG_FILE="$xcconfig"
carthage bootstrap --platform ios --no-use-binaries
- The two first lines generate a temporary
xcconfig
file with theMACH_O_TYPE
build setting set tostaticlib
. - The third line sets the path to the temporary
xcconfig
file in theXCODE_XCCONFIG_FILE
environmental variable.xcodebuild
overrides the build settings of the Xcode project with the ones found inside theXCODE_XCCONFIG_FILE
. - The fourth line runs Carthage, which uses
xcodebuild
under the hood to generate the frameworks, and then places the static ones under the pathCarthage/Build/iOS/Static
.
Using this method there is no need to modify the Xcode projects found under Carthage/Checkouts
.
Generate static frameworks: CocoaPods
In order to generate static frameworks using CocoaPods, the .podspec
file has to set the static_framework
specification to true
, as explained here. To simulate this without making modifications to the .podspec
files, it is possible to add a preinstall step to the Podfile
, as explained here:
dynamic_frameworks = []# Make all the other frameworks into static frameworks by overriding the static_framework? function to return true
pre_install do |installer|
installer.pod_targets.each do |pod|
if !dynamic_frameworks.include?(pod.name)
puts "Overriding the static_framework? method for #{pod.name}"
def pod.static_framework?;
true
end
end
end
end
The results: app size and launch time
I managed to switch 25 out of the 27 dependencies to static frameworks. In order to measure the app size, I archived and exported it as a Development
build. In order to measure the launch times I set the DYLD_PRINT_STATISTICS
environmental variable to 1
, as explained here.
App size
These are the results:
Launch times
The launch time tests were performed running the app from Xcode using the release
configuration, on an iPhone 8 with iOS 12.1.1. In order to get a better understanding of the results, I ran the app 15 times:
- Before the first run, the device is rebooted to clear the dyld cache, and a fresh installation of the app is done.
- Before the 11th run the device is also rebooted to clear the dyld cache (but a fresh installation of the app is NOT done).
- Runs 2nd to 10th and 12th to 15th happen without rebooting the device, and without doing a fresh installation of the app.
These are the results, where the total launch times can be visualised in the first graph, while the second graph shows the time spent in each of the four launch steps. The results for runs 2nd to 10th and 12th to 15th are averaged:
Conclusions
Note that the analysis done here is on an app exported from Xcode, and not the final app downloaded from the AppStore, which can go through further optimisations before ending up in the users’ phones. The main conclusions that can be obtained from these results are below:
- Using static frameworks instead of dynamic frameworks reduces the app size, in my case by a moderate 14.55%. Most probably this is due to the compiler being able to remove unused symbols, which is not possible when using dynamic frameworks.
- Using static frameworks instead of dynamic frameworks reduces app launch time considerably. In my case the improvements are between 28% and 47%.
- The dyld cache alleviates the slow launch times, but only after the first run of the app. Still, using static frameworks instead of dynamic frameworks leads to better performance. Furthermore, relying on the dyld cache may not be a good idea, as it is not possible to know for how long it is kept around before it is cleared out.