前言
如果你想将你开发的控件与别人分享,一种方法是直接提供源代码文件。然而,这种方法并不是很好。因为它会暴露所有的实现细节,而这些实现你可能并不想开源出来。此外,开发者也可能并不想看到你的所有代码,因为他们可能仅仅希望将你的这份漂亮代码的一部分植入自己的应用中。
OS X完美地支持这一点,因为Xcode就提供了一个项目模板,包含着默认构建目标(target)和可以容纳类似于图片、声音、字体等资源的文件。你可以为iOS创建Framework.下图有个错误,我们生成的framework可以是动态库,也可以是静态库。根据自己设置来区分,虽然我们可以做动态库,但是和系统的动态库是不一样的,系统的动态库可以做到多个程序共享一个,而我们的动态库由于沙盒机制限制,只是加载时机不一致,但是还是1对1的,很难做到多程序共用。
比较
我们可以看出.a的封装和.framework的封装差不多,也有模拟器和真机合并的过程,通过上边的图片我们可以看出.a 和.framework的区别,就是.a+.h+soureFile=.framework。可以看出我们直接封装.framework其实是最好的。那么我们就来看看framework怎么封装的。
目标
本文将基于Xcode7创建一个简单的工程,通过两种方法来教大家如何制作一个自己的framework,目的就是简单易学的制作framework。这种方法可以使得你的代码易分享,在多个工程中复用,并且可以隐藏实现细节,控制公开的头文件。
步骤
1、打开Xcode,新建工程。
不要选择“Application”,选择“Framework & Library”。选择第一个,然后Next。
2、创建功能类。
这里我创建一个继承自NSObject的SayHello类
3、实现功能。
在新创建的类里面声明方法并实现。这里我写一个sayHello的方法,以便后面测试使用。
4、更改参数
在TARGETS下选中工程,在Build Settings下更改几个参数。修改Mach-O Type为Static Library就是静态库,Dynamic Library就是动态库.后面再详细说说动态库和静态库的区别.
5、增加armv7s
在Architectures下增加armv7s,并选中。将Build Active Architecture Only 设置为NO。
6、设置Headers
将你要公开的头文件拖至Public下,要隐藏的放在Private或者Project下,当然,隐藏的头文件就无法再被引用。
然后需要在Test.h(必须是公开的,否则无法引用)中将你所有要公开的.h引入。
打包Framework
1、选中TARGETS下的工程,点击上方的Editor,选择Add Target创建一个Aggregate.
2、选择Other下的Aggregate,点击Next创建。
3、嵌入脚本。选中刚刚创建的Aggregate,然后选中右侧的Build Phases,点击左下方加号,选择New Run Script Phase
将这段脚本复制进去:
# FMK_NAME是framework的名字,如果和工程名不一致,需要修改一下
#比如我的工程名字叫"WeChat",但是framework叫"ToolSDK",那么此处需要改成"ToolSDK"
FMK_NAME=${PROJECT_NAME}
if [ "${ACTION}" = "build" ]
then
INSTALL_DIR=${SRCROOT}/Products/${FMK_NAME}.framework
DEVICE_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphoneos/${FMK_NAME}.framework
SIMULATOR_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphonesimulator/${FMK_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}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${INSTALL_DIR}/${FMK_NAME}"
#这个是合并完成后打开对应的文件夹,你就可以直接看到文件了
open "${SRCROOT}/Products"
fi
注意下面的这2个选项的设置:
脚本中最重要的是使用到了“lipo -create 真机framework路径 模拟器framework路径 -output 输出路径”,这个函数是把模拟器和真机的framework合成一个framework,这样生成的frame在模拟器环境/真机环境都可以运行。
4、编译。如图所示,command+B编译。这里Generic iOS Device的意思是“iOS通用设备”,大概就是说模拟器和真机都能用。
5、编译成功后会自动跳出一个finder,保存这个.framework,这就是我们需要的framework。
至此,打包framework的方法介绍完成!
最后就是用我们的Framework了,倒入另一个Xcode中,我们打开这个framework看看,发现只有Headers,里面有两个.h,其中一个是我们之前添加的FrameworkDemo.h文件,另一个就是我们的SayHello.h 。
然后引入头文件:
由于我们测试的方法是实例方法,那么我们实例化一个实例对象,然后就可以让这个实例对象调取相应的方法了:
至此,完成Framework的制作和使用。
总结
最后需要注意的是:
1、.h文件的外漏一定要保证是自己的想要外漏的。不想外漏的就别外漏了。
2、开始打包的时候,一定要在选中模拟器和选中真机上边分别编译一次, 我觉得之前在家里没有真机的时候编译的好像不对。
3、在终端上边合并的时候可能是error并生成一个.lipo文件,不要怕,大胆修改成同名的不挂后缀的同名文件。
4、调用的时候分清楚是类方法还是实例方法,方便调用。
5、在制作framework或者lib的时候,如果使用了category,则使用改FMWK的程序运行时会crash,此时需要在该工程中 other linker flags添加下面两个参数中的一个 -ObjC(链接OC文件) / -all_load(链接所有文件)。
6、带有图片资源的需要把图片打包成Bundle文件,和framework一起拷贝到相应的项目中。
7、公开的类中如果引用的private的类,打包以后对外会报错,找不到那个private的类,可以把那个private的.h放到.m中(也没亲测)
8、namespace 冲突。静态库用了某第三方库,项目也用了同样的第三方库,在编译的时候就会有 duplicate symbol 错误,因为有两份同样的第三方库。解决办法就是把用到的第三方库加上自定义前缀,包括类名、delegate 协议、常量名,尤其需要注意 Category 的方法名要修改。
9、封装静态库的时候应尽量避免引入重量级第三方库,多自己进行封装。
10、一个静态库要有自己独有的前缀,所有类名、常量等都要加同样的前缀。
11、真机+模拟器支持。(和第2条意思一样)Xcode 默认只会用当前环境(真机或模拟器)生成静态库,这样的 SDK 不方便其他项目开发时调试。解决办法就是通过脚本生成一份通用库,build_universal_library.sh,via SO.
12、文档。静态库的方便是使用者直接拿你提供的方法来用,无需关注具体实现;不方便在于看不到实现,出现问题无法排查,因此需要把 SDK 的版本、更新历史、使用、FAQ 等写成文档,方便使用,也显得 SDK 比较正式规范。
13、图片等资源文件用 bundle 方式打包。一个简单制作 bundle 的方法:新建文件夹,重命名为 YourSDK.bundle,然后 Show Package Contents 打开,加入图片。使用图片的时候需要指明 bundle: [UIImage imageNamed:@"YourSDK.bundle/img.png"]。也可以用 Target 方式制作 bundle,比如 iOS Library With Resourceshttp://www.galloway.me.uk/tutorials/ios-library-with-resources/.
14、如果 SDK 有用到 Category,注意项目设置 Other Linker Flags 添加 -ObjC。(后边介绍了-ObjC的作用)
补充
编译过程:
从C代码到可执行文件经历的步骤是:源代码 > 预处理器 > 编译器 > 汇编器 > 机器码 > 链接器 > 可执行文件
在最后一步需要把.o文件和C语言运行库链接起来,这时候需要用到ld命令。源文件经过一系列处理以后,会生成对应的.obj文件,然后一个项目必然会有许多.obj文件,并且这些文件之间会有各种各样的联系,例如函数调用。链接器做的事就是把这些目标文件和所用的一些库链接在一起形成一个完整的可执行文件。Other linker flags设置的值实际上就是ld命令执行时后面所加的参数
下面逐个介绍3个常用参数:
-ObjC:加了这个参数后,链接器就会把静态库中所有的Objective-C类和分类都加载到最后的可执行文件中
-all_load:会让链接器把所有找到的目标文件都加载到可执行文件中,但是千万不要随便使用这个参数!假如你使用了不止一个静态库文件,然后又使用了这个参数,那么你很有可能会遇到ld: duplicate symbol错误,因为不同的库文件里面可能会有相同的目标文件,所以建议在遇到-ObjC失效的情况下使用-force_load参数。
-force_load:所做的事情跟-all_load其实是一样的,但是-force_load需要指定要进行全部加载的库文件的路径,这样的话,你就只是完全加载了一个库文件,不影响其余库文件的按需加载
如何在framework中使用xib和图片:
添加图片 : 使用Assert.xcassets方式。
在framework下选择添加文件 -> 选择Assets.xcassets 文件,选择确定,默认名称Media.xcasset,然后直接使用就可以了;
添加图片还有一个是使用bundle,但是我感觉这样使用有点费事,使用Assert.xcassets完全可以满足,就不介绍这个复杂的方式了.使用bundle的方式参考 : https://www.jianshu.com/p/34a4fd9871d2
使用xib : 在外部调用的时候需要这样写
//加载方式1
// NSBundle *SDKBundle = [NSBundle bundleForClass:[CuijxQRController class]];
//加载方式2 bundleWithIdentifier是Framework的BundleIdentifier
NSBundle *SDKBundle = [NSBundle bundleWithIdentifier:@"com.wecan.Framework-Xib"];
CuijxQRController *qr = [[CuijxQRController alloc] initWithNibName:@"CuijxQRController" bundle:SDKBundle];
[self presentViewController:qr animated:YES completion:nil];
当然,如果外部不想这样的话, 也可以重写vc的init方法,
- (instancetype)init {
return [[LoginViewController alloc] initWithNibName:@"LoginViewController" bundle:[NSBundle bundleForClass:[self class]]];
}
// 外界使用的时候
LoginViewController * log = [[LoginViewController alloc] init];
[self presentViewController:log animated:YES completion:nil];
后来进行过了几次面试,他们都问到了动态库,静态库的区别,我还是按照 我们生成的framework是静态库来说的,但是面试官都不置可否,后面又查了一些资料,补充一下.
静态库:链接时会被完整的复制到可执行文件中,被多次使用就有多份拷贝。
动态库:链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用(如系统的UIKit.framework等),节省内存。
静态库和动态库是相对编译期和运行期的:静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库;而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。
静态库 好处:
模块化,分工合作,提高了代码的复用及核心技术的保密程度
避免少量改动经常导致大量的重复编译连接
也可以重用,注意不是共享使用
动态库 好处:
使用动态库,可以将最终可执行文件体积缩小,将整个应用程序分模块,团队合作,进行分工,影响比较小
使用动态库,多个应用程序共享内存中得同一份库文件,节省资源
使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的。
iOS8之后,iOS有了App Extesion特性,而且Swift也诞生了。由于iOS主App需要和Extension共享代码,Swift语言机制也需要动态库,于是苹果后来提出了Embedded Framework,这种动态库允许APP和APP Extension共享代码,但是这份动态库的生命被限定在一个APP进程内。简单点可以理解为被阉割的动态库。
但是这种动态库(Embedded Framework) 和系统的 UIKit.Framework 还是有很大区别,传统的动态库是给多个进程用的,而这里的动态库(Embedded Framework)是给单个进程里面多个可执行文件用的。系统的 Framework 不需要拷贝到目标程序中,我们自己做出来的 动态库(Embedded Framework) 哪怕是动态的,最后也还是要拷贝到 App 中(App 和 Extension 的 Bundle 是共享的)。所以苹果没有直接把这种Embedded Framework称作动态库而是叫Embedded Framework。
iOS中的Embedded Framework可以理解为独立的没有main函数的可执行文件。
静态库可以简单理解为一堆目标文件(.o/.obj)的打包体(并非二进制文件),而动态库可以简单理解为 一个没有main函数的可执行文件。