Hello World!

Little Jonathan’s First Reverse Engineering

Before I was knee high to a grasshopper as a kid in Minnesota my dad, a mechanical engineer by education, did a bad, bad thing he would occasionally regret. He taught me how to take things apart…

When I was a few years old, I approached my grandfather with an odd request. “Grampa, can I borrow a screwdriver? I need one of the flat ones.” After a series of carry-this-with-the-point-facing-downward and please-don’t-run warnings, he fished the requested tool from a kitchen drawer. Some time later, much to his surprise and chagrin, I proudly reappeared with two door knobs and a latch mechanism in hand. I had noticed the knobs would operate the latch when turned in one direction, but when turned in the other direction nothing would happen. By methodically disassembling the door latch I had learned that the rectangular shaft the knob turns inside of the door was made of metal and accepted by an asymmetrically worn plastic part within the latch mechanism. I found the reason that turning the knob worked in only one direction. My sharing this discovery amused my parents. My bemused grandfather asked me to please put his front door back together.

I am a software developer now, so I’m essentially an overgrown version of that methodically tinkering kid. At Enharmonic we spend a lot of time writing complex iOS applications. During the course of this work it is not uncommon for us to push beyond documentation to ascertain how iOS actually behaves internally.

Scenario

Imagine that you’ve been asked to create a view controller for displaying a PDF using a UIWebView. This view controller should monitor the scroll position of the PDF to display auxiliary UI when the user scrolls to a particular point inside.

It seems pretty straightforward to implement this behavior: subclass UIWebView, load the PDF, override the UIScrollViewDelegate methods to monitor the scroll view’s scroll position, update your auxiliary UI as needed. Done!

Unfortunately, the documentation disallows subclassing UIWebView.

Subclassing Notes
The UIWebView class should not be subclassed.

UIWebView embeds web or document content in a scrolling UI. You can load remote or local HTML, MS Office, iWork, RTF/RTFD, or PDF files. It acts in a way that is similar to the central scrolling view of the Safari web browser. UIWebView exposes its UIScrollView, the scrolling view that hosts the web view’s content, as a readonly property as of iOS 5. The documentation for what you can do with the webview hosted scrollview is terse: “Your application can access the scroll view if it wants to customize the scrolling behavior of the web view.”

Object Diagram -- UIWebView and its UIScrollView.

UIWebView implements the UIScrollViewDelegate protocol. A scroll view’s delegate can respond to scrolling events when the user interacts with the scroll view and can customize zooming behavior by providing a view within the scroll view for the system to scale up and down as the user pinches.

In the scenario described above you would need to find a way to insert some code into the UIScrollViewDelegate methods of the UIWebView without getting in the way of Apple’s implementation of the UIWebView to override the delegate methods. You are not allowed to accomplish this by subclassing UIWebView. Since the web view is its scroll view’s delegate, it may not respond well to some other object becoming its scroll view’s delegate. A UIScrollView can have only one delegate and the web view definitely needs to know about scrolling, but so does your view controller. They can’t both be the delegate. The documentation does not address whether being allowed to “…access the scroll view if it wants to customize the scrolling behavior of the web view” extends to becoming that scroll view’s delegate instead of its web view nor does it mention how to do so without causing problems for the web view.

How can you implement the behavior you need? Would setting the scroll view’s delegate to an object other than its web view lead to problems in the web view itself? Grab a screwdriver. You are going to tear this doorknob apart to reverse engineer your way to answers that the documentation does not directly provide.

Test Harness

We have created a sample project called WebTest2 for testing UIWebView. Download the project here. Open it in Xcode and build and run the app on a device or on the simulator. It should look like the image below.

WebTest2 App Screenshot

Open ViewController.m. Notice that it has an IBOutlet property connected to a UIWebView loaded from the app’s main storyboard file. The view controller loads a PDF containing a Beethoven piano sonata during the viewDidLoad method shown below.

- (void)viewDidLoad {
[super viewDidLoad];
NSAssert(self.webView != nil, @"Check IB -- Wire-up the web view.");

NSString *path = [[NSBundle mainBundle]
pathForResource:@"IMSLP00008-Beethoven__L.v._-_Piano_Sonata_08"
ofType:@"pdf"];
NSData *data = [NSData dataWithContentsOfFile:path];
[self.webView loadData:data
MIMEType:@"application/pdf" textEncodingName:@"utf-8"
baseURL:[NSURL URLWithString:@""]];

/*
[self.webView.scrollView setDelegate:self];
*/
}

Note: You may also notice some commented-out code in ViewController.m and that we’ve implemented a subclass of UIWebView with some commented-out UIScrollViewDelegate methods inside. You will need this code later on in this tutorial.

LLDB

You have probably used the LLDB debugger in Xcode to investigate bugs in your iOS or OS X code. Xcode’s UI allows you to add breakpoints, to step through your C, C++, Objective-C, or Swift code, and to investigate the values of local variables as your project as it is running on a device or on the simulator. In addition to the features exposed by Xcode’s UI, LLDB contains a powerful set of debugging features through a command line interface.

Investigating the scroll view’s delegate callbacks

Open ViewController.m and add a breakpoint at the top of the viewDidLoad method by clicking in the trough to the left of the code on line number 20 (ViewController.m:20).

Breakpoint set on line 20.

Build and run the app. The debugger should stop at the breakpoint. If the (lldb) prompt is not visible on the screen use the (⇧⌘Y) keyboard shortcut to toggle the debug area, and then use the (⇧⌘C) keyboard shortcut to place the cursor in the console.

To see which object is the delegate of the web view’s scroll view type po self.webView.scrollView.delegate (po is short for “print object”) command at the (lldb) prompt.

(lldb) po self.webView.scrollView.delegate
nil

That’s curious. The web view’s scroll view does not report having a delegate at all. How does a UIWebView handle scroll and zoom events if it is not the delegate of its scroll view?

In ViewController.m try setting the webview.scrollview.delegate to the view controller itself. Uncomment the existing code shown below.

/*
[self.webView.scrollView setDelegate:self];
*/

You need not implement any delegate methods, yet. All of the methods in UIScrollViewDelegate are marked optional.

Deactivating breakpoints.

Disable all breakpoints by clicking the breakpoint button in the debug area toolbar. Now build and run with breakpoints disabled. Scroll up and down in the pdf just to see if becoming the scroll view’s delegate without implementing any delegate methods causes any obvious trouble.

You will notice that nothing changes behaviorally. The web view continues to work as before. Scrolling works as always and the page number view at the top of the web view updates normally. Somehow, even though your view controller is now the delegate of its scroll view, the web view continues to act as though it receives the delegate callbacks, even when another object becomes the delegate of its scroll view.

When you want content within a scroll view to zoom in and out when the user pinches on the screen, you implement viewForZoomingInScrollView: in the scroll view’s delegate and return the UIView you want the system to scale. You can return nil if you do not want the scroll view to zoom at all. Since the web view as it is configured in the test project zooms in and out, it seems likely that the web view returns a view when this delegate method is called. What happens when you override this method in the view controller and return nil? Since the view controller is now the delegate of the scroll view, does this signal to the scroll view that it should not zoom or does the web view maintain control? Might it cause the app to crash or introduce some other nasty behavior? Will the scroll view even call this delegate method on the view controller?

Implement viewForZoomingInScrollView: in ViewController.m to return nil. Do not uncomment the more complicated code in this file just yet. Copy and paste the following code instead.

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView;
{
NSLog(@"%@ %@ %@", self, NSStringFromSelector(_cmd), scrollView);
return nil;
}

This code will log self, the method name, and the scroll view to the console and return nil.

Build and run.

Notice in the console that viewForZoomingInScrollView: on the View Controller is getting called many times as the WebView is loaded and initially displayed.

Try pinching to zoom while watching the console. If running in the simulator you can hold down the option key to get two touch points, then click and drag to simulate a pinch.

Xcode console when viewForZoomingInScrollView returns nil.

Notice that the web view no longer zooms when you pinch and that viewForZoomingInScrollView: is called on the view controller when you attempt to zoom. Now try scrolling. Notice that the page number view and all other behavior remains unchanged. Returning nil from our view controller and setting our view controller as the scroll view’s delegate seems to short-circuit the zooming of the web view, but other behavior remains unchanged. The scroll view seems to treat the view controller as its delegate in this case.

Leave the app running and add a breakpoint in the view controller’s viewForZoomingInScrollView: implementation.

Xcode viewForZoomingInScrollView breakpoint.

Try to zoom. The debugger should break.

To look at the description of the web view’s scroll view, type po scrollview into the (lldb) prompt and hit the enter key.

(lldb) po scrollView
; layer = ; contentOffset: {0, 0}; contentSize: {320, 8401.9512195121952}>

Notice that the scroll view is a subclass of UIScrollView_UIWebViewScrollView. It looks like Apple has implemented a custom (private, as denoted by the underscore) UIScrollView subclass for the UIWebView. This may be a clue.

Now take a look at the backtrace by typing bt or backtrace into LLDB.

(lldb) bt
* thread #1: tid = 0x25f377, 0x0000000101f0059d WebTest2`-[ViewController viewForZoomingInScrollView:](self=0x00007fa4b87877f0, _cmd=0x000000010336f950, scrollView=0x00007fa4b87922b0) + 125 at ViewController.m:34, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
* frame #0: 0x0000000101f0059d WebTest2`-[ViewController viewForZoomingInScrollView:](self=0x00007fa4b87877f0, _cmd=0x000000010336f950, scrollView=0x00007fa4b87922b0) + 125 at ViewController.m:34
frame #1: 0x000000010268fdec CoreFoundation`__invoking___ + 140
frame #2: 0x000000010268fc42 CoreFoundation`-[NSInvocation invoke] + 290
frame #3: 0x0000000102720016 CoreFoundation`-[NSInvocation invokeWithTarget:] + 54
frame #4: 0x0000000102e038a9 UIKit`-[_UIWebViewScrollViewDelegateForwarder forwardInvocation:] + 172
frame #5: 0x00000001026f6f4f CoreFoundation`___forwarding___ + 495
frame #6: 0x00000001026f6cd8 CoreFoundation`__forwarding_prep_0___ + 120
frame #7: 0x0000000102c24abd UIKit`-[UIScrollView _getDelegateZoomView] + 90
frame #8: 0x0000000102c28937 UIKit`-[UIScrollView _zoomScaleFromPresentationLayer:] + 24
frame #9: 0x000000010311f677 UIKit`-[UIWebPDFView _viewCachingBoundsInUIViewCoords] + 117
...

Note the call to -[_UIWebViewScrollViewDelegateForwarder forwardInvocation:] in the backtrace. This may be a clue.

Hypothesis

Hypothesis: _UIWebViewScrollViewDelegateForwarder somehow ensures that the web view continues to work when another object becomes the _UIWebViewScrollView‘s delegate, performing some kind of magic to decide which of the delegates recieves call backs.

Subclassing UIWebView as an Investigative Tool.

Now you will replace the current, default UIWebView with a subclass called TESTWebView included in the project you downloaded. First, revert the code in ViewController.m to its original state. The easiest way to do this is to navigate in Terminal.app to the directory containing the project and typing git reset --hard. The code you download has a basic git setup for this purpose. You may want to selectively checkout the view controller implementation file or git stash your changes first if you’ve done other experimentation in the project or changed provisioning and code signing settings as git reset --hard will reset the project completely.

Open Main.storyboard and select the UIWebView. Navigate to the Identity Inspector and set the class to TESTWebView as shown below.

Setting custom UIWebView subclass.

Note: Do not ship a subclass of UIWebView to users; Apple asks you in the documentation not to do this, but it’s often informative to do dirty things like subclassing Apple’s code to understand how things work.

Build and run. The app should behave as it did before you subclassed UIWebView. This serves as verification that implementing an empty subclass of UIWebView does not break anything. Scrolling and zooming as well as the little page number view should work as before.

Uncomment the scroll view delegate methods in TESTWebView.m shown below.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
{
NSLog(@"%@ %@ %@", self, NSStringFromSelector(_cmd), scrollView);
if ([super respondsToSelector:_cmd])
{
[super scrollViewDidScroll:scrollView];
}
}

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView;
{
NSLog(@"%@ %@ %@", self, NSStringFromSelector(_cmd), scrollView);
UIView *viewForZooming = nil;
if ([super respondsToSelector:_cmd])
{
viewForZooming = [super viewForZoomingInScrollView:scrollView];
}
return viewForZooming;
}

Build and run. Notice that the delegate methods are called repeatedly on launch, much as you saw previously. Add a breakpoint in each new method in TESTWebView, then scroll the PDF to drop into the debugger during -[TESTWebView scrollViewDidScroll:]. Show the backtrace with bt.

(lldb) bt
* thread #1: tid = 0x28ee34, 0x000000010ed22280 WebTest2`-[TESTWebView scrollViewDidScroll:](self=0x00007fa2b8c26510,
_cmd=0x000000011053b1ed, scrollView=0x00007fa2ba056200) + 112 at TESTWebView.m:16,
queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
* frame #0: 0x000000010ed22280 WebTest2`-[TESTWebView scrollViewDidScroll:](self=0x00007fa2b8c26510, _cmd=0x000000011053b1ed, scrollView=0x00007fa2ba056200) + 112 at TESTWebView.m:16
frame #1: 0x000000010f6906ec CoreFoundation`__invoking___ + 140
frame #2: 0x000000010f69053e CoreFoundation`-[NSInvocation invoke] + 286
frame #3: 0x000000010f722c76 CoreFoundation`-[NSInvocation invokeWithTarget:] + 54
frame #4: 0x000000010fe49b2b UIKit`-[_UIWebViewScrollViewDelegateForwarder forwardInvocation:] + 94
frame #5: 0x000000010f6f8c57 CoreFoundation`___forwarding___ + 487
frame #6: 0x000000010f6f89e8 CoreFoundation`__forwarding_prep_0___ + 120
frame #7: 0x000000010fc16068 UIKit`-[UIScrollView(UIScrollViewInternal) _notifyDidScroll] + 66
frame #8: 0x000000010fc0386b UIKit`-[`UIScrollView` setContentOffset:] + 651
frame #9: 0x000000010fc081ac UIKit`-[`UIScrollView` _updatePanGesture] + 2066
...

Type c and hit enter in the debugger to continue. Next try pinching. The debugger should now break in the TESTWebView‘s viewForZoomingInScrollView: method. Type bt and compare the backtraces. Note any similarities.

In ViewController.m, once again set the scroll view delegate and reimplement viewForZoomingInScrollView: as shown below.

- (void)viewDidLoad {
...
[self.webView.scrollView setDelegate:self];
}

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView;
{
NSLog(@"%@ %@ %@", self, NSStringFromSelector(_cmd), scrollView);
return nil;
}

Place a breakpoint after the NSLog line in -[ViewController viewForZoomingInScrollView:]. Build and run with the breakpoints disabled. Watch the console for log messages while scrolling and zooming. Do you discern a pattern or order for the scroll view delegate calls?

Some Useful LLDB Commands

The LLDB command line has a number of powerful commands built in. Try typing breakpoint list into LLDB.

(lldb) breakpoint list
Current breakpoints:
1: name = 'objc_exception_throw', locations = 1, resolved = 1, hit count = 0
1.1: where = libobjc.A.dylib`objc_exception_throw, address = 0x000000010c300d9d, resolved, hit count = 0
2: file = '/Users/jonathan/WebTest2/WebTest2/TESTWebView.m', line = 26, locations = 1, resolved = 1, hit count = 2
2.1: where = WebTest2`-[TESTWebView viewForZoomingInScrollView:] + 112 at TESTWebView.m:26, address = 0x000000010be09360, resolved, hit count = 2

This shows a list of all of your breakpoints. Now find the viewForZoomingInScrollView breakpoint in the list and note its number. In the above it’s “2.1”, but depending on your Xcode setup or other breakpoints you have enabled it may be a different number. To disable that breakpoint type breakpoint disable 2.1 replacing “2.1” with your number and hit enter.

(lldb) breakpoint disable 2.1
1 breakpoints disabled.

Now type breakpoint list again.

(lldb) breakpoint list
Current breakpoints:
...
2: file = '/Users/jonathan/WebTest2/WebTest2/TESTWebView.m', line = 26, locations = 1
2.1: where = WebTest2`-[TESTWebView viewForZoomingInScrollView:] + 112 at TESTWebView.m:26, address = 0x000000010be09360, unresolved, hit count = 2 Options: disabled

You should see “Options: disabled” next to the breakpoint you identified. To re-enable a breakpoint, type breakpoint enable and then the number and hit enter.

To step through code in the debugger you can use the execution control buttons in the debug area.

Xcode debug execution controls.

I find using the mouse to click those little buttons tedious. Luckily, there are commands for all of these in the LLDB command line.


– Type n and hit enter to move the point of execution to the next line of code (step over). I remember this as “n for next.” This is equivalent to clicking the button shown below in the Xcode debugger execution controls.

Step over button.

  • Type s and hit enter to step into the function or method on that line. I remember this as “s for step-in.” This is equivalent to clicking the button shown below in the Xcode debugger execution controls.

Step in button.

  • Type finish and hit enter to step out of a function or method. This is equivalent to clicking the button shown below in the Xcode debugger execution controls.

Step out button.

  • Type c to continue execution. I remember this as “c to continue.” This is equivalent to clicking the button shown below in the Xcode debugger execution controls.

Continue button.

You can repeat the previously entered command by hitting the enter key at the (lldb) prompt. For example, if you wanted to step 5 times, you can type n enter, enter, enter, enter, enter. LLDB will repeat the n command without retyping.

Using breakpoint delete and the number will delete a breakpoint.

Note: Xcode will recreate any breakpoints that you have set in Xcode the next time you run the debugger, but will not recreate any breakpoints you set directly in the command line.

The game is afoot

Build and run again with breakpoints disabled. Once the app is running, set a breakpoint in TESTWebView‘s viewForZoomingInScrollView: (you may already have one set there) and enable breakpoints. Pinch to zoom. The debugger should break in the TESTWebView‘s’ viewForZoomingInScrollView: method.

...
if ([super respondsToSelector:_cmd])
{
viewForZooming = [super viewForZoomingInScrollView:scrollView]; //Step over this line
}
...

Step over (n for next) until you have reached just beyond the viewForZooming = line below the “Step over this line” comment in the code listing above. Remember, once you type n for next once you can repeatedly tap the enter key until the debugger stops on the line you want. Now po the viewForZooming variable.

(lldb) po viewForZooming
; layer = >

This shows that the web view does return a view to zoom. This makes sense given that we’ve seen the scroll view zooming. Notice the size of the UIWebPDFView! It’s a very tall pdf. Beethoven wrote a bunch of notes in there.

Use the c command to continue. Repeat until the debugger stops at the view controller’s breakpoint. Notice that the backtrace is similar to those you have seen before. You might have noticed the order of the delegate callbacks. It looks like the _UIWebViewScrollViewDelegateForwarder is designed to call the UIWebView first and then call its scroll view’s delegate. It also looks like its scroll view’s delegate can overrule the web view’s viewForZoomingInScrollView return value since returning nil from the view controller caused the web view not to zoom.

Try breaking on all implementations of viewForZoomingInScrollView:

(lldb) breakpoint set -r "viewForZoomingInScrollView"
Breakpoint 5: 5 locations.

In LLDB breakpoint set means “set a breakpoint.” The -r flag means “match this regular expression.” You can use a more complicated regular expression to catch certain method signatures that match a pattern, but right now you’re just looking to break on any method that matches “viewForZoomingInScrollView”, so this command will work.

If you would like to see all 5 locations LLDB knows about where viewForZoomingInScrollView is implemented, type breakpoint list before moving on. This shows you all of the locations LLDB has resolved for the regular expression you asked it to match for this breakpoint. There is some interesting stuff in the list that shows some other scroll view delegates that implement this method in Apple’s frameworks.

Use the c command to continue. Note the backtrace each time the debugger stops by typing bt. You’ll also notice that the debugger stops in -[UIWebView viewForZoomingInScrollView:] because you called through to super‘s implementation in the subclass, but you see disassembly instead of the actual code. Since Xcode doesn’t have the source code available for UIWebView‘s’ implementation, it will show you disassembly instead.

You may have noticed -[_UIWebViewScrollViewDelegateForwarder forwardInvocation:] in the backtraces for the delegate methods. You can set a breakpoint in that method using the breakpoint set --name command.

Build and run with breakpoints enabled. Once the debugger stops execution, add a symbolic breakpoint in -[_UIWebViewScrollViewDelegateForwarder forwardInvocation] like so.

(lldb) breakpoint set --name "-[_UIWebViewScrollViewDelegateForwarder forwardInvocation:]"
Breakpoint 4: where = UIKit`-[_UIWebViewScrollViewDelegateForwarder forwardInvocation:], address = 0x00000001029cfacd

The “–name” flag means “break on this exact method signature by name”. If you type breakpoint list again, you’ll notice that the regular expression breakpoint you set before did not persist. Remember that every time you build and run, LLDB clears your list of breakpoints. When Xcode starts LLDB, it reinserts the breakpoints you set in Xcode, which is why those persist between runs.

Type c to continue and print a backtrace each time the debugger stops. Do you notice any further similarities?

More LLDB Tricks

Build and run again with breakpoints enabled. Pinch to zoom. Once the debugger pauses execution, add your breakpoint set -r "viewForZoomingInScrollView" symbolic breakpoint as before. Type c to continue. The debugger should stop in -[UIWebView viewForZoomingInScrollView:]. You are now stopped inside of Apple’s code. Since Xcode does not have source code for UIWebView, it shows you the disassembly you saw before.

<code>-[UIWebView viewForZoomingInScrollView:]</code> assembly.” title=”<code>-[UIWebView viewForZoomingInScrollView:]</code> assembly.” /></p>
<p>When you set a symbolic breakpoint like this one, the debugger will break before the function prologue. The function prologue is code that prepares the stack and processor registers for executing the code of the function itself. After the function prologue executes, arguments to functions will be in known registers. For example during an Objective-C message send, the pointer to “self” is always placed in a particular register, which is different per platform (such as armv7 vs arm64 or i386). Before the function prologue executes that may not be the case. It is the function prologue’s job to place the values in these known registers. It is sometimes useful to be able to print out the arguments to a function or method, so LLDB has an option you can add to <code>breakpoint set</code> called <code>--skip-prologue</code> that will tell it to break after the prologue has placed those registers into a known state. You can then print values stored in registers to explore arguments to the function or method.</p>
<p>To see this in action, build and run with breakpoints enabled. Wait for the debugger to stop and type <code>breakpoint set --skip-prologue true -r to cause LLDB to set a regular expression breakpoint just after the prologue. Type c to continue until you again see disassembly in Xcode. The debugger should have stoped in -[UIWebView viewForZoomingInScrollView:] as before. If you want to see the value of “self” (the TESTWebView) in this method on 32-bit arm processors (such as the iPhone 4S) you would type po $r0, which translates to “printout register r-zero.” This is where the pointer to self is stored on 32-bit arm. On 64-bit arm processors, the command is po $x0. On x86 processors… well, this can get complicated. Luckily, you do not need to memorize these; LLDB maintains an alias to the first argument’s register it uses on all platforms called $arg1. You can type po $arg1 to get the first argument to a C function on whichever architecture you are running. The [objc_msgSend()][Messaging] variadic C function, one of the functions the Objective-C runtime uses to send messages to objects, has a particular argument order. The first argument is self, the second argument is the selector, the third argument is the first argument to the method (the scrollView in viewForZoomingInScrollView:, for example) and so on.

(lldb) po $arg1
>

“Self” (stored in the register corresponding to the $arg1 alias) is the TestWebView instance.

To see the second, selector argument, you look in $arg2. This is the _cmd argument you have seen in calls to NSLog in the test project. Since it’s an Objective-C selector, you will want to wrap $arg2 (_cmd) in a call to NSStringFromSelector(), otherwise LLDB will treat it as an integer.

(lldb) po NSStringFromSelector($arg2)
viewForZoomingInScrollView:

To see the first argument to the Objective-C method (the third argument to objc_msg_send()) you use $arg3 like so.

(lldb) po $arg3
; layer = ; contentOffset: {10, 18}; contentSize: {395.36166408245765, 10328.695109977556}>

If there were further arguments, you could use $arg4, etc.

To see the state of commonly used registers on the processor, use register read. This is what that looks like on a 32-bit iOS device.

(lldb) register read
General Purpose Registers:
r0 = 0x145527f0
r1 = 0x2d084885 "viewForZoomingInScrollView:"
r2 = 0x152a3200
r3 = 0x376300a1 libobjc.A.dylib`objc_msgSendSuper2 + 1
r4 = 0x14520260
r5 = 0x14573320
r6 = 0x145731e0
r7 = 0x00120fe8
r8 = 0x37f9b0a4 CoreFoundation`NSInvocation._frame
r9 = 0x3860650c (void *)0x38606520: UIWebView
r10 = 0x14665c80
r11 = 0x00000004
r12 = 0x2cae549d UIKit`-[UIWebView viewForZoomingInScrollView:] + 1
sp = 0x00120fac
lr = 0x000157fd WebTest2`-[TESTWebView viewForZoomingInScrollView:] + 213 at TESTWebView.m:28
pc = 0x2cae549c UIKit`-[UIWebView viewForZoomingInScrollView:]
cpsr = 0x20000030

That’s a little tough to read. LLDB has a “make this easier to read” command (well, it’s not called that, but that’s how I think of it): register read -A

Note: You may get different output based on the device architecture and version of Xcode used.

(lldb) register read -A
General Purpose Registers:
arg1 = 0x145527f0
arg2 = 0x2d084885 "viewForZoomingInScrollView:"
arg3 = 0x152a3200
arg4 = 0x376300a1 libobjc.A.dylib`objc_msgSendSuper2 + 1
r4 = 0x14520260
r5 = 0x14573320
r6 = 0x145731e0
fp = 0x00120fe8
r8 = 0x37f9b0a4 CoreFoundation`NSInvocation._frame
r9 = 0x3860650c (void *)0x38606520: UIWebView
r10 = 0x14665c80
r11 = 0x00000004
r12 = 0x2cae549d UIKit`-[UIWebView viewForZoomingInScrollView:] + 1
r13 = 0x00120fac
r14 = 0x000157fd WebTest2`-[TESTWebView viewForZoomingInScrollView:] + 213 at TESTWebView.m:28
r15 = 0x2cae549c UIKit`-[UIWebView viewForZoomingInScrollView:]
flags = 0x20000030

LLDB also has a built-in help system. If you’re searching for a command and do not know exactly what it is called use the apropos command. Try apropos thread.

(lldb) apropos thread
The following built-in commands may relate to 'thread':
...
breakpoint command add -- Add a set of commands to a breakpoint, to be
executed whenever the breakpoint is hit. If no
breakpoint is specified, adds the commands to the
last created breakpoint.
breakpoint modify -- Modify the options on a breakpoint or set of
breakpoints in the executable. If no breakpoint is
specified, acts on the last created breakpoint.
With the exception of -e, -d and -i, passing an
empty argument clears the modification.
breakpoint set -- Sets a breakpoint or set of breakpoints in the
executable.
...
thread -- A set of commands for operating on one or more
threads within a running process.
thread backtrace -- Show the stack for one or more threads. If no
threads are specified, show the currently selected
thread. Use the thread-index "all" to see all
threads.
thread continue -- Continue execution of one or more threads in an
active process.
...
The following settings variables may relate to 'thread':

frame-format -- The default frame format string to use when displaying stack
frame information for threads.
thread-format -- The default thread format string to use when displaying
thread information.
target.process.thread.trace-thread -- If true, this thread will single-step
and log execution.

If you know which command you are interested in, you can learn more about it by typing help and the command name. Like so.

(lldb) help breakpoint
The following subcommands are supported:
clear -- Clears a breakpoint or set of breakpoints in the executable.
command -- A set of commands for adding, removing and examining bits of
code to be executed when the breakpoint is hit (breakpoint
'commands').
delete -- Delete the specified breakpoint(s). If no breakpoints are
specified, delete them all.
disable -- Disable the specified breakpoint(s) without removing it/them.
If no breakpoints are specified, disable them all.
enable -- Enable the specified disabled breakpoint(s). If no breakpoints
are specified, enable all of them.
list -- List some or all breakpoints at configurable levels of detail.
modify -- Modify the options on a breakpoint or set of breakpoints in
the executable. If no breakpoint is specified, acts on the
last created breakpoint. With the exception of -e, -d and -i,
passing an empty argument clears the modification.
name -- A set of commands to manage name tags for breakpoints
set -- Sets a breakpoint or set of breakpoints in the executable.
For more help on any particular subcommand, type 'help <command></command> '.
<command></command> ```

The help system will give you more information on a subcommand as well. For instance, if you wanted to know more about `breakpoint set`, you can type `help breakpoint set`.

```text
(lldb) help breakpoint set
Sets a breakpoint or set of breakpoints in the executable.
Syntax: breakpoint set
Command Options Usage:
breakpoint set [-DHo] -l [-s ] [-i ] [-c ] [-x ] [-t ] [-T ] [-q ] [-f ] [-K ] [-N ]
breakpoint set [-DHo] -a
[-s ] [-i ] [-c ] [-x ] [-t ] [-T ] [-q ] [-N ] ...
...
-F ( --fullname )
Set the breakpoint by fully qualified function names. For C++ this
means namespaces and all arguments, and for Objective C this means
a full function prototype with class and selector. Can be
repeated multiple times to make one breakpoint for multiple names.
...
-p ( --source-pattern-regexp )
Set the breakpoint by specifying a regular expression which is
matched against the source text in a source file or files specified
with the -f option. The -f option can be specified more than once.
If no source files are specified, uses the current "default source
file"
...

Discovery

By now you’ve probably noticed a pattern emerging within the backtraces in the test project. When the UIScrollViewDelegate methods in both the UIWebView and delegate you set (the view controller) gets called, you can see from the backtraces that a consistent series of steps lead to each delegate method. First, there is a call to the web view’s implementation of each method. Second, the scroll view’s delegate gets called. By default, the scroll view’s delegate property getter returns nil, but the web view gets the callbacks whether or not the scroll view has a delegate. It also looks like when you do set a delegate on the scroll view, the delegate’s viewForZoomingInScrollview‘s return value overrides the return value of the web view’s implementation, otherwise the web view’s return value is used.

Initial Conclusions

You have noticed the delegate forwarder object seems designed to insinuate itself between the scroll view and its web view and between the scroll view and your delegate. Given its name, it seems likely that Apple designed _UIWebViewScrollViewDelegateForwarder to forward UIScrollViewDelegate messages to both objects to allow you full access to the scroll view’s delegate methods without disrupting the web view.

Here is an object diagram including what you’ve discovered so far.

Object Diagram.

What you have found with LLDB helps explain Apple’s intentions when they say “Your application can access the scroll view if it wants to customize the scrolling behavior of the web view”. Besides the behavior you have seen so far, given that there is a class named _UIWebViewScrollViewDelegateForwarder, it stands to reason that Apple intends for you to become the delegate of the scroll view if you need to do so. You could probably conclude now that it is safe to become the scroll view’s delegate, but you might not be comfortable with this just yet. You do not yet know how the forwarder is implemented and whether its implementation could introduce unforeseen fragility into your app.

To learn how Apple implemented the delegate forwarder and how they place it between the scroll view and its delegates, read iOS Reverse Engineering Part II: class-dump and Hopper Disassembler.

References

Additional Resources