第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
动态库: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"
)
多架构支持: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:是否启用BitcodeDEPLOYMENT_TARGET:最低iOS版本
知识体系总览
下面这张图总结了iOS编译的核心流程和关键配置点:
避坑指南
我在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