For some reason Apple reserves the ability to create iPhone frameworks for their own use. They are a very useful ability to have because they package headers together with a universal binary. „Universal“ meaning that you get binary code for multiple platforms rolled into a single file. Frameworks are also dynamically loaded which means that less binary code fills up your memory as only stuff is put there that is actually needed.
But alas, no frameworks for us.
Until now I am selling my components in the Dr. Touch’s Parts Store as source code which at the same time means that I could not provide demonstration versions to try out in your own code. If you have the code, what incentive is there to pay the price?
It has been suggested to me by several people independently to make a static library to solve this problem. Could be time- or otherwise limited, but then people could try the parts out and see how well they fit with their own work.
Also next to still offering access to my repositories at full price, I could be selling indidual versions without right to upgrade to get started at a substantial discount. Later if you find that the component really IS worth what I am asking for, you could upgrade to the lifetime upgrade plan via the SVN repo.
Another reason to figure out this Static Library Magic is so that I can bundle my own utility classes that I keep adding to and improving in a central place. This gives me the choice of adding an external SVN reference or to simply copy the static library into my new projects.
In this article I am describing how to create a universal static library for both Intel and Arm architectures, how to glue them together into a single file and how to add it to a new demo project.
Step 1 – Code for a new utility static library
Start out by creating an iPhone static library project.
So that there is actually code inside our static library we create a class extension for NSURL to give us a dictionary of all the parameters passed in a URL. This is quite useful when analyzing the parameters from the applicationion:didFinishLaunchingWithOptions if your app is launched via its URL scheme.
NSURL+DT.h
@interface NSURL (DT) // dictiary which contains param = value for each parameter - (NSDictionary *) dictionaryOfParameters; @end |
NSURL+DT.m
#import "NSURL+DT.h" @implementation NSURL (DT) - (NSDictionary *) dictionaryOfParameters { NSString *paramName; NSString *paramValue; NSMutableDictionary *tmpDict = [NSMutableDictionary dictionary]; NSScanner *scanner = [NSScanner scannerWithString:[self query]]; // NOTE: This cannot deal with values that contain & or similar HTML entities while (![scanner isAtEnd]) { [scanner scanUpToString:@"=" intoString:&paramName]; [scanner scanString:@"=" intoString:nil]; [scanner scanUpToString:@"&" intoString:&paramValue]; [scanner scanString:@"&" intoString:nil]; [tmpDict setObject:paramValue forKey:paramName]; } return [NSDictionary dictionaryWithDictionary:tmpDict]; } @end |
Drag the header into the „Copy Headers“ section of the target. Drag the implementation file into the „Compile Sources“ section. Contrary to a regular iPhone app target you have to do that manually so that your static library actually contains anything to compile. Also note that the build steps differ from those for an app target.
Step 2 – Set up the targets
Your default Cocoa Touch static library comes with a single target. We want to be able to build for multiple targets with different architectures. So first we change the current target’s base SDK to iPhone SDK 3.0. This will build it for the arm6 and arm7 platforms. Rename the target by appending Dev to the target name so that we know this is the one set up for device.
Next we duplicate the target by right-clicking on it and „Duplicate“. Rename the second target by appending Sim to the name to signal that this is the one built for simulator.
We also set the base SDK for the Sim target to be the same version but for simulator. This causes the build to be made for the i386 platform.
The SDK you choose in the upper lefthand box overrides the set base SDK for your targets. So if you want the selected SDKs to be built for you have to set it to „Use Base SDK“. If this is set then building the individual targets obeys the individually set base SDK.
Let’s try building both targets. There is no Build-All option yet, so we build both targets one after the other.
It’s no problem that they have the same output file name because due to the different SDK they end up in different subfolders of your build folder, Debug-iphonesimulator and Debug-iphoneos. If you like you can get them renamed by changing the product name of the targets, but that’s optional. The following shows the layout of your project after all setup but before the builds as the products are still red.
Step 3 – A „Build All“ Target with merging
We don’t want to have to build all targets individually every time and also we still need to glue them together. So we create a new aggregation target which we call „Build & Merge Libraries“
To this combation target we add our previous targets as dependencies. This will cause the building of sub-targets if they are outdated.
Apple provides the lipo tool for merging multiple binaries into one. Most likely you have it already installed at /usr/bin/lipo, mine is dated July 3rd 2009 and I did not manually install it.
We add a new “Run Script Build Phase” after the two sub-targets and fill in our code to make a new output folder and merge the libraries.
Here’s the code. It basically instructs lipo where to find the two libraries for the different architectures and where to write the merged result lib archive.
# make a new output folder mkdir -p ${PROJECT_DIR}/build/${BUILD_STYLE}-iphoneos/DTUtilities # combine lib files for various platforms into one lipo -create "${PROJECT_DIR}/build/${BUILD_STYLE}-iphoneos/libDTUtilities.a" "${PROJECT_DIR}/build/${BUILD_STYLE}-iphonesimulator/libDTUtilities.a" -output "${PROJECT_DIR}/build/${BUILD_STYLE}-iphoneos/DTUtilities/libDTUtilities-${BUILD_STYLE}.a" |
Step 4 – Package it for distribution
If you want to use the library in another project or hand to somebody else you have to add the headers and any other resources (like images) that it uses. A framework would have taken care of this for us, but we need to transfer these files manually. So we’ll make two extra build steps to automate the copying of files and zipping it into a handy archive.
We add a new “Copy Files Build Phase” and set it up so that our header is copied. We want the header to go into our subfolder of the products folder.
Drag the header into this phase. If there where any PNGs or other resources we would drag them into here as well.
Finally, so that we can send it off we want to make a compressed archive out of our result. This can be accomplished by yet another run script build phase. I found that the ditto command does the best job of making archives that look like you made them in Finder.
ditto -c -k --keepParent "${PROJECT_DIR}/build/${BUILD_STYLE}-iphoneos/DTUtilities" "${PROJECT_DIR}/build/${BUILD_STYLE}-iphoneos/DTUtilities.zip" |
So in total our Build & Merge target looks like this:
If we now build this target, provided that we have chosen Base SDK in the top left picker, we get a ZIP file that contains all our resources and the merged static library with code that can be linked with arm6, arm7 and Intel binaries.
Step 5 – Use it in a different project
From this point on, this diverges from the topic of this article. Therefore if all you where looking for was how to make a Universal Static Library you can stop reading right now.
To actually use/test this library we’ll make a very simple project that just logs the parameters passed if our demo app is called via an URL scheme.
We create a new project for a new View-based application. Then we drag the header and library from our distribution folder into our project and choose that we want it to be copied to our project. Note that Xcode automatically adds the library to the linker phase of the target.
So that we can test the calling via URL we’ll add some code to the app delegate to NSLog the passed parameters. Don’t forget the import.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSLog(@"%@", launchOptions); NSURL *launchURL = [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey]; NSDictionary *parameters = [launchURL dictionaryOfParameters]; NSLog(@"%@", parameters); // Override point for customization after app launch [window addSubview:viewController.view]; [window makeKeyAndVisible]; return YES; } |
We need to add a URL scheme to our app so that it can be launched via URL. This is done by adding a URL identifier and a URL scheme to the info plist.
Now if we where to simply try this out we would get an exception that our dictionaryOfParameters method is an unrecognized selector. The reason being that C and C++ generates symbols for each function whereas Objective-C only generates one per class. We need to force the linker (ld) to also load the members of the class. This is achieved by the linker flag -ObjC.
The second problem being that NSURL itself is in a different archive than our category extension. It’s in fact in a dynamically bound object file in one of Apple’s frameworks. Thus the linker thinks it’s already got all that makes up NSURL and does not load our category extension. At runtime it turns out that this assumption was false and thus we get the crash. Fortunately we can force inclusion of all our objects from our static library by adding the linker flag -all_load.
-all_load will include all members of all classes from all your static libraries. If you happen to have dozens of static libraries and you want to force the full loading only of individual ones you can use the -force-load parameter as an alternative because it allows you to specify a single library archive to completely load. But for a single static library it’s probably ok to go with all_load.
These flags need to be added to the target’s “Other Linker Flags”.
Note: If you built previously you have to clean the build and make a new build for linker flag changes to take effect.
We can now try out our app. We hit Build&Run once so that we get the latest version installed. Next we go into mobile Safari in iPhone Simulator and enter a URL that begins with demo:// so that the iPhone OS starts our app.
This causes Safari to quit and our app to start. The results from NSLog in simulator also end up in the console app where we can see that all is working. The first NSLog shows the options dictionary, the second the dictionary that our category extension method returned.
Ok, Simulator works, that alone does not fully convince us. We’ll also want to try it out on device to see if the linker was able to get the arm code from the merged library. We’ll use a nifty feature of the debugger to have it not start the app, but instead wait until the next launch.
For the LibDemo executable set the debugger settings to “Wait for next launch/push notification”.
If we now debug then Xcode will not start our app, but wait faithfully until it is launched the next time. So if we repeat our test in Mobile Safari, this time on device, we’ll see on the console the same result proving that it works.
Conclusion
You can see that it is really not very difficult to package your code into static libraries that combine multiple architectures, in our case i386, arm6 and arm7. This way you can let other people try out our components or classes without having to hand them the source code. They only need the headers, the lib archive (.a) file and any resources (like PNGs) that your components are using.
We’ve been using a Debug build of our library all this time. This preserves the debug symbols for our own development. But for release and distribution builds we will probably want to make a Release build of the library so that debug symbols are stripped out.
Now that I have learned these steps I will start making static libraries for all my components on the Dr. Touch’s Parts Store so that people interested in using them have a way to try before they buy. I’m yet uncertain how I want to limit the functionality though. Maybe I’ll superimpose a big translucent “SAMPLE” over a control or maybe I’ll add some time limiting functionality. I’ll figure it out.
Besides of being able to provide free samples I am also contemplating offering one-time-only purchases of all my components without free upgrades to major new versions and without source code. This could be offered at a fraction of the cost of the source code itself.
So universal static library are a powerful tool to have in your arsenal as iPhone developer. Still be hope that some day Apple lets us make frameworks for iPhone as well.
Categories: Recipes
Thanks for the guide man. This worked great prior to iOS SDK 4.0. For some reason now the merge step is broken. The lipo script fails because Xcode won’t build each of the targets separately then combine them. I have to manually change the base sdk to use (dev or sim) and then change random settings to get it to work, then I am able to run a Merge on them. I tried looking into it, and it seems that you can no longer select ‘Use Base SDK’ as the target, which could be the problem. Am I missing something, or is that just it?
I’m having trouble creating the Simulator target– because Xcode 3.2.5 has different GUI than the version used in your instructions. I right-click the ‘DTUtilities Sim’ target, choose Get Info, and attempt to change Base SDK to ‘iPhone Simulator’. But that choice is no longer available. My only choices are ‘iOS 4.2’ or ‘Latest iOS (currently set to iOS 4.2)’.
I’m having the same issue with the merge step as the previous poster. The merge fails with Xcode 3.2.5 because Xcode’s GUI has changed. There is no longer a way to choose ‘Use Base SDK’.
Any suggestions?
Here’s a possible solution:
http://stackoverflow.com/questions/3520977/build-fat-static-library-device-simulator-using-xcode-and-sdk-4
You can still perform all the steps manually, switching to sim when building the sim library.
I’m having same difficulties. UI gets confused and tries to build iOS version as i386 etc. Quitting and relaunching sometimes fixes it etc. very annoying!
It would be awesome if this post have a update for the xcode4