Carthage将第三方库编译成静态库framework和xcframework教程

1,512 阅读5分钟

背景

这篇文章默认读者是对Carthage有了一定的了解, 如果不了解, 请查看我的前一篇文章 Carthage安装、使用及常见问题解决教程

为什么选择静态库

Carthage默认只能编译成动态库, 但是我们还是选择用静态库,因为如果注入太多动态库,App的启动时间就会变慢,也就是main函数之前动态链接的过程变久了。因此我们这里用Carthage来把动态库编译成静态库的方式解决这个问题。

如果全部是静态库,那么都会被链接到App包里面,这样看起来包会增大,还有一个方案就是,把多个静态库通过一个动态库的工程包裹,包成一个动态库进行链接,这样静态库不会被打入包里面,而且也不会有太多的启动耗时。这里需要注意的是,也不是和无脑合并使用动态库,Apple给我们建议是最多6个动态库是最合适的。

关于静态库合并成动态库需要注意以下:

直接起一个动态库的project,然后把我们需要组合的静态库都拖入Link Binary with Libraries中,为了保证所有的静态库都会被合并,需要在Build Settings的OTHER_LDFLAGS中加入-all-load参数,强制链接所有静态库的全部内容,而不是有时候只链接合并其中一部分而已。

xcframework和framework的区别

xcframework是苹果官⽅推荐的、⽀持的,可以更⽅便的表示⼀个多个平台和架构的分发⼆进制库的格式。需要Xcode11以上⽀持。是为更好的⽀持Mac Catalyst(用于 iPad)和ARM芯⽚的macOS。专⻔在2019年提出的framework的另⼀种先进格式。

XCFramework和传统的framework相⽐的三个优点

1 可以⽤单个.xcframework⽂件提供多个平台的分发⼆进制⽂件。

2 与Fat Header相⽐,可以按照平台划分,可以包含相同架构的不同平台的⽂件。

3 在使⽤时,不需要再通过脚本去剥离不需要的架构体系。

xcframework的目录如下, 里面包含了各个架构对应使用的framework

操作流程

1 创建一个App类型的项目

快捷键command + shift + n, 选择App类型

2 引入第三方库

cd 到项目根目录, 执行下面命令生成Cartfile文件

touch Cartfile

然后执行下面命令打开Cartfile文件

open Cartfile

在Cartfile文件中添加依赖的第三方库

github "SnapKit/SnapKit" ~> 5.0.0

github "scalessec/Toast-Swift" ~> 5.0.1

github "Moya/Moya" ~> 15.0

然后执行下面命令checkout下第三方库的源码

carthage update --no-use-binaries --platform iOS --no-build

该脚本和之前的不同,跟了三个参数,分别代表不用远程编译的预编译二进制,只用本地的源码编译,第二个参数代表架构,第三个代表不进行编译,只要把源码check out即可,编译我们等下用自己的脚本进行编译,这里如果直接编译,那就是默认的动态库

3 编写编译脚本, 生成framework

在项目根目录下执行下面命令, 生成一个脚本文件

touch build-static-carthage.sh

然后选择用文本编辑器或者Xcode打开, 添加以下代码

#!/bin/sh -e

# 用法:
#
# chmod +x ./build-static-carthage
# ./build-static-carthage -d SnapKit Moya -p ios

XCCONFIG=""

# Platform
PLATFORM_NAME=""

# framework search path
FMWK_SEARCH_PATHS="$(inherited) "
FMWK_SEARCH_PATHS+="./Carthage/Build/iOS/**"

# Dependencies
DEPENDENCIES=""

# Current flag cursor
CURRENT_FLAG=""

function ValidatePlatformName() {
    case "$1" in
    "ios" | "macos" | "tvos" | "watchos")
        echo "$1"
        ;;
    *)
        echo "$1"
        
        # 1代表脚本执行错误
        return 1
        ;;
    esac
}

function initConfig() {
    set -euo pipefail
    
    # mktemp /tmp下创建唯一的临时xcconfig
    XCCONFIG=$(mktemp /tmp/static.xcconfig.XXXXXX)

    # 清理临时xcconfig当 Interrupt, Hang Up, Terminate, Exit signals
    trap 'rm -f "$XCCONFIG"' INT TERM HUP EXIT

}

function chekcArgvs() {

    # 遍历参数数量 shift移除 -d SnapKit Moya -p ios  $# = 6
    while [ ! $# -eq 0 ]; do

        case "$1" in

        --platform | -p)

            CURRENT_FLAG="p"
            ;;

        --dependencies | -d)

            CURRENT_FLAG="d"
            ;;

        *)

            if [[ $CURRENT_FLAG == "p" ]]; then

                if [[ $PLATFORM_NAME == "" ]]; then

                    PLATFORM_NAME+=$(ValidatePlatformName $1)
                    
                    # 如果返回1 代表platform无效
                    if [ $? -eq 1 ]; then
                        echo "'$PLATFORM_NAME' ==> 无效的Platform" >&2
                        exit $?
                    fi
                    
                # 有值 只能指定单个platform
                else
                    echo "Platform参数过多,只能指定单个之一 ios、macos、watchos、tvos" >&2
                    exit 1
                fi

            elif [[ $CURRENT_FLAG == "d" ]]; then

                DEPENDENCIES+="$1 "

            else
                echo "'$1' 参数错误,请重新指定" >&2
                exit 1
            fi
            ;;
        esac
        shift
    done

    # check dependencies
    if [[ $DEPENDENCIES == "" ]]; then
        echo "请传入指定的依赖进行编译" >&2
        exit 1

    fi

    # check platform
    if [[ $PLATFORM_NAME == "" ]]; then
        echo "缺少指定平台参数" >&2
        exit 1
    fi
}

function writeConfig() {

    # 把以下属性写入临时的xcconfig文件进行临时依赖
    echo "MACH_O_TYPE = staticlib" >>$XCCONFIG

    echo "DEBUG_INFORMATION_FORMAT = dwarf" >>$XCCONFIG

    echo "FRAMEWORK_SEARCH_PATHS = $FMWK_SEARCH_PATHS" >>$XCCONFIG
    
    echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_12B45b = arm64 arm64e armv7 armv7s armv6 armv8' >>$XCCONFIG

    echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200 = $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_$(XCODE_PRODUCT_BUILD_VERSION))' >>$XCCONFIG

    echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >>$XCCONFIG

    echo 'ONLY_ACTIVE_ARCH=NO' >>$XCCONFIG

    echo 'VALID_ARCHS = $(inherited) x86_64' >>$XCCONFIG
    
    # 排除xcode12生成的模拟器含有arm64架构,不然合成模拟器和真机版本会报错
    echo 'EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64' >>$XCCONFIG
}

function exportConfig() {

    # 导出XCODE_XCCONFIG_FILE 共享给其他shell使用
    export XCODE_XCCONFIG_FILE="$XCCONFIG"
}

function build() {
    echo
    echo "Building static frameworks with Xcode temporary xconfig file:"
    echo $XCCONFIG
    echo
    echo "With contents:"
    while read line; do
        echo "$line"
    done <$XCCONFIG
    echo
    echo "Building with command:"
    echo "carthage build --no-use-binaries --platform $PLATFORM_NAME $DEPENDENCIES"
    echo

    # Build Carthage
    carthage build --no-use-binaries --platform $PLATFORM_NAME $DEPENDENCIES
}

initConfig

chekcArgvs $@

writeConfig

exportConfig

build

注: 此脚本同时处理了Xcode12+在编译模拟器架构时, 生成了arm64架构, 导致framework合成错误的问题

然后执行以下命令修复权限, 不然会报错 zsh: permission denied: ./build-static-carthage.sh

chmod +x ./build-static-carthage.sh

然后执行以下命令, 生成静态库framework

./build-static-carthage.sh -p ios -d SnapKit Toast-Swift Moya

生成的静态库framework目录如下

4 编写编译脚本, 生成xcframework

脚本跟生成framework的脚本类似, 不过最后调用的是carthage build --no-use-binaries --use-xcframeworks, 具体脚本如下

#!/bin/sh -e

# 用法:
#
# chmod +x ./build-static-carthage-xcframework
# ./build-static-carthage-xcframework -d SnapKit Moya -p ios

XCCONFIG=""

# Platform
PLATFORM_NAME=""

# framework search path
FMWK_SEARCH_PATHS="$(inherited) "
FMWK_SEARCH_PATHS+="./Carthage/Build/iOS/**"

# Dependencies
DEPENDENCIES=""

# Current flag cursor
CURRENT_FLAG=""

function ValidatePlatformName() {
    case "$1" in
    "ios" | "macos" | "tvos" | "watchos")
        echo "$1"
        ;;
    *)
        echo "$1"
        
        # 1代表脚本执行错误
        return 1
        ;;
    esac
}

function initConfig() {
    set -euo pipefail
    
    # mktemp /tmp下创建唯一的临时xcconfig
    XCCONFIG=$(mktemp /tmp/static.xcconfig.XXXXXX)

    # 清理临时xcconfig当 Interrupt, Hang Up, Terminate, Exit signals
    trap 'rm -f "$XCCONFIG"' INT TERM HUP EXIT

}

function chekcArgvs() {

    # 遍历参数数量 shift移除 -d SnapKit Moya -p ios  $# = 6
    while [ ! $# -eq 0 ]; do

        case "$1" in

        --platform | -p)

            CURRENT_FLAG="p"
            ;;

        --dependencies | -d)

            CURRENT_FLAG="d"
            ;;

        *)

            if [[ $CURRENT_FLAG == "p" ]]; then

                if [[ $PLATFORM_NAME == "" ]]; then

                    PLATFORM_NAME+=$(ValidatePlatformName $1)
                    
                    # 如果返回1 代表platform无效
                    if [ $? -eq 1 ]; then
                        echo "'$PLATFORM_NAME' ==> 无效的Platform" >&2
                        exit $?
                    fi
                    
                # 有值 只能指定单个platform
                else
                    echo "Platform参数过多,只能指定单个之一 ios、macos、watchos、tvos" >&2
                    exit 1
                fi

            elif [[ $CURRENT_FLAG == "d" ]]; then

                DEPENDENCIES+="$1 "

            else
                echo "'$1' 参数错误,请重新指定" >&2
                exit 1
            fi
            ;;
        esac
        shift
    done

    # check dependencies
    if [[ $DEPENDENCIES == "" ]]; then
        echo "请传入指定的依赖进行编译" >&2
        exit 1

    fi

    # check platform
    if [[ $PLATFORM_NAME == "" ]]; then
        echo "缺少指定平台参数" >&2
        exit 1
    fi
}

function writeConfig() {

    # 把以下属性写入临时的xcconfig文件进行临时依赖
    echo "MACH_O_TYPE = staticlib" >>$XCCONFIG

    echo "DEBUG_INFORMATION_FORMAT = dwarf" >>$XCCONFIG

    echo "FRAMEWORK_SEARCH_PATHS = $FMWK_SEARCH_PATHS" >>$XCCONFIG
    
    echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_12B45b = arm64 arm64e armv7 armv7s armv6 armv8' >>$XCCONFIG

    echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200 = $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_$(XCODE_PRODUCT_BUILD_VERSION))' >>$XCCONFIG

    echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >>$XCCONFIG

    echo 'ONLY_ACTIVE_ARCH=NO' >>$XCCONFIG

    echo 'VALID_ARCHS = $(inherited) x86_64' >>$XCCONFIG
    
    # 排除xcode12生成的模拟器含有arm64架构,不然合成模拟器和真机版本会报错
    echo 'EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64' >>$XCCONFIG
}

function exportConfig() {

    # 导出XCODE_XCCONFIG_FILE 共享给其他shell使用
    export XCODE_XCCONFIG_FILE="$XCCONFIG"
}

function build() {
    echo
    echo "Building static frameworks with Xcode temporary xconfig file:"
    echo $XCCONFIG
    echo
    echo "With contents:"
    while read line; do
        echo "$line"
    done <$XCCONFIG
    echo
    echo "Building with command:"
    echo "carthage build --no-use-binaries --use-xcframeworks --platform $PLATFORM_NAME $DEPENDENCIES"
    echo

    # Build Carthage framework
    # carthage build --no-use-binaries --platform $PLATFORM_NAME $DEPENDENCIES
    
    # Build Carthage xcframework
    carthage build --no-use-binaries --use-xcframeworks --platform $PLATFORM_NAME $DEPENDENCIES
}

initConfig

chekcArgvs $@

writeConfig

exportConfig

build

注: 此脚本同时处理了Xcode12+在编译模拟器架构时, 生成了arm64架构, 导致framework合成错误的问题

将此脚本命名为build-static-carthage-xcframework.sh, 然后执行

./build-static-carthage-xcframework.sh -p ios -d SnapKit Toast-Swift Moya

生成的静态库xcframework目录如下

5 校验framework/xcframework是否为静态库

通过下面的命令查看framework是否为静态库

file framework/xcframework下的二进制文件, 比如

file /Users/ysb/Desktop/FrameworkDemo/GPKitDemo/Carthage/Build/iOS/Static/Moya.framework/Moya

输出结果如下, 证明生成静态库framework成功

file /Users/darren/Desktop/FrameworkDemo/GPKitDemo/Carthage/Build/Moya.xcframework/ios-arm64_armv7/Moya.framework/Moya

输出结果如下, 证明生成静态库xcframework成功

如果含有 current ar archive random library字样, 则是静态库

如果含有 dynamically linked shared library字样, 则是动态库

6 framework/xcframework使用

将要使用的framew/xcframework拖进工程, 点击copy

然后需要使用的地方import xxx就可以, 下面以导入SnapKit.xcframework和ToastSwiftFramework.framework为例测试

注, 如果要用模拟器运行, 还需要设置测试工程, 去除模拟器编译的arm64架构, 不然会报错Could not find module 'SnapKit' for target 'arm64-apple-ios-simulator'

最后模拟器和真机都能成功运行