最近很多朋友都遇到了在iOS12系统上关于libswift_Concurrency的crash, 现象是通过xcode 13.2 & 13.2.1 构建的项目直接或间接使用了Concurrency 的功能, 在iOS12上稳定crash
原因分析
xcode 13.2 release notes中提到该Crash是Clang 编译器的bug, 13.2.1 release notes说已经修复, 实际测试并没有, 13.3 beta应该是修复了;
xcode编译器在低版本(12)上没有将libswift_Concurrency.dylib剔除, 反而是通过内嵌的方式将libswift_Concurrency.dylib放到了ipa的Frameworks路径下, 导致动态链接时, libswift_Concurrency被链接导致crash
error分析过程:
-
通过报错信息Library not loaded: /usr/lib/swift/libswiftCore.dylib 分析是动态库没有加载, 提示是libswift_Concurrency.dylib引用了该库, 但是libswift_Concurrency只有在iOS15系统上才会存在, 考虑为什么iOS12会启动加载呢,
iOS12的系统是不存在这个库的, 能加载只有一种情况即类似iOS12以下版本中swift核心库的处理方式,内嵌在了ipa中;
校验方式也很简答, 通过iOS12真机run一下, 崩溃后通过
image list
查看加载的镜像文件会找到libswift_Concurrency的路径是ipa/Frameworks下的, 通过解包ipa也证实了这一点 -
在按照xcode 13.2 release notes提供的方案, 将libswiftCore设置为weak并指定rpath后, crash信息变更, 此时error原因是___chkstk_darwin 符号找不到;
根据error Referenced from 发现还是libswift_Concurrency引用的, 可以通过
nm -u xxxAppPath/Frameworks/libswift_Concurrency.dylib
查看所有类型为U的符号, 其中确实包含了___chkstk_darwin, 到这里说明官方提供的解决方案并没有生效(至少是对于我的crash来说) -
error 提示期望该符号应该在libSystem.B.dylib中, 但是通过找到libSystem.B.dylib 并打印导出符号
nm -gAUj libSystem.B.dylib
发现即使是高版本的中libSystem.B.dylib也并没有该符号,那么如何确定该符号存在于哪个库呢, 这里用了一个取巧的方式, run iOS15以上真机确保不会crash, 并设置symbol符号
___chkstk_darwin
, xcode会标记所有存在该导出符号的库, 经过第1&2步骤思考, 认为是在查找libswiftCore核心库的符号可能性更大libSystem.B.dylib路径在~/Library/Developer/Xcode/iOS DeviceSupport/xxversion/Symbols/usr/lib)
-
如果校验呢, 通过xcode上iOS12 && iOS13两个不同版本的libswiftCore.dylib查看导出符号,可以发现, 12上的Core库不存在, 对比组15上是存在的, 所以基本可以断定symbol not found是这个原因造成的; 当然你也可以把其他几个库也采用相同的方式验证
通过在 ~/Library/Developer/Xcode/iOS DeviceSupport/xxversion/Symbols/usr/lib/swift/libswiftCore.dylib 不同的version路径下找到不同系统对应的libswiftCore.dylib库, 然后用
nm -gUAj libswiftCore.dylib
可以查看过滤后的全局符号验证, 本人实验iOS 12.5.4版本不存在, 15版本存在库的路径,可以通过linkmap或者运行一个demo打个断点, 然后通过image list查找
分析总结: 无论是xcode提供的解决方案还是分析过程, 发现根源还是因为在iOS12上链接了libswift_Concurrency造成的,既然问题出在异步库, 解决方案也很明了,移除项目中的libswift_Concurrency.dylib库即可
解决方案
使用其他xcode版本构建
使用xcode13.1构建 或者xcode13.3 Beta, 但是beta版无法构建app store ipa, xcode13.1不存在该bug
构建后移除libswift_Concurrency.dylib
添加脚本,每次移除嵌入的libswift_Concurrency.dylib, 移除xcode iOS12上未剔除的异步SDK
在Edit Scheme... -> Build -> Post-actions -> Click '+' to add New Run Script
rm "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/libswift_Concurrency.dylib" || echo "libswift_Concurrency.dylib not exists"
暂不升级使用了concurrency的三方库版本
降低查找到使用concurrency的三方库版本, 不使用该库就不会引入,后续等xcode修复后再升级; 一般项目都是通过cocoapods管理的, 可以直接指定之前的版本
查找项目中使用了libswift_Concurrency.dylib的库
该如何查找哪些三方库中有用到concurrency呢,
如果是源码, 全局搜索相关的await async可以找到部分SDK, 但是如果是SDK或者是间接使用的, 则只能通过符号查找
思路:
- 首先明确动态库的链接是依赖导出符号, 即xx库引用了target_xxx 动态库是通过调用target_xxx的导出符号(全局符号)实现的, 全局符号的标识是大写的类型, U表示当前库中未定义的符号, 即需要查找其他库动态链接的符号
- 如何查看项目中引用了指定动态库target_xxx的哪些符号呢, 可以通过linkmap文件查找, 但是由于libswift_Concurrency有可能是间接依赖的, linkmap中不存在对这个库的直接依赖, 所以没办法获取使用的符号, 这里通过获取libswift_Concurrency的所有符号匹配, libswift_Concurrency的路径可以通过上文提到的
image list
获取, 一般都是用的/usr/lib/swift下的
MachO中记录的动态库都是直接引用的, 所以otool -L xxx或者MachOView查看的方式, 无法查找到间接依赖的库
- 遍历项目中所有的库, 查找里面用到的未定义符号, (U)标识是外部符号, 匹配的则标识有调用关系
使用的脚本代码如下:
#!/bin/bash
SHELL_BASE_PATH=`pwd`.chomp
SYSTEM_LIB_BASE_PATH="$HOME/Library/Developer/Xcode/iOS DeviceSupport"
LIBSIWFTCONCURRENCY_SYMBOLSFILE="libswiftConcurrencySymbols.txt"
FRAMEWORK_FILE=""
FRAMEWORK_PATH=""
TMP_RESULT_PATH="$HOME/Desktop/iOS12 Crash Result"
# -f 指定单独的Framework/.a文件
# -p 指定包含Framework/.a的路径, 支持相对路径或者绝对路径
# -o 指定结果输出路径, 默认是~/Desktop/Desktop/iOS12 Crash Result
while getopts ":f:p:o:" optname
do
case "$optname" in
"f")
FRAMEWORK_FILE="$OPTARG"
;;
"p")
FRAMEWORK_PATH="$OPTARG"
;;
"o")
TMP_RESULT_PATH="$OPTARG"
;;
":")
echo "No argument value for option $OPTARG"
;;
"?")
echo "Unknown option $OPTARG"
;;
*)
echo "Unknown error while processing options"
;;
esac
#echo "option index is $OPTIND"
done
# 获取当前xcode中libswift_Concurrency.dylib的符号, 并排序去重
function log_success() {
echo -e "\033[32m $1 \033[0m"
}
function log_warning() {
echo -e "\033[33m $1 \033[0m"
}
function log_error() {
echo -e "\033[31m $1 \033[0m"
exit 1
}
function log_msg() {
echo -e "\033[34m $1 \033[0m"
}
function findConcurrencyPath() {
local pathslist=()
while IFS= read -r -d $'\0' file
do
pathslist+=("$file")
done < <(find "${SYSTEM_LIB_BASE_PATH}" -name "libswift_Concurrency.dylib" -print0)
# echo ${pathslist[-1]}
echo ${pathslist[${#pathslist[@]}-1]}
}
function checkExistAndClear() {
local path=$1
:> "${path}"
}
# 判断FRAMEWORK_PATH文件夹不存在
if [[ ! -d "${FRAMEWORK_PATH}" && ! -f "$FRAMEWORK_FILE" ]]; then
log_error "SDK路径不存在,请检查..."
fi
SALVEIFS=$IFS
IFS=$(echo -en "\n\b")
# 如果不存在该目录, 则新建
if [[ ! -d "${TMP_RESULT_PATH}" ]]; then
mkdir "${TMP_RESULT_PATH}" || log_error "${TMP_RESULT_PATH}创建失败..."
fi
# 获取当前xcode中libswift_Concurrency的库
_concurrencyPath=$(findConcurrencyPath)
# 如果为空, 表示未查询到异步库
# mktemp 产生的临时文件名一般以 tmp 为前缀,随机产生后缀,同时文件会放在 $TMPDIR 目录下。
# 但是我们可以通过 -t 前缀.XXXXXX 来设置临时文件名的模板,通过 -p 目录 指定在哪个目录中产生临时文件:
# TMPFILE=$(mktemp -p $HOME -t TEST.XXXXXXX)
tmp_symbols_path=$(mktemp -d -t tmp_symbols) || log_error "临时目录创建失败, 检查权限后重试..."
log_msg "临时目录${tmp_symbols_path}创建成功..."
# 设置退出清空临时文件
trap 'rm -rf "$tmp_symbols_path"' EXIT
# 判断文件存在, 提取符号文件, 否则退出
if [[ -f "${_concurrencyPath}" ]]; then
nm -gAUj "${_concurrencyPath}" > "${tmp_symbols_path}/${LIBSIWFTCONCURRENCY_SYMBOLSFILE}"
else
log_error "${SYSTEM_LIB_BASE_PATH} 路径下未找到libswift_Concurrency.dylib库, 请手动处理符号后进行去重排序命名为${LIBSIWFTCONCURRENCY_SYMBOLSFILE}存储到${TMP_RESULT_PATH}路径..."
fi
# 创建 result文件, 如果已经存在, 清空
result_libs=""${TMP_RESULT_PATH}"/result_libs.txt"
result_linksymbols=""${TMP_RESULT_PATH}"/result_linksymbols.txt"
$(checkExistAndClear ${result_linksymbols})
$(checkExistAndClear ${result_libs})
$(checkExistAndClear "${TMP_RESULT_PATH}/${LIBSIWFTCONCURRENCY_SYMBOLSFILE}")
$(checkExistAndClear "${TMP_RESULT_PATH}/targetlibs.txt")
# 处理符号, 模式匹配
while read line
do
line=${line##* }
echo "${line}" >> "${tmp_symbols_path}/libswift_Concurrency_tmp_symbols.txt"
done < "${tmp_symbols_path}/${LIBSIWFTCONCURRENCY_SYMBOLSFILE}"
# 排序去重
cat "${tmp_symbols_path}/libswift_Concurrency_tmp_symbols.txt" | sort | uniq > ""${TMP_RESULT_PATH}"/"${LIBSIWFTCONCURRENCY_SYMBOLSFILE}""
# 如果是FRAMEWORK_PATH遍历, 否则直接存储
file=$FRAMEWORK_FILE
if [ "${file##*.}"x = "framework"x ]||[ "${file##*.}"x = "a"x ];then
echo "$FRAMEWORK_FILE" > ""${TMP_RESULT_PATH}"/targetlibs.txt"
fi
# 遍历指定路径下的所有framework 和 .a库
find $FRAMEWORK_PATH -name "*.framework" -o -name "*.a" >> ""${TMP_RESULT_PATH}"/targetlibs.txt"
# 遍历每一个库, 获取所有未定义的符号(U), 即外部符号
while read line
do
# 优化打印函数,
log_msg "检测SDK $line..."
path="$line"
if [[ $line =~ ".framework" ]]; then
path=${line##*/}
path=${path%%.*}
path=$line/$path
# 输出到临时文件夹
nm -u "$path" > "${tmp_symbols_path}/symbols.txt"
else
# 输出到临时文件夹
nm -u "$path" > "${tmp_symbols_path}/symbols.txt"
fi
# 对比符号 排序
cat "${tmp_symbols_path}/symbols.txt" | sort | uniq > "${tmp_symbols_path}/libs_symbols.txt"
comm -12 "${tmp_symbols_path}/libs_symbols.txt" "${TMP_RESULT_PATH}/${LIBSIWFTCONCURRENCY_SYMBOLSFILE}" > "${tmp_symbols_path}/common_symbols.txt"
# 判断是否为空, 空表示未调用, 否则common_symbols.txt中存储的是引用的符号
if [[ -s "${tmp_symbols_path}/common_symbols.txt" ]]; then
# 不为空, 插入空行分割
echo -e >> "$result_linksymbols"
echo "================================$line" >> "$result_linksymbols"
echo "$path" >> "${result_libs}"
# 处理符号
while read symbol
do
# 记录 SDK Path + symbol 到引用文件夹
echo $symbol >> "${result_linksymbols}"
done < "${tmp_symbols_path}/common_symbols.txt"
fi
log_msg "SDK ${line} 处理完成..."
done < "${TMP_RESULT_PATH}/targetlibs.txt"
IFS=$(echo ${SALVEIFS})
log_success "查看${TMP_RESULT_PATH}目录下的result_linksymbols.txt获取所有链接libswift_Concurrency.dylib的符号"
log_success "查看${TMP_RESULT_PATH}目录下的result_libs.txt获取所有使用libswift_Concurrency.dylib的SDK名称"
使用方法:
-f: 指定单个framework/.a静态库进行检查
-p: 指定目录,检查目录下的所有framework/.a
-o 输出目录, 默认是~/Desktop/iOS12 Crash Result
由于本人对shell熟练度较低, 脚本可能存在诸多bug, 如果有任何使用问题可以直接联系本人, 欢迎一起交流学习
引用
如何检测哪些三方库用了libstdc++
使用 otool 命令查看 App 所使用的动态库
After upgrading to Xcode 13.2.1, debugging with a lower version of the iOS device still crashes at launching
Xcode 13.2 Release Notes