前言
本文旨在记录自制framework的过程、碰到的各种问题及解决方法
目标是制作一个带bundle资源的framework,并在新App里能使用这个framework,所以使用xcode创建项目这里只讨论App、framework和bundle
一、Products目录索引
不管是创建App还是framework、bundle,使用xcode创建会在根目录下生成Products索引,编译后的目标文件索引最后都会出现在这个Products索引里,要找到目标文件只需要选中索引然后右键Show in Finder就可以了。但是使用Xcode13创建项目却不显示Products目录了,这里记录一种把Products目录显示出来的方法。
使用Xcode13新建了一个名为CustomProject 的新项目
cd /Users/user/Desktop/MobileProject-Swift/CustomProject //进到项目根目录
open CustomProject.xcodeproj/project.pbxproj //打开project.pbxproj文件
在打开的project.pbxproj文件搜索“productRefGroup”关键字,搜索结果可能有多个,每个项目的键值不一样具体看项目,找到匹配对象,后面跟着“/* Products */”后缀的才是要修改的
把mainGroup等号右边的值复制到productRefGroup等号右边,cmd+s保存,Xcode会自动刷新,Products索引就可以显示出来了。
不管是创建App还是framework、bundle,没有Products索引的话都可以通过这种方法显示出来,在找目标文件时会方便很多。
二、制作资源bundle
1、添加bundle
选中项目,添加bundle,也可以跟创建项目一样,另起一个新项目(快捷键shift + cmd + n)创建一个只有bundle的项目。
2、修改配置参数
注意选中的TARGETS是CustomBundle,而不是CustomProject
TARGETS >>> General >>> Deployment Info 设置设备和最低支持版本(Xcode13只有一个Deployment Target可选)
TARGETS >>> Build Settings >>> BaseSDK 改成 iOS
TARGETS >>> Build Settings >>> Supported Platforms 改成 iOS
TARGETS >>> Build Settings >>> Enable Bitcode 改成 NO
TARGETS >>> Build Settings >>> Versioning System 改为None(为了去掉构建后Bundle里的可执行文件exec)
TARGETS >>> Build Settings >>> COMBINE_HIDPI_IMAGES 改为NO(否则Bundle图片会变为tiff格式)
3、添加资源(图片、音频、视频、xib、plist等)
把需要的资源拖进bundle目录索引下
Xcode13新建bundle的时候左侧目录栏不会生成目录索引,资源可以直接拖进Build Phases下的Copy Bundle Resources里,拖进去的资源会直接出现在根目录索引下,也可以在根目录下新建一个目录索引,把资源都拖进去,然后在Copy Bundle Resources把资源添加进去。
不推荐使用Assets或者Image Set,创建的名称与图片的真实名称可能不一样,用的时候会找不到目标资源
4、编译生成.bundle文件
编译前目录里的CustomBundle索引是红色的,表示还没有生成真正的.bundle文件,参考上面的图,Scheme选择CustomBundle,Build选择Any iOS Device然后cmd + b编译,编译后生成CustomBundle.bundle文件,目录里的索引变为黑色
选中目录里的CustomBundle索引,右键show in Finder打开.bundle文件所在目录,把CustomBundle.bundle文件拷贝一份放到单独的地方,后续要把它放到生成的framework里使用
三、制作framework
1、添加framework
选中项目,添加framework,也可以跟创建项目一样,另起一个新项目(快捷键shift + cmd + n)创建一个只有framework的项目。
建议单独创建framework项目,原因后面会说到。
2、修改配置参数
注意选中的TARGETS是CustomFramework,而不是CustomProject
TARGETS >>> General >>> Deployment Info 设置设备和最低支持版本
TARGETS >>> Build Settings >>> BaseSDK 改成 iOS
TARGETS >>> Build Settings >>> Supported Platforms 改成 iOS
TARGETS >>> Build Settings >>> Build Active Architecture Only 改成 NO(表示支持所有架构)
TARGETS >>> Build Settings >>> Architectures 添加 armv7s (表示支持真机32位处理器)
TARGETS >>> Build Settings >>> Excluded Architectures Debug或Release下添加Any iOS Simulator SDK,值为arm64(在模拟器下排除arm64架构,原因后面会说到)
TARGETS >>> Build Settings >>> Dead Code Stripping 改成 NO
TARGETS >>> Build Settings >>> Link With Standard Libraries 改成 NO
TARGETS >>> Build Settings >>> Mach-O Type 改为 Static Library (静态库的意思,只有系统的framework里有的是动态库,自制的都是静态库)
TARGETS >>> Build Settings >>> Other Linker Flags 添加-ObjC(如果用了分类加这个参数)
Other Linker Flags的参数说明:
编译过程:从C代码到可执行文件经历的步骤是:源代码 > 预处理器 > 编译器 > 汇编器 > 机器码 > 链接器 > 可执行文件
在最后一步需要把.o文件和C语言运行库链接起来,这时候需要用到ld命令。源文件经过一系列处理以后,会生成对应的.obj文件,然后一个项目必然会有许多.obj文件,并且这些文件之间会有各种各样的联系,例如函数调用。链接器做的事就是把这些目标文件和所用的一些库链接在一起形成一个完整的可执行文件。Other linker flags设置的值实际上就是ld命令执行时后面所加的参数
-ObjC:让链接器把静态库中所有的类和分类都加载到最后的可执行文件中,因为加载了其他的代码,会导致编译之后的App变大,但是如果静态库中有类和分类的话只有加入这个flag才行。
-all_load:让链接器把静态库中所有找到的目标文件都加载到可执行文件中,这个flag是专门处理-ObjC的一个bug的,用了-ObjC以后,如果类库中只有分类没有类的时候这些分类还是加载不进来。变通方法就是加入-all_load或者-force-load。
注意:假如使用了不止一个静态库文件,然后又使用了这个参数,很有可能会遇到 ld: duplicate symbol 错误,因为不同的库文件里面可能会有相同的目标文件
-force_load:所做的事情跟-all_load其实是一样的,但是需要指定要进行全部加载的库文件的路径,这样只是完全加载了一个库文件,不影响其余库文件的按需加载
3、把bundle添加到framework
找到上面生成的CustomBundle.bundle文件,拖入工程并连接到CustomFramework的target中
如果没有这一步,每次编译生成的framework里是没有.bundle文件的,需要单独添加进去,编译一次就需要添加一次
这样做的好处是每次重新编译生成framework时,.bundle文件都会被自动引入到framework中,无需关注是否单独添加的问题
4、设置framework头文件
把功能文件添加到framework里,如果是OC的文件,需要指定外部可以引用的头文件,两种方法
方法一:选中需要暴露的头文件,在Xcode右侧的Target Membership中把Project改为Public
方法二:选中整个项目,找到TARGETS下的CustomFramework,选择Build Phases下的Headers并点开,选中需要暴露的头文件直接拖拽到Public选项下
然后在framework的头文件里引入所有供外部引用文件的头文件,有几个引入几个
如果是swift的文件,需要使用public或open关键字让需要暴露的类/方法/属性暴露出来
关于public和open的区别,主要在于能否继承或override
1、public修饰的class只允许外部模块调用,但是不允许继承; 而open修饰的class既允许其他模块调用,也允许被子类继承。
2、public修饰的成员只允许其他模块调用,但不能被覆盖(override);而open修饰的成员既允许被其他模块调用,也允许成员被覆盖。
3、如果class声明为public,那么class的成员变量不能为open,因为public class已被限定为不可继承。
4、如果class声明为open,其他模块继承覆盖(override)父类的成员时,也需要把成员声明为open。
下面的问题就是bundle里的资源该如何使用,需要根据路径先找到bundle,然后加载bundle实例,再使用实例中需要的资源
5、编译framework
首先要明确编译framework是有Debug和Release两种模式的,切换模式通过Edit Scheme来实现
选中Xcode >>> Product >>> Scheme >>> Edit Scheme
或者在Xcode顶部工具栏的Scheme里选择Edit Scheme
在弹出的面板里先选择目标framework再选左侧的Run,最后选择Debug还是Release
选择模式后关闭面板,回到Xcode,Scheme选择CustomFramework,Build选择Any iOS Device然后cmd + b编译,编译后就会生成CustomFramework.framework文件
到这一步并不算完,因为现在编译得到的framework在模拟器下并不能使用,选中CustomFramework右键Show in Finder会发现,根据Scheme选择Debug和Release两种模式的不同,这一步只会生成Debug-iphoneos或Release-iphoneos两种真机环境下的framework
为了使编译得到的framework能在模拟器和真机下都能使用,需要分别在模拟器和真机下编译,再把得到的framework文件合并,要达到目的有两种方法:
方法一:分别在真机、模拟器下编译得到两个framework,然后使用lipo命令合并为一个
先分别编译,编译后选中CustomFramework右键Show in Finder,得到两个framework文件,这里用的Release模式下的文件,Debug模式下同理。
打开终端输入命令,注意空格:
lipo -create 模拟器下framework文件路径 真机下framework文件路径 -output 新的路径
如果Xcode版本是12以上,到这一步可能会出错:
fatal error: /Library/Developer/CommandLineTools/usr/bin/lipo: 模拟器下framework文件路径 and 真机下framework文件路径 have the same architectures (arm64) and can't be in the same fat output file
这是因为framework所支持的架构冲突导致的,正常打包的framework都会需要支持i386、armv7、x86_64、arm64等,Xcode12以后模拟器编译生成的framework中也会包含arm64,因此在合并的时候就会出现这个错误
可以使用以下命令查看编译出来的framework所支持的架构
lipo -info framework文件路径
可以看到都有arm64,所以在合并的时候冲突了,解决方法就是在模拟器编译framework时把arm64架构排除在外
TARGETS >>> Build Settings >>> Excluded Architectures >>> 添加Any iOS Simutator SDK,值为arm64
Excluded就有排除的意思,这里设置在模拟器编译的时候把arm64架构排除在外,如果是编译Debug下的framework就在Debug下添加arm64,如果是编译Release下的framework就在Release下添加arm64,添加好后重新编译framework,再使用lipo命令合并就不会报错了。
合并后会生成一个后缀是.lipo的文件
去掉后缀,名字改成framework文件的名字(这里叫CustomFramework),替换掉CustomFramework.framework里的CustomFramework文件
如果合并的是Debug模式下的文件,就替换Debug模式下的iphoneos里的文件,如果合并的是Release模式下的就替换Release模式下的iphoneos里的文件
替换后的CustomFramework.framework就是最终需要的framework,可以在真机和模拟器上使用
方法二:使用Script脚本
1、创建Aggregate
2、添加脚本
TARGETS >>> Build Phases >>> 左上角+ >>> New Run Script Phase
在新建的Run Script 窗口里添加生成framework的脚本代码
#!/bin/sh
#要build的target名
TARGET_NAME=${PROJECT_NAME}
if [[ $1 ]]
then
TARGET_NAME=$1
fi
UNIVERSAL_OUTPUT_FOLDER="${SRCROOT}/${PROJECT_NAME}/"
#创建输出目录,并删除之前的framework文件
mkdir -p "${UNIVERSAL_OUTPUT_FOLDER}"
rm -rf "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework"
#分别编译模拟器和真机的Framework
xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphonesimulator BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
#拷贝framework到univer目录
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework" "${UNIVERSAL_OUTPUT_FOLDER}"
lipo "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}" -remove arm64 -output "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}"
#合并framework,输出最终的framework到build目录
lipo -create -output "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${TARGET_NAME}.framework/${TARGET_NAME}"
#删除编译之后生成的无关的配置文件
dir_path="${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/"
for file in ls $dir_path
do
if [[ ${file} =~ ".xcconfig" ]]
then
rm -f "${dir_path}/${file}"
fi
done
#判断build文件夹是否存在,存在则删除
if [ -d "${SRCROOT}/build" ]
then
rm -rf "${SRCROOT}/build"
fi
rm -rf "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator" "${BUILD_DIR}/${CONFIGURATION}-iphoneos"
#打开合并后的文件夹
open "${UNIVERSAL_OUTPUT_FOLDER}"
可以看到脚本所做的事情也是分别编译真机和模拟器下的framework然后合并成一个,合并完成后会打开合并后的文件夹,得到的CustomFramework.framework就是最终需要的framework,可以在真机和模拟器上使用
3、注意
如果是单独创建的framework项目,到这一步确实拿到了需要的framework
如果是在一个app的项目里通过TARGETS左下角的加号创建的framework项目,framework项目目录索引在app项目的根目录索引下,这时候编译运行脚本是没法得到需要的framework的,因为脚本是根据PROJECT_NAME创建的framework输出目录,app项目与framework项目名称肯定不一样,编译后生成framework的路径也不一样,脚本执行的时候就会找不到目标文件,导致合并后的文件夹里并没有需要的framework,编译不会报错,只不过没得到想要的结果
所以想用又不想改脚本内容的话,记得单独创建framework项目就可以了
四、在其他项目中使用自制的framework
新建一个App项目,在这个项目里测试得到的CustomFramework.framework。
从iOS13开始Xcode新增了SceneDelegate来管理UI的生命周期,更适合开发ipad多场景应用,单场景的可以不用这个,先把SceneDelegate相关的东西删掉
初始化window,设置根控制器,把自制的framework和测试项目放到同一个目录下,通过下图中的方法在测试项目里引入自制的framework,引入后左侧目录索引会自动生成Frameworks目录索引,并且CustomFramework.framework已加入其中。
bundle资源也要加入到测试项目里
引入CustomFramework,编写测试代码
在模拟器运行代码,可能会报错
可以通过下图的方法解决这个报错
大致的解释是从Xcode的角度来看,iOS和iOS模拟器是两个不同的平台,原来framework是默认双平台的,会同时构建两个平台的framework,现在是做区分,这样的好处是可以在上传Appstore构建的时候省去剥离iOS模拟器相关的framework需求。
结尾
到这里整个制作framework的流程基本就介绍完了。
静态库能在提供一定可复用功能的同时又能不暴露内部实现的过程(swift语言制作的静态库可以看到内部实现过程),这是静态库的优势,但同样的高度定制和复杂的东西出问题的可能性也更大,随着版本更迭一个小问题也可能会变得很麻烦,所以相对应的文档和更新记录就比较重要,一个成熟的库说明文档和更新记录都必不可少。