Pod私有库二进制化

在制作 Pod私有库 之后,接下来我们在此基础上实现 Pod私有库的二进制化,什么是二进制化?其实通过CocosPods导入的我们制作的Pod私有库,实际是源码,会随着主工程一起编译,二进制化指的是通过编译把组件的源码转换成静态库或动态库,以提高该组件在App项目中的编译速度。打开 Pod 私有库示例工程,实现以下步骤:

1、添加 Static Library

  • 在 xcode 中创建新 Target -> YXPlayerSDKBinary:
    1
    YXPlayerSDK->New->Target->iOS->Framework & Library->Cocoa Touch Static Library

    如果项目最低支持到iOS8可以创建 Dynamic Framework。

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    YXPlayerSDK
    ├── Assets
    │   ├── YXPlayerSDKBinary.bundle
    │   │   ├── Info.plist
    │   │   └── playerPause@2x.png
    │   └── playerPause@2x.png
    ├── Classes
       ├── TestOne
       │   ├── TestViewController.h
       │   └── TestViewController.m
       └── TestTwo
       ├── DoubboViewController.h
       └── DoubboViewController.m
       ||
       || YXPlayerSDK 中 Classes 文件里的文件拖到 YXPlayerSDKBinary 中(link源码而不是复制)
       ||
    YXPlayerSDKBinary
    ├── TestOne
    │   ├── TestViewController.h
    │   └── TestViewController.m
    └── TestTwo
    ├── DoubboViewController.h
    └── DoubboViewController.m
  • 配置 YXPlayerSDKBinary

    在 YXPlayerSDKBinary Target 中的 Build Phase 界面:

    1、选择Editor\Add Build Phase\Add Copy Headers Build Phase

    2、在 Compile Sources 和 Headers 中添加拖进的文件

  • 设置 YXPlayerSDKBinary Target 中的 Build Settings 界面:

    1、 iOS Deployment Target 选择和YXPlayerSDK.podspec 中的 s.platform 保持一致

    2、Public Headers Folder Path 中配置为 include/YXPlayerSDKBinary

    3、禁掉无效代码和debug用符号:

    • Dead Code Stripping设置为NO
    • Strip Debug Symbol During Copy 全部设置为NO
    • Strip Style设置为Non-Global Symbols

    完成上述配置,选择目标为iOS Device,按下command + B进行编译,一旦成功,工程导航栏中Product目录下 libYXPlayerSDKBinary.a 文件将从红色变为黑色,表明现在该文件已经存在了。右键单击 libYXPlayerSDKBinary.a,选择 Show in Finder。在此目录下,你将看到静态库 libRWUIControls.a 以及定为 public 的头文件在此也可看到。

2、将 Static Library 构建为 Framework

  • 创建 framework 目录结构
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    YXPlayerSDKBinary.framework
    ├── Headers -> Versions/Current/Headers
    ├── Versions
    │   ├── A
    │   │   ├── Headers
    │   │   │   ├── DoubboViewController.h
    │   │   │   └── TestViewController.h
    │   │   └── YXPlayerSDKBinary
    │   └── Current -> A
    └── YXPlayerSDKBinary -> Versions/Current/YXPlayerSDKBinary

    在静态库构建过程中添加脚本来创建这种结构,在项目导航栏中选择 YXPlayerSDK,然后选择 YXPlayerSDKBinary 静态库目标,选择 Build Phases 栏,然后选择 Editor/Add Build Phase/Add Run Script Build Phase 来添加一个新的脚本在最后命名为 Build Framework:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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"
    # Copy the public headers into the private pod/include
    /bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \
    "${SRCROOT}/../${PROJECT_NAME}/Products/include"

    这个脚本首先创建了YXPlayerSDKBinary.framework/Versions/A/Headers目录,然后创建了一个framework所需要的三个连接符号(symbolic links)。

    • Versions/Current => A
    • Headers => Versions/Current/Headers
    • YXPlayerSDKBinary => Versions/Current/YXPlayerSDKBinary

    最后,将公共头文件从你之前定义的公共头文件路径拷贝到Versions/A/Headers目录下,-a参数确保修饰次数作为拷贝的一部分不会改变,防止不必要的重新编译。

  • 多架构编译

    每个CPU架构都需要不同的二进制数据,为了让创建的 framework 能在所有可能的架构上运行。就需要创建了二进制FAT(File Allocation Table,文件配置表),它包含了所有架构的片段(slice)。构建过程:

    1、创建新的 Target 构建 framework:

    1
    YXPlayerSDK->New->Target->Cross-platform->Other->Aggregate

    找到 Aggregate,点击Next,将目标命名为 Framework。为什么使用集合(Aggregate)目标来创建一个framework呢?为什么这么不直接?因为OS X对库的支持更好一些,事实上,Xcode直接为每一个OS X工程提供一个Cocoa Framework 编译目标。基于此,你将使用集合编译目标,作为Bash脚本的连接串来创建神奇的framework目录结构。

    2、在库工程中选择Framework目标,在Build Phases中添加一个依赖。展开Target Dependencies面板,点击 + 按钮选择 YXPlayerSDKBinary 静态库。

    3、在 Framework 静态库目标下,选择Build Phases栏,然后选择Editor/Add Build Phase/Add Run Script Build Phase来添加一个新的脚本在最后命名为 MultiPlatform Build:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    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="YXPlayerSDKBinary"
    RW_INPUT_STATIC_LIB="lib${RW_FRAMEWORK_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}"
    }
    # 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
    # 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
    # Determine the other platform
    if [ "$RW_SDK_PLATFORM" == "iphoneos" ]; then
    RW_OTHER_PLATFORM=iphonesimulator
    else
    RW_OTHER_PLATFORM=iphoneos
    fi
    # 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
    echo "input_1 : ${BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}"
    echo "input_2 : ${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}"
    echo "output : ${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}"
    # 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 platorm'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 lib into the private pod/lib
    ditto "${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework/Versions/A/${RW_FRAMEWORK_NAME}" \
    "${SRCROOT}/../${PROJECT_NAME}/Products/lib/${RW_INPUT_STATIC_LIB}"
    # Copy the framework to the pod/framework
    ditto "${RW_FRAMEWORK_LOCATION}" \
    "${SRCROOT}/../${PROJECT_NAME}/Products/Framework/${RW_F
    RAMEWORK_NAME}.framework"
    # Copy the resources bundle to pod/Assets
    ditto "${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.bundle" \
    "${SRCROOT}/../${PROJECT_NAME}/Assets/${RW_FRAMEWORK_NAME}.bundle"

    从脚本上最后可以看出,我们把构建出来的二进制文件放在 YXPlayerSDK 组件文件下的Products 文件中,所以在构建之前,我们先在 YXPlayerSDK 组件文件下增添一个 Products 文件存放 .a 和 .frameworky 以及它们的头文件,目录结构如下:

    1
    2
    3
    4
    5
    6
    7
    YXPlayerSDK
    ├── Assets
    ├── Classes
    └── Products
    ├── Framework #存放 Framework 文件
    ├── include #存放相关头文件
    └── lib #存放 .a 文件

    选择Framework集合方案(aggregate scheme),按下cmd+B编译该framework。构建完成之后,就能看到

    Products 文件夹下添加完之后的目录结构:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Products
    ├── Framework
    │   └── YXPlayerSDKBinary.framework
    │   ├── Headers -> Versions/Current/Headers
    │   ├── Versions
    │   │   ├── A
    │   │   │   ├── Headers
    │   │   │   │   ├── DoubboViewController.h
    │   │   │   │   └── TestViewController.h
    │   │   │   └── YXPlayerSDKBinary
    │   │   └── Current -> A
    │   └── YXPlayerSDKBinary -> Versions/Current/YXPlayerSDKBinary
    ├── include
    │   ├── DoubboViewController.h
    │   └── TestViewController.h
    └── lib
    └── libYXPlayerSDKBinary.a

3、打包 Bundle 资源

打开 YXPlayerSDK工程,点击Add Target按钮添加新的 target,导航到OS X/Framework and Library/Bundle。将新的Bundle命名为YXPlayerSDKResources,这里需要配置几个编译设置:

  • 因为正在创建一个在iOS上使用的bundle,这与默认的OS X不同。选择YXPlayerSDKResources目标,然后点击Build Settings栏,搜索base sdk,选择Base SDK这一行,按下delete键,这一步将OS X切换为iOS。

  • 同时你需要将工程名称改为YXPlayerSDKResources。搜索product name,双击进入编辑模式,将${TARGET_NAME}替换为YXPlayerSDKResources。

  • 默认情况下,有两种resolutions的图片可以产生一些有趣的现象。例如,当导入一个retina @2x版本的图片时,普通版的和Retina版的将会合并成一个多resolution的TIFF(标签图像文件格式,Tagged Image File Format)。这不是一件好事。搜索hidpi将COMBINE_HIDPI_IMAGES设置为NO。

  • 确保编译framework时,bundle也能被编译并将framework作为依赖添加到集体目标中。选中Framework目标,选择Build Phases栏,展开Target Dependencies面板,点击 + 按钮,选择RWUIControlsResources目标将其添加为依赖。

  • 在Framework目标的Build Phases中,打开MultiPlatform Build面板,在脚本的最后添加下述代码,这条指令将拷贝构建好的bundle指定位置:

    1
    2
    3
    # Copy the resources bundle to pod/Assets
    ditto "${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.bundle" \
    "${SRCROOT}/../${PROJECT_NAME}/Assets/${RW_FRAMEWORK_NAME}.bundle"

4、解决二进制化的依赖

使用源码的话,依赖第三方库或私有库,在 podspec 文件添加 s.dependency 就行,编译的时候 CocoaPods 会自己做好依赖的 link,但二进制化,是我们自己先编译好 .a 或 .framework,这种依赖怎么做呢?

1、在 YXPlayerSDK 组件工程里的 Podfile 添加如下:
1
2
3
4
target 'YXPlayerSDKBinary' do
pod '三方库'
pod '私有库'
end
2、在 podspec 文件添加如下,二进制化如果使用的是 .a 就是设置 vendored_libraries ,是 .framework 就设置 vendored_frameworks,如下:
1
2
3
4
5
6
7
8
9
//使用 .a
s.ios.vendored_libraries = 'YXPlayerSDK/Products/lib/*{a}'
s.dependency '三方库'
s.dependency '私有库
//使用 .framework
s.ios.vendored_frameworks = 'YXPlayerSDK/Products/Framework/*{framework}'
s.dependency '三方库'
s.dependency '私有库

注意一下,Podfile 里面依赖库的要和 podspec 依赖的库保持一致。

3、为了方便调试,加入环境变量 IS_SOURCE 实现Pod私有库源码和二进制的其换,完整的 YXPlayerSDK.podspec 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Pod::Spec.new do |s|
s.name = 'YXPlayerSDK'
s.version = '0.1.6'
s.summary = 'YXPlayerSDK'
s.description = <<-DESC
YXPlayerSDK.
DESC
s.homepage = 'http://git.jc/elephant-ios-component/YXPlayerSDK.git'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'muhuashanjin' => 'tanghy@gold-finance.com.cn' }
s.source = { :git => "http://git.jc/elephant-ios-component/YXPlayerSDK.git", :tag => "0.1.6" }
s.ios.deployment_target = '8.0'
if ENV['IS_SOURCE']
#------------ source code -------------
s.resource_bundles = {'YXPlayerSDK' => ['YXPlayerSDK/Assets/*.png']}
s.public_header_files = 'YXPlayerSDK/Classes/**/*.h'
s.source_files = 'YXPlayerSDK/Classes/**/*.{h,m}'
s.dependency 'PLPlayerKit','~> 2.4.3'
s.dependency 'SVProgressHUD'
s.dependency 'YXBase', '~> 0.1.9'
else
#------------ compile code -------------
s.resource_bundles = {'YXPlayerSDK' => ['YXPlayerSDK/Assets/YXPlayerSDKBinary.bundle/*.png']}
s.source_files = 'YXPlayerSDK/Products/include/**'
s.public_header_files = 'YXPlayerSDK/Products/include/*.h'
s.ios.vendored_libraries = 'YXPlayerSDK/Products/lib/*{a}'
# s.ios.vendored_frameworks = 'YXPlayerSDK/Products/Framework/*{framework}'
s.dependency 'PLPlayerKit','~> 2.4.3'
s.dependency 'SVProgressHUD'
s.dependency 'YXBase', '~> 0.1.9'
end
#------------ subspec -------------
#s.subspec 'TestOne' do |testOne|
# testOne.source_files = 'YXPlayerSDK/Classes/testOne/*'
# testOne.public_header_files = 'YXPlayerSDK/Classes/testOne/**/*.h'
# testOne.dependency 'AFNetworking', '~> 3.1.0'
#end
#s.subspec 'TestTwo' do |testTwo|
# testTwo.source_files = 'YXPlayerSDK/Classes/testTwo/*'
# testTwo.public_header_files = 'YXPlayerSDK/Classes/testTwo/**/*.h'
# testTwo.dependency 'YXPlayerSDK/TestOne'
#end
end

使用源码编译命令 :IS_SOURCE=1 pod install

使用二进制编译命令:pod install

4、配置完依赖库后,执行 pod install 加载依赖库,然后按下cmd+B 重新编译该framework,可能会遇到以下报错:
  • 1
    pod error: The 'xxxx ' target has transitive dependencies that include static binaries ...

    这是因为 Podfile 中加上 use_frameworks!,开启这个选项之后,所有以源码引入的pod都会编译成动态链接库,而如果依赖的Pod里面的库包含静态库,这样就造成 pod install 时会把静态库编译到App里面,源码编译成的动态库没法依赖它,出现上面的错误。

    解决方法:Pod私有库不以源码导入,而是二进制化后导入,再 pod install。

  • 1
    2
    build error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/libtool: can't locate file for: -lPods-YXPlayerSDKBinary
    build error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/libtool: file: -lPods-YXPlayerSDKBinary is not an object file (not allowed in a library)

    这是因为执行 pod install ,整个项目按照先编译被依赖Pod,然后依赖其他Pod的Pod也被构建出来,最终所有的组件被编译为一个lib-Pods-XXX.a 被添加进项目进去。

    解决方法:去掉 Build Phases 的 Link Binary With Libraries 中的 libPods-YXPlayerSDKBinary.a,然后重新编译。

5、调试二进制化的Pod私有库

接下来就可以在组件示例工程中引入调试,先本地引入调试,在 YXPlayerSDK 的 Podfile 中添加如下:

1
2
3
target 'YXPlayerSDK_Example' do
pod 'YXPlayerSDK', :path => '../'
end

运行成功之后,就可以推送到git仓库,打上tag,然后验证Pod私有库,推送到远程私有Spec Repo,再引入调试:

1
2
3
target 'YXPlayerSDK_Example' do
pod 'YXPlayerSDK', '~> 0.1.5'
end

完成之后,脱离组件示例工程再新建一个项目Demo,pod 引入私有库,注意几个问题:

  • 私有库二进制化后,Podfile 中需要开启 use_frameworks!,不然报错如下:
1
2
3
ld: warning: directory not found for option '-F/Users/thy/Desktop/Demo/Pods/YXPlayerSDK/YXPlayerSDK/Products/Framework'
ld: framework not found xxxxxx
clang: error: linker command failed with exit code 1 (use -v to see invocation)
  • Pod私有库,二进制化使用的是 framework, 如果编译项 OTHER_LDFLAGS 中为 -l”YXPlayerSDKBinary” ,需要改为-framework “YXPlayerSDKBinary”,反之亦然,不然报错如下:
1
2
3
4
5
6
7
8
9
#改为 -framework "YXPlayerSDKBinary"
ld: warning: directory not found for option '-F/Users/thy/Desktop/Demo/Pods/YXPlayerSDK/YXPlayerSDK/Products/Framework'
ld: library not found for -lYXPlayerSDKBinary
clang: error: linker command failed with exit code 1 (use -v to see invocation)
#改为 -l"YXPlayerSDKBinary"
ld: warning: directory not found for option '-F/Users/thy/Desktop/Demo/Pods/YXPlayerSDK/YXPlayerSDK/Products/Framework'
ld: framework not found YXPlayerSDKBinary
clang: error: linker command failed with exit code 1 (use -v to see invocation)

示范例代码:Private-Pod-Example

参考

iOS开发—创建你自己的Framework

CocoaPods组件平滑二进制化解决方案

Objective-C和Swift混编的一些经验