iOS 两个Target中资源文件的对比

779 阅读3分钟

1. 目标与背景

当前公司有两个产品,除了部分资源外两者差异不大,故放在同一个工程中,用多Target的方式来管理。在维护过程中,会遇到新增的文件或图片等资源在其中一个Target缺失的情况,如果每次都把两个产品都验证一次,工作量较大。

于是想到,所有的配置都在.project文件中记录,包括每个Target各包含有那些资源,可以通过解析.project 的方式来对两者包含的资源进行检查,并在每次编译前进行检查,以及时发现问题。

经过1周时间对这个思路进行了实践。目前已经能够检查包括编译文件、frameworks和copy resources(下图工程中的配置)以及.xcasset中资源的检查,并可以设置忽略的资源列表,下面记录一下自己的实践历程。

image.png

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.

image.png 这里我们以第一个Target为例,以同样的方式,搜索其ID,找到其定义: image.png 找到其BuildPhase配置项: image.png

在其定义中找到 main.m文件 image.png

找到其定义: image.png 其文件定义: image.png

可以看出,其所有的资源都会有一个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进行保护,不做进一步处理。 image.png

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前面. image.png image.png 2)在其中填入执行ruby脚本的shell命令 image

#!/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"

这样就配置好了,在运行或编译时,如果脚本运行不通过,就会直接报编译失败。

3. Github

完整的脚本实现已上传到github,希望对大家有所帮助。 target_verify