現(xiàn)在,ccocoapods已經(jīng)成為iOS工程的標(biāo)配,在這個(gè)工具的開發(fā)過程中,開源了一個(gè)專門用來操作工程的.xcodeproj文件的ruby庫Xcodeproj,利用它,我們自己也可以用ruby腳本來添加和刪除工程中的文件等,做到自動(dòng)化操作
問題的提出
在我們的組件化過程中,是通過子工程的方式來建立業(yè)務(wù)組件的.可能有人會(huì)問,為什么不用pod來建立業(yè)務(wù)組件呢?其實(shí)當(dāng)時(shí)也有考慮過,pod更適合已經(jīng)比較成熟的組件,而我們現(xiàn)在的業(yè)務(wù)變動(dòng)還很大,并且pod在開發(fā)的過程中,新增文件什么的,還要運(yùn)行下pod install才能運(yùn)行,綜合考慮,在業(yè)務(wù)早期,還是使用子工程的方式更便捷,能取得各方面的權(quán)衡
當(dāng)我們采用子工程來建立業(yè)務(wù)組件,那么通常建立了一個(gè)模板化的組件工程(可以通過多種方式建立,此處不述了)后,還要做4件事,才能添加到主工程中,如下圖所示:

- 拖動(dòng)工程到主工程中
- 設(shè)置Target Dependencies,因?yàn)槊總€(gè)組件工程有個(gè)資源bundle的target,如果不設(shè)置依賴,當(dāng)他們改動(dòng)時(shí)候,主工程并不會(huì)去編譯它們.
- 設(shè)置Link Binary With Libraries
- 拷貝資源
雖然事情不到,總歸還是覺得建立組件和將組件添加到工程,是割裂的,難免有遺憾.
我們每天都使用的cocoapods,就是一個(gè)腳本就建立好了工程和設(shè)置完成了依賴,于是就想著用ruby借助xcodeproj庫來將建立工程和設(shè)置結(jié)合起來.
xcodeproj介紹
Xcodeproj是cocoapods團(tuán)隊(duì)在寫cocoapods過程中開源出來的庫,它的工程代碼賞心悅目,結(jié)構(gòu)化程度很高.并且還提供了很多單元測(cè)試.不過遺憾的是,它沒有個(gè)詳細(xì)的使用文檔,加上使用了在國內(nèi)比較小眾的ruby語言編寫的,所以使用起來,還是頗費(fèi)一番周折的.
網(wǎng)上有不少中文的使用教程,它們都是簡單的添加.h或者.m文件等等,相對(duì)來說比較簡單.對(duì)于怎么給工程添加子工程,倒是沒有人敘述過.無奈只能自己各種嘗試,還是不得要領(lǐng),又想到,cocoapods怎么是怎么做到的呢?
于是為了解決我的這個(gè)問題,我將cocoapods源碼也下載下來進(jìn)行閱讀分析,其實(shí)最理想還是能調(diào)試就好了,但無奈,對(duì)ruby的熟悉度有限,再加上這個(gè)工程著實(shí)龐大,還是沒辦法.不過在閱讀源碼的過程中還是有很大的收獲的.
各種嘗試
從cocoapods源碼的閱讀過程中,發(fā)現(xiàn)了xcodeproj庫竟然有個(gè)file_references_factory.rb文件,在其中
def new_reference(group, path, source_tree)
ref = case File.extname(path).downcase
when '.xcdatamodeld'
new_xcdatamodeld(group, path, source_tree)
when '.xcodeproj'
new_subproject(group, path, source_tree)
else
new_file_reference(group, path, source_tree)
end
configure_defaults_for_file_reference(ref)
ref
end
然后在group.rb中
def new_reference(path, source_tree = :group)
FileReferencesFactory.new_reference(self, path, source_tree)
end
難道問題這么簡單,直接就可以使用啊
趕緊寫段代碼試試,命名為createProjectDependcy.rb
require 'xcodeproj'
def addSubProj
projectPath = "DemoMain.xcodeproj"
project = Xcodeproj::Project.open(projectPath)
project.main_group.new_reference("Modules/YLBusiness1/YLBusiness1.xcodeproj",:group)
project.save
end
addSubProj
在終端執(zhí)行
ruby ./createProjectDependcy.rb
再看看工程

成功了!!
看來問題其實(shí)很簡單啊,就按普通文件的方式來添加就好了,這方法內(nèi)部,已經(jīng)針對(duì)是.xcodeproj處理了
不過當(dāng)我們這個(gè)時(shí)候手動(dòng)添加 Target Dependencies或者Link Binary的時(shí)候,xcode會(huì)crash掉!!

這還沒完,這種方式添加的子工程,當(dāng)我們?cè)趚code中刪除的時(shí)候,會(huì)導(dǎo)致工程中的products這個(gè)group中的文件消失了!!
刪除前

刪除后

xcodeproj竟然有這么嚴(yán)重的問題,一直沒有人反饋過........
怎么辦?
難道這個(gè)問題解決不了了??
解決方式
既然xcodeproj這段代碼是有問題的,那么要解決問題,只能我們自己修改了.
首先,既然上面的代碼能夠添加成功,那么說明總體上應(yīng)該是沒啥問題的,只是代碼中有些問題,至于問題出在哪,目前還不清楚
我們首先用手動(dòng)拖動(dòng)的形式,來給主工程添加子工程,然后將project.pbxproj文件保存下來,再通過上面代碼的方式來添加,也把project.pbxproj文件保存下來,兩個(gè)進(jìn)行對(duì)比,看看有什么不同的地方
具體的對(duì)比過程是乏味冗長的,通過對(duì)比發(fā)現(xiàn),手動(dòng)拖動(dòng)生成的,多了一個(gè)不在xcode工程可視化中出現(xiàn)的group
4C5117FE2255AD3500914224 /* Products */ = {
isa = PBXGroup;
children = (
4C5118042255AD3500914224 /* libBusiness1.a */,
4C5118062255AD3500914224 /* Business1Tests.xctest */,
4C5118082255AD3500914224 /* Business1Bundle.bundle */,
);
name = Products;
sourceTree = "<group>";
};
在projectReferences的ProductGroup中使用的是上面建立的group
projectReferences = (
{
ProductGroup = 4C5117FE2255AD3500914224 /* Products */;
ProjectRef = 4C5117FD2255AD3500914224 /* YLBusiness1.xcodeproj */;
},
而通過xcodeproj這段代碼生成的是在原來的products這個(gè)group下添加了引用,以下 4CFBA7F42099B5BC00E39A19這個(gè)Products group是原來存在的!
4CFBA7F42099B5BC00E39A19 /* Products */ = {
isa = PBXGroup;
children = (
4C51181C2255AEF600914224 /* DemoMain.app */,
4C51181D2255AEF600914224 /* DemoMainTests.xctest */,
4C51181E2255AEF600914224 /* DemoMainUITests.xctest */,
D5EEDB2A75FE09CBA854F57C /* libBusiness1.a */,
73D1A8603F14BDF13804CD30 /* Business1Tests.xctest */,
A5CD1082BFBEC674BC72C28A /* Business1Bundle.bundle */,
);
name = Products;
sourceTree = "<group>";
};
使用的時(shí)候
{
ProductGroup = 4CFBA7F42099B5BC00E39A19 /* Products */;
ProjectRef = A06BA4184D5F5B2B56B8D071 /* YLBusiness1.xcodeproj */;
},
問題就是出在這里了,添加子工程,不應(yīng)該重用工程原來的products group,重用了,導(dǎo)致刪除的時(shí)候,會(huì)清掉這個(gè)group.
至于為什么添加依賴會(huì)導(dǎo)致xcode crash,從這里的分析看,應(yīng)該也是和這個(gè)projectReferences有關(guān).
既然知道問題所在,那么我么就可以想辦法解決了
從源碼中可看到,添加子工程的方法是FileReferencesFactory中的 new_subproject方法
def new_subproject(group, path, source_tree)
ref = new_file_reference(group, path, source_tree)
ref.include_in_index = nil
product_group_ref = find_products_group_ref(group, true)
subproj = Project.open(path)
subproj.products_group.files.each do |product_reference|
container_proxy = group.project.new(PBXContainerItemProxy)
container_proxy.container_portal = ref.uuid
container_proxy.proxy_type = Constants::PROXY_TYPES[:reference]
container_proxy.remote_global_id_string = product_reference.uuid
container_proxy.remote_info = 'Subproject'
reference_proxy = group.project.new(PBXReferenceProxy)
extension = File.extname(product_reference.path)[1..-1]
reference_proxy.file_type = Constants::FILE_TYPES_BY_EXTENSION[extension]
reference_proxy.path = product_reference.path
reference_proxy.remote_ref = container_proxy
reference_proxy.source_tree = 'BUILT_PRODUCTS_DIR'
product_group_ref << reference_proxy
end
attribute = PBXProject.references_by_keys_attributes.find { |attrb| attrb.name == :project_references }
project_reference = ObjectDictionary.new(attribute, group.project.root_object)
project_reference[:project_ref] = ref
project_reference[:product_group] = product_group_ref
group.project.root_object.project_references << project_reference
ref
end
這段代碼中的
product_group_ref = find_products_group_ref(group, true)
是獲取工程中原來的Products group,這個(gè)造成了上面的問題,所以,我們要修改它
稍微修改下此代碼,當(dāng)然,我們這里,因?yàn)椴⒉皇窃谠瓉泶a的類上寫,需要把一些類的前綴都加上
def add_new_subProj(group, path, source_tree)
ref = Xcodeproj::Project::FileReferencesFactory.send(:new_file_reference, group, path, source_tree)
ref.include_in_index = nil
ref.name = Pathname(path).basename.to_s
#product_group_ref = group.new_group("Products") 這種方式創(chuàng)建的group會(huì)掛載在main_group下,這會(huì)導(dǎo)致刪除的時(shí)候,出現(xiàn)一個(gè)空的group,而手動(dòng)拖動(dòng)的就不會(huì),所以改為group.project.new(Xcodeproj::Project::PBXGroup)
#從xcode手動(dòng)添加子工程來看,它要?jiǎng)?chuàng)建一個(gè)包含子工程的group
product_group_ref = group.project.new(Xcodeproj::Project::PBXGroup) #find_products_group_ref(group, true)
product_group_ref.name = "Products" #手動(dòng)拖動(dòng)創(chuàng)建的group名字是Products,所以我們這里新創(chuàng)建的名字也賦值為products
subproj = Xcodeproj::Project.open(path)
subproj.products_group.files.each do |product_reference|
container_proxy = group.project.new(Xcodeproj::Project::PBXContainerItemProxy)
container_proxy.container_portal = ref.uuid
container_proxy.proxy_type = Xcodeproj::Constants::PROXY_TYPES[:reference]
container_proxy.remote_global_id_string = product_reference.uuid
container_proxy.remote_info = 'Subproject'
reference_proxy = group.project.new(Xcodeproj::Project::PBXReferenceProxy)
extension = File.extname(product_reference.path)[1..-1]
reference_proxy.file_type = Xcodeproj::Constants::FILE_TYPES_BY_EXTENSION[extension]
reference_proxy.path = product_reference.path
reference_proxy.remote_ref = container_proxy
reference_proxy.source_tree = 'BUILT_PRODUCTS_DIR'
product_group_ref << reference_proxy
end
attribute = Xcodeproj::Project::PBXProject.references_by_keys_attributes.find { |attrb| attrb.name == :project_references }
project_reference = Xcodeproj::Project::ObjectDictionary.new(attribute, group.project.root_object)
project_reference[:project_ref] = ref
project_reference[:product_group] = product_group_ref
group.project.root_object.project_references << project_reference
ref
end
def addSubProjTest
projectPath = "DemoMain.xcodeproj"
project = Xcodeproj::Project.open(projectPath)
add_new_subProj(project.main_group,"Modules/YLBusiness1/YLBusiness1.xcodeproj",:group)
project.save
end
addSubProjTest
測(cè)試一下,完成了!!
刪除,添加dependcy等操作都完全可以了!!
到這里,其實(shí)已經(jīng)可以添加了
當(dāng)然了,上面的代碼依然不完美,

我們看到,手動(dòng)拖動(dòng)進(jìn)來的 ,建立的PBXContainerItemProxy對(duì)象的remote_info賦值為的是子工程的target的名字,而我們上面代碼創(chuàng)建的是
container_proxy.remote_info = 'Subproject'
雖然不改,不會(huì)出錯(cuò)什么的,以后一旦在xcode中添加link等等,xcode會(huì)自動(dòng)修正這個(gè)值,但,我們?cè)诮⒌臅r(shí)候,就做到和xcode的默認(rèn)行為一致會(huì)更好.
增加一個(gè)方法
#根據(jù)productReference 找到其對(duì)應(yīng)的target
def get_target_with_productReference(productReference,project)
project.native_targets.each { |target|
if target.product_reference == productReference
puts "target = #{target}"
return target
end
}
end
然后修改上面代碼中的
#container_proxy.remote_info = 'Subproject'
subproj_native_target = get_target_with_productReference(product_reference,subproj)
container_proxy.remote_info = subproj_native_target.name
完美!
添加Link Binary With Libraries
核心是調(diào)用
native_target.frameworks_build_phase.add_file_reference(reference_proxy)
添加依賴
native_target.dependencies << target_dependency
添加資源
native_target.resources_build_phase.files << build_file
我將上面的綜合起來,生成一個(gè)類
class SubProjectDispose
attr_reader :mainproj_path, :subproj_path, :main_project ,:sub_project,:subproj_ref_in_mainproj,:subproj_product_group_ref
def initialize(mainproj_path,subproj_path)
@mainproj_path = mainproj_path
@subproj_path = subproj_path
@main_project = Xcodeproj::Project.open(mainproj_path)
end
#根據(jù)productReference 找到其對(duì)應(yīng)的target
def get_target_with_productReference(productReference,project)
project.native_targets.each { |target|
if target.product_reference == productReference
puts "target = #{target}"
return target
end
}
end
def add_new_subProj(group, path, source_tree)
@subproj_ref_in_mainproj = Xcodeproj::Project::FileReferencesFactory.send(:new_file_reference, group, path, :group)
@subproj_ref_in_mainproj.include_in_index = nil
@subproj_ref_in_mainproj.name = Pathname(subproj_path).basename.to_s
#product_group_ref = group.new_group("Products") 這種方式創(chuàng)建的group會(huì)掛載在main_group下,這會(huì)導(dǎo)致刪除的時(shí)候,出現(xiàn)一個(gè)空的group,而手動(dòng)拖動(dòng)的就不會(huì),所以改為group.project.new(Xcodeproj::Project::PBXGroup)
#從xcode手動(dòng)添加子工程來看,它要?jiǎng)?chuàng)建一個(gè)包含子工程的group
product_group_ref = group.project.new(Xcodeproj::Project::PBXGroup)
product_group_ref.name = "Products" #手動(dòng)拖動(dòng)創(chuàng)建的group名字就是Products
@sub_project = Xcodeproj::Project.open(path) #打開子工程
@sub_project.products_group.files.each do |product_reference|
puts "product_reference = #{product_reference},name = #{product_reference.name},path = #{product_reference.path}"#product_reference = FileReference,name = ,path = ChencheMaBundle.bundle reference_proxy.file_type = wrapper.plug-in
container_proxy = group.project.new(Xcodeproj::Project::PBXContainerItemProxy)
container_proxy.container_portal = @subproj_ref_in_mainproj.uuid
container_proxy.proxy_type = Xcodeproj::Constants::PROXY_TYPES[:reference]
container_proxy.remote_global_id_string = product_reference.uuid
#container_proxy.remote_info = 'Subproject' #這里和手動(dòng)添加的是不一致的,手動(dòng)的,這里是targets的名字
subproj_native_target = get_target_with_productReference(product_reference,@sub_project)
container_proxy.remote_info = subproj_native_target.name
reference_proxy = group.project.new(Xcodeproj::Project::PBXReferenceProxy)
extension = File.extname(product_reference.path)[1..-1]
puts("product_reference.path = #{product_reference.path}")
if extension == "bundle"
#xcodeproj的定義中,后綴為bundle的對(duì)應(yīng)的是'bundle' => 'wrapper.plug-in',但是我們手動(dòng)拖動(dòng)添加的是 'wrapper.cfbundle'
reference_proxy.file_type = 'wrapper.cfbundle'
elsif
reference_proxy.file_type = Xcodeproj::Constants::FILE_TYPES_BY_EXTENSION[extension]
end
reference_proxy.path = product_reference.path
reference_proxy.remote_ref = container_proxy
reference_proxy.source_tree = 'BUILT_PRODUCTS_DIR'
product_group_ref << reference_proxy
end
@subproj_product_group_ref = product_group_ref
attribute = Xcodeproj::Project::PBXProject.references_by_keys_attributes.find { |attrb| attrb.name == :project_references }
project_reference = Xcodeproj::Project::ObjectDictionary.new(attribute, group.project.root_object)
project_reference[:project_ref] = @subproj_ref_in_mainproj
project_reference[:product_group] = product_group_ref
group.project.root_object.project_references << project_reference
product_group_ref
end
def add_subproject()
add_new_subProj(self.main_project.main_group,self.subproj_path,:group)
add_frameworks_build_phase()
add_dependencies()
add_copy_bundle_resource()
end
def add_frameworks_build_phase()
puts("self.subproj_product_group_ref = #{self.subproj_product_group_ref}")
reference_proxys = self.subproj_product_group_ref.children.grep(Xcodeproj::Project::PBXReferenceProxy)
reference_proxys.each do |reference_proxy|
if (reference_proxy.file_type == Xcodeproj::Constants::FILE_TYPES_BY_EXTENSION["a"]) || (reference_proxy.file_type == Xcodeproj::Constants::FILE_TYPES_BY_EXTENSION["bundle"]) then
puts("reference_proxy = #{reference_proxy}")
native_target = self.main_project.native_targets.first
native_target.frameworks_build_phase.add_file_reference(reference_proxy)
end
end
end
def add_dependencies()
#添加target的dependencies,需要的是子工程的target
# @main_project = Xcodeproj::Project.open(self.mainproj_path)
# @sub_project = Xcodeproj::Project.open(self.subproj_path) #打開子工程
# @subproj_ref_in_mainproj = @main_project.objects_by_uuid['0CC9D5720EABC826EE0ECB3B'] #使用uuid可以獲取任何一個(gè)對(duì)象
native_target = self.main_project.native_targets.first
@sub_project.native_targets.each do |nativeTarget|
if (nativeTarget.product_type == Xcodeproj::Constants::PRODUCT_TYPE_UTI[:static_library]) || (nativeTarget.product_type == Xcodeproj::Constants::PRODUCT_TYPE_UTI[:bundle]) then
puts("nativeTarget.productType = #{nativeTarget.product_type}")
container_proxy = self.main_project.new(Xcodeproj::Project::PBXContainerItemProxy)
container_proxy.container_portal = @subproj_ref_in_mainproj.uuid
container_proxy.proxy_type = Xcodeproj::Constants::PROXY_TYPES[:native_target] #1
container_proxy.remote_global_id_string = nativeTarget.uuid
container_proxy.remote_info = nativeTarget.product_name
target_dependency = @main_project.new(Xcodeproj::Project::PBXTargetDependency)
target_dependency.name = nativeTarget.name
target_dependency.target_proxy = container_proxy
native_target.dependencies << target_dependency
end
end
end
def add_copy_bundle_resource()
puts("add_copy_bundle_resource")
# @main_project = Xcodeproj::Project.open(self.mainproj_path)
# @sub_project = Xcodeproj::Project.open(self.subproj_path) #打開子工程
# @subproj_product_group_ref = @main_project.objects_by_uuid['37A142A1F74B773563256D88'] #使用uuid可以獲取任何一個(gè)對(duì)象
native_target = self.main_project.native_targets.first
build_file = @main_project.new(Xcodeproj::Project::PBXBuildFile)
reference_proxys = self.subproj_product_group_ref.children.grep(Xcodeproj::Project::PBXReferenceProxy)
reference_proxys.each do |reference_proxy|
puts("reference_proxy.file_type = #{reference_proxy.file_type}")
if reference_proxy.file_type == 'wrapper.cfbundle' && reference_proxy.path.include?(".bundle") then
puts("reference_proxy = #{reference_proxy}")
build_file.file_ref = reference_proxy;
native_target.resources_build_phase.files << build_file
end
end
end
def close()
self.main_project.save()
end
end
使用的時(shí)候
dispose =SubProjectDispose.new("DemoMain.xcodeproj","Modules/YLBusiness1/YLBusiness1.xcodeproj")
dispose.add_subproject()
dispose.close()
后記
實(shí)在沒想到xcodeproj竟然存在這么嚴(yán)重的一個(gè)bug,不過好在它們的代碼可讀性非常好,雖然不能單點(diǎn)調(diào)試,不過閱讀起來,也基本上大差不差了
在使用的時(shí)候,多用Dash查看文檔,多用git的文件修改對(duì)比來進(jìn)行分析,將會(huì)大大的增加對(duì)xcodeproj格式和操作的理解,從而寫出需要的代碼來