Instant Feedback in iOS Engineering Workflows

Instagram Engineering
Instagram Engineering
4 min readApr 23, 2018

--

At Instagram and Facebook, we’ve seen how adopting React Native in product development has allowed our engineers to move and iterate faster on products. Viewing the effects of our changes is now just a single tap of ⌘R away! But working with native code doesn’t provide the same experience. Instead, engineers need to recompile, link, install, and launch the application they’re developing every time they want to see the results of their changes.

Even on a small application, that process drops people out of their workflow. And at our scale, the result is that engineers spend a significant amount of time waiting for their computers to complete work, and often end up context-switching away to other applications, losing even more time.

Late last year we set out to bring the advantages of React Native’s developer experience to engineers working on our native iOS codebase, written in Objective-C, with the goal of allowing engineers to see their changes in seconds. To achieve this, we skip most of the steps necessary to restart the application by loading the new code while it’s still running.

How Native Code Reloading Works

To reload native code, the first step is to turn the modified code into something that we can load into a running application: a dynamic library. This operates on a file-level granularity: we compile only the files that have changed, then link them into a dynamic library. This is very fast — engineers using the tool are primarily iterating on a single file.

An iOS device won’t permit an application to load dynamic libraries from outside of its bundle. Additionally, it won’t allow an application to write to its own bundle. Therefore, all loadable binaries must have been in the application’s bundle when it was installed, and loading our new library is impossible.

Fortunately, neither of these limitations apply to the iOS simulator. Each simulator’s filesystem is simply a directory in host Mac’s filesystem. Additionally, the dlopen function in the iOS simulator doesn't filter by file location, which makes the ability to write directly to the application's bundle irrelevant anyway.

Within the application code, we’ve added a startup function that stars a small, debug-only background task that watches a specific directory. After creating the dynamic library, the tool locates it and moves it into that directory. This triggers a callback in the application, which takes these steps:

  • Load the new dynamic library.
  • Determine which Objective-C classes have been added.
  • Find the original versions of these classes. Objective-C classes are themselves objects, and they’re easy to look up by name.
  • Use the Objective-C runtime APIs to replace the implementations of methods on the original versions of the classes with the new implementations. This applies to all methods of the class, as we don’t have any way to determine which have changed and which haven’t.
  • Write a response back to the same directory for the tool to read.

Our new code is now running in the application, and will be called whenever one of these methods is next invoked. Of course, this is only available while in development with the iOS simulator, not on the device or in the production app.

Building a Developer Experience

Reinstalling and restarting an application to see new changes isn’t fast, but it’s simple and reliable. Click a button or press a keyboard shortcut in Xcode, and eventually, the results of your code will appear on-screen. We match this in our tooling: we place a button in Xcode’s toolbar, and add a keyboard shortcut to trigger the loading of new code.

We have an another advantage over full restarts: we can keep all of the current application state. A restart will reset the application back to its entry point — for our apps, that’s generally a feed. Engineers working on other products will have to click their way back to their products’ views before they can see if their changes worked correctly.

However, after new code has been loaded, nothing happens: all of the previous state that has been built up is unaffected by the engineer’s changes. Fortunately, ComponentKit, a declarative iOS UI framework used at Facebook, has a useful method for performing an instant relayout: +[CKComponentDebugController reflowComponents]. Views built with ComponentKit can invoke it after new code is loaded, causing their new layout code to be evaluated and the on-screen views to update immediately. The result is that when working on UI code written with ComponentKit, engineers can make changes to their view layouts, and see them within seconds. Previously, engineers would need to batch their updates and think about when they wanted a full restart. They can now work interactively and iteratively.

Conclusion

Our tool has become popular with our teams here, especially among engineers working with ComponentKit. In larger apps, it has allowed engineers to see the results of their changes up to twenty times faster. The combination of fast performance and tight integration with existing developer tools and frameworks has made native code reloading an important part of many iOS engineers’ workflows here at Instagram and Facebook.

Nate Stedman is an iOS engineer at Instagram New York.

--

--