日期:2014-05-20  浏览次数:20873 次

在Xcode中手动创建通用Framework及静态库文件

经常我们都会将自己写过的一些代码段整理出来,然后封装、打包,归档,等到别的项目需要使用的时候,再整个添加到新工程里去。这是个好习惯,而为了简单起见,通常都会将代码编译成一个链接库(.a, .so, .dll等),当然,要是没有团队协作而是在自己的机器上随便写写的话,随便设个环境变量之类的就好。但要是哪天需要在团队合作中使用,或者要开源给别人用之类的,就会碰到一群不知所以的程序猿,好比史前人类看到电脑一样的问你这个是什么那个怎么弄…所以,通常我们都会写一个install的脚本,帮你设好一堆的环境变量,把头文件丢到 /usr/local/include, 把库文件丢到 /usr/local/lib 里去。

 
以上情况不仅仅在写C/C++的时候才会遇到,而是一个比较通用的情况。最近便在整理曾经写的iOS的一个小库的时候,需要贡献出来给团队使用,而且,还不止一个团队在用这个库,于是想到了是否也可以将这个小库写成一个Framework呢?
 
原来的打算是将代码段编译成动态链接库(Dynamic Library),但是一Google便放弃了,因为Apple不允许,好吧,那就搞成静态链接库(Static Library)吧,纠结了半天终于用 lipo 命令编译出了一个 static library,于是蛋疼如我的想到,是否能将其放到系统目录下,这样以后所有的Project都能直接引用而不需要每次都复制到工程目录里?
 
于是果断把 library 丢到了 /usr/lib, /usr/local/lib 里,都不行…,并且尝试了把头文件丢到 /usr/include, /usr/local/include 里,Xcode的智能补全提示找到了文件,但是编译报错说找不到,既然如此,那一定是有什么地方不对(废话…)
 
根据GCC一类编译器的设定,在编译时会带上一系列的环境变量,诸如头文件搜索路径等,并且,由于iOS存在真机、模拟器两种不同的平台,以及一个Xcode版本可以同时支持多个不同版本的SDK,想到,是否这一系列的环境设置,是根据平台+SDK版本而有区分的?
 
既然如此,那么头文件的搜索路径和库文件的搜索路径应该是在Xcode对应的Library中,在Stack Overflow里找到这么一篇文章 Canonical list of Xcode Environment Variables(Xcode环境变量的权威列表),发现了几个可能的环境变量 SDKROOT, PLATFORM_DEVELOP_USR_DIR, XCODE_APP_SUPPORT_DIR。其中SDKROOT明显就是当前使用的SDK的根目录,而PLATFORM_DEVELOP_USR_DIR,就是搜索头文件和库的对应的 usr 目录,另一个XCODE_APP_SUPPROT_DIR,便是最接近整个Xcode资源目录的变量。(详细的对应目录稍后说明)
 
在搞定一堆脚本后,把头文件和库放到了对应的 usr 目录下,这次Xcode终于找到头文件了,但随之新的问题也出现了,在Target的 Link Binary With Libraries 中,无法搜索到对应的静态链接库。仔细查看了 lib 目录后发现,Xcode支持的仅有3种链接对象:Framework,dylib,Mach-O。而后两种由于都是动态链接的形式添加到项目中的,所以都不被Apple允许。于是只剩下Framework这一种方式。探寻Xcode的目录结构之后,发现SDK对应的 /System/Frameworks 目录,便是Xcode存放所有Frameworks的地方,并且没有其他系统或者配置项依赖,只要在该目录下建立对应的 Name.framework 目录,并按照格式放入文件,既可被Xcode搜索到。
 
前因后果至此终于理清楚了,于是我们手把手来建立一个自己的Fake Framework吧。
 
Step 1: 创建工程
创建一个任意类型的App工程,此处选择Single View,只是因为简单而已。该工程的作用在于能够同时对编写的库进行有效的测试,当然建其他类型的工程也是可以的。
 
 
Step 2: 添加库代码,或者编写之
这个具体想写什么就随意了,这里偷懒,就把之前写的 UIView (ParticalCurl) 贴进去
 
 
Step3: 确保库相关的代码能够编译通过
我觉得这是一句废话…好吧,但这确实是很重要的一步,在你的项目中使用库相关的代码,并且确保都能编译通过以及能够正常的工作。
 
Step4: 建立新Target, 创建适合iPhone真机使用的静态库
终于到了关键核心步骤了,各位看官久等了!
选择 File -> New -> Target, 如下图:
 
 
在新建对话框中,选择 Framework & Library -> Cocoa Touch Static Library,如图
 
 
将Target命名为 [library-name].iphoneos,比如 blogguide.iphoneos,如图
 
 
随后,选中刚刚建立的blogguide.iphoneos这个target,切换到 Build Phases这个Tab,点开Compile Sources结点,删掉里面所有的东西,将库相关的 *.m 文件拖拽进去,然后点开Copy Headers,将库相关的 *.h 头文件拖到public下,如下图:
 
 
随后,选择 Product -> Edit Schema,将编译配置设置为 Release,如图
 
 
至此,一个可以在iphone真机上使用的静态链接库的工程就算搞定了,接下来,只要选择 Device 编译就好,如图:
 
 
选择 Target 和对应的 Platform,按下 CMD+B 编译就好了
 
Step 5: 重复Step4,建立可以在模拟器下使用的静态链接库
第五步的操作和第四步完全相同,但是注意新建的Target的命名还是得改下,可以叫做blogguide.iphonesimulator。如图:
 
同样,选择 对应的 Target和Platform进行编译。
 
Step 6: 合并两个平台的静态链接库,使用lipo命令
好吧,这并不是最激动人心的时刻,因为这一步其实是可以省略的,不过如果不怕麻烦要多搞几个不同的二进制文件,其实就随意了…
我只是给点建议,合并在一起比较容易管理,并且最终产物是一个通用的静态链接库,最终用户并不用操心这个库是使用在哪个平台上的,用就可以了,配置起来也很方便。
操作如下:
建立新的Aggregate类型的target,如图:
 
 
名字这种东西就是个代号,所以随意了…
在右下角选择 Add Build Phase -> Add Run Script
 
 
贴进去如下代码:
 
DESKTOP_DIR="/Users/$(whoami)/Desktop/libblogguide"
mkdir -p $DESKTOP_DIR
cp -r "${BUILT_PRODUCTS_DIR}/usr/local/include" "${DESKTOP_DIR}/include/"
#create the lib
lipo -create 
    "${BUILT_PRODUCTS_DIR}/../Release-iphonesimulator/libblogguide.iphonesimulator.a" \
    "${BUILT_PRODUCTS_DIR}/libblogguide.iphoneos.a" \
    -output "${DESKTOP_DIR}/libblogguide.a"
 
选择blogguide->iOS Device编译,如果之前的步骤都正常,那么如上的脚本在执行之后,将会在你的桌面上创建一个libblogguide的目录,并且其中会有一个libblogguide.a的静态链接库,以及一个include文件夹,包含了这个库的头文件。
 
Xcode在编译新工程时,会在 ~/Library/Developer/Xcode/DerivedData/下,创建 [工程名]-xxxxxxx 的目录,用来存放编译出的临时文件和对应的打包文件,当然,这个目录我们可以通过Xcode的环境变量获得,就是 $BUILT_PRODUCTS_DIR,指向的是该target的目录。在一个target目录的上层,存在 [BuildConfig]-[Platform] 格式的若干目录,分别存放了对应平台编译的结果。lipo命令的作用,就是创建通用文件,具体说明可以直接在命令行中 man lipo 查看。
 
Step 7: 创建Framework
激动人心的时刻终于来了!
一个Framework的基本目录结构为:
 
Name.framework -|
               -|- Headers
               -|- Name(bin)
 
只要按照这个目录结构存放我们的库,那么,在Xcode中就能以Framework的形式载入。
于是按照Step 6的方式,再创建一个Aggregate Target,命名为install-lib,添加一个Run Script。之后,且慢。由于我们需要把我们的framework放到 Xcode 本身的目录中去,而这个目录是/Application目录中的一个子目录,所以需要管理员权限才能写入,虽然sudo命令支持通过CR模式传入密码,可以用另一个脚本echo密码,然后设置SUDO_ASKPASS环境变量为密码脚本,但始终不太美观,尤其是别的使用者要安装在自己的电脑上,还要改密码脚本。
 
于是,我们可以用Apple Script来执行,Apple Script中有do shell script *** with administrator privileges的指令,可以弹出输入密码的对话框。
在工程中添加新的Shell Script文件,叫做 install.sh,在install-lib的Run Script中,贴入如下代码:
 
osascript -e "set shellScript to \"/bin/sh install.sh $whoami $XCODE_APP_SUPPORT_DIR\"" \
          -e "do shell script shellScript with administrator privileges"
 
该段脚本使用osascript命令执行Apple Script,并通过Apple Script,使用管理员权限执行install.sh脚本。
 
注意我们在Apple Script所执行的脚本中给install.sh传入了一个 $XCODE_APP_SUPPORT_DIR 的参数,以此来获得Xcode的安装路径。
 
随后,我们就来编写install.sh,来达到安装Framework的目的。在install.sh中写下如下代码:
 
#! /bin/sh
# Install BlogGuide after build
USERNAME=$1
SUPPORT_LIBRARY=$2
DESKTOP_DIR="/Users/${USERNAME}/Desktop/libblogguide"
# change the head file include type
INCLUDEPATH="${DESKTOP_DIR}/include"
includeFiles=$(ls $INCLUDEPATH)
for headfile in $includeFiles; do
    filePath="${INCLUDEPATH}/${headfile}"
    #echo "process file: $filePath" >> /tmp/aggtarget.log
    includes=$(grep "#import \"" ${filePath} | awk -F"#import \"" '{print $2}' | awk -F "\"" '{print $1}')
    #echo "Find import head: $includes" >> /tmp/aggtarget.log
    for includePiece in $includes; do
        filename=$(echo $includePiece | awk -F"." '{print $1}')
        extension=$(echo $includePiece | awk -F"." '{print $2}')
        #echo "file: $filename, extension: $extension" >> /tmp/aggtarget.log
        sed -i "" "s/#import \"${filename}.${extension}\"/#import \<PYUtility/${filename}.${extension}\>/g" $filePath
    done
done

# install to each SDK
PLATFORM_ROOT_DIR=${SUPPORT_LIBRARY}/../../Platforms
PLATFORM_LIST=$(ls $PLATFORM_ROOT_DIR)
#echo $PLATFORM_LIST >> /tmp/aggtarget.log
for platform in $PLATFORM_LIST; do
    SDK_ROOT_DIR=${PLATFORM_ROOT_DIR}/$platform/Developer/SDKs
    SDK_LIST=$(ls $SDK_ROOT_DIR)
    for sdk in $SDK_LIST; do
        FRAMEWORKS_ROOT_DIR=${SDK_ROOT_DIR}/$sdk/System/Library/Frameworks
        BLOGGUIDE_FRAMEWORK_DIR=${FRAMEWORKS_ROOT_DIR}/BlogGuide.framework
        rm -rf ${BLOGGUIDE_FRAMEWORK_DIR}
        mkdir ${BLOGGUIDE_FRAMEWORK_DIR}
        mkdir ${BLOGGUIDE_FRAMEWORK_DIR}/Headers
        cp -r "${INCLUDEPATH}""${BLOGGUIDE_FRAMEWORK_DIR}/Headers"
        cp "${DESKTOP_DIR}/libblogguide.a""${BLOGGUIDE_FRAMEWORK_DIR}/BlogGuide"
    done
done
 
然后就编译吧~编译时,会弹出输入密码的对话框:
 
 
编译成功后,重启Xcode,就可以在 Link Binary With Libraries里搜到之前创建的BlogGuide.framework了
 
 
至此,大功搞成!
按此方法创建的framework,由于是使用的静态链接库,因而不会别苹果拒绝,同时,这样的framework给自定义库的使用、分享、传播带来了很多便利之处,虽然麻烦了一点,其实我只是想说我比较蛋疼而已。