Swift构建通用版本Framework以及Framework的使用及其注意事项

前段时间公司app中有个BookRoom模块,也就是绘本阅读的模块需要重新做,并且封装成framework的形式进行(fcs项目已经很大,很冗余,每次Xcode编译项目都需要大量的时间,至少有5分钟),所以使用swift构建的framework遇到的问题坑我基本上都遇到过。于是想把整个过程记录下来,肯定有人和我一样遇到类似的问题,以便下次遇到可以快速解决;

新建Framework项目

  1. 新建项目选择Cocoa Touch Framework项目,取名为BookRoomKit,选择Swift语言
    新建framework
    bookroomkit

  2. 然后新建一个BookRoomManager单例类用来传值设置,并且需要申明为public,这样我们才能够在其他项目上使用我们提供的framwork的代码;

  3. 编译framework;我这里以真机发布版本framework的为例。
    首先使用Product->Scheme->Edit Scheme 打开如下界面,设置为Release环境,
    设置编译环境为release
    然后按照下图选择,第1步代表真机运行,第2步运行,第3步代表编译成功的BookRoomKit.framework;
    编译framework
    选中BookRoomKit.framework,右击Show inFinder,会显示编译好的framework在finder中的位置,如下图:
    show in finder

  4. 接下来我们新建一个壳工程,也就是测试工程,用来使用BookRoomKit.framework;并且命名为BookRoomDemo,如下图:
    BookRoomDemo App
    BookRoomDemo
    将BookRoomKit.framework拖入项目中,勾选下面三项,然后点击Finish
    勾选的三项
    编写代码前,记得将壳工程切换为release环境,然后真机运行,因为编译的framework必须与壳工程运行的环境一致才能够运行成功。
    编写测试代码
    运行之后你会发现,程序运行不起来,奔溃的错误信息是:

    1
    2
    3
    dyld: Library not loaded: @rpath/BookRoomKit.framework/BookRoomKit
    Referenced from: /var/containers/Bundle/Application/C3208286-60C0-456D-B512-C26FB1E6A254/BookRoomDemo.app/BookRoomDemo
    Reason: image not found

奔溃信息日志
这里就涉及一个问题,那就是我之前的一步操作是直接从Finder中将framework拖拽进壳工程项目的,这样操作默认会将framework放置在Linked Frameworks and Libraries下面,如下图;

第一:Embedded Binaries和Linked Frameworks and Libraries是有区别,具体区别看这篇文章What is the difference between Embedded Binaries and Linked Frameworks;第三:从ios8开始,苹果官方支持我们构建的dynamic framework,我们构建的BookRoomKit.framework就是dynamic frameworks,这种类型的framework它需要签名code-signed以及嵌入到我们的app,否则我们真机运行时就会奔溃Embedded Binaries with iOS Framework,还有一个问题就是,我们构建的BooRoomKit.framework往往需要使用第三方的网络请求框架,JSON解析框架等等,这些都因为代码签名的问题都得在壳工程中添加
说了这么多,解决方案就是:选中Linked Frameworks and Libraries下面的BookRoomKit.framework点击删除,然后将左侧导航栏的framewotk拖拽到Embedded Binaries下面,这时Linked Frameworks and Libraries默认也会有,如下图:
正确的方法
再次运行,你会发现运行成功,而且打印了hello bookroom
hello bookroom

Framework合并

  1. 使用lipo -info查看framework支持的cpu架构,分别对应真机版本和模拟器版本的framework信息
    lipo -info
  2. 使用lipo -create指令将模拟器和真机的framework合并成通用版本
    真机与模拟器的framework
    具体操作如下:首先,切换到/BookRoomKit/Build/Products;然后执行lipo -create -output [name] [path1] [path2]这条命令;执行完成后在Products目录下生成一个BookRoomKit文件。
    lipo -create1
    lipo -create2
    然后将上图红圈的文件复制到Release-iphoneos中去覆盖原来的版本,最后将Release-iphonesimulator中的框架文件里的/Modules/BookRoomKit.swiftmodule里的文件复制到Release-iphoneos对应的文件夹下。这样我们就得到了一个通用的的框架。
    通用的framework
    最后用这个framework替换掉测试工程的framewotk就可以在真机和模拟器运行了

测试framework专用

为了便于测试工程使用framework,我这里教大家把两个项目放到一个Xcode里面打开,也不用Xcode打开两个项目,这样便于测试使用,也不用来回在framework工程和壳工程之间切换。操作如下:关闭BookRoomKit项目,打开BookRoomDemo项目,将之前的操作取消回退,删除集成进来的Book Room.framework。然后将BookRoomKit.xcodeproj拖拽进BookRoomDemo项目中,编译BookRoomKit,如下图:
使用一个项目打开
然后将编译好的framework拖拽进BookRoomDemo中的Embedded Binaries,如下图
3F6F9E91-A8CB-4ACE-B0B4-D21ABD9C5BA8.png
通过上面的操作你就可以方便的测试framework编写的代码,不用每次都编译构建framework,当你运行BookRoomDemo的时候你就能够使用framework暴露出来的方法调用。

Framework中使用image,xib,storyboard,font等资源文件

  1. image, xib,storyboard都是需要传递相应的bundle。而我看到网上一些旧的教程都很繁琐,还要建立什么resources.bundle之类的。经过我的实践,我这种方法更为简单。
    1
    let bundle = Bundle(identifier: "xcqromance.BookRoomKit") // framework的bundle ID

storyboard的加载,xib类似

1
2
3
let sb = UIStoryboard(name: "BookHome", bundle: bundle)
let vc = sb.instantiateViewController(withIdentifier: "BookViewController") as! BookViewController
viewController.navigationController?.pushViewController(vc, animated: true)

image的加载

1
2
let image = UIImage(named: "bookroom_down_bg_blue", in: bundle, compatibleWith: nil)
let imageView = UIImageView(image: image)

  1. font的加载则是比较麻烦,得先注册,才能使用!所以我写了个OC的NSObject分类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+ (UIFont *) loadMyCustomFont:(NSString *)name size: (CGFloat)size type: (NSString *)type {
NSString *fontPath = [[NSBundle bundleWithIdentifier:@"xcqromance.BookRoomKit"] pathForResource:name ofType:type];
NSData *inData = [NSData dataWithContentsOfFile:fontPath];
CFErrorRef error;
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)inData);
CGFontRef font = CGFontCreateWithDataProvider(provider);
if (! CTFontManagerRegisterGraphicsFont(font, &error)) {
CFStringRef errorDescription = CFErrorCopyDescription(error);
NSLog(@"Failed to load font: %@,%@", errorDescription,name);
CFRelease(errorDescription);
}
CFRelease(font);
CFRelease(provider);
NSString *fontName = (__bridge NSString *)CGFontCopyPostScriptName(font);
UIFont* uifont = [UIFont fontWithName:fontName size:size];
return uifont;
}

然后在单例模初始化的时候进行注册。使用这个字体的方法和平时的一样,将fontName传递下就可以了

1
2
3
4
5
let label = UILabel()
label.text = "hello word"
label.sizeToFit()
label.center = view.center
label.font = UIFont(name: "Kreon-Bold", size: 17)

Github上面下载BookRoomDemo

———————2017.04.10更新———————
每次使用终端lipo -create创建通用版本很繁琐、低效,于是想到了将这个过程脚本化;

使用脚本一键构建通用版本的framework(真机、模拟器通吃的版本)

步骤如下:

  1. build active architecture only设置为No
    build active architecture only -> No
  2. 新建一个target,用来构建通用版本framework
    new target
    选择Cross-platform->other->Aggregate->Next
    Aggregate
    命名为univeralBuilder,新建一个New Run Script Phase
    D124D6A4-B28B-4632-95F2-568278624B06.png
    在shell里面添加以下内容,注意将第九行的FRAMEWORK_NAME改为自己framework的名字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Merge Script

# 1
# Set bash script to exit immediately if any commands fail.
set -e

# 2
# Setup some constants for use later on.
FRAMEWORK_NAME="Your framework name"

# 3
# If remnants from a previous build exist, delete them.
if [ -d "${SRCROOT}/build" ]; then
rm -rf "${SRCROOT}/build"
fi

# 4
# Build the framework for device and for simulator (using
# all needed architectures).
xcodebuild -target "${FRAMEWORK_NAME}" -configuration Release -arch arm64 -arch armv7 -arch armv7s only_active_arch=no defines_module=yes -sdk "iphoneos"
xcodebuild -target "${FRAMEWORK_NAME}" -configuration Release -arch x86_64 -arch i386 only_active_arch=no defines_module=yes -sdk "iphonesimulator"

# 5
# Remove .framework file if exists on Desktop from previous run.
if [ -d "${HOME}/Desktop/${FRAMEWORK_NAME}.framework" ]; then
rm -rf "${HOME}/Desktop/${FRAMEWORK_NAME}.framework"
fi

# 6
# Copy the device version of framework to Desktop.
cp -r "${SRCROOT}/build/Release-iphoneos/${FRAMEWORK_NAME}.framework" "${HOME}/Desktop/${FRAMEWORK_NAME}.framework"

# 7
# Replace the framework executable within the framework with
# a new version created by merging the device and simulator
# frameworks' executables with lipo.
lipo -create -output "${HOME}/Desktop/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${SRCROOT}/build/Release-iphoneos/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}" "${SRCROOT}/build/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}"

# 8
# Copy the Swift module mappings for the simulator into the
# framework. The device mappings already exist from step 6.
cp -r "${SRCROOT}/build/Release-iphonesimulator/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule/" "${HOME}/Desktop/${FRAMEWORK_NAME}.framework/Modules/${FRAMEWORK_NAME}.swiftmodule"

# 9
# Delete the most recent build.
if [ -d "${SRCROOT}/build" ]; then
rm -rf "${SRCROOT}/build"
fi

3.最后一步:选择Aggregate target和Simulator然后build,你会看到桌面由构建好通用版本的framework。
参考资料:http://arsenkin.com/ios-universal-framework.html

2017.11.07添加

Framework依赖第三方库

有人在下面提问,如果我新建的framework还依赖第三方库怎么解决?而这也是一个很正常的需求。比如我的BookRoomKit.framework有一个绘本zip包解压的功能,此时我有两种解决方案,第一将ZipArchive的代码拖入到我的framework中,还有一种是ZipArchive.framework引入到我的framework中。明显我们会选择第二种方式,一旦有更新直接替换新的的framework就行。而第二种方式又有两种方法引入:第一:在壳工程中使用pods、carhtage第三方库管理工具来添加;第二:直接将依赖的framework拖入壳工程。其实这两种方式的本质都是一样的,都有一个很关键的点就是Framework 的Build Settings中设置好依赖的第三方库的Framework Search Paths,同时在Build Phases的Link Binary With Binaries添加依赖的第三方库,下面我就以第二种方法直接将依赖的第三方库拖入壳工程进行配置为例。

  1. 将下载好的ZipArchive.framework拖入壳工程,记得勾选Copy items if needed,此时你在壳工程就已经能够使用这个库了,这是因为你拖入第三库ZipArchive时Xcode已经给你配置好Framework Search Paths,你可以在壳工程的builds settings搜索到。如下图:

    但是你此时在壳工程中使用ZipArchive.framework,运行起来是会crash的,奔溃信息和前文一张名为奔溃信息日志的图片是一样的,
    1
    2
    3
    dyld: Library not loaded: @rpath/ZipArchive.framework/ZipArchive
    Referenced from: /var/containers/Bundle/Application/74F5D1D3-D6B5-4C68-9629-9CAF6C16F133/BookRoomDemo.app/BookRoomDemo
    Reason: image not found

所以解决方案也就是一样的,在EmbeddedBinaies中添加ZipArchive.framework

  1. 设置BookRoomKit.xcodeproj的framework Search Paths
    路径务必填写正确,我写的相对路径:$(PROJECT_DIR)/../BookRoomDemo/BookRoomDemo
  2. BookRoomKit.xcodeproj的Link Binary With Binaries添加ZipArchive.framework,选在Add Other,添加进来即可。
    接下来你就可以在BookroomKit使用ZipArchive了,具体代码你可以在BookRoomDemo下载。