1. 目標與背景
當前公司有兩個產(chǎn)品徙硅,除了部分資源外兩者差異不大,故放在同一個工程中焰情,用多Target的方式來管理陌凳。在維護過程中,會遇到新增的文件或圖片等資源在其中一個Target缺失的情況烙样,如果每次都把兩個產(chǎn)品都驗證一次冯遂,工作量較大。
于是想到谒获,所有的配置都在.project文件中記錄,包括每個Target各包含有那些資源壁却,可以通過解析.project 的方式來對兩者包含的資源進行檢查批狱,并在每次編譯前進行檢查,以及時發(fā)現(xiàn)問題展东。
經(jīng)過1周時間對這個思路進行了實踐赔硫。目前已經(jīng)能夠檢查包括編譯文件、frameworks和copy resources(下圖工程中的配置)以及.xcasset中資源的檢查盐肃,并可以設置忽略的資源列表爪膊,下面記錄一下自己的實踐歷程权悟。
2. 實現(xiàn)方法
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榔昔,其在字典的最外層定義,其代表的是該工程的根節(jié)點瘪菌。
在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许蓖,但看了文檔之后,發(fā)現(xiàn)提供的API太少调衰,無法獲取編譯文件列表等數(shù)據(jù)膊爪,故無法使用。
后面找到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 腳本實現(xiàn)
基本思路是,通過2.2中的方法竹勉,獲取到對應的文件列表飞盆,然后對列表進行對比,找出其中的不同孽水,并設置相應的忽略文件列表女气,來應對不同Target可能有的差異主卫。
主要具體實現(xiàn)如下:
從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為空术荤,導致最終的路徑也是空值瓣戚,最終發(fā)現(xiàn)其在源文件中也是空的子库,原因暫時還不清楚刚照,如下圖无畔。這里就先用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 工程集成
為了方便能及時發(fā)現(xiàn)問題,故將這些檢查項集成到工程中赶促,每次編譯前先進行檢查芳杏,方法如下:
1)新建一個run script,重命名為TargetVerify泊脐。
注意:一般創(chuàng)建的run script 會被放在最后秕铛,這里的執(zhí)行順序是按Build Phase中的排列來的,我們希望它在編譯前執(zhí)行谨湘,所以需要把它拖動到Compile Sources前面.
2)在其中填入執(zhí)行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"
這樣就配置好了,在運行或編譯時乖仇,如果腳本運行不通過这敬,就會直接報編譯失敗崔涂。
具體實現(xiàn)的腳本已上傳到github冷蚂,希望對大家有所幫助。
target_verify