The evolution of Fb’s iOS app structure

The evolution of Fb’s iOS app structure
The evolution of Fb’s iOS app structure

Fb for iOS (FBiOS) is the oldest cellular codebase at Meta. For the reason that app was rewritten in 2012, it has been labored on by 1000’s of engineers and shipped to billions of customers, and it could possibly help a whole bunch of engineers iterating on it at a time.

After years of iteration, the Fb codebase doesn’t resemble a typical iOS codebase:

  • It’s filled with C++, Goal-C(++), and Swift.
  • It has dozens of dynamically loaded libraries (dylibs), and so many courses that they will’t be loaded into Xcode without delay.
  • There’s virtually zero uncooked utilization of Apple’s SDK — all the things has been wrapped or changed by an in-house abstraction.
  • The app makes heavy use of code era, spurred by Buck, our customized construct system.
  • With out heavy caching from our construct system, engineers must spend a complete workday ready for the app to construct.

FBiOS was by no means deliberately architected this manner. The app’s codebase displays 10 years of evolution, spurred by technical selections essential to help the rising variety of engineers engaged on the app, its stability, and, above all, the consumer expertise.

Now, to have fun the codebase’s 10-year anniversary, we’re shedding some gentle on the technical selections behind this evolution, in addition to their historic context.

2014: Establishing our personal cellular frameworks

Two years after Meta launched the native rewrite of the Fb app, Information Feed’s codebase started to have reliability points. On the time, Information Feed’s knowledge fashions have been backed by Apple’s default framework for managing knowledge fashions: Core Data. Objects in Core Information are mutable, and that didn’t lend itself nicely to Information Feed’s multithreaded structure. To make issues worse, Information Feed utilized bidirectional knowledge circulate, stemming from its use of Apple’s de facto design sample for Cocoa apps: Model View Controller.

In the end, this design exacerbated the creation of nondeterministic code that was very troublesome to debug or reproduce bugs. It was clear that this structure was not sustainable and it was time to rethink it.

Whereas contemplating new designs, one engineer investigated React, Fb’s (open supply) UI framework, which was changing into fairly standard within the Javascript group. React’s declarative design abstracted away the difficult crucial code that brought on points in Feed (on internet), and leveraged a one-way knowledge circulate, which made the code a lot simpler to motive about. These traits appeared nicely suited to the issues Information Feed was dealing with. There was just one drawback.

There was no declarative UI in Apple’s SDK. 

Swift wouldn’t be announced for a few months, and SwiftUI (Apple’s declarative UI framework) wouldn’t be introduced till 2019. If Information Feed needed to have a declarative UI, the staff must construct a brand new UI framework. 

In the end, that’s what they did.

After spending a number of months constructing and migrating Information Feed to run on a brand new declarative UI and a brand new knowledge mannequin, FBiOS noticed a 50 p.c efficiency enchancment. A number of months later, they open-sourced their React-inspired UI framework for cellular, ComponentKit

To at the present time, ComponentKit continues to be the de facto alternative for constructing native UIs in Fb. It has supplied numerous efficiency enhancements to the app through view reuse swimming pools, view flattening, and background format computation. It additionally impressed its Android counterpart, Litho, and SwiftUI.

In the end, the selection to exchange the UI and knowledge layer with customized infra was a trade-off. To attain a pleasant consumer expertise that may very well be reliably maintained, new staff must shelve their trade information of Apple APIs to study the customized in-house infra. 

This wouldn’t be the final time FBiOS must decide that balanced finish consumer expertise with developer expertise and pace. Going into 2015, the app’s success would set off what we confer with as a characteristic explosion. And that introduced its personal set of distinctive challenges.

2015: An architectural inflection level

By 2015, Meta had doubled down on its “Mobile First” mantra, and the FBiOS codebase noticed a meteoric rise within the variety of day by day contributors. As increasingly merchandise have been built-in into the app, its launch time started to degrade, and folks started to note. Towards the top of 2015, startup efficiency was so gradual (practically 30 seconds!) that it risked being killed by the telephone’s OS.

Upon investigation, it was clear that there have been many contributing elements to degraded startup efficiency. For the sake of brevity, we’ll focus solely on those that had a long-term impact on the app’s structure:

  • The app’s ‘pre-main’ time was rising at an unbounded charge, because the app’s dimension grew with every product.
  • The app’s ‘module’ system gave every product ungoverned entry to all of the app’s resourcing. This led to a tragedy of the commons issue as every product leveraged its ‘hook’ into startup to carry out computationally costly operations in order that preliminary navigation to that product can be snappy.

The adjustments that have been wanted to mitigate and enhance startup would essentially alter the way in which product engineers wrote code for FBiOS.

2016: Dylibs and modularity

In line with Apple’s wiki about improving launch times, numerous operations must be carried out earlier than an app’s ‘predominant’ perform will be known as. Typically, the extra code an app has, the longer it will take.

Whereas ‘pre-main’ contributed solely a small subset of the 30 seconds being spent throughout launch, it was a selected concern as a result of it might proceed to develop at an unbounded charge as FBiOS continued to amass new options.

To assist mitigate the unbounded development of the app’s launch time, our engineers started to maneuver massive swaths of product code right into a lazily loaded container generally known as a dynamic library (dylib). When code is moved right into a dynamically loaded library, it isn’t required to load earlier than the app’s predominant() perform.

Initially, the FBiOS dylib construction seemed like this:

Facebook iOS

Two product dylibs (FBCamera and NotOnStartup) have been created, and a 3rd dylib (FBShared) was used to share code between the assorted dylibs and the principle app’s binary.

The dylib answer labored superbly. FBiOS was in a position to curb the unbounded development of the app’s startup time. Because the years glided by, most code would find yourself in a dylib in order that startup efficiency stayed quick and was unaffected by the fixed fluctuation of added or eliminated merchandise within the app.

The addition of dylibs triggered a psychological shift in the way in which Meta’s product engineers wrote code. With the addition of dylibs, runtime APIs like NSClassFromString() risked runtime failures as a result of the required class lived in unloaded dylibs. Since most of the FBiOS core abstractions have been constructed on iterating by all of the courses in reminiscence, FBiOS needed to rethink what number of of its core techniques labored.

Except for the runtime failures, dylibs additionally launched a brand new class of linker errors. Within the occasion the code in Fb (the startup set) referenced code in a dylib, engineers would see a linker error like this:

Undefined symbols for structure arm64:
  "_OBJC_CLASS_$_SomeClass", referenced from:
      objc-class-ref in libFBSomeLibrary-9032370.a(FBSomeFile.mm.o)

To repair this, engineers have been required to wrap their code with a particular perform that would load a dylib if essential:

Instantly:

int predominant() 
  DoSomething(context);


Would appear like this:

int predominant() 
  FBCallFunctionInDylib(
    NotOnStatupFramework,
    DoSomething,
    context
  );


The answer labored, however had fairly a number of code smells:

  • The app-specific dylib enum was hard-coded into numerous callsites. All apps at Meta needed to share a dylib enum, and it was the reader’s accountability to find out whether or not that dylib was utilized by the app the code was operating in.
  • If the fallacious dylib enum was used, the code would fail, however solely at runtime. Given the sheer quantity of code and options within the app, this late sign led to a number of frustration throughout improvement.

On prime of all that, our solely system to safeguard in opposition to the introduction of those calls throughout startup was runtime-based, and lots of releases have been delayed whereas last-minute regressions have been launched into the app.

In the end, the dylib optimization curbed the unbounded development of the app’s launch time, but it surely signified an enormous inflection level in the way in which the app was architected. FBiOS engineers would spend the subsequent few years re-architecting the app to easy among the tough edges launched by the dylibs, and we (ultimately) shipped an app structure that was extra strong than ever earlier than.

2017: Rethinking the FBiOS structure

With the introduction of dylibs, a number of key elements of FBiOS needed to be rethought:

  • The ‘module registration system’ may not be runtime-based.
  • Engineers wanted a strategy to know whether or not any codepath throughout startup may set off a dylib load.

To handle these points, FBiOS turned to Meta’s open supply construct system, Buck.

Inside Buck, every ‘goal’ (app, dylib, library, and so on.) is asserted with some configuration, like so:

apple_binary(
  identify = "Fb",
  ...
  deps = [
    ":NotOnStartup#shared",
    ":FBCamera#shared",
  ],
)

apple_library(
  identify = "NotOnStartup",
  srcs = [
    "SomeFile.mm",
  ],
  labels = ["special_label"],
  deps = [
    ":PokesModule",
    ...
  ],
)

Every ‘goal’ lists all info wanted to construct it (dependencies, compiler flags, sources, and so on.), and when ‘buck construct’ is named, it builds all this info right into a graph that may be queried.

$ buck question “deps(:Fb)”
> :NotOnStartup
> :FBCamera

$ buck question “attrfilter(labels, special_label, deps(:Fb))”
> :NotOnStartup

Utilizing this core idea (and a few particular sauce), FBiOS started to provide some buck queries that would generate a holistic view of the courses and features within the app throughout construct. This info can be the constructing block of the app’s subsequent era of structure.

2018: The proliferation of generated code

Now that FBiOS was in a position to leverage Buck to question for details about code within the dependency, it may create a mapping of “perform/courses -> dylibs” that may very well be generated on the fly.


  "features": 
    "DoSomething": Dylib.NotOnStartup,
    ...
  ,
  "courses": 
    "FBSomeClass": Dylib.SomeOtherOne
  


Utilizing that mapping as enter, FBiOS used it to generate code that abstracted away the dylib enum from callsites:

static std::unordered_map<const char *, Dylib> functionToDylib 
   "DoSomething", Dylib.NotOnStartup ,
   "FBSomeClass", Dylib.SomeOtherOne ,
  ...
;

Utilizing code era was interesting for a number of causes:

  • As a result of the code was regenerated primarily based on native enter, there was nothing to examine in, and there have been no extra merge conflicts! On condition that the engineering physique of FBiOS may double yearly, this was a giant improvement effectivity win.
  • FBCallFunctionInDylib no-longer required an app-specific dylib (and thus may very well be renamed to ‘FBCallFunction’). As a substitute, the decision would learn from static mapping generated for every utility throughout construct.

Combining Buck question with code era proved to be so profitable that FBiOS used it as bedrock for a brand new plugin system, which ultimately changed the runtime-based app-module system.

Transferring sign to the left

With the brand new Buck-powered plugin system. FBiOS was in a position to change most runtime failures with build-time warnings by migrating bits of infra to a plugin-based structure.

When FBiOS is constructed, Buck can produce a graph to indicate the situation of all of the plugins within the app, like so:

Facebook iOS

From this vantage level, the plugin system can floor build-time errors for engineers to warn: 

  • “Plugin D, E may set off a load of a dylib. This isn’t allowed, for the reason that caller of those plugins lives within the app’s startup path.”
  • “There is no such thing as a plugin for rendering Profiles discovered within the app … because of this navigating to that display won’t work.”
  • “There are two plugins for rendering Teams (Plugin A, Plugin B). One among them must be eliminated.”

With the outdated app module system, these errors can be “lazy” runtime assertions. Now, engineers are assured that when FBiOS is constructed efficiently, it gained’t fail due to lacking performance, dylibs loading throughout app startup, or invariants within the module runtime system.

The price of code era

Whereas migrating FBiOS to a plugin system has improved the app’s reliability, supplied sooner alerts to engineers, and made it attainable for the app to trivially share code with our different cellular apps, it got here at a value:

  • Plugin errors will not be on Stack Overflow and will be complicated to debug.
  • A plugin system primarily based on code era and Buck is a far cry from conventional iOS improvement. 
  • Plugins introduce a layer of indirection to the codebase. The place most apps would have a registry file with all options, these are generated in FBiOS and will be surprisingly troublesome to seek out.

There is no such thing as a doubt that plugins led FBiOS farther away from idiomatic iOS improvement, however the trade-offs appear to be price it. Our engineers can change code utilized in many apps at Meta and ensure that if the plugin system is blissful, no app ought to crash due to lacking performance in a not often examined codepath. Groups like Information Feed and Teams can construct an extension level for plugins and ensure that product groups can combine into their floor with out touching the core code.

2020: Swift and language structure

Whereas most of this text has targeted on architectural adjustments stemming from scale points within the Fb app, adjustments in Apple’s SDK have additionally pressured FBiOS to rethink a few of its architectural selections.

In 2020, FBiOS started to see an increase within the variety of Swift-only APIs from Apple and a rising sentiment for extra Swift within the codebase. It was lastly time to reconcile with the truth that Swift was an inevitable tenant in FB apps. 

Traditionally, FBiOS had used C++ as a lever to construct abstraction, which saved on code dimension due to C++’s zero overhead principle. However C++ doesn’t interop with Swift (but). For most FBiOS APIs (like ComponentKit), some form of shim must be created to make use of in Swift — creating code bloat.

Right here’s a diagram outlining the problems within the codebase:

Facebook iOS

With this in thoughts, we started to kind a language technique about when and the place numerous bits of code must be used:

Facebook iOS

In the end, the FBiOS staff started to advise that product-facing APIs/code shouldn’t comprise C++ in order that we may freely use Swift and future Swift APIs from Apple. Utilizing plugins, FBiOS may summary away C++ implementations in order that they nonetheless powered the app however have been hidden from most engineers.

Such a workstream signified a little bit of shift in the way in which FBiOS engineers considered constructing abstractions. Since 2014, among the greatest elements in framework constructing have been contributions to app dimension and expressiveness (which is why ComponentKit selected Goal-C++ over Goal-C).

The addition of Swift was the primary time these would take a backseat to developer effectivity, and we anticipate to see extra of that sooner or later.

2022: The journey is 1 p.c  completed

Since 2014, FBiOS structure has shifted fairly a bit:

  • It launched numerous in-house abstractions, like ComponentKit and GraphQL.
  • It makes use of dylibs to maintain ‘pre-main’ occasions minimal and contribute to a blazing-fast app startup.
  • It launched a plugin system (powered by Buck) in order that dylibs are abstracted away from engineers, and so code is well shareable between apps.
  • It launched language pointers about when and the place numerous languages must be used and commenced to shift the codebase to replicate these language pointers.

In the meantime, Apple has launched thrilling enhancements to their telephones, OS, and SDK:

  • Their new telephones are quick. The price of loading is way smaller than it was earlier than.
  • OS enhancements like dyld3 and chain fixups present software program to make code loading even sooner.
  • They’ve launched SwiftUI,  a declarative API for UI that shares a number of ideas with ComponentKit.
  • They’ve supplied improved SDKs, in addition to APIs (like interruptible animations in iOS8) that we may have constructed customized frameworks for.

As extra experiences are shared throughout Fb, Messenger, Instagram, and WhatsApp, FBiOS is revisiting all these optimizations to see the place it could possibly transfer nearer to platform orthodoxy. In the end, we’ve seen that the best methods to share code are to make use of one thing that the app provides you without spending a dime or construct one thing that’s just about dependency-free and may combine between all of the apps.

We’ll see you again right here in 2032 for the recap of the codebase’s 20-year anniversary!