浅析.framework的生成

起因

最近项目需要将在独立仓库生成的.framework集成进来,同时又希望是static framwork,原因是动态framework在启动时链接会消耗启动时间,这是我们不想看到的。而我们自己独立仓库使用的cocoapods管理,使用cocoapods-packager打出来的framework没有将Resources单独分开,导致带资源bundle的framework被静态链接时会被Testflight打回(正常编译没问题)。
所以我们要做的就是将cocoapods-packager生成的static framework中资源剥离开单独在Build Phases中设置。当然,从生成framwork,到剥离Resources,再到集成项目设置都是在我们CI中自动化实现。

framework是什么

要把Resources从framework中拆分出来,要先弄清楚framework是什么。从苹果官方文档可以找到答案。

A framework is a hierarchical directory that encapsulates shared resources, such as a dynamic shared library, nib files, image files, localized strings, header files, and reference documentation in a single package. Multiple applications can use all of these resources simultaneously. The system loads them into memory as needed and shares the one copy of the resource among all applications whenever possible.

简单来说,一个framework就是一个多层目录,包含代码(library),头文件(header files)和资源(nib, image, localized strings)。
从苹果的文档中也可以看到,framework的层级大致如下。其中MyFramework和Resources并非实体目录,而知识一个symbol link,连接到Versions/Current下面的MyFramework和Resources。Current也是一个symbol link,链接到A。

1
2
3
4
5
6
7
8
9
10
11
12

MyFramework.framework/
MyFramework -> Versions/Current/MyFramework
Resources -> Versions/Current/Resources
Versions/
A/
MyFramework
Resources/
English.lproj/
InfoPlist.strings
Info.plist
Current -> A

上述内容可以通过命令验证,我们新建了一个framework,通过ls -l可以看到结果。

alt text

alt text

至于为什么要搞这么复杂,需要link到Current再link到A,可以自行参考文末文档。

cocoapods-packager怎么做的

知道了framework只是一个目录,那把Resources直接从framework中移动出来应该就能解决问题。但总有些不放心,所以我们再看下cocoapods-packager是怎么生成framwork的。

cocoapods-packager生成framework大致分以下几个步骤。

  1. 编译代码,这是为了检验代码正确性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def compile
defines = "GCC_PREPROCESSOR_DEFINITIONS='$(inherited) PodsDummy_Pods_#{@spec.name}=PodsDummy_PodPackage_#{@spec.name}'"
defines << ' ' << @spec.consumer(@platform).compiler_flags.join(' ')

if @platform.name == :ios
options = ios_build_options
end

xcodebuild(defines, options)

if @mangle
return build_with_mangling(options)
end

defines
end
  1. 创建framwork目录,以及Headers, Resources等子目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def create_framework
@fwk = Framework::Tree.new(@spec.name, @platform.name.to_s, @embedded)
@fwk.make
end

def make
make_root
make_framework
make_headers
make_resources
make_current_version
end

def make_current_version
current_version_path = @versions_path + Pathname.new('../Current')
`ln -sf A #{current_version_path}`
`ln -sf Versions/Current/Headers #{@fwk_path}/`
`ln -sf Versions/Current/Resources #{@fwk_path}/`
`ln -sf Versions/Current/#{@name} #{@fwk_path}/`
end

值得一提的是,创建完目录有个link的过程,是通过ls -sf src target来创建symbol link,这点和我们之前说到的一致。

  1. 生成静态library
1
2
3
4
5
6
7
8
9
10
def build_static_library_for_ios(output)
static_libs = static_libs_in_sandbox('build') + static_libs_in_sandbox('build-sim') + vendored_libraries
libs = ios_architectures.map do |arch|
library = "#{@static_sandbox_root}/build/package-#{arch}.a"
`libtool -arch_only #{arch} -static -o #{library} #{static_libs.join(' ')}`
library
end

`lipo -create -output #{output} #{libs.join(' ')}`
end

先单独对每种架构生成.a,再将所有合并生成一个universal library。

  1. 拷贝头文件,license和资源
1
2
3
4
5
6
7
def build_static_framework
# ....

copy_headers
copy_license
copy_resources
end

这里做的就是将之前编译产物里的头文件,license和资源拷贝到framework目录下,代码就不赘述。

资源分离方案

通过苹果官方文档和对cocoapods-packager源码的分析,我们可以确定framework其实就是一种特殊的目录结构,所以想要达到我们的目的,将资源从中分离出来,要做的其实就是文件和目录的操作。

  1. 删除framework根目录中Resources的symbol link
1
rm xxx.framework/Resources
  1. 移动Versions/A/Resources里面的内容,并且删除Versions/A/Resources目录
1
2
3
4
5
for filename in os.listdir(resource_dir):
file_path = os.path.join(resource_dir, filename)
LOG.info("move file: {} to target dir: {}".format(file_path, target_path))
shutil.move(file_path, target_path)
shutil.rmtree(resource_dir)

参考文档

What are Frameworks