available没你想象中的可靠

0 阅读4分钟

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,提交之后就不是了。

image.png

图片 1提交之前

image.png

图片 2 提交之后

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

分析

探索编译参数

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

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

探索符号依赖

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

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

解决方案

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

防劣化

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

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

告警格式如下:

image.png

 

番外篇

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

image.png

图片 3编译参数

图片 4 产物依赖

图片 5 符号依赖

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

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

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

调用命令行编译

# 编译
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的顺序,发现结果不一样

代码如下:

#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库。

// 源码来源: 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)
}