Handling private frameworks in Xcode ≥ 7.3
Asked Answered
R

1

18

With Xcode 7.3 / iOS 9.3 Apple removed all private frameworks from the iOS SDKs. For research purposes (not App Store!) I need to work with a private framework (namely BluetoothManager.framework, but this is also an issue for any other private frameworks).

Because these frameworks are no longer delivered in the iOS SDKs, I get a build (linker) error if my project attempts to link to this framework explicitly.

Any ideas for a long(er)-term solution?

Rattler answered 3/5, 2016 at 9:9 Comment(2)
For what it's worth, waiting for more luck I solved it temporarily by installing the iOS9.2 SDK into XCode 7.3 : from there it's possible to go with the private frameworks.Placia
Related question: #36176430Greeley
C
26

You can solve this problem by linking to the private framework dynamically, instead of the more common way of linking at build time. At build time, the BluetoothManager.framework would need to exist on your development Mac for the linker to be able to use it. With dynamic linking, you defer the process until runtime. On the device, iOS 9.3 still has that framework present (and the other ones, too, of course).

Here is how you can modify your project on Github:

1) In Xcode's Project Navigator, under the Frameworks, remove the reference to BluetoothManager.framework. It was probably showing in red (not found) anyway.

2) Under the project Build Settings, you have the old private framework directory explicitly listed as a framework search path. Remove that. Search for "PrivateFrameworks" in the build settings if you have trouble finding it.

3) Make sure to add the actual headers you need, so the compiler understands these private classes. I believe you can get current headers here for example. Even if the frameworks are removed from the Mac SDKs, I believe this person has used a tool like Runtime Browser on the device to generate the header files. In your case, add BluetoothManager.h and BluetoothDevice.h headers to the Xcode project.

3a) Note: the generated headers sometimes don't compile. I had to comment out a couple struct typedefs in the above Runtime Browser headers in order to get the project to build. Hattip @Alan_s below.

4) Change your imports from:

#import <BluetoothManager/BluetoothManager.h>

to

#import "BluetoothManager.h"

5) Where you use the private class, you're going to need to first open up the framework dynamically. To do this, use (in MDBluetoothManager.m):

#import <dlfcn.h>

static void *libHandle;

// A CONVENIENCE FUNCTION FOR INSTANTIATING THIS CLASS DYNAMICALLY
+ (BluetoothManager*) bluetoothManagerSharedInstance {
   Class bm = NSClassFromString(@"BluetoothManager");
   return [bm sharedInstance];
}

+ (MDBluetoothManager*)sharedInstance
{
   static MDBluetoothManager* bluetoothManager = nil;
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
      // ADDED CODE BELOW
      libHandle = dlopen("/System/Library/PrivateFrameworks/BluetoothManager.framework/BluetoothManager", RTLD_NOW);
      BluetoothManager* bm = [MDBluetoothManager bluetoothManagerSharedInstance];
      // ADDED CODE ABOVE
      bluetoothManager = [[MDBluetoothManager alloc] init];
   });
   return bluetoothManager;
}

I placed the call to dlopen in your singleton method, but you could put it elsewhere. It just needs to be called before any code uses the private API classes.

I added a convenience method [MDBluetoothManager bluetoothManagerSharedInstance] because you'll be calling that repeatedly. I'm sure you could find alternate implementations, of course. The important detail is that this new method dynamically instantiates the private class using NSClassFromString().

6) Everywhere you were directly calling [BluetoothManager sharedInstance], replace it with the new [MDBluetoothManager bluetoothManagerSharedInstance] call.

I tested this with Xcode 7.3 / iOS 9.3 SDK and your project runs fine on my iPhone.

Update

Since there seems to be some confusion, this same technique (and exact code) still works in iOS 10.0-11.1 (as of this writing).

Also, another option to force loading of a framework is to use [NSBundle bundleWithPath:] instead of dlopen(). Notice the slight difference in paths, though:

handle = dlopen("/System/Library/PrivateFrameworks/BluetoothManager.framework/BluetoothManager", RTLD_NOW);
NSBundle *bt = [NSBundle bundleWithPath: @"/System/Library/PrivateFrameworks/BluetoothManager.framework"];
Cordelia answered 3/5, 2016 at 9:59 Comment(20)
I reckon that will work, but of course it's an extremely brittle solution. Any API changes will be catastrophic.Locally
@trojanfoe, every time a new iOS version is released, you need to get the updated private headers (from the site I linked ... they regenerate each release). If you stay up to date with the new headers, you'll be fine. By definition, using private APIs is brittle. You do so if you need functionality that doesn't exist in public APIs (as is the case here).Cordelia
Quite a nice and clean solution, thank you !! @trojanfoe: as soon as you begin to play with the Forbidden Holly PrivateFrameworks you mandatorily put yourself in trouble - that's the price to pay :)Placia
@JBA. Thanks. This solution also has the benefit that it does actually let the compiler check for you that the APIs you're using still exist (assuming you get the fresh set of headers with each iOS release). As we know, private APIs can disappear at any time :(Cordelia
Thanks for posting this - I'm getting a compiler error in BluetoothDevice.h Redefinition of BTDeviceImpl It looks like the struct is already defined at the top of the file, within the interface. With that little tweak, it's good to go. You should fork and send a PR - or i can if you want (but you deserve the credit!)Cermet
@Alan_s, yeah, commenting out a couple redundant struct typedefs in the reverse generated header files was necessary for me, too. Answer updated. Go ahead and do the Github PR if you like. An upvote on Stack Overflow is enough credit for me ;)Cordelia
I am trying to implement Private Framework "MobileWIFI.Framework" in xcode 7.3 , but it gives me this error: "_WiFiManagerClientCopyNetworks", referenced from: -[AppDelegate application:didFinishLaunchingWithOptions:] in AppDelegate.o "_WiFiManagerClientCreate", referenced from: -[AppDelegate application:didFinishLaunchingWithOptions:] in AppDelegate.o ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation) i tried with framework ios 9.3, ios 8.1, ios 7 , it gives same error.Pontic
@MuhammadFaizanKhatri, please ask a new question here on Stackoverflow. This comment does not relate to the original questioner's problem.Cordelia
@Cordelia i have posted a new question for it :Here is the LinkPontic
@nate, your solution works fine. But I'm curious why Xcode dose not complain linking errors(we still reference undefined APIs in BluetoothManager.h) with dynamic link?Reichel
@Quanlong, Xcode does not know that you're linking to a private framework. It doesn't search for usage of dlopen() and then inspect the framework name you give it. Linking to BluetoothManager.framework does not happen at all until you run your app. To satisfy the compiler (which is different than the linker), you only need to have the private API headers. That's the key that makes this solution work. Dynamic linking.Cordelia
If this solution still works on iOS10? Please see Leon Dobnik's answerRaphael
@tatiana_c, it still works for me on iOS 10.1. I just tested it. Also, Leon's answer isn't an answer.Cordelia
Could you add a link to download the project? I can't do work this then change the headers class and the private framework :(Ineludible
@user3745888, The link is right in my answer ("modify your project on github"). The person asking the question updated his project to use the answer.Cordelia
Seems like this no longer works on iOS 11, the private frameworks directories no longer contain the binaries.Greeley
@newenglander, it still works. The private frameworks directories on the Mac were stripped of binaries in 9.3 already. This technique only opens them on the device, where they still exist, and can never be removed. Note: I've heard that apps have been rejected for using dlopen(), so I've replaced it in mine with calls to load libs via [NSBundle bundleWithPath:], which basically does the same thing.Cordelia
@Greeley I just tested this exact library on my stock 11.1.2 device, and it works. Of note is that the path for a bundle requires you to strip off the last path component. So, e.g. use NSBundle *bundle = [NSBundle bundleWithPath: @"/System/Library/PrivateFrameworks/BluetoothManager.framework"]Cordelia
I was checking for the files on my jailbroken device on iOS 11 and didn't see any binaries in the directories in PrivateFrameworks. That being said I don't see them on a jailbroken device running iOS 9 either, so I guess nothing changed in that respect.Greeley
@Greeley Depends on what you mean by "checking". The files have actually been moved into the dyld_shared_cache since long before iOS 9, but making a call to dlopen(@"/System/Library/PrivateFrameworks/Something.framework/Something") still works, even though you can't find that file on the file system. Anyway, this shouldn't affect your ability to use this technique above.Cordelia

© 2022 - 2024 — McMap. All rights reserved.