Michael Primeaux

Parallel and Distributed Systems


Universal Frameworks for iOS

As of the time of this writing, Xcode does not offer a framework project to for Cocoa Touch (only Cocoa). Apple’s primary reasons behind this decision are security, performance, and memory footprint. As such Xcode limits iOS developers to the Static Library project type. Static libraries are quite cumbersome to work with since we must make compile-time decisions when working with the iOS Simulator or an iOS device. The iOS simulator on Mac OS X uses the i386 architecture whereas the iOS devices use either armv6 or armv7.

Additionally, iOS 5 introduces a new compile-time memory management feature known as Automatic Reference Counting (ARC). The abridged version is you will never need to type retain or release again, which dramatically simplifies the development process while reducing crashes and memory leaks. The compiler has a complete understanding of your objects, and releases each object the instant it is no longer used, so apps execute much faster, with predictable, smooth performance. However, it’ll take the community some time to refactor various open source libraries to support ARC. Therefore, we have a much larger need today than ever before to move these source files into static libraries that are compiled with ARC disabled so we can compile our primary applications with ARC enabled.

Alternatively, if you are including files that don’t yet support ARC in a project that is ARC enabled, you can set the -fno-objc-arc compiler flag for each of these files. To do this in Xcode, go to your active target and select the “Build Phases” tab. In the “Compiler Flags” column, set -fno-objc-arc for each of the source files. Of course, this can be a very laborious process.

Apple defines a framework as a bundle (a structured directory) that contains a dynamic shared library along with associated resources, such as nib files, image files, and header files. When you develop an application, your project links to one or more frameworks. For example, iOS application projects link by default to the Foundation, UIKit, and Core Graphics frameworks. Your code accesses the capabilities of a framework through the application programming interface (API), which is published by the framework through its header files. Because the library is dynamically shared, multiple applications can access the framework code and resources simultaneously. The system loads the code and resources of a framework into memory, as needed, and shares the one copy of a resource among all applications. A universal (or multi-architecture) file is nothing more than an application bundle.

As I mentioned above, iOS does not support dynamic shared libraries but using lipo (a tool which comes with iOS SDK) we are able to merge several static libraries into a single static library. By using lipo, we are able to create a static library that supports all architectures (armv6, armv7, and i386) packaged as a universal framework. Here’s how.

Within Xcode, either create a new iOS project or open an existing iOS project. If you do decide to create a new project then feel free to choose any application template you prefer. Which one you choose is irrelevant. Add a “Cocoa Bundle” target (not Cocoa Touch Bundle). Select the newly created bundle target in the left navigation panel, select the “Build Settings” tab and modify the following settings as required:

  • Architectures (ARCHS): Standard (armv6 armv7 armv7s). In Xcode 4.2, use the value of $(ARCHS_STANDARD_32_BIT). If you wish to compile for older devices then add a new line with a value of armv6. Please see this Apple Developer forum post for more details.
  • Base SDK (SDKROOT). Latest iOS (iOS X.X).
  • Build Active Architecture Only (ONLY_ACTIVE_ARCH). No. This allows us to compile for armv6 and armv7.
  • Valid Architecture (VALID_ARCHS): Standard (armv6 armv7 armv7s). In Xcode 4.2 and greater, use the value of $(ARCHS_STANDARD_32_BIT). If you wish to compile for older devices then add a new line with a value of armv6.
  • Dead Code Stripping (DEAD_CODE_STRIPPING): No
  • Link With Standard Libraries (LINK_WITH_STANDARD_LIBRARIES): No
  • Mach-O Type (MACH_O_TYPE): Relocatable Object File. This is the most important change. Here, we instruct the compiler to treat the Bundle as a relocatable file, by doing this, we can turn it into a framework with the wrapper setting.
  • Wrapper Extension (WRAPPER_EXTENSION). framework. Here we change the Bundle to a Framework. To Xcode, frameworks is just a folder with the extension .framework, which has inside one or more compiled binary sources, resources and some folders, a folder, usually called Headers, contains all the public headers.
  • Generate Debug Symbols (GCC_GENERATE_DEBUGGING_SYMBOLS): No.
  • Generate Position-Dependent Code (GCC_DYNAMIC_NO_PIC): No
  • Targeted Device Family (TARGETED_DEVICE_FAMILY). iPhone/iPad.

Here are the steps…

  • With the bundle target still in focus, select the “Info” tab and ensure the “Bundle OS Type code” is equal to FMWK.
  • Add any source code and resources to the bundle. With the bundle target selected, click the “Build Phase” tab. At the bottom, press the “Add Phase” button and then “Add Copy Headers“.
  • Open the newly created “Copy Headers” section and separate your public headers from private or project headers.
  • Open the “Compile Source” section and add any .m, .c, .mm, .cpp and any other compilable source file. If your framework contains any non-compilable files such as images, sounds, and other resources, you can add them to the “Copy Bundle Resources” section.

You can access any non-compilable resources by using NSBundle.

[[NSBundle mainBundle] pathForResource:@"MyFramework.framework/Resources/FileName" ofType:@"fileExtension"];

Next we’ll create a new Aggregate Target. As mentioned earlier, to join both architectures products into one, we must to use lipo.

  • Add a new target by pressing the “Add Target” button.
  • Under the Cocoa Touch section, select the “Other” subcategory and then choose the “Aggregate” target. The “Product Name” is arbitrary.
  • Add a new “Run Script” phase under this newly created target. Copy and paste the following script into the “Run Script” phase.
# The framework name and version
X_FRAMEWORK_NAME=CoreFramework
X_FRAMEWORK_VERSION=A

# This folder contains the final output of the framework.
X_INSTALL_DIR=${SRCROOT}/Products/${X_FRAMEWORK_NAME}.framework

# This working directory will be deleted after completion.
X_WORKING_DIR=build
X_DEVICE_DIR=${X_WORKING_DIR}/${CONFIGURATION}-iphoneos/${X_FRAMEWORK_NAME}.framework
X_SIMULATOR_DIR=${X_WORKING_DIR}/${CONFIGURATION}-iphonesimulator/${X_FRAMEWORK_NAME}.framework

echo "******************************************************"
echo "X_DEVICE_DIR = ${X_DEVICE_DIR}"
echo "X_SIMULATOR_DIR = ${X_SIMULATOR_DIR}"
echo "******************************************************"
echo "SYMROOT = ${SYMROOT}"
echo "OBJROOT = ${OBJROOT}"
echo "PROJECT_DIR = ${PROJECT_DIR}"
echo "CONFIGURATION_BUILD_DIR = ${CONFIGURATION_BUILD_DIR}"
echo "CONFIGURATION = ${CONFIGURATION}"
echo "CONFIGURATION_TEMP_DIR = ${CONFIGURATION_TEMP_DIR}"
echo "DERIVED_FILE_DIR = ${DERIVED_FILE_DIR}"
echo "BUILD_PRODUCTS_DIR = ${BUILT_PRODUCTS_DIR}"
echo "BUILD_DIR = ${BUILD_DIR}"
echo "TARGET_TEMP_DIR = ${TARGET_TEMP_DIR}"
echo "PROJECT_TEMP_DIR = ${PROJECT_TEMP_DIR}"
echo "PRODUCT_NAME = ${PRODUCT_NAME}"
echo "******************************************************"

# Build both simulator and device architectures.
xcodebuild clean
xcodebuild -configuration ${CONFIGURATION} -target "${X_FRAMEWORK_NAME}" -sdk iphoneos -SYMROOT=${SYMROOT} -OBJROOT=${OBJROOT}
xcodebuild -configuration ${CONFIGURATION} -target "${X_FRAMEWORK_NAME}" -sdk iphonesimulator -SYMROOT=${SYMROOT} -OBJROOT=${OBJROOT}

# Clean the oldest.
if [ -d "${X_INSTALL_DIR}" ]
then
rm -rf "${X_INSTALL_DIR}"
fi

# Recreate the folder structure for the final product binaries.
mkdir -p "${X_INSTALL_DIR}"
mkdir -p "${X_INSTALL_DIR}/Versions"
mkdir -p "${X_INSTALL_DIR}/Versions/${X_FRAMEWORK_VERSION}"
mkdir -p "${X_INSTALL_DIR}/Versions/${X_FRAMEWORK_VERSION}/Resources"
mkdir -p "${X_INSTALL_DIR}/Versions/${X_FRAMEWORK_VERSION}/Headers"

# Create the required symbolic links. Please note the paths MUST relative, 
# otherwise the symbolic links will be invalid when the folder is copied/moved.
ln -s "${X_FRAMEWORK_VERSION}" "${X_INSTALL_DIR}/Versions/Current"
ln -s "Versions/Current/Headers" "${X_INSTALL_DIR}/Headers"
ln -s "Versions/Current/Resources" "${X_INSTALL_DIR}/Resources"
ln -s "Versions/Current/${X_FRAMEWORK_NAME}" "${X_INSTALL_DIR}/${X_FRAMEWORK_NAME}"

# Copy the headers and resources files to the final product folder.
cp -R "${X_DEVICE_DIR}/Headers/" "${X_INSTALL_DIR}/Versions/${X_FRAMEWORK_VERSION}/Headers/"
cp -R "${X_DEVICE_DIR}/" "${X_INSTALL_DIR}/Versions/${X_FRAMEWORK_VERSION}/Resources/"

# Remove artifacts from the resources folder.
rm -r "${X_INSTALL_DIR}/Versions/${X_FRAMEWORK_VERSION}/Resources/Headers" "${X_INSTALL_DIR}/Versions/${X_FRAMEWORK_VERSION}/Resources/${X_FRAMEWORK_NAME}"

# Use lipo to merge both binary files (i386 + armv6/armv7) into one universal files.
lipo -create "${X_DEVICE_DIR}/${X_FRAMEWORK_NAME}" "${X_SIMULATOR_DIR}/${X_FRAMEWORK_NAME}" -output "${X_INSTALL_DIR}/Versions/${X_FRAMEWORK_VERSION}/${X_FRAMEWORK_NAME}"

# Remove the working directory
rm -r "${X_WORKING_DIR}"

You’ll need to modify the first non-comment line in the script to be equal to the product name of your framework bundle target.

Now build the Aggregate target. It does not matter which architecture you select to build (iOS Device or Simulator) since the script creates a working folder, compiles the framework target twice (once for the iOS device and once for the iOS Simulator) and generates the output to a folder named “Products” located in the $(SRCROOT) root folder

If you previously upgraded from Xcode 3.x to 4.2, then your existing (SYMROOT) value will very likely cause the above script to fail. The failure is caused because the script expects your (SYMROOT) value to be set to the new Xcode 4 value of “build”, which refers to a relative path to $(SRCROOT). Unfortunately, you are not able to modify this setting in Xcode 4 due to an already reported bug. To work around this bug, close Xcode and simply delete the existing SYMROOT key and value from your com.apple.dt.Xcode.plist file, which is located in ~/Library/Preferences/.

Isn’t it about time for the WWDC to be here again?