Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
315 views
in Technique[技术] by (71.8m points)

ios - Handling private frameworks in Xcode ≥ 7.3

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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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"];

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...