第50章:iOS编译:使用CMake编译iOS静态库/动态库

说实话,跨平台开发里最让人头疼的就是iOS编译。Android那边好歹有NDK撑着,iOS这边呢?Xcode一把梭,CMake好像插不上手。但我在做移动端SDK的时候发现,其实CMake完全能搞定iOS的库编译,只是需要一些特殊配置。

今天我们就来聊聊,怎么用CMake给iOS编译静态库和动态库。嗯,这里要注意,iOS的动态库使用限制比较多,但静态库是绝对没问题的。

为什么要在iOS上用CMake?

你可能会问:Xcode不香吗?我个人习惯是这样的——如果你的项目需要同时支持iOS、Android、甚至桌面端,用CMake统一管理构建脚本,比维护三套工程文件要省心得多。我在一个跨平台音视频SDK项目里,就是用CMake一套脚本搞定iOS和macOS的编译,改个参数就能切换。

说白了,CMake在iOS上的角色就是:生成一个能用的Xcode工程,或者直接调用工具链编译出.a或.dylib文件。

核心思路:工具链与架构

iOS编译和桌面端最大的区别在于:

  • 工具链不同:要用Apple的Clang,而不是系统默认的GCC
  • 架构不同:arm64(真机)、x86_64(模拟器),老设备还有armv7
  • SDK路径不同:需要指定iPhoneOS.sdk或iPhoneSimulator.sdk

CMake从3.14版本开始,对iOS的支持就相当完善了。你只需要设置几个关键变量:

set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_OSX_SYSROOT iphoneos)
set(CMAKE_OSX_ARCHITECTURES "arm64")

就这么简单?嗯,基本思路是这样,但实际项目中坑不少。我曾经因为忘记设置CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED,导致生成的Xcode工程死活编译不过,折腾了半天才发现是签名问题。

实战:编译一个iOS静态库

我们先从最基础的静态库开始。假设你有一个简单的数学工具库:

// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int multiply(int a, int b);

#endif
// math_utils.c
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

对应的CMakeLists.txt是这样的:

cmake_minimum_required(VERSION 3.14)
project(MathUtils VERSION 1.0.0 LANGUAGES C)

# 关键:设置为iOS系统
set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_OSX_SYSROOT iphoneos)
set(CMAKE_OSX_ARCHITECTURES "arm64")

# 关闭签名(如果是生成Xcode工程,这步很重要)
set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO")

# 添加静态库
add_library(MathUtils STATIC
    math_utils.c
    math_utils.h
)

# 设置输出名称
set_target_properties(MathUtils PROPERTIES
    OUTPUT_NAME "math_utils"
    PUBLIC_HEADER "math_utils.h"
)

编译命令:

mkdir build && cd build
cmake .. -G Xcode -DCMAKE_TOOLCHAIN_FILE=../ios.toolchain.cmake
cmake --build . --config Release
小提示:如果你不想用Xcode生成器,也可以用Ninja或Unix Makefiles,但需要手动指定编译器路径。我个人更推荐用Xcode生成器,因为调试时可以直接在Xcode里跑。

动态库:iOS的限制与变通

iOS对动态库的限制大家都知道——系统只允许使用系统自带的动态库,第三方应用不能随意加载自定义动态库(除非是Framework或者App Extension)。但有一种情况例外:如果你在开发自己的Framework,或者做App Extension,动态库是允许的。

编译动态库的CMake配置和静态库几乎一样,只需要把STATIC改成SHARED

add_library(MathUtils SHARED
    math_utils.c
    math_utils.h
)

# 设置Mach-O类型为mh_dylib
set_target_properties(MathUtils PROPERTIES
    FRAMEWORK TRUE
    FRAMEWORK_VERSION "A"
    MACOSX_FRAMEWORK_IDENTIFIER com.example.mathutils
    PUBLIC_HEADER "math_utils.h"
)
注意:iOS上的动态库必须签名才能使用。如果你是在模拟器上测试,可以暂时关闭签名;但真机部署时,一定要配置好Code Signing Identity。

多架构支持:Universal库

实际项目中,你不可能只编译arm64版本。用户可能在模拟器上调试,也可能在真机上运行。这时候就需要打一个Universal库,同时包含arm64和x86_64。

CMake支持通过CMAKE_OSX_ARCHITECTURES指定多个架构:

set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")

但这里有个坑——CMake默认会为每个架构单独编译,然后通过lipo合并。如果你用Xcode生成器,Xcode会自动处理合并;如果用其他生成器,可能需要手动调用lipo。

我曾经踩过这个坑:用Ninja编译时,生成了两个独立的.a文件,但没有合并。最后我写了个自定义命令来合并:

add_custom_command(TARGET MathUtils POST_BUILD
    COMMAND lipo -create
        "$<TARGET_FILE:MathUtils>"
        "$<TARGET_FILE:MathUtils>"
    -output "$<TARGET_FILE_DIR>/libmath_utils_universal.a"
    COMMENT "Creating universal library..."
)

使用工具链文件

如果你觉得手动设置一堆变量太麻烦,可以用现成的工具链文件。iOS社区有一个很流行的ios.toolchain.cmake,它封装了所有iOS相关的配置。

使用方式很简单:

cmake .. -DCMAKE_TOOLCHAIN_FILE=/path/to/ios.toolchain.cmake \
         -DIOS_PLATFORM=OS \
         -DIOS_ARCH="arm64"

这个工具链文件支持:

  • IOS_PLATFORM:OS(真机)、SIMULATOR(模拟器)、SIMULATOR64(64位模拟器)
  • IOS_ARCH:指定架构
  • ENABLE_BITCODE:是否启用Bitcode
  • DEPLOYMENT_TARGET:最低iOS版本
核心要点:使用工具链文件可以省去很多重复配置,但要注意版本兼容性。我建议把工具链文件放在项目目录下的cmake/文件夹里,方便团队统一使用。

知识体系总览

下面这张图总结了iOS编译的核心流程和关键配置点:

iOS编译核心流程 CMake配置入口 系统名称 CMAKE_SYSTEM_NAME=iOS SDK路径 CMAKE_OSX_SYSROOT 架构列表 CMAKE_OSX_ARCHITECTURES 编译目标 静态库 (.a) add_library(... STATIC) 动态库 (.dylib/Framework) add_library(... SHARED) 工具链文件 ios.toolchain.cmake 可简化配置

避坑指南

我在iOS编译上踩过的坑,列出来给大家参考:

  • 签名问题:生成Xcode工程时,一定要设置CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO,否则Xcode会强制要求签名。如果你需要真机调试,再单独在Xcode里配置签名。
  • Bitcode兼容性:iOS 14之后Bitcode已经不是必须的了,但如果你要支持旧版本,记得在CMake里开启ENABLE_BITCODE
  • 模拟器与真机:编译模拟器版本时,CMAKE_OSX_SYSROOT要设置为iphonesimulator,而不是iphoneos。我曾经搞混过,结果编译出来的库在模拟器上跑不了。
  • 最低版本:通过CMAKE_OSX_DEPLOYMENT_TARGET设置最低iOS版本,建议设置为12.0或更高,因为Apple已经停止支持32位应用了。

总结

用CMake编译iOS库,说白了就是三件事:设置系统为iOS、指定SDK路径、选好架构。剩下的和普通CMake项目没什么两样。我个人觉得,如果你在做一个跨平台库,花点时间把iOS的CMake配置搞定,绝对比维护多个Xcode工程要划算。

嗯,今天就聊到这里。记住,静态库是iOS上的主流选择,动态库除非你有特殊需求(比如Framework),否则尽量别碰。下次遇到iOS编译问题,先检查那三个关键变量有没有设对。


公众号:蓝海资料掘金营,微信deep3321