之前美团出了一篇讲在二进制依赖库的情况下,调试的时候查看依赖库源码的办法,见美团 iOS 工程 zsource 命令背后的那些事儿
这篇文章发出来之后,很快有人就跟进了更加简便的,见lldb 入坑指北(2)- 15行代码搞定二进制与源码映射
二者都是很好的文章,解决了在二进制依赖库的环境下,运行环境查看源码的问题。当时看到这些文章的时候,头脑一热:能否不依赖于lldb运行环境,在写代码的时候就可以跳转到源码去看呢?经过一番捣鼓,有了结果 iOS组件化过程中的源码查看。这篇文章只是个demo,提供了思路,但是没有一个工程化的代码。今天就来补充一下一些工程代码。当然,这里的代码是依赖于我司现在用的二进制解决方案的,具体到不同的二进制解决方案的时候,可以参照这个代码修改一下。
思路再重复一遍:
- 1、二进制文件里面其实包含了编译时候的源码路径的,只要Xcode能找到这个路径的代码,就可以跳转过去。
- 2、我们需要根据二进制文件找到打包它的时候,源码的存放位置。并且下载二进制库对应的源码版本,放到对应的位置。
- 3、将这些源码工程用一个拖进一个新的壳工程里面。然后将这个壳工程集成进我们主工程的workspace里面。
思路简单,但是搞成工程事情就多了。我说下我的选型思路:
第一,我们需要在podfile里面找到我们使用了二进制依赖的库,podfile是ruby写的。
第二,我们需要下载源码,这部分自己写程序是没问题的,但是cocoapods不是有下载的功能?
第三,我们需要搞个壳工程,并集合进去workspace里面。这个是不是和Pods.xcodeproj一样的么?那cocoapods就有这个能力。
综上,我们可以用ruby写,借助cocoapods的代码来帮助我们实现工程化。既然要用cocoapods,就需要先看懂一些源码,调试看最快了,请参考我之前写的Ruby调试
先放一段结果视频,视频上的主工程是cocoTest
,然后它的podfile里面引用了一个二进制的依赖库StaticLiBit
.(故意写少了个b),二进制库里面有个Animal
类。注意视频开始的时候,点击Animal的定义,跳转到了二进制库的头文件,但是再次跳转到gogoPower
方法定义的时候,它切换到了我们的源码工程里面去了。这样就实现了在写代码的时候,查看二进制库源码的功能。
下面让我们一步一步,手把手教你怎么搞。我刚接触ruby,代码写得有点难看,就别喷这个点了。。
1 定义哪些二进制库要查看二进制源码的
因为我们可能引入了一大堆的二进制库,而里面想看的其实不多,那么可以搞个白名单,只有在白名单里面的库,我们才去下载源码。所以我定义了一个sourcePodList.rb
,里面每一行就写一个库的名字。然后用ruby将这个文件读入,这样就有了白名单数组
def Pod.readSourceFile(sourceFilePath = nil) #读入需要引入源码的二进制库名单
if sourceFilePath == nil || !File.exist?(sourceFilePath)
puts "\033[33mWarning sourceFile not found\033[0m\n"
return []
end
sourcePods = []
File.open(sourceFilePath, "r") do |file|
file.each_line do |line|
sourcePods << line.strip
# puts line
end
end
sourcePods
end
2 在podFile里面查找哪些库是使用了二进制引入的
假设我的二进制引入的命令是这样的。后面新增的:bin=>true就是意味着打开了引入二进制库。如果不存在这个参数,则认为走源码引入。
pod_ek 'StaticLiBit', :git=>"http://ek.com/StaticLibBit.git", :commit => 'eaceb85197c79947cb16af21d9d3ec16c3397abf', :bin=>true
你需要知道ruby每一行都可以看成一个函数调用。我们可以定义一个pod_ek
的方法,然后就可以执行上面的命令。这样就可以读取到这行里面的所有的参数。并且将一些不需要用到的参数去除。读取完podFile里面的二进制库之后,我们再和第一步里面定义的白名单进行匹配,得出我们最终需要下载源码的依赖库名单。这里我是放到了全局变量@@dependencies
里。
def Pod.pod_ek(name = nil, *requirements)
params = requirements[0].clone
if params[:bin] == true
params.delete(:bin)
params.delete(:subspecs)
params.delete(:testspecs)
params.delete(:appspecs)
pod = { "name" => name, "params" => params }
# puts pod
@@dependencies << pod
end
end
def Pod.readUNQPodFile(file_path = nil, sourcePods = [])
if file_path == nil || !File.exist?(file_path)
puts "\033[33mWarning UNQPod.rb file not found\033[0m\n"
return
end
File.open(file_path, "r") do |file|
file.each_line do |line|
if line.lstrip.start_with? "pod_ek"
eval(line, nil, file_path) # 你需要知道ruby每一行都可以看成一个函数调用
# puts line
end
end
end
dependencies = []
@@dependencies.each do |dependency|
# puts dependency["name"]
if sourcePods.include?(dependency["name"])
puts "found bin lib:#{dependency["name"]}"
dependencies << dependency
end
end
@@dependencies = dependencies
end
3、下载我们需要的源码库
这一步就比较特殊了,每家厂的二进制依赖方案不同,怎么从podfile里面引入的二进制依赖库转换到源码库的下载的pod命令,这个需要你们自己解决了。(因为我的只需要将:bin=>true删除就行),所以我只放出下载代码。dependency["params"]
这里面存放最基础的pod命令里面需要的参数就行了。cocoapods会解析后进行下载,毕竟是个中心化的依赖管理器。。。
def Pod.downloadSource(dependencies = [])
download_results = []
dependencies.each do |dependency|
download_request = Downloader::Request.new(:name => dependency["name"],
:params => dependency["params"],
)
begin
download_result = Downloader.download(download_request, nil, :can_cache => true) # 第二个参数传递一个文件夹路径的话,可以将库拷贝过去
podPath = download_request.slug({})
rr = @@cache_root + 'Pods' + podPath
download_results << {"name" => dependency["name"], "podPath" => rr}
# puts rr
rescue Pod::DSLError => e
raise Informative, "Failed to load '#{name}' podspec: #{e.message}"
rescue => e
raise Informative, "Failed to download '#{name}': #{e.message}"
end
end
download_results
end
4、移动下载好的源码到二进制库编译的路径
首先,我们需要从二进制依赖库.a里面找到编译的路径,见下面代码
def Pod.findLib(libName = nil)
#这个方法就是根据库的名字,在Pods里面找到这个二进制库
if libName == nil
puts "\033[33mWarning libName is nil\033[0m\n"
return ""
end
# @@sandbox_root = Config.instance.sandbox_root, /工程目录/Pods
libFolderPath = @@sandbox_root + libName
libPath = .... #根据自己的二进制方案,找到这个二进制.a文件
libPath
end
# 去找到二进制里面的目录
def Pod.findLibSourcePath(libName = nil)
if libName == nil
puts "\033[33mWarning libName is nil\033[0m\n"
return ""
end
libPath = Pod.findLib(libName)
if File.exist?(libPath)
# 这里需要这个二进制库是在库名字的目录下build出来的,否则查找不正确
path = `str=\`dwarfdump #{libPath} | grep 'DW_AT_decl_file' | grep #{libName} | head -n 1\`;str=${str#*\\"};echo ${str%%#{libName}*}#{libName}`
return path.strip # 这里居然有个换行符!!! else
puts "\033[33mWarning lib:#{libPath} is not found\033[0m\n"
return nil
end
end
找到编译路径之后,我们需要将第三步下载的源码移动到对应的地方
def Pod.copyLibsToDest(download_results)
#移动需要的pod到指定位置
download_results.each do |result|
# 去找到二进制里面的目录
libDestPath = Pod.findLibSourcePath(result["name"])
if libDestPath == nil
return
end
puts "find #{result["name"]} DestPath:#{libDestPath}"
if !libDestPath.start_with?("/") && !libDestPath.start_with?("./")
libDestPath = Dir.pwd + "/" + libDestPath #有些很奇怪的以当前文件夹来做路径的,则需要拼接上当前路径。否则copy库的时候会有问题。
end
# 拷贝cache里面的库到指定位置
result["libDestPath"] = libDestPath
puts "copy lib to :#{libDestPath}"
if File.exist?(libDestPath)
FileUtils.chmod_R "u=wrx,go=rx", libDestPath # 加回权限,否则删除不了
FileUtils.rm_rf libDestPath
elsif
FileUtils.mkdir_p(File.dirname(libDestPath))
end
FileUtils.cp_r(result["podPath"], libDestPath)
end
end
5、用移动好的源码,创建一个壳工程。并添加到主工程
主要难点是在遍历添加文件上,这个弄了我好久才搞定。。
$codeExt = [".m", ".h", ".c", ".cpp", ".mm", ".pch"]
$sourceExt = [".m", ".c", ".cpp", ".mm"]
def Pod.addLib(lib_path, group)
if File.directory? lib_path
Dir.foreach(lib_path) do |file|
Pod.addLibFiles(lib_path+"/"+file, group)
end
else
end
end
def Pod.addLibFiles(file_path, group, target)
# puts "ff:#{file_path}"
if File.directory? file_path
# puts "AddGroup:#{file_path}"
g=group.new_group(File.basename(file_path))
Dir.foreach(file_path) do |file|
if file !="." and file !=".."
addLibFiles(file_path+"/"+file, g, target)
end
end
if g.empty?
group.children.delete_at(group.children.index(g))
else
g.sort
end
else
if $codeExt.include?(File.extname(file_path))
# basename=File.basename(file_path)
# puts basename
# puts "AddFile:#{File.basename(file_path)}"
file_ref = group.new_reference(file_path)
if $sourceExt.include?(File.extname(file_path))
target.add_file_references([file_ref])
end
target.add_resources([file_ref])
FileUtils.chmod "u=rx,go=rx", file_path
end
end
end
# 创建工程文件
def Pod.createXcodeProj(targetName, download_results, workspacePath)
projName = "SourcePod"
projPath = "sourcePod/#{projName}.xcodeproj"
puts "create #{projPath}"
proj = Xcodeproj::Project.new(projPath)
download_results.each do |result|
target = proj.new_target(:static_library, result["name"], :ios, '9.0')
Pod.addLibFiles(result["libDestPath"], proj.main_group, target)
end
proj.save()
# 添加到之前的workspace
workspace = Xcodeproj::Workspace.new_from_xcworkspace(workspacePath)
root = workspace.document.root
found = false
root.children.each do |child|
if child.is_a?(REXML::Element)
location = child.attributes['location']
if location.include?(projName)
# root.delete_element(child)
found = true
break
end
end
end
if !found
workspace << projPath
workspace.save_as(workspacePath)
end
end
这里有个坑啊。。就是workspace里面是可以添加重名的project的,如果上面的代码一直运行,就会在一个workspace里面添加了很多很多个壳工程。。所以我们需要进行删除。
def Pod.deleteSourceProj(workspacePath)
puts "try remove sourcePod.proj"
projName = "SourcePod"
workspace = Xcodeproj::Workspace.new_from_xcworkspace(workspacePath)
root = workspace.document.root
found = false
root.children.each do |child|
if child.is_a?(REXML::Element)
location = child.attributes['location']
if location.include?(projName)
root.delete_element(child)
found = true
puts "found and delete SourceProj"
break
end
end
end
if found
workspace.save_as(workspacePath)
end
end
6、最后,搞个方法将这些串起来
我们的方法最好提供开关功能,因为经测试某些xcode版本会出现编译不过的问题。或者只提供拷贝代码到编译目录的功能。这样的话,其实不需要依赖之前的lldb的文章就可以在运行环境看源码了。
# SourceOpen是总开关;
# OnlyInRuntime为true的时候,不增加SourcePod工程,只拷贝代码到目录,运行时可以进入源码
# target 主工程的target名字
# workspacePath 主工程workspace路径
def Pod.sourceLook(sourceOpen, onlyInRuntime, targetName, workspacePath)
# 创建工程文件, 并添加
if !sourceOpen
Pod.deleteSourceProj(workspacePath)
else
#下载需要的pod
sourcePods = Pod.readSourceFile(@@sourceFile)
if sourcePods.empty?
Pod.deleteSourceProj(workspacePath)
return
end
Pod.readUNQPodFile(@@podrb_path, sourcePods)
download_results = Pod.downloadSource(@@dependencies) # [{"name"=>"PINCache", "podPath"=>"External/PINCache/26171f2f43ff1f67d79600e4dfd4d53e"}]
#移动需要的pod到指定位置
Pod.copyLibsToDest(download_results)
if !onlyInRuntime
Pod.createXcodeProj(targetName, download_results, workspacePath)
else
Pod.deleteSourceProj(workspacePath)
end
end
end
7、接入工程
我们最后加入一个类似的main的入口函数
def Pod.sourceLookEvnMain
puts " ***** Source Look Begin *******"
isJenkis = ENV.fetch('IS_JENKINS', '0')
puts "isJenkis=#{isJenkis}"
if isJenkis == '0' # jenkin不需要这个功能
sourceOpen = false
onlyInRuntime = true
sourcePodConfigPath = "sourcePodConfig.rb"
if File.exist?(sourcePodConfigPath)
sourcePodConfig = eval(File.read(sourcePodConfigPath))
sourceOpen = sourcePodConfig['sourceOpen'] if sourcePodConfig.key?('sourceOpen')
onlyInRuntime = sourcePodConfig['onlyInRuntime'] if sourcePodConfig.key?('onlyInRuntime')
else
File.open(sourcePodConfigPath, "w") do |aFile|
aFile.puts("# SourceOpen是总开关; OnlyInRuntime为true的时候,不增加SourcePod工程,只拷贝代码到目录,运行时可以进入源码")
aFile.puts("{")
aFile.puts("'sourceOpen' => #{sourceOpen},")
aFile.puts("'onlyInRuntime' => #{onlyInRuntime},")
aFile.puts("}")
end
end
puts "sourceOpen=#{sourceOpen}"
puts "onlyInRuntime=#{onlyInRuntime}"
Pod.sourceLook(sourceOpen, onlyInRuntime, @@targetName, @@workspacePath)
end
puts " ***** Source Look End *******"
end
# 程序开始
Pod.sourceLookEvnMain
最后在podFile里面加上这句,然后就可以在每次pod install
的时候,自动下载并集成需要看源码的库了。
post_install do |installer|
# 启用二进制看源码脚本
instance_eval File.read("/scripts/sourcePod.rb")
end
总的源码地址 https://github.com/Ekulelu/sourcePod 。
欢迎各位大佬点赞加关注。