1. 目标与背景
当前公司有两个产品,除了部分资源外两者差异不大,故放在同一个工程中,用多Target的方式来管理。在维护过程中,会遇到新增的文件或图片等资源在其中一个Target缺失的情况,如果每次都把两个产品都验证一次,工作量较大。
于是想到,所有的配置都在.project文件中记录,包括每个Target各包含有那些资源,可以通过解析.project 的方式来对两者包含的资源进行检查,并在每次编译前进行检查,以及时发现问题。
经过1周时间对这个思路进行了实践。目前已经能够检查包括编译文件、frameworks和copy resources(下图工程中的配置)以及.xcasset中资源的检查,并可以设置忽略的资源列表,下面记录一下自己的实践历程。
2. 实现方法
2.1 .pbxproj 文件介绍
.prxproj 文件本质上是一种旧风格的 Property List 文件。
其结构用我们熟悉的JSON格式可以把整个文件的表示为下面形式。
{
"archiveVersion" : "1",
"classes" : {
},
"objects" : {
"0C3E934A20280D7E00C7CF6B" : {
"fileRef" : "0C3E934920280D7E00C7CF6B",
"isa" : "PBXBuildFile"
},
...
...
},
"objectVersion" : "46",
"rootObject" : "8C7D6FB81A709259009D5B46"
}
其中最重要的是objects字段,里面包含了所有的配置。下面以Compile Sources中的main.m文件来管中窥豹看一下.pbxproj的组织方式。
首先找到rootObject 的ID,其在字典的最外层定义,其代表的是该工程的根节点。
在objects中搜索该ID,找到其定义。接着找到其所包含的Target,这里指向的是Target的ID.
这里我们以第一个Target为例,以同样的方式,搜索其ID,找到其定义:
找到其BuildPhase配置项:
在其定义中找到 main.m文件
找到其定义:
其文件定义:
可以看出,其所有的资源都会有一个ID值来标识,这个ID值在整个文件中是唯一的,以此来组织起整个逻辑。
.pbxproj的的详细解释可以参考下面两篇文章:
xcode project file format
Let's talk about project pbxproj
2.2 .pbxproj 文件的解析
因为自己对python比较熟悉,所以刚开始就想找一个用python解析的库,于是找到了mod-pbxproj,但看了文档之后,发现提供的API太少,无法获取编译文件列表等数据,故无法使用。
后面找到xcodeproj,其是CocoaPods 写的 一个Ruby 解析库,可以满足需求,但这意味着自己也要用ruby来完成脚本。好在ruby和python一样是脚本语言,有很多相通的地方,学习起来难度也不大。
脚本中使用到的基本方法如下:
# 解析.project文件
project = Xcodeproj::Project.open(project_path)
# 获取到target,其中target_name_first是要取得的target的名称
target_first = project.targets.select { |a_target| a_target.name.eql?(target_name_first)}
# 获取Compile Sources
phase = target.source_build_phase
# 获取Link Binary With Libraries
phase = target.frameworks_build_phase
# 获取Copy Bundle Resources
phase = target.resources_build_phase
2.3 脚本实现
基本思路是,通过2.2中的方法,获取到对应的文件列表,然后对列表进行对比,找出其中的不同,并设置相应的忽略文件列表,来应对不同Target可能有的差异。
主要具体实现如下:
从Target中取得文件路径:
def file_arr_for_target(target, class_obj)
if class_obj == $pbx_sources_class
phase = target.source_build_phase
elsif class_obj == $pbx_frameworks_class
phase = target.frameworks_build_phase
elsif class_obj == $pbx_resources_class
phase = target.resources_build_phase
else
raise "unknow recognize class"
end
# puts phase
file_arr = Array.new
phase.files.to_a.each do |pbx_build_file|
begin
if pbx_build_file.file_ref.is_a?(Xcodeproj::Project::Object::PBXVariantGroup)
pbx_build_file.file_ref.children.each do |item|
file_arr << item.real_path.to_s
end
else
file_arr << pbx_build_file.file_ref.real_path.to_s
end
rescue
# 部分值不是PBXVariantGroup类,也不是PBXFileReference 类,会处理失败走到这里,对比源文件为空值,暂不处理。
next
end
end
return file_arr
end
这里在实际测试的时候遇到两个问题:
1)在取文件路径的时候,部分配置的fileRef为空,导致最终的路径也是空值,最终发现其在源文件中也是空的,原因暂时还不清楚,如下图。这里就先用rescue进行保护,不做进一步处理。
2)本地化过的文件,取值方式与其他不同,因为其相对于其他文件,又多了一层,需要通过遍历的方式去取得相应真正的资源文件,处理如下:
if pbx_build_file.file_ref.is_a?(Xcodeproj::Project::Object::PBXVariantGroup)
pbx_build_file.file_ref.children.each do |item|
file_arr << item.real_path.to_s
end
3).xcasset中的具体资源,未在.pbxproj中配置。但其中的图片也是检查的重点。看了相关的介绍,其本质上是文件夹的集合。故最终通过文件遍历的方式来进行检查。
def get_items_arr_in_folder(folder_path)
items_arr = Array.new
Dir.foreach(folder_path) do |file|
if file == "." or file == ".." or file == ".DS_Store"
next
end
path = File.join folder_path, file
items_arr << path
if File.directory? path
items_arr += get_items_arr_in_folder path
end
end
items_arr
end
def get_relative_paths_arr_in_folder(folder_path)
paths_arr = get_items_arr_in_folder folder_path
paths_arr.map do |path|
path.slice! folder_path
path
end
end
def verify_assets(first_asset, last_asset)
first_asset_list = get_relative_paths_arr_in_folder first_asset
last_asset_list = get_relative_paths_arr_in_folder last_asset
puts "\n--------\ncount:#{first_asset_list.length}, #{last_asset_list.length}\n--------\n"
abnormal_list = first_asset_list - last_asset_list - $asset_ignore_keys
reverse_abnormal_list = last_asset_list - first_asset_list - $asset_reverse_ignore_keys
return abnormal_list, reverse_abnormal_list
end
Asset Catalog Format Reference
2.4 工程集成
为了方便能及时发现问题,故将这些检查项集成到工程中,每次编译前先进行检查,方法如下:
1)新建一个run script,重命名为TargetVerify。
注意:一般创建的run script 会被放在最后,这里的执行顺序是按Build Phase中的排列来的,我们希望它在编译前执行,所以需要把它拖动到Compile Sources前面.
2)在其中填入执行ruby脚本的shell命令
#!/bin/sh
# 将此文件里面的命令放到 Build Phases -> Run Script 脚本中
echo "start verify target..."
pwd
declare -a cmd_list=("ruby ./script/target_verify/target_verify.rb ./xxx.xcodeproj <#target name first#> <#target name last#>"
"ruby ./script/target_verify/asset_verify.rb ./xxxx/xxxx.xcassets ./xxxx/xxx.xcassets")
for cmd in "${cmd_list[@]}"
do
eval "$cmd"
if [ $? -ne 0 ]
then
echo "FAILED"
exit 1
fi
done
echo "finished target verify and no issue found"
这样就配置好了,在运行或编译时,如果脚本运行不通过,就会直接报编译失败。
具体实现的脚本已上传到github,希望对大家有所帮助。
target_verify