2018 Xcode8 创建自己的Framework(一)
新年定个小目标,比如先学习组件化开发o( ̄▽ ̄)d
最近研究组件化开发,涉及自己生成静态库,创建Framework。查询资料的过程中,发现一个超级实用的译文教程,然而这个超实用的译文教程因其发表于2015年,部分内容已有出入(尽管不大😂😂😂),特在这里边学习边记录,因理解能力有限,部分内容仍有引用自原文。
尊重原创作者,原译文地址在此:iOS开发——创建你自己的Framework
原文地址在此:How to Create a Framework for iOS
这里是学习记录过程,如果有大佬能不吝赐教,荣幸之至😍😍😍
文中所涉及工程资源:
1.一个可重用的旋钮(即我们学习过程中需要封装的控件),下载地址:点此 <很重要>
2.对此控件开发过程感兴趣的朋友可以移步原译文:iOS自定义控件教程:制作一个可重用的旋钮
概念补充:
库,分为静态库和动态库,本质上来说是可执行代码的集合。非独立程序,为其他程序提供服务。
- 静态库,存在两种形式.a 和.framework。在实际使用当中,两者的关系可以简单的用一个公式标明
.framework = .a + .h
- 动态库,存在.framework和.tbd两种形式。这个两种形式在系统库中很常见,苹果在 iOS8 以后为我们提供了Cocoa Touch Framework 来有条件的创建和使用动态库
创建一个静态库工程
打开 Xcode,新建一个静态库工程,如下图示
Static Library创建完工程之后,会发现自动创建了一个.h和.m 文件。为了方便Framework使用,集中在该.h 文件中导入想要公开的类,在这里无需实现.m,所以删除.m 文件。到这里,我们可在工程放入我们想要封装的工具或者控件了。
因为这个学习教程是一个封装控件作为主要工作的,所以我们要添加一些的必要的依赖,我这里添加的依赖如下如所示,实际开发中可能还需要添加别的依赖,视情况酌情添加。这里记得我们要把导入工程的文件,替换#import<Foundation/Foundation.h> 为#import<UIKit/UIKit.h>哦
依赖添加在当前的 Build Phases ,还需要添加一个Header Phase,依次选择导航栏Editor—>Add Build Phase —> Add Headers Build Phase ,
Headers Phases添加完成后,展开 Header Phase,将.h 文件拖入 public。
在这里引用原译文对 headers 里面的几个组头名进行解释,应该会帮助了解到这里的作用:
Note:在你弄清楚之前,这三个组的名称可能会让你迷惑,Public是你期望的,Private下的头文件依然是可以暴露出来的,因此名字可能有些误导。讽刺的是,在Project下的头文件对你的工程来说才是“私有”的,因此,你将会更多地希望你的头文件或者在Public下,或者在Project下。
敲黑板,在这里要注意到的是,所有导入到该.h 中的文件必须也是对外公开的,也就是导入到.h的文件也必须出现在 public这个地方。否则,导入到工程里面编译报错。
在这里我们还需要把要暴露的控件头文件拖到 public 当中哦。
创建 UI 控件
这里我们暂时只学习创建静态库,控件直接从原作的 Git Hub 上下载并将所需文件拖进来的,这里我选择了原作主要的控件类。实际开发中,可以自行添加(内心 OS:在这个静态库里开发,又看不到控件的样子!!!别着急,这么影响开发的问题原作已为我们考虑到,后面会介绍😎😎😎)
当你把这几个文件拖进来后,你就会发现,它们的头文件已经被放入 Headers 的 project 中,
光是把要暴露的头文件放在 public 中,还无法是使用者找到这个控件的,所以我们要在工程创建时我们留下的那个.h 文件中,导入头文件哦。这样,在实际使用当中,它的导入会是这个样子的
#import <ToolsFoundation/ToolsFoundation.h>
就像我们平时导入 UIKit 一样,
#import <UIKit/UIKit.h>
配置 Build Settings
这里我们需要在工程名静态库里的 Build settings 做一些事情,
这里我们搜索 public,在 Public Headers Folder Path 这个地方双击,修改内容为如下:
include/$(PROJECT_NAME)
这里在原译文中所给出的理由是这样的:
“你需要提供一个目录名,表示你将把拷贝的公共头文件存放到哪里。这样确保当你使用静态库的时候可以定位到相关头文件的位置”
我们在开发的过程中,可能存在一些实际使用时永远不会被执行的代码和debug相关内容。这里对使用者用处不大,我们可以禁用掉这些功能,仍然在 Build Setings 中,搜索以下内容,做出对应的设置
• Dead Code Stripping 设置为NO 延伸: 《Dead Code Stripping》
• Strip Debug Symbol During Copy 全部设置为NO
• Strip Style 设置为Non-Global Symbols
选择 target 设备,到这里编译运行,如果没错那是好的,如果有错误看看提示,视具体情况解决。编译成功后,可以发现之前 product 文件夹下的.a 文件名由红变黑了。点击右键在访达中显示.a文件所在位置
在这里可以看到我们之前想要公开的头文件和 .a 文件都已经生成了,怎么样是不是和我们之前用的其他的 Framework 越来越像了呢,有暴露的头文件和.a 静态库。But~这里的.framework文件呢?哈哈哈,欲知 Framework 何在且看下文。。。
创建一个联调工程
联调工程,什么鬼?就是我们又创建一个新的工程,工程里面我们会用到我们的刚才静态库文件,在这里,我们便于在静态库工程里开发,联调工程里测试。怎么样?哈哈,调试不要太方便哦\( ̄︶ ̄)/。
这里的操作步骤,我们要在刚刚静态库工程所在的文件家里同样创建一个新的开发工程哦,single view application 哦。关闭之前的静态库工程,把静态库文件夹中.xcodeproj工程文件拖到开发工程。
note:这里需要注意的是这个静态库工程拖入进来后就算是打开了,这个工程无法在别的地方开启了。
这里面,先删除了默认创建的 viewcontroller的.h、.m 和 main.storyboard 文件。从下载的控件文件夹中,直接把原来RWViewController.h、RWViewController.m 和 mian.storyboard 拖进来。(当然我这里是偷懒步骤,实际开发中可以结合自己要开发的工程进行测试)。拖进来之后,在下图位置中添加控件的静态库文件。
在你所需要测试的文件中import 静态库暴露的头文件,现在可以这样导入了
惊不惊喜,意不意外,看着有点样子了,哈哈哈哈哈
创建 Framework
Framework 本身的结构就是静态库加一组头文件。你可能觉得已经创建好,毕竟在 .a 文件所在文件中你看到了 include 的里面已经有了一组头文件。
这里还需要记录 Framework 的两个特点:
1. 目录结构。Framework有一个能被Xcode识别的特殊的目录结构,你将会创建一个build task,由它来为你创建这种结构。
2. 片段(Slice)。目前为止,当你构建库时,仅仅考虑到当前需要的结构(architecture)。例如,i386、arm7等,为了让一个framework更有用,对于每一个运行framework的结构,该framework都需要构建这种结构。一会你就会创建一个新的工程,构建所有需要的结构,并将它们包含到framework中。
一下是一个来自原文的 Framework 结构表
我们可以通过添加一段脚本,来创建这种结构。添加方法,看图👈👈👈
image看清楚了嘛,红色框选位置1,没错就是我们要开发的静态库工程文件,点它喵的,然后target控件静态库Build Phases,点它喵的。按照图示步骤来,都 👌的,我这里之所以出现不可选中,是因为我已经点了创建了,嘻嘻~创建好之后我们把它的名字Run Script改为 Build Framework。然后把如下代码贴进去:
set -e
export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework"
# Create the path to the real Headers die
mkdir -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"
现在选择静态库工程,选择iOS device,Build 一下。在静态库工程的 product 里面找到生成的.a文件。右键在 finder 里面显示看看
image在这里是不是看着离目标一步一步接近了?哈哈哈,这里我们已经完成大部分的工作了。然鹅,这里还没有静态 lib 文件。这就是下一步我们将要做的
多架构支持
简单的说 app 可以运行在不同架构的设备上,那我们的 Framework 也要做到支持多架构哦。
arm7: 在最老的支持iOS7的设备上使用
arm7s: 在iPhone5和5C上使用
arm64: 运行于iPhone5S的64位 ARM 处理器 上
i386: 32位模拟器上使用
x86_64: 64为模拟器上使用
下面的一段 js 代码将帮助我们实现多架构的支持, 并将模拟器和真机的framework合并到一起了,详细原理,我还没吃透(看不懂。。。)所以先上步骤。为了方便向 run script统一添加将每个步骤的代码放到一起了,不过没关系,代码中有各个步骤英文注解
1. 你可以用我们上文用到的开发工程(联调工程),也可以用静态库工程。去在 target 里面新建一个 iOS/Framework。在这里我直接在开发工程里面创建了,以下是在开发工程(联调工程)里操作的一些步骤:
如上图所示,点击 target 下的小加号,选择创建一个 Framework 工程,在新建的库工程的 Build phases 里面添加依赖。这个依赖就是我们之前生成.a静态库。
接下来,仍是在 Build phases 中添加一段脚本语言,进行到这一步,真是完全懵逼过来的,那些脚本看不懂啊(╥╯^╰╥),只能复制粘贴了。添加脚本的方法仍与前文提到的相同,不再赘述。仍旧是双击 run script ,改名字为MultiPlatform Build。
添加如下脚本代码:
set -e
# 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"
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}"
}
# 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
# 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}"
# 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 是否真的支持多架构。
打开终端,cd 这个 .framework 键入一下命令
lipo -info xxx.framework
lipo.png
得到如上信息,表明成功了!(づ。◕‿‿◕。)づ
实践是检验真理的唯一标准,接下来你可以自己创建一个新的工程,去把这个生成的.framework导入工程中,然后测试一下吧!