available没你想象中的可靠

available没你想象中的可靠

随着iOS系统的更新,iOS官方提供了许多的高版本才支持的功能。但是往往我们需要支持多个版本的iOS系统,在支持新功能的同时也要照顾低版本用户。通常情况下,我们会使用@available(iOS 14.0, *)等方式来做版本限制,但是这可能会引入非预期的crash。

背景

示例代码如上图所示,UniformTypeIdentifiers.h是一个iOS14以后的系统库,typeWithIdentifier也是一个iOS14以后才支持的API,那么如果我们把用到API的地方放到@available(iOS 14.0, *)的判断条件内,是不是就可以了呢?

当你在iOS13上运行时会发现报错,内容如上图所示,在低版本中,dyld在启动我们的程序时找不到对应的动态库。

那么为什么我的代码会新增对系统库UniformTypeIdentifiers的依赖呢?对比了两次提交的构建产物发现,我的代码提交之前对UniformTypeIdentifiers的依赖是weak,提交之后就不是了。

图片 1提交之前

图片 2 提交之后

很显然,我的available包裹的代码也参与的编译,导致系统认为我依赖了UniformTypeIdentifiers库,然后给我设置了强行依赖。

分析

探索编译参数

既然是编译后出现问题,那么想当然是编译的参数设置有问题。查看了build setting,build phases发现都没有设置UniformTypeIdentifiers的依赖。打开编译log搜索UniformTypeIdentifiers关键字,发现使用的是-framework进行的链接。

很显然这里是使用了strong的方式去依赖,如果改成-weak_framework方式,他就是weak了,但是这里并没有改动,说明这不是根本原因。肯定是某个地方强依赖了符号导致的。

探索符号依赖

进入对应的编译产物目录,找到对应的.o文件,通过nm命令查看对应的符号,发现对于UTType的依赖是强依赖,并不是weak。

这说明即使我们的代码使用了Available包裹,能够让他在低版本运行时不会因为方法找不到,但是也可能会导致因为低版本的系统库没有对应的动态库从而导致启动的时候就会crash。

解决方案

知道了问题然后解决方案就很多了,首先我们现在build phases中设置对于UniformTypeIdentifiers是weak,也就是可选依赖。

防劣化

那么怎么防止后续再出现同样的问题呢?

我们可以通过脚本在每次构建之后分析当前的动态库依赖,如果发现新增动态库或者动态库的依赖方式发生变化。则告警。

告警格式如下:

番外篇

当我把同样的代码放到demo工程中的时候。表现却不一样。同样的代码,同样的最低系统支持,同样的编译参数。

图片 3编译参数

图片 4 产物依赖

图片 5 符号依赖

可以看到,符号依赖也是weak的。这就很奇怪了。按照上面的结论这里应该是strong才对。那么到底是为什么同样的代码在不同的工程中产生了不一样的结论呢?

抛开无关的条件之后,最基本的代码如下所示。

less 复制代码
#import <UIKit/UIKit.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
 
void test(void) {
    if (@available(iOS 14.0, *)) {
        UTType *t = [UTType typeWithIdentifier:@"public.image"];
        (void)t;
    }
}

调用命令行编译

perl 复制代码
# 编译
xcrun clang -c test.m -o test.o \
    -target arm64-apple-ios12.0 \
    -isysroot $(xcrun --sdk iphoneos --show-sdk-path) \
    -fmodules \
    -fmodules-cache-path=/tmp/modcache
 
# 检查符号
nm -m test.o | grep "OBJC_CLASS.*UTType"
 

结果发现依然是weak

但是当我们改变一下import的顺序,发现结果不一样

代码如下:

less 复制代码
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import <UIKit/UIKit.h>
 
void test(void) {
    if (@available(iOS 14.0, *)) {
        UTType *t = [UTType typeWithIdentifier:@"public.image"];
        (void)t;
    }
}
 

如果我们先导入UTI,然后再导入UIKit。他就会变成strong。

所以是在编译的时候系统分析符号依赖时,如果先导入UTI,他会认为此时符号是strong的,然后我们编译完成真正的用到了UTI的符号,此时就会变成强依赖符号。最后生成的产物就会强依赖UniformTypeIdentifiers库。

arduino 复制代码
// 源码来源: https://github.com/llvm/llvm-project/blob/main/clang/lib/AST/DeclBase.cpp
bool Decl::isWeakImported() const {
  bool IsDefinition;
  if (!canBeWeakImported(IsDefinition))
    return false;
 
  // ⚠️ 关键:只检查"最新声明"的属性
  for (const auto *A : getMostRecentDecl()->attrs()) {
    if (isa<WeakImportAttr>(A))
      return true;
 
    if (const auto *Availability = dyn_cast<AvailabilityAttr>(A)) {
      if (CheckAvailability(getASTContext(), Availability, nullptr,
                            VersionTuple()) == AR_NotYetIntroduced)
        return true;
    }
  }
 
  return false;  // 默认返回 false(Strong)
}
相关推荐
for_ever_love__3 小时前
UI学习:数据驱动ce l l
学习·ui·ios·objective-c
KillerNoBlood3 小时前
2026移动端跨平台开发面经总结
android·算法·flutter·ios·移动开发·鸿蒙·kmp
人月神话-Lee4 小时前
【图像处理】颜色科学与灰度化——人眼看到的和数字记录的不一样
图像处理·人工智能·计算机视觉·ios·swift
号码认证服务5 小时前
给用户打电话,怎么在对方手机显示为“XX证券”?号码认证办理步骤
android·运维·服务器·ios·智能手机·iphone·webview
MonkeyKing5 小时前
iOS 启动优化实战:pre-main耗时、二进制重排与动态库裁剪全解析
ios
MonkeyKing5 小时前
iOS 卡顿优化实战:离屏渲染、混合图层与圆角优化全解析
ios
库奇噜啦呼7 小时前
【iOS】源码学习-消息流程分析
学习·ios·cocoa
2501_915918417 小时前
iOS性能数据监控:从概念到工具实践,让应用运行更流畅
android·macos·ios·小程序·uni-app·cocoa·iphone
aiopencode21 小时前
iOS开发中Xcode安装不完整问题解决方案与配置指南
后端·ios
Joseph1821 小时前
深度拆解 DanceUI:从声明式视图到原生渲染的全链路技术解析
ios·swiftui