vlambda博客
学习文章列表

iOS Flutter 启动崩溃排查

大家好,我是微微笑的蜗牛🐌。

PS:这篇文章属于问题排查记录文。

问题描述

前段时间有几个同事遇到了一个比较诡异的问题,当 Flutter 工程以本地模式开发时,运行起 App 就崩溃。这里的 Flutter 工程是以 Submodule 的方式嵌入到 iOS 原生工程中。

崩溃截图如下:


从截图来看,当 dyld 在加载 Flutter 的产物 App.framework 时,在 @rpath 下找不到该动态库,但其实 App.framework 是有生成的。

关于 @rpath 的说明,可参考这篇文章:https://blog.krzyzanowskim.com/2018/12/05/rpath-what/

@rpath 有一个搜索路径是 xx.app/Frameworks,去到里面一看,果真没有。到这里就比较清楚了,崩溃的根本原因是由于 App.framework 没有拷贝到 xx.app/Frameworks 目录下。

但在我的电脑上,同样的项目编译运行却没有出现崩溃,那么这大概率是环境问题。

我试着在自己电脑的工程中全局搜索 App.framework,发现在 Pods--xx-frameworks 中有 install_framework "xx/App.framework" 这句代码,而在出现问题的电脑上,却缺失了。

install_framework 有一个作用就是将 embedded framework 拷贝到 xx.app/Frameworks 目录中。而这些代码是由 Cocoapods 生成的,因此进一步猜想是 Cocoapods 的问题。

可是呢,我查看了两个环境里的 Cocoapods 版本,是一样的,一下子陷入了无头绪状态。

在尝试了多种办法未果后,我决定看看 CocoaPods 源码,从如何生成这些 install_framework 代码开始。

排查过程

1. 通过搜索 install_framework 关键字,在 generator/embed_frameworks_script.rb 中,找到了代码生成逻辑。

frameworks_by_config.each do |config, frameworks|
  frameworks.each do |framework|
    contents_by_config[config] << %(  install_framework "#{framework.source_path}"\n)
  end
end

原来工程中生成的 install_framework 这一串代码(下图所示),是通过遍历传入的 frameworks_by_config,然后在内层遍历 frameworks,拿到 framework.source_path 完整路径后生成的。


2. 然后找到 framework_paths_by_config 定义的文件,在 target/aggregate_target.rb 中。打印出各个依赖 library 的 framework_path,未发现 App.framework

def framework_paths_by_config
  @framework_paths_by_config ||begin
    framework_paths_by_config = {}
    user_build_configurations.each_key do |config|
      relevant_pod_targets = pod_targets_for_build_configuration(config)
      framework_paths_by_config[config] = relevant_pod_targets.flat_map do |pod_target|
        library_specs = pod_target.library_specs.map(&:name)

         // 这里打印
        pod_target.framework_paths.values_at(*library_specs).flatten.compact.uniq
      end
    end
    framework_paths_by_config
  end
end

3. 接着想到去 framework_paths 的源头看看,这样就来到 xcode/framework_paths.rbFrameworkPaths 类。在它的 initialize 方法中打印出所有 framework 的路径,也未发现 App.framework 身影。

def initialize(source_path, dsym_path = nil, bcsymbolmap_paths = nil)

  // 打印路径
  @source_path = source_path
  @dsym_path = dsym_path
  @bcsymbolmap_paths = bcsymbolmap_paths
end

4. 接下来查找使用 FrameworkPaths.new() 的地方,发现在 target/pod_target.rbframework_paths 方法中有用到。

def framework_paths
  @framework_paths ||begin
    file_accessors.each_with_object({}) do |file_accessor, hash|

      frameworks = file_accessor.vendored_dynamic_artifacts.map do |framework_path|

        // 省略...
        framework_source = "${PODS_ROOT}/
#{relative_path_to_sandbox}"

        // 省略...
        Xcode::FrameworkPaths.new(framework_source, dsym_source, bcsymbolmap_paths)
      end
      /
/ ...
    end
  end
end

可以看到,这里遍历了 file_accessor.vendored_dynamic_artifacts 的结果,拼接得到库的完整路径,再用 FrameworkPaths 类来进行结构化保存。

5. 然后再查找 vendored_dynamic_artifacts 的定义,在 sandbox/file_accessor.rb 中。

它调用到了 vendored_dynamic_librariesvendored_dynamic_frameworks

def vendored_dynamic_artifacts
    vendored_dynamic_libraries + vendored_dynamic_frameworks
end

6. 再看看 vendored_dynamic_librariesvendored_dynamic_frameworks 两个方法的实现。

vendored_dynamic_libraries 定义:

def vendored_dynamic_libraries
  vendored_libraries.select do |library|
    Xcode::LinkageAnalyzer.dynamic_binary?(library)
  end
end

vendored_dynamic_frameworks 定义:

def vendored_dynamic_frameworks
  (vendored_frameworks - vendored_xcframeworks).select do |framework|
    Xcode::LinkageAnalyzer.dynamic_binary?(framework + framework.basename('.*'))
  end
end

它们都调用了 LinkageAnalyzer.dynamic_binary 方法。

7. 再进一步查找 dynamic_binary 的定义,在 xcode/linkage_analyzer.rb

方法返回值为 bool,内部会通过 MachO.open(binary).dylib 来判断是否是动态库。

def self.dynamic_binary?(binary)

    @cached_dynamic_binary_results ||= {}
    return @cached_dynamic_binary_results[binary] unless @cached_dynamic_binary_results[binary].nil?
    return false unless binary.file?

    @cached_dynamic_binary_results[binary] = MachO.open(binary).dylib?

    puts "dynamic_binary #{binary}#{MachO.open(binary).dylib?}"

  rescue MachO::MachOError
    @cached_dynamic_binary_results[binary] = false
  end
end

通过打印 log,发现在有问题的环境中,当参数为 App.framework 时,MachO.open 发生异常了,导致判断不是动态库。而在正常环境中,MachO.open 无异常。

那么很有可能是这里的问题了,google 一下,发现 MachOruby-macho 中的符号,它是 Cocoapods 引入的三方库。

灵光一闪,马上去对比了两个环境中的 ruby-macho 版本,结果不一样🤩。有问题的环境 ruby-macho 版本较旧,正常环境中的版本为 2.5.1

接下来,就是在有问题的环境中尝试升级 ruby-macho 版本,看是不是这个问题。升级之后,一切恢复了正常,^_^。

一点总结

就这样,困扰了我许久的问题,终于得到了解决。从源头入手,顺藤摸瓜,一步步找出症结所在。总结下来,其实也没什么技术含量,需要的是耐心和不断尝试的心态。