A Story About Swizzling "the Right Way™" and Touch Forwarding
Some people think of me as the guy that does crazy things to ObjC and swizzles everything. Not true. In PSPDFKit I’m actually quite conservative, but I do enjoy spending time with the runtime working on things such as Aspects - a library for aspect oriented programming.
After my initial excitement, things have stalled a bit. I shipped Aspects in our PDF framework, and people started complaining that it sometimes freezes the app, basically looping deep within the runtime, when the New Relic SDK was also linked.
Of course I tried to fix this. Contacting New Relic didn’t bring any results at first, even after two paying customers started to report the same issue. After periodically bugging them for over a month I finally got a non-canned response, pointing me to a blog entry about method swizzling.
This basically says that using method_exchangeImplementations
is really bad, and that pretty much everybody does swizzling wrong. And they indeed have a point. Regular swizzling messes not only with your brain but also with assumptions that the runtime makes. Suddenly _cmd
no longer is what it is supposed to be, and while in most cases it does not matter, there are a few cases where it does very much.
How most people swizzle (including me)
This is the swizzling helper that I’ve used during the last few years:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
This is a very common approach, with a small twist that it takes a block and uses imp_implementationWithBlock
to create an IMP trampoline out of it. Usage is as follows:
1 2 3 4 5 |
|
(Yes, Wacom’s framework for stylus support is horrible. There are way better ways to hook into touch handling, such as subclassing UIApplication
’s sendEvent:
.)
Note the cast to objc_msgSend
. While this (by luck) worked without casting in the earlier days, this will probably crash your arm64 build if you don’t cast this correctly, because the variable argument casting specifications changed. Add #define OBJC_OLD_DISPATCH_PROTOTYPES 0
to your files to make sure this is detected at compile time, or even better, use Xcode 6 and enable error checking on this:
The Crash
This works as expected in most cases, but has the issue that the original implementation will be called with a different _cmd
than it expects. This can be a problem when _cmd
is actually used, such as in the touch forwarding logic. I learned this the hard way after swizzling touchesMoved:withEvent:
to inject additional logic. The app crashed with the popular doesNotRecognizeSelector:
exception.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Somehow UIKit wants to call pspdf_wacomTouchesMoved:withEvent:
on a class that I definitely did not swizzle, and so of course the runtime throws an exception. But how did we end up here? Investigating the stack trace, UIKit’s forwardTouchMethod
looks interesting. Let’s see what this actually does.
Touch forwarding in UIKit
The base class for UIView
is UIResponder
, and it implements all basic touch handling:
(Note: I don’t have access to the UIKit sources, so this might not be 100% accurate. The snippets are based on disassembling UIKit and manually converting this back to C.)
1 2 3 |
|
Here it gets interesting. _cmd
is used directly in this C function that (at least the name suggests) then forwards our touches up the responder chain. But let’s keep digging, just to make sure. For curiosity’s sake, I translated the whole function, including legacy behavior. (I don’t remember any announcement where Apple changed this in iOS 5. Is this somewhere documented? Hit me up on Twitter if you know more.)
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 |
|
At this point I was a few hours in, digging through Apple’s touch forwarding code. You can use Hopper to read through _wantsForwardingFromResponder:toNextResponder:withEvent:
. Most of the code seems to track forwarding phases, checks for exclusiveTouch
, different windows and there’s even a dedicated _UITouchForwardingRecipient
class involved. There’s quite a lot more logic in UITouch than I would have expected.
Forwarding using _cmd
is not restricted to touch handling at all - on the Mac it’s used for mouse[Entered|Exited|Moved]:
as well.
A different approach on swizzling
Our naive use of method_exchangeImplementations()
broke the _cmd
assumption and resulted in a crash. How can we fix this? New Relic suggested using the direct method override. Let’s try that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
This solves our problem. We preserve the correct selector (there’s no pspdf_wacomTouchesMoved:withEvent:
method anymore) and thus UIKit’s touch forwarding works as expected. The method replacing logic is also simpler.
However, there are downsides to this approach as well. We are now modifying the touchesBegan:withEvent:
method of our custom UIView
subclass. There is no default implementation yet, so we get the IMP from UIResponder and then manually call this. Imagine if at some later point, somebody else would swizzle touchesBegan:withEvent:
on UIView
directly using the same technique. Assuming UIView
has no custom touch handling code, they would get the IMP from UIResponder and add a new method to UIView
. But then our method gets called, which already captured the IMP of UIResponder
and completely ignores the fact that we modified UIView
as well.
Epilogue
There are solutions to this problem, but they are extremely complex, such as CydiaSubstrate’s MSHookMessageEx, but since this requires a kernel patch (and thus a jailbreak), it’s not something you would use in an App Store app.
If you read trough the whole article and are wondering why I’m not simply subclassing the touch handlers, you are right. This is the usual approach. However we recently added stylus support for a variety of vendors, and this is built via external driver classes, so that we don’t have to “pollute” the framwork with the different approaches. Wacom is the only vendor that requires direct touch forwarding, and every vendor has it’s own way to manage touches and views. Integrating all these into a single class would result in a very hard-to-maintain class, and licensing issues would also prevent us from shipping the framework binaries directly. Furthermore, only some companies use the stylus code, so we designed this to be modular. (e.g. Dropbox just uses PSPDFKit as a Viewer, and thus doesn’t need that part.)
Further Reading: