Write code, compile, wait, navigate to screen in application. Fix problem, recompile wait. Oh it was not fixed, start again… Does it sound familiar? For compiled languages there seems to be no workaround this bottleneck. Even though this is true, there are situations where we can reduce the waiting time, increasing our productivity and happiness. Enter code injection.
The problem
When a program is launched by the OS, the code is read from the binary file and loaded in volatile memory and it is then executed by the CPU. When that program is written in a compiled language, the classic approach to update the code is to kill it, recompile it, and load it again from scratch. This guarantees a fresh start and ensures you are running your ‘bleeding edge’ version.
The problem with this approach is that recompiling and relaunching the program can take a small, but not insignificant, amount of time. One recompile-relaunch cycle may not be a waste of time in itself, but a developer can go through this cycle hundreds of times in a day, so the time sums up considerably. Adding to this problem, if the program itself is an application, then the developer needs to navigate to the section she is working on and interact with the UI manually to check her updates.
What about iOS apps?
Thinking in the domain of iOS apps, this fact is even worse. Many iOS apps are not overly complicated, but the process of developing an application is time consuming and involves hundreds of iterations and small tweaks to a screen. Working in an environment where small iterations evolve a product, it is not uncommon that a developer needs to change small UI values or needs to do some small logic changes to a small part of an application. Being highly graphical applications, it’s faster to develop and ‘transform’ the code if you can see your changes directly updated on the screen after saving.
Code Injection
Code Injection is a way of inserting code in a running process without killing it and relaunching it. You still need to recompile the changed files, but if you inject the code you can remove all the waiting time to relaunch, and the time to setup the state of your application, like navigating to a specific screen.
Available tools
Currently there are two open source tools available:
Both tools take advantage of Objective-C runtime to modify the code while it is running. Injection for xcode offers more features, such as some adjustement sliders, and integration with Apportable, or recent (limited) Swift support.
How does it work?
You can always look at the source code for details, but in a nutshell:
The tool will be watching your source code and then will trigger a recompile when changes are found. The recompile is done through clang directly instead of through XCode. A client side of the tool will be running in the device, waiting to changes to be ‘injected’ into the application. Then, leveraging Objective-C runtime, the actual code is modified.
After the code is modified, the tool will offer you as developer ways to ‘reload’ after changes; you may want to load a whole view controller back into memory from it’s XIB file, or trigger setNeedsDisplay in your custom view.
Using dyci
As this is my tool of choice, I will show you how to use it.
Set up
- Clone the repository and run install script
1 2 |
|
The script is doing three things:
- Proxy clang to capture the build commands it issues, so it can know how to compile your source code
- Installs plugins for XCode and Appcode in your computer
Sets up a script and .dyci folder in your $HOME
Embed client code into your app
Dyci will run in your application, watching for a .dyci directory in your $HOME, so you need to include it in your code. I prefer to use Cocoapods as it is simpler. Ensure you don’t ship this to your users, by linking it to debug configuration only. In your podfile:
1
|
|
Usage
Triggering an rebuild
You have two choices to trigger a recompile of your code. The first one is to use the provided plugins for XCode or Appcode.
A second choice is to run the rebuild script yourself once a source file is saved in your repository. I first learned this technique from Krzystof Zabłocki’s approach in KZPlayground, and haven’t used the plugins since.
You will need a file watcher tool, for example kicker, to trigger scripts when files are changed. The script you want to run is installed by dyci under $HOME/.dyci/scripts/dyci-recompile.py.
To trigger recompiles using kicker, you need to install the gem and to create a custom recipe. I prefer to make the recipe available globally in your system:
1 2 3 |
|
The custom recipe in ~/.kick/code_injection.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
Then on your repository root run:
1
|
|
Trigger a full project recompile and run your application. From now on, when you save a file, it will automatically be recompiled. Save a file and test in your console that the file is recompiled, you should see the logged lines and a success. The script will generate a dynamic library that can then be loaded by the client code in your application.
Injecting the code
Once dyci has rebuilt your modified file(s), the code running in your application will grab the newly built dynamic library and load it into your application. Then it modifies the necessary code.
Once your application is running, check that upon initialization, the app is watching for changes under your ~/.dyci directory. If this is not the case, then the app will not receive new built libraries and your code will not update.You should see some log lines like:
1 2 3 4 5 6 7 8 9 10 |
|
So, using XCode, when you save a file it gets recompiled and injected, watch for this in console:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Updating after injection
So your code is injected, but nothing happens in the user interface? If you think about it, this is completely normal, as iOS is not aware that something changed, so nothing is updated in your screen, unless you force it to do so. The updates you may want to do are different depending on what you are modifying, so bear in mind that just injecting the code is not enough.
The library offers some hooks where you can write test-only code to update what you need. Those methods are declared in the category NSObject+DyCInjection. Use them as you want to trigger layoutSubviews reloadData for example.
Pixel perfect tweaks
You generally need to ensure that what your designer wanted is shown exacly as she wanted in your application. I Use code injection, paired with a very simple app: Uberlayer.
This app lets you put an image over anything in your screen. Overlay it exactly on top of iOS simulator and you can incrementally update the code till it matches perfectly a desired application design.
Gotchas
After you install a new version of XCode, dyci needs to be reinstalled, because dyci is not proxying clang anymore.
Some code is harder to modify using code injection, for example the code in a singleton initialization.
Dyci only works in simulator, because of the hook in your computer’s file system. Injection for xcode does work on devices though.
I didn’t mention Swift because it is a less dynamic language than Objective-C and I expect injection to be harder to do. ‘injection for xcode’ added limited support for it though.
Do not link against the code injection library in release, or you risk removing it and shipping it to your users. You really don’t want that.
Conclusion
No more excuse for this:
We have reviewed a very useful tool to avoid long compile-wait-launch-navigate cycles and improve productivity. Code injection is not a silver bullet of course (is there a silver bullet in programming?), but a very useful tool to use for small changes to code or prototyping. You should really consider using it more often as it can be a huge time saver.
Good for:
- Tweaking UI values
- Pixel-perfect tweaks: Paired with my favourite pixel-perfecting app: Uberlayer
- Changing existing logic
- Prototyping
- Changing strings
- Visual debugging
Not so good for:
- Writing new code and debugging it
- Unit testing
- Changing singletons or long-lived application objects (like services)
- Rewriting lots of code