In a recent article, Lyft engineers Artur Stepaniuk and Max Husar described how Lyft handles the complexity of creating an app extension for their iOS app without breaking the tight RAM and binary size constraints set by Apple nor impair user experience.
Lyft's iOS app includes an extension to integrate it with Apple Maps and provide information about Lyft offers from within the Maps app. As Stepaniuk and Husar explain, the key to create an iOS app extension in an efficient way is to correctly manage dependencies to maximize code reuse across the app and the extension while optimizing binary size and memory usage.
The main complexity factor in this process is the impossibility of using dynamic linking to avoid the cost of loading them into memory at launch time, which would make the app too slow on launch. This makes static linking the only viable option, albeit at the cost of increasing the app binary size as well as its memory footprint. However,
A larger binary size can lead to longer download and install times, potentially reducing the number of installs. The worst-case scenario is hitting the 200 MB download size limit, which triggers an additional confirmation dialog during app download when using cellular data.
On the memory side, Lyft engineers found out that extensions may be limited to using between 20 and 50MB of RAM depending on the iOS version, device model, and other factors.
To reduce the app binary size and memory footprint, Lyft engineers analyzed the dependency graph of their app to identify the modules that contributed most to that. Since Lyft uses Bazel, they relied on a graph visualization software, Graphviz, to create an image from the data produced by Bazel using the query --output=graph
command.
To measure the binary size impact in detail, each module can be added as the only dependency to the Apple Maps extension and analyzed using the
binary-size-diff
tool.
binary-size-diff
is a tool to compare the binary size difference between the base branch and a given pull request. This makes it possible to measure the actual effect of removing (or including) a dependency.
Once that information is available, the next step is identifying any dependencies that appear to be unnecessarily included. To this aim, Lyft engineers used another feature of Bazel's to show the transitive dependencies between two modules.
bazel query 'allpaths(INITIAL_MODULE_PATH:INITIAL_MODULE_NAME, TARGET_MODULE_PATH:TARGET_MODULE_NAME)' --output=graph | grep -v ' node \[shape=box\];' > relations.dot
This command shows which modules are included along the path connecting the module under investigation to each of its larger dependencies, so you can either remove them or make the target module not dependent on them. In a specific case, the Lyft team decided to duplicate a service to create a minimalist dependency with the aim to break the dependency with larger modules.
Using this approach Lyft engineers could reduce the binary size of their extension from 45MB to 15MB. While a 30MB reduction is not significant for a server-side or desktop app, it amounts to 15% of the "safe" size limit of 200MB.
Stepaniuk and Husar's article contains additional details related to the process of releasing an app extension, such as how to ensure the extension is available for all supported regions, the effect of using the APPLICATION_EXTENSION_API_ONLY
build setting, and SiriKit idiosyncrasies, so do not miss it if you are interested in these topics.