2

In our testing framework (based on Kiwi, which in turn is based on XCTest), we're using the "swizzling on load" technique described on NSHipster here to switch some stuff with mocks on load. It has been working well enough until we upgraded to XCode 7, and now somehow +load methods are called twice. As far as I understand this should never happen no matter what?

Here's the stack trace of the first +load call (takes place before main):

 Foo`+[FooManager(self=FooManager, _cmd="load") load] + 149 at FooExtensions.mm:152
 libobjc.A.dylib`call_load_methods + 292
 libobjc.A.dylib`load_images + 129
 ...
 dyld`dyld::useSimulatorDyld(int, macho_header const*, char const*, int, char const**, char const**, char const**, unsigned long*) + 1053
 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 202
 dyld`dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*) + 428
 dyld`_dyld_start + 71

And this is how the second callstack looks like:

FooTests`+[FooManager(self=FooManager, _cmd="load") load] + 149 at FooExtensions.mm:152
ibobjc.A.dylib`call_load_methods + 292
ibobjc.A.dylib`load_images + 129

libdyld.dylib`dlopen + 70
CoreFoundation`_CFBundleDlfcnLoadBundle + 185
CoreFoundation`_CFBundleLoadExecutableAndReturnError + 336
Foundation`-[NSBundle loadAndReturnError:] + 641
XCTest`_XCTestMain + 542
IDEBundleInjection`____XCBundleInjection_block_invoke_2 + 20
CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 16
CoreFoundation`__CFRunLoopDoBlocks + 195
CoreFoundation`__CFRunLoopRun + 1016
CoreFoundation`CFRunLoopRunSpecific + 470
CoreFoundation`CFRunLoopRunInMode + 123
GraphicsServices`GSEventRunModal + 192
GraphicsServices`GSEventRun + 104
UIKit`UIApplicationMain + 160
Foo`UIApplicationMain(argc=<unavailable>, argv=<unavailable>, principalClassName=0x00000000, delegateClassName=@"AppDelegate") + 227 at ApplicationHooks.m:56
Foo`main(argc=5, argv=0xbfff7778) + 146 at main.mm:15
libdyld.dylib`start + 1

It looks like the dynamic loader initially calls the +load methods as expected, but then the XCTest runtime calls them again.

The thing is, even dispatch_once doesn't work as static variables don't seem to be initiated properly so the dispatch_once_t tokens change between +load invocations! The only thing that worked was creating a C++ class and delegating the dispatch_once call to it (using a proper c++ static variable for the dispatch_once_t).

EDIT - I'm pretty sure this +load order change is related, but I don't see how changing the order would have caused it to be run twice.

EDIT2 - It seems that this behavior is not new. From a comment in a related blog post:

If you are running a test bundle that is injected into an app, and both the app and the test bundle link to the same .a file, any load method in that .a file will be triggered twice.

Community
  • 1
  • 1
Ohad Schneider
  • 36,600
  • 15
  • 168
  • 198
  • Pretty sure your bundle is being unloaded in-between calls to load. Try creating an `__attribute__((destructor))` function and see if that's true. If my theory is correct, you can solve it by having your test target not link to the framework which has the category, or by runtime-linking using `dlopen` and friends. – Richard J. Ross III Oct 15 '15 at 18:07
  • Thanks for the quick response! I implemented both constructor and destructor as described here: http://stackoverflow.com/q/2053029/67824 and fired an NSLog in each of them. Interestingly, the bundle never unloaded, but it did load twice. In light of that, should I still try your workaround? If so, my test target needs the library that has the category (like I said we're using it to swizzle some stuff with mock on `+load`) so I guess I should go the `dlopen` route? – Ohad Schneider Oct 18 '15 at 14:55
  • BTW is it necessarily a bad thing that the bundle is loaded twice? Otherwise maybe I can just get away with doing the swizzling in the app delegate (`didFinishLaunchingWithOptions`)? – Ohad Schneider Oct 18 '15 at 22:43

1 Answers1

2

It appears that you have included the same class extension in both the main application binary and in your test bundle. This would also explain why you see a separate setup of static dispatch_once tokens on the second load.

Why this is the case may be a number of things:

  • .mm-file is included in both targets
  • Xcode loads the test binary twice due to changes in test-procedure
  • Both test target and application link to the same .a file containing the +load method
Ohad Schneider
  • 36,600
  • 15
  • 168
  • 198
Mats
  • 8,528
  • 1
  • 29
  • 35
  • I think the cause is even simpler than that - I really am linking against the bundle including the class extension in both the app and the test target (see my last edit). – Ohad Schneider Oct 19 '15 at 13:17