主要内容翻译自:《How to Create a Framework》
在iOS中如何创建framework呢?
跟着本文的教程,你将学会以下的技能:
- 在Xcode中创建基本的静态库工程(static library)
- 创建一个依赖于你创建的静态库工程的应用
- 探索怎样将静态库工程转换为框架(framework)
- 最后,你将学会怎样将图片资源打包到自定义的资源包( resource bundle),并在你制作的框架中进行资源引用(其中,我将补充nib文件的引用)
- 最后的最后,我将补充如何在你制作的框架中引用第三方静态库(路径引用,非拷贝第三方库)
准备开始
这篇文章的目的不仅仅是像其他文章一样简单的介绍制作静态库,还会详细的讲解其中的原理。
首先请下载 RWKnobControl 源码。当你跟着学习创建静态库的教程后,你将学会怎样来使用它们。
什么是框架呢?(framework 库、框架)
框架或者叫做库就是资源的集合. 里面集成了一个Xcode能够很容易地纳入到工程里的一个独立的结构,里面包含了静态库和头文件。
在OS’X系统中,是可以创建动态链接库的。通过动态库链接技术,程序可以不用再次来链接库就可以实现库的更新(热更新技术的一种)。在运行时中,只会拷贝一份静态库中的代码,就可以让这份代码共享到所有使用它的进程,所以呢,这种技术可以减少内存的开销和提高系统的性能。你看,这是不是一种很牛掰的技术呢?
在iOS系统中,你不能够添加自定义的库到你的程序中(其实是可以的,但是不能上架,会被拒绝而已。所以你可以使用在企业应用的分发中。 你看,就象苹果拒绝了JSPatch一样。);所以,你只能够使用苹果提供的动态链接库。(有什么?CoreFundation..UIKit 等等)。
但是!这并不意味着在iOS中使用库是不行的。客观请看:静态链接库是可以用的哦,上面说的是动态链接库。
创建一个静态库工程
打开Xcode并选择创建一个Cocoa Touch Static Library工程。如下图:(掩饰Xcdoe工程版本为8.2.1,其他版本的界面可能有出入,但是你还是会找到我所提到的东西的,耐心点找找。)
并将工程名字命名为:RWUIControls(可不可以不是这个名字呢?后面教程使用了动态打包framework bundle的脚本,看了讲解后 你就知道能不能了。)
一个静态库工程是由编译成静态库的头文件和实现文件来组成。
为了使你的静态库用起来很简便,你只需要暴露出一个头文件就行了,是怎样的一个头文件呢?头文件里需要暴露出你需要使用者使用的类的头文件就行了。
当你创建 RWUIControls这个静态库工程的时候,工程默认为你创建了RWUIControls.h和RWUIControls.m两个文件,其中RWUIControls.m是不需要实现的,你可以删了它(move to trash)。
打开RWUIControls.h头文件,在里面添加如下库:(因为该静态库使用了UIKit的相关接口,你可以依照你制作的库的实际情况来添加你需要的库。)
#import <UIKit/UIKit.h>
当然了,你会发现你无法import。因为静态库工程无法找到。如何来解决呢,首先选择 Build Phases来展开Link Libraries面板。点击添加按钮+ 来添加你需要的库(该演示教程需要添加UIKit)
如果就这样创建一个静态库,它是不起任何作用的,因为静态库需要联合头文件来起作用。
接下来,你需要创建一个新的构建方式属性(Build Phase),它的作用是告诉编译器哪些头文件是可以提供给外部访问的。
在Xcode的顶部菜单栏选择 Editor\Add Build Phase\Add Copy Headers Build Phase(如果你发现你找不到 Add Copy Headers Build Phase, 首先保证当前Xcode页面位于Build Phase 页面;如果你发现 AddCopyHeadersBuildPhase是灰色不可选的,先尝试点击下Xcode的空白处再来选择)
请牢记上图操作:将你需要提供给外部访问的头文件拽入 Copy Headers的 Public中(Private:很明显的意思是对外不开放 Project:也是不开放的哦,你只需记住Public Private 的使用。)
创建一个UI控制类
静态库工程的配置已经设置好了,你需要将你想打包到静态库的文件导入 RWUIControls下。将你之前下载的打包文件解压,找到RWKnobControl,并将它拖入到RWUIControls中 并选择 copy item。如下图:
这样的操作将会添加实现文件到编译列表中,默认情况下,头文件会被添加Project列中(也同样意味着 private)
接下来,你需要将RWKnobControl.h弄到Public中去。可以拖拽到Public,也可以如下图方式进行设置(注意红色标注):
除此之外,你需要将你想暴露出的头文件添加到静态库的头文件中。这样做的好处在于,以免使用者在库里去查找他想用的头文件,他只需要导入静态库的头文件就行了。请移步到 RWUIControls.h(这个是你创建静态库工程时自动生成的,你还记得么?),并添加如下代码:
#import <RWUIControls/RWKnobControl.h>
配置 (Configuring Build Settings)
到此为止,你已经学会了创建静态库的大部分知识了,再坚持下,跟着做以下配置,你的库对于使用者来说会更加友好。
首先,你需要为公开头文件提供一个路径。这种做法保证了静态库在使用过程中能够定位到相应的头文件。
在工程导航栏中点击项目(TARGETS),并且选择RWUIControls, 选择Build Settings标签,在搜索框中输入 public header. 双击 *Public Headers Folder Path 进行如下图的输入:
待会儿将会为你展示这个路径下的东西。
接下来,你需要做其他的设置,尤其是那些在静态库中的配置。编译器为你提供了选择,是否自动移除从来没有使用过的僵尸代码(dead code).并且你还可以选择移除debug标记等等。
为别人创建了第三方静态库,最好是将以上提到的两点设置选择为NO,如下设置:
- Dead Code Stripping – Set this to NO
- Strip Debug Symbols During Copy – Set this to NO for all configurations
- Strip Style – Set this to Non-Global Symbols
command + B进行编译一下。你好像什么都没看到,那就对了,说明没有警告和错误提示。再来创建一下,这里我们选择 iOS Device 进行编译。创建完成后,你将会在Xcode工程左边的目录 Products下看到 libRWUIControls.a由之前的红色变成了黑色(如果没有,请选择模拟器编译一次,再选择真机编译一次;如果还是红色,多半是Xcode的问题,clear或者重启试试。),鼠标右键选择 Show in Finder 你将会看到如下目录:
发现什么了吗?这个include文件夹就是你之前在配置里设置的(include/$(PROJECT_NAME)),里面包含的两个头文件,就是你暴露出来的。
创建一个使用你创建的静态库的工程
在此教程下,你将创建一个使用你自己创建的静态库的工程。
先关闭静态库工程。然后我们来创建一个新工程。选择 Single View Application,并且给工程命名为UIControlDevApp.将类文件的前缀添加为RW,并且将工程修改为只支持 iPhone。最后将工程保存到RWUIControls静态库工程同一级目录中。
在UIControlDevApp工程中引入RWUIControls.xcodeproj(拖拽就行了),如下图:
诶,这种目录结构是不是好像在哪见过呢?CocoaPods工程! 这样做的好处在哪里呢,你可以在一个工作空间中修改静态库,同时也可以运行依赖该库的工程,进行效果校对。这样很方便。
接下来,请将你之前下载的代码包中的DevApp文件夹一起拖拽入 UIControlDevApp中(选择Copy),如下图:
接下来,你将在你的示意工程中配置静态库依赖:
- 在工程导航栏中选择UIControlDevApp工程
- 在UIControlDevApp中选择Build Phases 标签
- 打开Target Dependencies 面板,选择+进行添加
- 找到RWUIContols 静态库,选择添加。
为了反向关联静态库,展开Link Binary With Libraries 面板,添加 libRWUIControls.a 如下图:
好了,到了这一步 你可以运行你创建的工程了。如果你严格按照之前的步骤,你将会看到如下图所示的效果:
创建framework
迄今为止,你已经学会了创建静态库。接下来,我们将来创建framework。
在之前创建静态库的时候,你已经完成了大部分创建framework的工作,接下来,你只需要跟着以下步骤就可以完成framework的创建。(看着有点复杂)
framework的目录结构。framework有着一个特别的目录结构,为了让Xcode能识别出它是framework。接下来你将跟着我来创建这样的一个目录结构。
切片。就目前来讲,当你编译库时,仅仅是针对当前的环境编译了相应的库(真机发布?真机调试?模拟器发布?模拟器调试?),也就是 i386,arm7,等等。为了能够让framework在各种环境下适用,我们需要将各个环境编译的库放到framework对应的目录结构下。
Framework 的结构
接下来我将创建一个脚本来动态创建静态库。首先在Xcode的工程导航栏中选中 RWUIControls工程,点击RWUIControls 静态库项目。选择 Build Phases标签,在Xcode菜单栏中选择 Editor / Add Build Phase/Add Run Script Build Phase. 如下图:
当前创建的Script支持 Bash脚本的运行。我们来双击 RunScript标签进行重命名为 Build Framework, 然后将以下的代码拷贝进代码框:
set -e export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework" # Create the path to the real Headers diemkdir -p "${FRAMEWORK_LOCN}/Versions/A/Headers" # Create the required symlinks/bin/ln -sfh A "${FRAMEWORK_LOCN}/Versions/Current"/bin/ln -sfh Versions/Current/Headers "${FRAMEWORK_LOCN}/Headers"/bin/ln -sfh "Versions/Current/${PRODUCT_NAME}" \ "${FRAMEWORK_LOCN}/${PRODUCT_NAME}" # Copy the public headers into the framework/bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \ "${FRAMEWORK_LOCN}/Versions/A/Headers"
代码解读:
脚本首先创建了这样的一个目录结构 RWUIControls.framework/Versions/A/Headers
接下来创建了如下的引用链接:
- Versions/Current => A
- Headers => Versions/Current/Headers
- RWUIControls => Versions/Current/RWUIControls
最后,将公开的头文件拷贝到 Versions/A/Headers 目录中。其中 -a 参数表示当你做一些修改时,与此拷贝不同步,由此来保证不必要的重复编译。
接下来做如下图的编译:
右键选择工程目录里的 Products下的 libRWUIControls.a, 选择 Show in Finder. 接下来选择查看包结构,你将看到之前提到framework的结构。
但是你会发现一个问题,在目录结构中没有你想创建的静态库,别慌,接下来我们将会创建另外一个脚本。
支持多框架的创建###
iOS app需要在如下众多的框架环境下运行:
- arm7: 支持iOS7的老设备
- arm7s: iPhone 5 和 5C
- arm64: 64-bit ARM processor in iPhone 5S
- i386: For the 32-bit simulator
- x86_64: Used in 64-bit simulator
众多设备需要不同环境下编译的静态库,好象这么做有点复杂,我们可以采取将各个平台下的静态库添加到framework中。
我们将利用 RWUIControls项目来创建。选择 RWUIControls项目(target),然后点击 Add Target 按钮,选择添加 Aggregate:
并命名为 Framework。
备注: 为什么要这么绕呢?还记得开篇讲过iOS审核的问题吗?那么我们可以采取创建 Aggregate的方式来绕过那个麻烦的问题,同时我们还可以在此创建脚本来添加静态库。是不是很神奇?
为了保证在创建Framework前,静态库已经创建了,你需要为 Framework添加项目依赖。如下图:
最重要的步骤是,你需要创建一个支持多环境的库,接下来你应该象之前你创建一个脚本那样,为framework创建一个脚本。如下图:
将脚本的名字更改为 MultiPlatform Build. 并拷贝以下代码到脚本里:
#If we're already inside this script then die if [ -n "$RW_MULTIPLATFORM_BUILD_IN_PROGRESS" ]; then exit 0 fi export RW_MULTIPLATFORM_BUILD_IN_PROGRESS=1 RW_FRAMEWORK_NAME=${PROJECT_NAME}RW_INPUT_STATIC_LIB="lib${PROJECT_NAME}.a" RW_FRAMEWORK_LOCATION="${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework"
- set -e保证了创建一个完整的framework:当脚本编译错误,将终止
- 接下来, 这个 RW_MULTIPLATFORM_BUILD_IN_PROGRESS
变量确保了这个脚本不会被循环引用,如果循环引用了,那么就终止。 - 然后设置了一些变量. 注意:framework的名字必须和工程名字相同
,如下RWUIControls, 静态库名字为 libRWUIControls.a.
接下来创建一些函数,后面我们将使用到,将代码复制到脚本里的后面。
function build_static_library {# Will rebuild the static library as specified # build_static_library sdk xcrun xcodebuild -project "${PROJECT_FILE_PATH}" \ -target "${TARGET_NAME}" \ -configuration "${CONFIGURATION}" \ -sdk "${1}" \ ONLY_ACTIVE_ARCH=NO \ BUILD_DIR="${BUILD_DIR}" \ OBJROOT="${OBJROOT}" \ BUILD_ROOT="${BUILD_ROOT}" \ SYMROOT="${SYMROOT}" $ACTION} function make_fat_library { # Will smash 2 static libs together # make_fat_library in1 in2 out xcrun lipo -create "${1}" "${2}" -output "${3}" }
- build_static_library
将 SDK 作为一个参数, (例如 *iphoneos7.0)
*来创建库, 大多数参数都会通过, 注意这个参数 ONLY_ACTIVE_ARCH
是确保当前的构造版本中所有的框架环境都支持当前的SDK。 - make_fat_library
用 lips
来合并两个静态库。 将输入的两个静态库合并后输出. 关于lipo的更多知识请点击lipo。
接下来就是来怎么使用以上的两个函数了,你需要知道哪些SDK,以及这些SDK的路径位置。
# 1 - Extract the platform (iphoneos/iphonesimulator) from the SDK name if [[ "$SDK_NAME" =~ ([A-Za-z]+) ]]; then RW_SDK_PLATFORM=${BASH_REMATCH[1]} else echo "Could not find platform name from SDK_NAME: $SDK_NAME" exit 1 fi
# 2 - Extract the version from the SDK if [[ "$SDK_NAME" =~ ([0-9]+.*$) ]]; then RW_SDK_VERSION=${BASH_REMATCH[1]} else echo "Could not find sdk version from SDK_NAME: $SDK_NAME" exit 1 fi
# 3 - Determine the other platform if [ "$RW_SDK_PLATFORM" == "iphoneos" ]; then RW_OTHER_PLATFORM=iphonesimulator else RW_OTHER_PLATFORM=iphoneos fi
# 4 - Find the build directory if [[ "$BUILT_PRODUCTS_DIR" =~ (.*)$RW_SDK_PLATFORM$ ]]; then RW_OTHER_BUILT_PRODUCTS_DIR="${BASH_REMATCH[1]}${RW_OTHER_PLATFORM}" else echo "Could not find other platform build directory." exit 1 fi
以上4个声明的意思都非常相近, 用判断来给这两个参数赋值 RW_OTHER_PLATFORM
和 RW_OTHER_BUILT_PRODUCTS_DIR.
以下讲解四个 if 的作用
- SDK_NAME必须是 iphoneos7.0 或 iphonesimulator6.1的格式。 正则表达是确保以非数字开头的字符串。 所以它就确定为 iphoneos或者 iphonesimulator
- 同样的道理,保证为如下, 7.0or 6.1etc.
- 应该还是看得懂吧,判断环境进行赋值。 是iphonesimulator 还是 iphoneos
- 从创建的版本目录中获取平台名字,用其他版本的来替换。这一步保证其他平台能够从创建的路径中查找到。
添加如下脚本代码到脚本的后面:
# Build the other platform. build_static_library "${RW_OTHER_PLATFORM}${RW_SDK_VERSION}"
# If we're currently building for iphonesimulator, then need to rebuild
# to ensure that we get both i386 and x86_64 if [ "$RW_SDK_PLATFORM" == "iphonesimulator" ]; then build_static_library "${SDK_NAME}"fi
# Join the 2 static libs into 1 and push into the .framework make_fat_library "${BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \ "${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \ "${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}"
- 首先对你之前创建的函数的调用,目的是构建起他版本
- 如果你当前创建的模拟器版本,Xcode会默认只创建如下两种库:i386或x86_64.为了能够创建这两种架构的库,第二步调用 build_static_library来创建 iphonesimulatorSDK, 并且确保两种库已经创建。
- 最后来调用 make_fat_library函数,让静态库融入到framework中的路径里。
坚持!最后一步了,将以下代码添加到脚本的最后面:
# Ensure that the framework is present in both platform's build directories cp -a "${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}" \ "${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework/Versions/A/${RW_FRAMEWORK_NAME}"
# Copy the framework to the user's desktop ditto "${RW_FRAMEWORK_LOCATION}" "${HOME}/Desktop/${RW_FRAMEWORK_NAME}.framework"
- 第一条命令是确保framework存在于两种路径里
- 最后一步是将创建好的framework拷贝到你的桌面。当然,这一步是可选的。但是这样会很方便你去使用。
好了,所有的操作都搞定了,只需要选择 Framework进行编译一下(随便你选什么设备都行):
你可以查看framework包内容:
如何来检查当前拷贝出的framework是否支持所有环境的呢?
打开你的终端(terminal),如下图操作:
你将看见列出了五种环境,armv7 armv7s i386 x86_arm64 OK 搞定。
怎么使用?这里我就不赘述了,直接将framework拖入到你工程的 framework目录下,然后导入头文件就行了。
以上我们讲解了静态库的打包,但是我们还没讲解资源的打包(图片、字体、nib)。
创建一个Bundle
打开UIControlDevApp工程,选择RWUIControls子工程(还记得之前我们在一个工作空间里管理两个工程吗?)。点击 Add Target按钮:
当前你创建的Bundel默认是 OSX的,你需要将它更改为 iOS。 操作如下图:
接下来你需要更改产品名字(后面脚本会用到,更改和静态库的名字一样),如下图:
默认情况下,当你在Bundle中添加两倍图时,系统会将它编译为多分辨率的格式 TIFF,这样是你不希望的,因为会导致奇怪的问题。你可以做如下图的设置:
好了,请记住:当你编译framework时,bundle也需要被framework引用。如下图操作:
好了,我们最后可以像framework的脚本一样,添加一个拷贝Bundle到桌面的操作,添加如下代码到 MuiltiPlatform Build 脚本中:
# Copy the resources bundle to the user's desktop ditto "${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.bundle" \ "${HOME}/Desktop/${RW_FRAMEWORK_NAME}.bundle"
选择framework 进行编译,你会在你的桌面上发现两个包:xxx.framework, xxx.bundle
好了创建Bundle已经成功,但是如何使用呢?
图片的引用:
UIImage *image = [UIImage imageNamed:@"RWUIControls.bundle/RWRibbon"];
nib的引用:
NSString * bundlePath =[[NSBundle mainBundle] pathForResource:@“RWUIControls" ofType:@“bundle”];
NSBundle * bundle = [NSBundle bundleWithPath:bundlePath];
[UIStoryboard storyboardWithName:@“xxx” bundle:bundle];
引用xib也是同样的道理,把mainBundle替换为你创建的bundle就行了。
但是!请注意
这里我加一点补充:如果xib/storyBoard中的文件与你创建的类进行了关联,例如:stroyBoard中的 MainViewController 与你创建的 MainViewController class进行了关联,你需要将 MainViewController.h 文件给暴露出来(这个类在静态库中,静态库引用了你创建的Bundle,你需要在静态库中将 MainViewController.h拖入到 Public中)。
补充知识点:###
如果你有个非常大的工程B,你想将它作为主工程A的插件(第三方库),并且B中使用了Cocoapods来管理第三方库,那么按照之前讲解之作framework的流程来看,你需要将B中的代码全部拖到 framework 工程中进行编译。
试想,如果B工程是多个人开发或者需要经常更新,怎么办?还是拷贝代码到framework工程吗?太麻烦了是吧?
猜想下,我们可以创建一个framework工程C,如果C与B在同一个工作空间中(workspace), 并且C中引用(是引用,不是拷贝)B中所有的代码,那么C是不是可以随着B的更新而更新呢? 答案是:尝试下~(当然是可以的)
且看下图:
如果你在 SystematicAnatomyControls(静态库)中引用第三方库 如:
#import <AFNetworking/AFNetworking.h>
你在编译时,Xcode会告诉你无法找到。为什么?(跨工程路径问题,找不到Cocopods中的库)
怎么解决呢?
先在主工程中做如下操作:
PODS_ROOT 为你自定义添加的,添加方式如下:
讲解:/../表示回退到上一级目录,我为什么这么做?--参照Pods的做法,主工程依赖Pods的内容,那么主工程也得找到Pods中的文件,下图是我工程的目录情况,同样也告诉你我为什么用 /../ 这种方式来查找文件。
然后我们来做如下图操作:
讲解:表示从哪里查询头文件;“ ” 冒号的意思是忽略路径中的空格。
本文主要参照《How to Create a Framework》;
如翻译有误,请指正。
在文末我做了相应的补充,希望能够帮到你。
本文所采用的技术已做相应的论证,主要使用于Unity导出的Xcode工程依赖原生工程静态库。
如有其它相关问题,欢迎畅讨!