Comments:"hookshot - hacking the Objective-C runtime for fun and profiles - Cue:tech"
URL:http://tech.blog.cueup.com/hookshot-hacking-the-objective-c-runtime-for
Today we're open sourcing hookshot, a library for iOS applications that lets you instrument your code to generate thread activity graphs:
and aggregated profiling stats:
The open source repo contains 5-minute installation instructions and an example project.
Instrumenting vs. Sampling
hookshot contains (among other things) an instrumenting profiler. Contrary to its name, Apple's profiler in Instruments is actually a sampling profiler. We find both useful for different occasions. Specifically, we find hookshot most useful for:
- Thread activity graphs with drill-in
- Seeing accurate counts of calls
- More precise per-call timings of messages
- Measurement of messages with highly variable performance
How it works
Hookshot works by transparently wrapping your classes with a custom NSProxy subclass that records profiling data. It replaces alloc for instrumented classes with code that activates this subclass. When it receives messages, it calls custom hooks before and after delegating to the original implementations.
Building hookshot was:
Necessary. (because the quest for a highly performant app is never over!) A blast. (because getting under the hood of Objective C internals is something we're strange enough to find fun)Objective-C's flexible runtime
Objective-C has a very malleable run-time layer - there is a level or two of indirection between a message call and the code you wrote to handle said message. This level of indirection can be used to build interesting things through the Objective C Runtime API.
One very common use of the runtime API is method swizzling, in which you replace the implementation of a given message with your own implementation. One of our favorites is Mike Ash's implementation of zeroing weak references.
NSProxy for instrumentation
The Foundation framework provides NSProxy as a way to create objects that stand in for others and are aware of messages being passed to them.
So, first we built an NSProxy subclass for instrumentation. We have a global registry of callbacks to call before and after a given message, and override forwardInvocation to pass the call through to the original implementation.
Hijacking alloc
The hookshot API allows us to instrument classes instead of individual objects. This means a user doesn't have to modify every single instantiation of an object they want instrumented.
Making this API possible requires us to override the class's allocWithZone.
This splits into two interesting cases - one where the class defines its own allocWithZone and one where it inherits it. Here's the core of the logic:
We also don't return an NSProxy that wraps the result, we actually change the class of the object itself. This is necessary because otherwise any references in the instrumented class to self would still reference the un-instrumented original object.
Preventing instrumentation
Some messages don't play nice with instrumentation. In some cases, it's just that a method is called a lot and the cost of instrumentation starts skewing the results. invokeUsingIMP also fails with an error message on selectors with certain C++ return types and with a crash on selectors that use varargs.
We allow prevention of instrumentation with a simple trick. When looking for message sampleMessage, an NSProxy subclass will first see if it has an implementation of that message, and if so, call it. It's only when that fails that the forwardInvocation happens.
So, when we want to prevent instrumentation for sampleMessage, we copy the original implementation in to the NSProxy subclass.
There is one catch - since we want to prevent instrumentation of different messages for different classes, we need a unique NSProxy subclass for each instrumented class. Fortunately, the runtime allows you to create dynamic subclasses! This is what classes[class] in the above snippet refers to:
A few last notes
The code itself contains a few more details - specifically around detecting and handling a few edge cases like KVO and methods with complex return types.
We love pull requests and we'd love to hear if hookshot is useful to you!
Lastly - if you enjoy crazy iOS runtime hacking as much as we do, we are hiring.