drekka drekka - 9 months ago 65
iOS Question

iOS: objc_copyClassNamesForImage not working on device

I have a test app (which does nothing because I'm using it to test a bug) that includes 3 frameworks I've built. The frameworks are copied using a copy files phase into the Frameworks directory of the app. I have the following app delegate code:

#import "AppDelegate.h"
@import ObjectiveC;

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[NSBundle allFrameworks] enumerateObjectsUsingBlock:^(NSBundle *framework, NSUInteger idx, BOOL *stop) {
unsigned int count = 0;
__unused const char** classes = objc_copyClassNamesForImage([[framework executablePath] UTF8String], &count);
NSRange inFramework = [framework.executablePath rangeOfString:@".app/Frameworks"];
if (inFramework.length > 0) {
NSLog(@"Framework %@, classes: %i", framework.executablePath.lastPathComponent, count);
}
}];
return YES;
}


@end

Which prints out the frameworks from the Frameworks directory and the number of classes contained in each.

When I run this code in the simulator I get the following results:

2016-11-04 12:02:17.682 RuntimeTest[54326:623229] Framework PEGKit, classes: 24
2016-11-04 12:02:17.705 RuntimeTest[54326:623229] Framework Alchemic, classes: 57
2016-11-04 12:02:17.707 RuntimeTest[54326:623229] Framework StoryTeller, classes: 10


However when I run it on a device I get:

2016-11-04 12:07:04.215417 RuntimeTest[1035:365233] Framework PEGKit, classes: 0
2016-11-04 12:07:04.224495 RuntimeTest[1035:365233] Framework Alchemic, classes: 0
2016-11-04 12:07:04.254946 RuntimeTest[1035:365233] Framework StoryTeller, classes: 0


The device is an iPhone 7 with iOS 10.1. I'm pretty sure that this code has worked in the past and it appears that
objc_copyClassNamesForImage
is somehow broken.

My working theory is that this could be some 10.1 bug. Or perhaps something that is set/not set in the build properties.

In anyone able to confirm this? or have any idea what might be wrong?

Answer Source

Ok I've solved the problem. It appears that on a device the executable paths you get back are not quite correct. I've found that if I create a file reference to the executable, then generate a new URL from the file reference I get a slightly different path. Usually prefixed with /private. Using this new path allows me to interrogate the framework and get the class counts.

FYI, here is the code I'm now using:

-(void) printFrameworkDetails:(CFBundleRef) framework {

    // Get the path to the executable in the framework. Note that on devices, this path is not entirely correct.
    CFURLRef executableURL = CFBundleCopyExecutableURL(framework);
    CFAutorelease(executableURL);

    // Get a file reference from the executable URL, then convert to a system path. This process of going through a file reference corrects the incorrect executable path returned on a device.
    CFURLRef fileRefereceURL = CFURLCreateFileReferenceURL(NULL, executableURL, NULL);
    CFAutorelease(fileRefereceURL);
    CFStringRef executablePathString = CFURLCopyFileSystemPath(fileRefereceURL, kCFURLPOSIXPathStyle);
    CFAutorelease(executablePathString);
    const char *filepath = CFStringGetCStringPtr(executablePathString, kCFStringEncodingUTF8);

    // Now get the number of classes.
    unsigned int ccount = 0;
    const char** classes = objc_copyClassNamesForImage(filepath, &ccount);
    NSLog(@"\n\tframework : %@\n\texecutable: %@\n\tclasses   : %i", framework, executablePathString, ccount);
    free(classes);

}