动态库 vs 静态库
iOS
中的framework
既可以是动态库,也可以是静态库
image.png
-
Framework
实际上是一种打包方式,将库的二进制文件,头文件和有关的资源文件打包到一起,方便管理和分发。可以是动态的,也可以是静态的。 -
系统的
Framework
是动态库,不需要拷贝到目标程序中。我们自己做出来的Framework
哪怕是动态的,最后也还是要拷贝到App
中(App
和Extension
的Bundle
是共享的),因此苹果又把这种Framework
称为 Embedded Framework。 -
在
iOS 8
之前,iOS
平台不支持使用动态Framework
,开发者可以使用的Framework
只有苹果自家的UIKit.Framework,Foundation.Framework
等。iOS 8/Xcode 6
推出之后,iOS
平台添加了动态库的支持,同时Xcode 6
也原生自带了Framework
支持(动态和静态都可以)。 -
Xcode
创建framework
时,默认是动态库,可以改为静态库
如果是动态库
-
Xcode
的默认设置,什么都不用改 -
需要在工程的
General
里的Embedded Binaries
添加这个动态库才能使用,不然会找不到。
-
cocoapods
的use_framework!
使用的是动态库方式。只不过添加Embedded Binaries
的过程通过脚本做了,不需要手动操作。 -
在主工程的的
Frameworks
文件夹下,以xxx.framework
的形式独立存在。不会和主工程合并。 -
对于资源文件,(包括图片,故事版等),在这个
Frameworks
下,与framework
中的可执行文件在同一级。
如果是静态库
-
和主工程合并为同一个,看不到独立的文件。
-
大多数文章介绍,资源文件(图片,故事版等)需要一个同名的
xxx.bundle
文件,和静态Framework
一同引入到主工程中。编译链接之后,静态Framework
已经与主工程合并为一个,看不到,但是这个xxx.bundle
文件却是在yyy.app
包中,和主工程同一级别。所以访问资源,就是访问这个xxx.bundle
中的资源。
-
如果用
cocoapods
打包,也就是不加use_framework!
,打出来的就是静态库,同名的xxx.bundle
文件会自动生成。 -
可以简单理解为“静态的framework”只能放代码,不能放资源。使用后,“静态的framework”和主工程代码合并,成为一个可执行文件。至于资源文件,需要放在一个独立的“bundle”文件中,加入主工程。
选哪种?Embedded Framework
-
个人偏向于选择动态的
framework
,也就是Embedded Framework
-
静态
framework
需要将资源文件分离出来,额外创建同名的.bundle
文件,需要多很多步骤。本来,framework
本质上是一种打包方式,可以将代码,头文件,资源都放在一起;但是现在又要将资源文件重新独立出来,感觉在走回头路。 -
动态的
framework
,也就是Embedded Framework
,在使用后仍然能保持独立性,分离更彻底。并且把代码,头文件,资源文件都打包在一起,对外统一提供接口,有更好的封装和隔离。 -
今后,扩展会有发展,“共享”的成分会逐步增多。
-
从本质上来说,静态
framework
只能包含代码和头文件,并不能包含资源,没有把framework
的优势体现出来。
资源读取
-
这里只考虑动态的
framework
的资源读取过程。至于静态framework
,其实应该从主工程的角度来考虑资源读取问题,这里暂时不考虑。 -
可以把动态的
framework
当做一个独立的打包单元,或者文件夹,或者是一个比较大的独立的bundle
。资源(图片或者故事版)和可执行文件处于同一级。(XCode
默认build
之后是这样的)
- Frameworks文件夹是General->Embedded Binaries操作之后固定生成的,下面可以放很多的framework。
- KJTCashierDemo是主工程的可执行文件。
- KJTSingle.storyboaddc是主工程中的资源
- KJTCashier是framework中的可执行文件,与主工程的可执行文件相互独立。
- KJTPay.storyboaddc是framework中资源,与KJTCashier在同一目录,由KJTCashier来读取,跟主工程KJTCashierDemo没关系。
- KJTCashier. framework相当于一个独立的bundle,可执行文件,资源等全部自给自足,跟主工程的可执行文件和资源都没有关系。
- 资源(图片,故事版)等,在读取时,都有一个
bundle
参数。所以,读取的关键就是确定bundle
参数。
// 读图片函数
+ (nullable UIImage *)imageNamed:(NSString *)name inBundle:(nullable NSBundle *)bundle compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection NS_AVAILABLE_IOS(8_0);
// 故事版函数
+ (UIStoryboard *)storyboardWithName:(NSString *)name bundle:(nullable NSBundle *)storyboardBundleOrNil;
- 如果
bundle
参数为nil
,那么就是只主工程的bundle
,所以,有不需要bundle
参数的图片读取函数。
// load from main bundle
+ (nullable UIImage *)imageNamed:(NSString *)name;
如何确定bundle参数?
-
对于动态
framework
,它本身就是一个bundle
。打包之后,与主工程的资源和可执行文件都在一个.app
包中。 -
动态
framework
这个bundle
,包含自己的可执行文件,资源等等,它们在同一目录下。 -
可以通过下面这个函数,确定动态
framework
的bundle
。
// 可执行文件和资源同在一个bundle中,类编译后就在可执行文件中
return [NSBundle bundleForClass:[self class]];
生成
framework
之后,所有的类都被编译到了动态framework
的可执行文件中。类所在的bundle
,也就是可执行文件所在的bundle
,也就是动态framework
这个bundle
。
例子代码
// 从故事版读取Controller
+ (nullable __kindof UIViewController *)controllerWithStoryboardName:(NSString *)storyboardName identifier:(NSString *)identifier {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:storyboardName bundle: [NSBundle bundleForClass:[self class]]];
if (storyboard == nil) {
return nil;
}
__kindof UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:identifier];
vc.hidesBottomBarWhenPushed = YES;
return vc;
}
// 读取本地图片
+ (nullable UIImage *)imageWithName:(NSString *)name {
UIImage *image = [UIImage imageNamed:name inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil];
return image;
}
对外的头文件
-
系统会默认生成一个和
framework
同名的头文件,作为对外的唯一接口。这个文件要保留好,不要删除。这个文件如果不存在,会有警告。 -
其他要暴露的头文件,需要在
Build Phase
的Header
里面添加到Public
部分。
- 并且需要包含在默认生成的,与
framework
同名的头文件中。
合并framework
-
一般的使用习惯,先用模拟器,然后用真机。但是
XCode
生成的framework
,要么是模拟器的,要么是真机的,很不方便。 -
所以,网上很多文章就提出了使用脚本命令进行合并的方法,很好用。
- 在
Build Phases
中点击左上角+
,选择New Run Script Phase
添加一项
- 将下面的脚本代码粘贴至提示
“Type a script or drag…”
输入框内
if [ "${ACTION}" = "build" ]
then
INSTALL_DIR=${SRCROOT}/Products/${PROJECT_NAME}.framework
DEVICE_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework
SIMULATOR_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi
mkdir -p "${INSTALL_DIR}"
cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
#ditto "${DEVICE_DIR}/Headers" "${INSTALL_DIR}/Headers"
lipo -create "${DEVICE_DIR}/${PROJECT_NAME}" "${SIMULATOR_DIR}/${PROJECT_NAME}" -output "${INSTALL_DIR}/${PROJECT_NAME}"
open "${DEVICE_DIR}"
open "${SRCROOT}/Products"
fi
- 测试一下脚本,在
release
环境下分别选择真机和模拟器运行,运行成功后会自动打开Finder
并定位到工程根目录下的Products:
- 注意,工程中的
Products:
和这里的Products:
是不一样的。
如何调试?
-
有两种方法,一种是
workspace
中的两个project
;另外一种是一个project
中的两个target
。这里采用第二种。 -
在工程里新建一个
target
,作为使用者,也就是主工程。并且在Build Phase
部分建立相互依赖。也就是主工程target
依赖framework target
-
主工程
target
使用framework target
中文件就像使用自己的文件一样方便。 -
如果要调试,在
framework target
中相应的地方打断点就可以了,和在一个target
调试感觉差不多。
如何使用?
-
由于使用是给第三方的,所以和在一个
project
下两个target
的调试情况不一样,不能建立依赖。 -
新建一个
Demo
工程,将合并过的framework
加入进来。Files -> Add Files To “”...
。没有独立的资源bundle
,资源文件也在framework
中。因为这是“动态的”。
- 在
General -》Embedded Binaries
添加framework
,否则会找不到这个framework
- 注意,外部主工程只能调用
framework
导出的头文件中的方法。这与调试的时候差异很大。
其他的一些配置
- 最低支持版本需要设置一下,默认是最新的版本。
Build Setting -> iOS Development Target
网友评论