现在,ccocoapods已经成为iOS工程的标配,在这个工具的开发过程中,开源了一个专门用来操作工程的.xcodeproj文件的ruby库Xcodeproj,利用它,我们自己也可以用ruby脚本来添加和删除工程中的文件等,做到自动化操作
问题的提出
在我们的组件化过程中,是通过子工程的方式来建立业务组件的.可能有人会问,为什么不用pod来建立业务组件呢?其实当时也有考虑过,pod更适合已经比较成熟的组件,而我们现在的业务变动还很大,并且pod在开发的过程中,新增文件什么的,还要运行下pod install才能运行,综合考虑,在业务早期,还是使用子工程的方式更便捷,能取得各方面的权衡
当我们采用子工程来建立业务组件,那么通常建立了一个模板化的组件工程(可以通过多种方式建立,此处不述了)后,还要做4件事,才能添加到主工程中,如下图所示:
- 拖动工程到主工程中
- 设置Target Dependencies,因为每个组件工程有个资源bundle的target,如果不设置依赖,当他们改动时候,主工程并不会去编译它们.
- 设置Link Binary With Libraries
- 拷贝资源
虽然事情不到,总归还是觉得建立组件和将组件添加到工程,是割裂的,难免有遗憾.
我们每天都使用的cocoapods,就是一个脚本就建立好了工程和设置完成了依赖,于是就想着用ruby借助xcodeproj库来将建立工程和设置结合起来.
xcodeproj介绍
Xcodeproj是cocoapods团队在写cocoapods过程中开源出来的库,它的工程代码赏心悦目,结构化程度很高.并且还提供了很多单元测试.不过遗憾的是,它没有个详细的使用文档,加上使用了在国内比较小众的ruby语言编写的,所以使用起来,还是颇费一番周折的.
网上有不少中文的使用教程,它们都是简单的添加.h或者.m文件等等,相对来说比较简单.对于怎么给工程添加子工程,倒是没有人叙述过.无奈只能自己各种尝试,还是不得要领,又想到,cocoapods怎么是怎么做到的呢?
于是为了解决我的这个问题,我将cocoapods源码也下载下来进行阅读分析,其实最理想还是能调试就好了,但无奈,对ruby的熟悉度有限,再加上这个工程着实庞大,还是没办法.不过在阅读源码的过程中还是有很大的收获的.
各种尝试
从cocoapods源码的阅读过程中,发现了xcodeproj库竟然有个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
在终端执行
ruby ./createProjectDependcy.rb
再看看工程
成功了!!
看来问题其实很简单啊,就按普通文件的方式来添加就好了,这方法内部,已经针对是.xcodeproj处理了
不过当我们这个时候手动添加 Target Dependencies或者Link Binary的时候,xcode会crash掉!!
这还没完,这种方式添加的子工程,当我们在xcode中删除的时候,会导致工程中的products这个group中的文件消失了!!
删除前
删除后
xcodeproj竟然有这么严重的问题,一直没有人反馈过........
怎么办?
难道这个问题解决不了了??
解决方式
既然xcodeproj这段代码是有问题的,那么要解决问题,只能我们自己修改了.
首先,既然上面的代码能够添加成功,那么说明总体上应该是没啥问题的,只是代码中有些问题,至于问题出在哪,目前还不清楚
我们首先用手动拖动的形式,来给主工程添加子工程,然后将project.pbxproj文件保存下来,再通过上面代码的方式来添加,也把project.pbxproj文件保存下来,两个进行对比,看看有什么不同的地方
具体的对比过程是乏味冗长的,通过对比发现,手动拖动生成的,多了一个不在xcode工程可视化中出现的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这个group下添加了引用,以下 4CFBA7F42099B5BC00E39A19这个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>";
};
使用的时候
{
ProductGroup = 4CFBA7F42099B5BC00E39A19 /* Products */;
ProjectRef = A06BA4184D5F5B2B56B8D071 /* YLBusiness1.xcodeproj */;
},
问题就是出在这里了,添加子工程,不应该重用工程原来的products group,重用了,导致删除的时候,会清掉这个group.
至于为什么添加依赖会导致xcode crash,从这里的分析看,应该也是和这个projectReferences有关.
既然知道问题所在,那么我么就可以想办法解决了
从源码中可看到,添加子工程的方法是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,这个造成了上面的问题,所以,我们要修改它
稍微修改下此代码,当然,我们这里,因为并不是在原来代码的类上写,需要把一些类的前缀都加上
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") 这种方式创建的group会挂载在main_group下,这会导致删除的时候,出现一个空的group,而手动拖动的就不会,所以改为group.project.new(Xcodeproj::Project::PBXGroup)
#从xcode手动添加子工程来看,它要创建一个包含子工程的group
product_group_ref = group.project.new(Xcodeproj::Project::PBXGroup) #find_products_group_ref(group, true)
product_group_ref.name = "Products" #手动拖动创建的group名字是Products,所以我们这里新创建的名字也赋值为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
测试一下,完成了!!
删除,添加dependcy等操作都完全可以了!!
到这里,其实已经可以添加了
当然了,上面的代码依然不完美,
我们看到,手动拖动进来的 ,建立的PBXContainerItemProxy对象的remote_info赋值为的是子工程的target的名字,而我们上面代码创建的是
container_proxy.remote_info = 'Subproject'
虽然不改,不会出错什么的,以后一旦在xcode中添加link等等,xcode会自动修正这个值,但,我们在建立的时候,就做到和xcode的默认行为一致会更好.
增加一个方法
#根据productReference 找到其对应的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
核心是调用
native_target.frameworks_build_phase.add_file_reference(reference_proxy)
添加依赖
native_target.dependencies << target_dependency
添加资源
native_target.resources_build_phase.files << build_file
我将上面的综合起来,生成一个类
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
#根据productReference 找到其对应的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") 这种方式创建的group会挂载在main_group下,这会导致删除的时候,出现一个空的group,而手动拖动的就不会,所以改为group.project.new(Xcodeproj::Project::PBXGroup)
#从xcode手动添加子工程来看,它要创建一个包含子工程的group
product_group_ref = group.project.new(Xcodeproj::Project::PBXGroup)
product_group_ref.name = "Products" #手动拖动创建的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' #这里和手动添加的是不一致的,手动的,这里是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的对应的是'bundle' => 'wrapper.plug-in',但是我们手动拖动添加的是 '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可以获取任何一个对象
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可以获取任何一个对象
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
使用的时候
dispose =SubProjectDispose.new("DemoMain.xcodeproj","Modules/YLBusiness1/YLBusiness1.xcodeproj")
dispose.add_subproject()
dispose.close()
后记
实在没想到xcodeproj竟然存在这么严重的一个bug,不过好在它们的代码可读性非常好,虽然不能单点调试,不过阅读起来,也基本上大差不差了
在使用的时候,多用Dash查看文档,多用git的文件修改对比来进行分析,将会大大的增加对xcodeproj格式和操作的理解,从而写出需要的代码来