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.rb
的 FrameworkPaths
类。在它的 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.rb
的 framework_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_libraries
和 vendored_dynamic_frameworks
。
def vendored_dynamic_artifacts
vendored_dynamic_libraries + vendored_dynamic_frameworks
end
6. 再看看 vendored_dynamic_libraries
和 vendored_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 一下,发现 MachO
是 ruby-macho
中的符号,它是 Cocoapods 引入的三方库。
灵光一闪,马上去对比了两个环境中的 ruby-macho
版本,结果不一样🤩。有问题的环境 ruby-macho
版本较旧,正常环境中的版本为 2.5.1
。
接下来,就是在有问题的环境中尝试升级 ruby-macho
版本,看是不是这个问题。升级之后,一切恢复了正常,^_^。
一点总结
就这样,困扰了我许久的问题,终于得到了解决。从源头入手,顺藤摸瓜,一步步找出症结所在。总结下来,其实也没什么技术含量,需要的是耐心和不断尝试的心态。