Applications tend to grow and so does their source code. A monolithic application, having a very large code base, poses challenges regarding maintainability, deployment, and modifications. Hence, structuring the codebase into manageable components is a vital part of every app. This is done for various reasons: reducing code complexity; maintaining, testing, and reusing existing parts; or even optimizing the app itself.
iOS gives various ways like module, library, bundle, and framework of packaging and delivering the components. They sound similar but there are some key differences in how they work internally.
Module: Module is a single unit of code distribution, a framework or application that is built and shipped as a single unit and that can be imported by another module with swift’s import keyword.
Libraries: The files that define pieces of code and data that are not a part of your Xcode target.
Bundle: A bundle is a file directory with subdirectories inside. On iOS, bundles serve to conveniently ship-related files together in one package — for instance, images, nibs, or compiled code. The system treats it as one file and you can access bundle resources without knowing its internal structure.
Types of Bundles
Although all bundles support the same basic features, there are variations in the way you define and create bundles that define their intended usage:
1. Application — An application bundle manages the code and resources associated with a launch-able process. For example, an iOS application bundle contains info.plist, executable, resources, and other supported files.
2. Frameworks — A framework bundle manages a dynamic shared library and its associated resources, such as header files. An application can link against one or more frameworks to take advantage of the code they contain.
3. Plug-Ins — macOS supports plug-ins for many system features.
Framework: It is a package that can contain resources such as dynamic libraries, strings, headers, images, storyboards, etc.
Frameworks are also bundles ending with .framework extension. They can be accessed by NSBundle / Bundle class from code and, unlike most bundle files, can be browsed in the file system that makes it easier for developers to inspect its contents. Frameworks have versioned bundle format which allows for storing multiple copies of code and headers to support older program version
Static and Dynamic Libraries
Static library: A unit of code linked at compile-time, which does not change. (Can only contain code). The code that the app uses is copied to the generated executable file by a static linker during compilation time. However, iOS static libraries are not allowed to contain images/assets (only code). You can get around this limitation by using a media bundle.
Dynamic library: A unit of code and/or assets linked at runtime that may change. They are different from static libraries in the sense that they are linked with the app’s executable at runtime, but not copied into it. As a result, the executable is smaller and, because the code is loaded only when it is needed, the startup time is typically faster. They are usually shared between applications, therefore the system needs to store only one copy of the library and let different processes access it. As a result, invoking code and data from dynamic libraries happens slower than from the static ones.
NOTE: Dynamic libraries outside of a framework bundle, which typically have the file extension .dylib, are not supported on iOS, watchOS, or tvOS, except for the system Swift libraries provided by Xcode. You’re not allowed to create these, as this will get your app rejected. Only Apple is allowed to create dynamic libraries for iOS.
System iOS and macOS libraries are dynamic. This means that your app will receive improvements from Apple’s updates without new build submission. This also may lead to issues with interoperability. That’s why it is always a good idea to test the app on the new OS version before it becomes released.
Text Based .dylib Stubs:
When system libraries are linked, such as UIKit or Foundation, we don’t want to copy their entirety into the app, because it would be too large. Text-based .dylib stub, or .tbd, is a text file that contains the names of the methods without their bodies, declared in a dynamic library. It results in a significantly lower size of .tbd compared to a matching .dylib. Along with method names, it contains the location of the corresponding .dylib, architecture, platform, and some other metadata.
Comparison between static and dynamic:
Two important factors that determine the performance of apps are their launch times and their memory footprints. Reducing the size of an app’s executable file and minimizing its use of memory once it’s launched make the app launch faster and use less memory once it’s launched. Using dynamic libraries instead of static libraries reduces the executable file size of an app. They also allow apps to delay loading libraries with special functionality only when they’re needed instead of at launch time. This feature contributes further to reduced launch times and efficient memory use.
When an app is launched, the app’s code — which includes the code of the static libraries it was linked with — is loaded into the app’s address space. Linking many static libraries into an app produces large app executable files. The below diagram shows the memory usage of an app that uses functionality implemented in static libraries. Applications with large executables suffer from slow launch times and large memory footprints. Also, when a static library is updated, its client apps don’t benefit from the improvements made to it. To gain access to the improved functionality, the app’s developer must link the app’s object files with the new version of the library. And the app’s users would have to replace their copy of the app with the latest version. Therefore, keeping an app up to date with the latest functionality provided by static libraries requires disruptive work by both developers and end-users.
How Static library works
A better approach is for an app to load code into its address space when it’s actually needed, either at launch time or at runtime. Dynamic libraries are not statically linked into client apps; they don’t become part of the executable file. Instead, dynamic libraries can be loaded (and linked) into an app either when the app is launched or as it runs.
The below diagram shows how implementing some functionality as dynamic libraries instead of as static libraries reduce the memory used by the app after launch.
How Dynamic library works
Using dynamic libraries, programs can benefit from improvements to the libraries they use automatically because their link to the libraries is dynamic, not static. That is, the functionality of the client apps can be improved and extended without requiring app developers to recompile the apps. Apps written for OS X benefit from this feature because all system libraries in OS X are dynamic libraries. This is how apps that use Carbon or Cocoa technologies benefit from improvements to OS X.
Another benefit dynamic libraries offer is that they can be initialized when they are loaded and can perform clean-up tasks when the client app terminates normally. Static libraries don’t have this feature.
One issue that developers must keep in mind when developing dynamic libraries is maintaining compatibility with client apps as a library is updated. Because a library can be updated without the knowledge of the client app’s developer, the app must be able to use the new version of the library without changes to its code. To that end, the library’s API should not change. However, there are times when improvements require API changes. In that case, the previous version of the library must remain on the user’s computer for the client app to run properly. Dynamic Library Design Guidelines explores the subject of managing compatibility with client apps as a dynamic library evolves.
How Dynamic Libraries Are Used
When an app is launched, the OS X kernel loads the app’s code and data into the address space of a new process. The kernel also loads the dynamic loader ( /usr/lib/dyld ) into the process and passes control to it. The dynamic loader then loads the app’s dependent libraries. These are the dynamic libraries the app was linked with. The static linker records the filenames of each of the dependent libraries at the time the app is linked. This filename is known as the dynamic library’s install name. The dynamic loader uses the app’s dependent libraries’ install names to locate them in the file system. If the dynamic loader doesn’t find all the app’s dependent libraries at launch time or if any of the libraries are not compatible with the app, the launch process is aborted.
The dynamic loader — in addition to automatically loading dynamic libraries at launch time — loads dynamic libraries at runtime, at the app’s request. That is, if an app doesn’t require that a dynamic library be loaded when it launches, developers can choose to not link the app’s object files with the dynamic library, and, instead, load the dynamic library only in the parts of the app that require it. Using dynamic libraries this way speeds up the launch process.
Static and Dynamic Frameworks:
Static frameworks contain a static library packaged with its resources. Dynamic frameworks contain the static/dynamic library with its resources. In addition to that, dynamic frameworks may conveniently include different versions of the same dynamic library in the same framework.
Just like a dynamic shared library, frameworks provide a way to factor out commonly used code into a central location that can be shared by multiple applications. Only one copy of a framework’s code and resources reside in-memory at any given time, regardless of how many processes are using those resources. Applications that link against the framework then share the memory containing the framework. This behavior reduces the memory footprint of the system and helps improve performance.
Note: Only the code and read-only resources of a framework are shared. If a framework defines writable variables, each application gets its own copy of those variables to prevent them from affecting other applications.
To understand the static and dynamic framework embedding better, we created one project with FeedContent dependency. We selected the Framework with Dynamic and static library, with different option embed without signing and No embed.
Static and Dynamic frameworks:
To see the contents of the frameworks, check Result folder
Dynamic Framework: 188 KB
Static Framework: 139 KB
Dynamic and static framework implementation: https://github.com/shilpabansal/StaticDynamicFrameworkImplementation
Update Dependency module’s type
To update the different settings of the way framework is embedded
Static Framework: The static library is always compiled and embedded in the executable
1. Embed with/without sign-in: If the archive is created with a static library with embed, it will be creating 2 copies of the code. as the code is already there in the app executable, it will be only increasing the binary size and the files in the Framework folder won’t be used ever.
2. If the framework is already signed in, for the client “embed without sign-in” option can be selected, whereas if it’s not already signed-in, it should be signed in with the client.
3. The best option to use a static framework is without embedding.
1. The launch time is faster, as it already loaded in memory with app code
2. Static libraries are guaranteed to be present in the app and have the correct version.
3. No need to keep an app up to date with library updates.
4. Better performance of library calls.
1. The app has to be compiled and released again in order to change anything
2. Inflated app size.
3. Must copy the whole library even if using a single function.
4. Launch time degrades because of the bloated app executable.
Dynamic framework: The dynamic framework is never part of the executable, it linked at the loading time or run time-based on the option chosen by the client
1. Not Embedded: If the framework is not embedded, it will crash the app, as its not part of the executable
2. Embed: The ideal way to use it is with embed
1. Faster startup time, if loaded on-demand option is chosen.
2. Can benefit from library improvements without app re-compile. Especially useful with system libraries.
3. Takes less disk space since it is shared between applications.
4. Loaded by pieces: no need to load the whole library if using a single function.
1. Can potentially break the program if anything changes in the library.
2. Slower calls to library functions, as it is located outside application executable.
Walmart Apps are combination of Dynamic and static frameworks.
At Sam’s, considering the risk of crashes and to have better performance and launch time, we normally use static frameworks. The modularization is achieved by making 4 categories of Modules.
- Interface Module — These modules provide two important roles to the rest of the modules:
- Houses all the feature module interfaces (protocols), and their pertinent data models
- Holds the dependency injection container, which is what the feature modules use to register their available functionality, and resolve dependencies on other modules
- Feature Module — These modules will be the majority of the code in our application and will consist of a “vertical slice” of some functionality in the app that maintains state.
- Library Module — These modules represent shared, but stateless and dependency free, building blocks like utility methods, reusable UI components or styles, and base definitions or configurations
- App Module — This is the core of the Sam’s app, where the app delegate lives.
- Loads in all the feature modules by registering their assemblies
- Handles application duties of registering for notifications, handling deep links, primary navigation, initializing frameworks
As the app size keeps growing in number of lines of code and contributors, modularizatin becomes mandatory since it:
- Enforces better separation of concerns
- Makes it easier for a larger team to contribute to the codebase
- Integrates features without the need to build and run the whole application on a developer’s machine
It’s a controversial topic whether the Dynamic or Static framework is better? A lot depends on the requirements and priorities of the app. Dynamic framework provides the benefits of having less build time, but comes with the risk of runtime crashes. Static framework helps in achieving better performance and launch time. Your app can be a combination of both the options based on the usage and product priorities.