Being Objective About Objective-C
Time for a deep dive about Objective-C, Swift, and "dead code," with one of TextNow's resident iOS gurus Emma Sinclair. If this tweaks your interest as a iOS developer, you should definitely check out our current job openings.
In our modern iOS working environment, it's important to choose the right tool for the job. Need to tighten up your unit tests? Here's a pattern for that. Need to cover up that terrible API and improve dependency injection? Here's an approach using protocols. Need to improve your very long Swift code? Here's Swift's pseudo-functional APIs.
As we move further away from Objective-C and towards Swift (ok, at most organizations), we keep refactoring more and more Objective-C out of our code bases. But for those sections of our code bases that are either working perfectly in Objective-C or explicitly rely on Objective-C runtime functionality, or make a habit of swapping pointers around in Objective-C++, we allow our implementations to stay in their original language. Predictably, the pattern tends to be that the older an application is, and the more successful that application is, the more likely it is that very, very large portions of the application are still in Objective-C. Sometimes these portions are composed of in-house code, and sometimes of third-party code and nobody's totally sure of the reason why it's still hanging around but nobody's confident enough to remove, either.
All of this is perfectly natural and fairly common across the industry right now, so if I'm describing your code base, rest assured, you're not alone!
Unfortunately, the downside of this new world in which we find ourselves is that, increasingly, we end up with interop code which is bridged back and forth between Swift and Objective-C. This bridging is done automatically in both directions: a Swift interface file is auto-generated based on the user-created "bridging header," and an Objective-C interface file is auto-generated every time interop code is compiled, allowing the user to import the MyTargetName-Swift.h header and gain access to Swift code. All of this back and forth has a lot of advantages (you mean we get to decide when and if we want to refactor? Wow!).
I'm here to point out one of the fundamental differences between Objective-C and Swift that most developers don't have a lot of experience with, and which can cause issues without even calling the offending code: dynamic dispatch.
A Tale of Two Third-Party Categories
Part 1: Our heroes accidentally write code identical to someone else’s, and that’s fine, until it isn’t
Recently, we integrated a third-party framework with some great new gif functionality to our app. We were very happy with the code they delivered to us — a classic off-the-shelf vendored feature framework. But once it hit QA, we suddenly realized that some specific image loading functionality that we use was broken. Oh noes, we thought. How on earth did this happen? The image loading which was affected seemingly had nothing to do with this new framework we had introduced, and this bug would happen whether or not the user had interacted with this new third-party functionality.
Eventually, using process of elimination, we sorted which code the different manifestations of this bug had in common — functions calling into a preëxisting third-party framework we were using for image manipulation. We searched for all uses of these functions, and, lo and behold, they showed up in our new gif framework as well.
The key here was that both our application and this new framework used the same third-party image manipulation library — and this image manipulation library implemented some of what it did using Objective-C categories.
Objective-C categories (i.e. @implementation UIView (MyNewFunctionality) … @end) do a special thing. Whereas Swift extensions are implemented statically, where all of the functionality in them is known at compile time, Objective-C categories are implemented dynamically, where all of the functionality is dispatched at runtime. In Objective-C, categories enter new functions into their classes’ dispatch tables, i.e. the list of all of the functions the class can respond to.
This is one of the most powerful things about Objective-C, but also one of the most dangerous. In our case, we had unknowingly brought in two categories on the same class declaring the same methods, which had two different implementations for those methods. The one that “wins” and actually ends up in the dispatch table is whichever one is compiled last, which is something that’s possible to predict, but functionally pretty random when you’re compiling a large application.
Fixing this was pretty simple once we sorted it out — we prefixed the names of these functions in our copy of the category, and that fixed the conflict. An interesting side note is that this is another manifestation of the Objective-C need for prefixing class names with acronyms, except here we had to prefix the actual function names. Both are symptoms of a system built for dynamic dispatch.
But a more mysterious example hit us over a year ago, in the fray of migrating to iOS 13.
Part 2: Our heroes run into a zombie
During our upgrade to iOS 13 last year, we had to replace the implementation for our app drawer. The old code we were using had an unworkable bug under iOS 13, and was unnecessarily complex anyway. In the end, we reduced the size of the drawer code by a factor of ten and significantly reduced the complexity of its approach. Yay!
There were many things we had to address for iOS 13. Some years are more complex than others, and this was a more complex transition for us than usual. So, we left the old third-party drawer code in, planning to remove it in the following release. It wasn’t called any longer, and was just sitting around waiting to be removed: “dead” code.
But when we released our shiny new iOS-13 version, with this dead code sitting around but no longer used, we still saw the symbols for this old code showing up in our crash logs. They were the normal, low-running, hard-to-fix crashes that many applications deal with, so their existence wasn’t the issue. The issue was: how on earth was this dead code still being used by our application, when it clearly wasn’t called anywhere?
If you’re not familiar with method swizzling, it’s another use of the Objective-C dispatch tables: you can swap the implementation for a function with a new implementation you’re defining. The right place to do this is in the +load function for your class within a dispatch_once block, as described in Mattt’s NSHipster post from 2014, which is exactly how it was done in this zombie code as well. And, as described in that post, our zombie code was forwarding on to the original functions after its swizzled implementations were called. Unfortunately, it was also implemented in a category, with the same intention as Mattt describes there — to add some global behaviour to all UIViewControllers.
The tricky thing about this is that all we had to do in order to cause this swizzling to execute was to compile the code, and then have any other line of code in our application call +load on UIViewController or a subclass of it, either explicitly or implicitly, which happens in any normal application. The sheer existence of the code causes this to happen, regardless of whether any of it is ever used. Of course, this is the normal way that global swizzling from a category is supposed to work — but we didn’t know that this old dead code contained that kind of implementation, so we blundered on obliviously. The low-running crash that pointed this issue out to us continued on after we removed the zombie code, now showing up in UIViewController rather than in the now-removed zombie code.
In both of these cases, Objective-C code that was unknown to us ended up changing the functions that we intended to call. Luckily, in the second case, the swizzled implementations didn’t add anything to our program execution; but they could have, and there could have been a similarly mysterious bug as in our first tale. (Of course, we might have found this issue before release if that had happened!)
Objective-C can be a friend, but as we start to think in Swift more and more, it’s important for us to remember that Objective-C categories are not the same as Swift extensions; they’re dynamic additions to the dispatch table, and the compiler will not warn us about conflicts between them. In our first tale, that meant that we got a random implementation of some methods that were implemented in two different categories on the same class; in our second, it meant that we continued routing common UIKit functions through code we thought was dead. If there’s a moral here, it’s perhaps to both always know what your Objective-C code is doing, and to remember that no matter what you’re coding, having less code is almost always better.