Lazy load the dynamic frameworks in runtime for iOS to reduce the App's launch time & more!
Binary is being loaded dynamically from SSD to RAM - Generated by AI
Dynamic loading is a mechanism by which a computer program can, at run time, load a library (or other binary) into memory, retrieve the addresses of functions and variables contained in the library, execute those functions or access those variables, and unload the library from memory.
It is one of the 3 mechanisms by which a computer program can use some other software; the other two are static linking and dynamic linking. Unlike static linking and dynamic linking, dynamic loading allows a computer program to start up in the absence of these libraries, to discover available libraries, and to potentially gain additional functionality.
MacOS officially supports Dynamic loading, you can follow the Dynamic Library Programming Topics. How about the iOS? Unfortunately, there aren't any official documents for iOS yet. Also, from the Xcode settings, we only see 2 options are static linking and dynamic linking, we assume Apple doesn't support Dynamic loading for iOS officially.
However, MacOS & iOS use the same Darwin kernel. Luckily, Darwin is mostly compatible with POSIX. POSIX introduced the dlfcn which is a standard instruction to work with run-time dynamic loading. You can see Apple open-sourced the dylb base on it.
This library bring the dynamic loading for iOS by using methods from POSIX's dlfcn.
As the name, there're many benefits when using the Interface modules design. One of them is reducing the build time, today, along with dynamic loading, we'll get one more benefit: reduce the App's launch time: when App starts, it only load the interface modules, when app is running, it will load the concrete modules on demand by dynamic loading. You can see engineers in Meta has talked about this approach: The evolution of Facebook’s iOS app architecture. This library is mostly inspired from them!
For now, we only support Swift Package Manager:
dependencies: [
.package(url: "https://github.com/duyquang91/DylibRuntimeloader", .from("1.0.0"))
]
Note This is a dynamic library
These steps will guide you how to use the library, you can download this repo & open the Demo project, everything is there for a quick experiment.
As mentioned above, it is a best practice to use this library with the Interface module design. First, we create a new package: AnimalInterface at path: ./Demo/Animals/AnimalInterface and declare a public protocol Animal:
import Foundation
public protocol Animal {
func speak() -> String
}
Note It must be a Dynamic library.
This simple packge describes how an animal speaks
Now we create a new packge: Dog at path: ./Demo/Animals/Dog/ then declare AnimalInterface & DylibRuntimeLoader as dependencies, the Package.swift should look like this:
import PackageDescription
let package = Package(
name: "AnimalImplementation",
platforms: [.iOS(.v11)],
products: [
.library(
name: "AnimalImplementation",
type: .dynamic,
targets: ["AnimalImplementation"]),
],
dependencies: [
.package(path: "../AnimalInterface"),
.package(path: "../../..")
],
targets: [
.target(name: "AnimalImplementation",
dependencies: ["AnimalInterface", "DyLibRuntimeLoader"]),
]
)
Note It must be a Dynamic library.
Now, the Dog must conform the Animal interface:
import AnimalInterface
struct Dog: Animal {
func speak() -> String {
"woof"
}
}
You can see we don't declare Dog as a public struct so when we import it into Demo project, we can't use it. The Dynamic loading technique actually will load the Dog instance into the memory & cast it into public Animal interface. Now, let's expose our Dog instance as a symbol by using DylibRuntimeLoader's dyLibCreator: method:
import DyLibRuntimeLoader
import AnimalInterface
@_cdecl("load_animal")
func load() -> UnsafeMutableRawPointer {
dyLibCreator(factory: Dog(), forType: Animal.self)
}
struct Dog: Animal {
func speak() -> String {
"woof"
}
}
Warning: You have to use the
@_cdecl("load_animal")
attribute, otherwise we can't retrieve the symbol to this instance later. DylibRuntimeLoader also mentioned this in the method's inline documentation.
Now, build & export this Dog package to a dynamic XCFramework (or Fat framework). If you're using Swift Package Manager, can use this tool: swift-create-xcframework. The final build is located at: ./Demo/Animals/Dog/AnimalImplementation.xcframework
We repeat the quite same steps from Dog package to create a dynamic package: Cat. The only difference is the speak method:
import DyLibRuntimeLoader
import AnimalInterface
@_cdecl("load_animal")
func load() -> UnsafeMutableRawPointer {
dyLibCreator(factory: Cat(), forType: Animal.self)
}
struct Cat: Animal {
func speak() -> String {
"meow"
}
}
You can find the final dynamic XCFramework: ./Demo/Animals/Cat/AnimalImplementation.xcframework
Now, it is time to expirement the magic!
Open the Demo project at path: Demo/DynamicLoadingDemo/DynamicLoadingDemo.xcodeproj to check the dependencies:
Note DyLibRuntimeLoader & DyLibSampleInterface are linked dynamically
How about the implementation frameworks Dog & Cat? We don't link it as the traditional way like DyLibRuntimeLoader & AnimalInterface above, our project & Xcode shouldn't aware about it: don't link or import it, just copy it to the Frameworks
directory by a new manual "Script" in the "Build Phases":
Note I used a script from Carthage for quick demo, you can write your own script to work with XCFramework.
First, let's try to copy the Dog framework. From now on, whenever we want to load the instance of Animal, just use the DyLibRuntimeLoader's dyLibLoad: method
let animal = try dyLibLoad(withSymbol: "load_animal", fromFramework: .framework(name: "AnimalImplementation"), forType: Animal.self)
You can find this code at the Demo's ViewController: ./Demo/DynamicLoadingDemo/DynamicLoadingDemo/ViewController.swift
Warning Use corresponding directory you copied the concrete framework into it, otherwise the framework can't be loaded.
Now, run the Demo project to figure it out:
App is launched | Load the Dog instance | Tes the animal.speak() |
---|---|---|
Now, close the Xcode & let's try to tamper the Demo app. Find the current booted simulator's UUID:
xcrun simctl list | egrep '(Booted)'
Then go to find our app's Frameworks
folder:
~/Library/Developer/CoreSimulator/Devices/{sim_uuid}/Data/Containers/Bundle/Application/{app_uuid}/DynamicLoadingDemo/Frameworks
Now try these actions by your self & restart the app to see the results:
Action | Result |
---|---|
Delete DyLibRuntimeLoader or AnimalInterface framework |
App will be crashed immediately because they're linked dynamically. When app is launched, iOS will load all linked dynamic frameworks, if any is missing, app will be crashed. |
Delete AnimalImplementation framework |
App is launched normally because Xcode is not aware about it while linking & iOS isn't aware about it as well because we run a manual script to copy it later. |
Replace the AnimalImplementation framework from ./Demo/Animals/Cat/AnimalImplementation.xcframework/ios-arm64_x86_64-simulator/AnimalImplementation.framework then check how animal.speak() func work? |
The animal.speak() now should notice "meow". We just bypass the iOS integrity check, tamper & change the behaviour of the App. To do this on a real device, you need to jailbreak. |
The experiment above is an evident for the Dynamic loading for iOS, it may not quite exact how engineers at Meta did with The evolution of Facebook’s iOS app architecture. I just try to mimic them to verify if it is really work, the result is fancy!
We can easily preceive some benefits from this dynamic loading approach:
- As mentioned by Meta's engineers, by dynamic loading, the App's launch time will be reduced significantly, especially with the bunch of dynamic frameworks!
- The implementation modules can hide the whole implementation & just expose as symbols, it maybe useful for some circumstances.
- The implementation modules can be easily replaced without any integrity checks from Xcode/iOS so we can change the app's behaviour or perhaps update the App on the fly? Who know :D
I already integrated the DylibRuntimeLoader to my open source app: Loan Calculator Plus & it's been approved by Apple. You can download it from the Appstore. It means Dynamic loading is unofficially supported by Apple but legal on Appstore. That's why Facebook app is alive till now.