project.pbxproj
介绍
project.pbxproj
文件被包含于 Xcode 工程文件 .xcodeproj 之中,存储着 Xcode 工程的各项配置参数。它本质上是一种旧风格的 Property List 文件,历史可追溯到 NeXT 的 OpenStep,其可读性不如 xml 和 json。Property List 在苹果家族的历史上存在三种格式:OpenStep,XML 和 Binary,*除了 OpenStep 被废弃不支持写入以外,其余格式都提供 API 支持读写。该文件以明确的编码信息开头,通常是UTF-8。这意味着该文件在其开始时不能承载BOM(Byte Ordering Mark:即字节顺序标记,它是插入到以UTF-8、UTF16或UTF-32编码Unicode文件开头的特殊标记,用来识别Unicode文件的编码类型。对于UTF-8来说,BOM并不是必须的,因为BOM用来标记多字节编码文件的编码类型和字节顺序(big-endian或little-endian),否则解析将失败。与 json 最明显的差别是:数组用小括号括起来并用逗号隔开元素;字典用大括号括起来并用分号隔开键值对,键值之间用等号连接;二进制数据用尖括号<>
括起来:1234567数组:project.pbxproj => ( "1", "2", "3" )json => [ "1", "2", "3" ]字典:project.pbxproj => { "key" = "value"; ... }json => { "key" : "value", ... }对 Xcode 工程进行添加、删除文件或者配置编译参数等,实际上都是对
project.pbxproj
文件的修改。使用git diff
查看所做文件的修改,会发现唯一被修改的就是这个 project.pbxproj 文件,其他的文件并没有被修改。解析文件格式
根元素总共有 5 个键值对,Key 分为:
archiveVersion
,classes
,objectVersion
,objects
和rootObject
。上图中所有的配置对象都在objects
中,对象的Key 都为 UUID,对应的 Value 可当做是一个字典。这里的 key 是 UUID 作为交叉引用的索引,保证每个配置信息对象的唯一性。因为 UUID 根据主机Mac地址或(伪)随机数等加上时间戳生成(ps:UUID生成规则),避免了多人在同一时间段操作修改工程文件带来的问题。也就是说工程中每项配置对象都有个唯一的 UUID,然后其他配置对象想引用某个配置对象直接使用它的 UUID 即可。这就跟我们编程时使用指针指向某个对象的地址一样,其他对象的属性想引用它,只需要给属性传个指针地址就行了。
文件的引用由 PBXBuildFile 中属性 fileRef 指向 PBXFileReference,它有一个path属性标明文件路径,但需要注意的是这里的 path 显示的路径可能不是项目目录下的全路径,只是显示文件名,要得到文件全路径,需要使用 PBXFileReference 的 UUID 索引到 PBXGroup、PBXReferenceProxy等中一层层递归向上搜索路径来拼接成全路径。
objects
的键值对根据内容类型被分成了若干个 section,采用注释的方式分节也使得可读性更强。section 的数量跟工程有关,尤其是每个工程的 BuildPhase 和 Target 差别都很大,section 列表就是上图中objects
后的分支结构,显示在 Xcode 中能看见所有的公共配置信息都存在于 project.pbxproj 中。主要包含跟文件相关的 BuildFile,Group 和 FileReference,跟编译相关的 BuildPhase 和 Build Configuration(List),以及一些列 Target 和 TargetDependency等。着重介绍几个属性:
PBXProject
PBXProject
标识着整个工程,由根元素的rootObject
引入。
PBXBuildFile
PBXBuildFile
是文件类,被PBXBuildPhase
等作为文件包含或被引用的资源。PBXFileReference
PBXFileReference
用于跟踪项目引用的每一个外部文件,比如源代码文件、资源文件、库文件、生成目标文件等。PBXGroup
PBXGroup
用于组文件,或者嵌套组, 是 Xcode 中用来组织文件的一种方式,它对文件系统没有任何影响,无论你创建或者删除一个 Group,都不会导致 folder 的增加或者移除。 当然如果在你删除时选择 Move to Trash 就是另外一说了。在 Group 中的文件的关系, 不会与 folder 中的有什么冲突, 它只是 Xcode 为你提供的一种分离关注的方式。Group 之间的关系, 也是在
project.pbxproj
中定义的,这个文件中包含了 Xcode 工程中所有 File 和 Group 的关系。Group 在我们的工程中就是黄色的文件夹,而 Folder 是蓝色的文件夹(一般在 Xcode 工程中, 我们不会使用 Folder)。
PBXNativeTarget
PBXNativeTarget
就是工程中的Target ,它指定了一个用于产品(product), 并且包含了从工程中的一些文件中构建产品的命令。这些命令使用构建设置(build settings)和构建阶段(build phases)的方式来组织, 可以在 Xcode 编辑器中改变这些设置。如果工程中有多个target,都会在这个section中有所体现。PBXSourcesBuildPhase
PBXSourcesBuildPhase
用于构建阶段中编译源文件。
PBXResourcesBuildPhase
PBXResourcesBuildPhase
用于构建阶段需要复制的资源文件。文件格式具体分析详见 Xcode Project File Format。
操作 Property List 的途径
plutil命令提供处理 Property list 文件的能力,将Property list 文件转成 XML 格式命令如下:
1$ plutil -convert xml1 -s -r -o project.pbxproj.xml project.pbxproj在 Cocoa 的
NSPropertyListSerialization
也提供了类似的功能,更面向对象,其实plutil
和NSPropertyListSerialization
底层都是调用CoreFoundation
的CFPropertyList
相关的 API,所以功能类似。但两者在读入 project.pbxproj 文件时,字典中键值对的顺序会跟文件中原始的顺序不一致。这是因为字典为了实现快速查找会将 key 按序存储(比如字典序或用红黑树排序)。
大多数操作 project.pbxproj 文件工具的原理,也就是利用
plutil
转成 json 或 xml 后进行处理。操作 project.pbxproj 文件
Xcodeproj 是一个使用 Ruby 来创建和修改 Xcode 工程文件的工具. 属于Cocoapods的一个组件,Cocoapods 就是利用Xcodeproj组件,脚本化的管理任务和构造友好的Xcode库,它同时支持
Xcode workspaces (.xcworkspace)
、configuration files (.xcconfig)
和Xcode Scheme files (.xcscheme)
。安装
Xcodeproj 通过 RubyGems 安装,命令如下:
1$ [sudo] gem install xcodeproj源码分析
API文档地址,这里解析一下 Xcodeproj module 下 几个重要的类:
Project
Xcodeproj 组件用这个 class 来抽象 Xcode project 文件,可用这个类来操作已存在的Xcode project 文件,甚至可以创建一个Xcode project 文件。调用 Project 的 API 返回 AbstractObject 的实例,这个 AbstractObject 的类就是用来包装 project.pbxproj 中的 objects,以此来描述 Xcode project 文件,进行操作。
Object
123456789101112131415161718192021222324252627Object├── AbstractObject│ ├── AbstractBuildPhase│ ├── PBXCopyFilesBuildPhase│ ├── PBXFrameworksBuildPhase│ ├── PBXHeadersBuildPhase│ ├── PBXResourcesBuildPhase│ ├── PBXRezBuildPhase│ ├── PBXShellScriptBuildPhase│ └── PBXSourcesBuildPhase│ ├── AbstractTarget│ ├── PBXAggregateTarget│ ├── PBXLegacyTarget│ └── PBXNativeTarget│ ├── AbstractObjectAttribute│ ├── PBXBuildFile│ ├── PBXBuildRule│ ├── PBXContainerItemProxy│ ├── PBXFileReference│ ├── PBXGroup│ ├── PBXVariantGroup│ └── XCVersionGroup│ ├── PBXProject│ ├── PBXReferenceProxy│ ├── PBXTargetDependency│ ├── XCBuildConfiguration│ └── XCConfigurationList上面就是Object module 中类的结构图,用 Object module 来抽象文件中的属性,其中的类名和 project.pbxproj 文件中属性名一样,不同在于添加了一些抽象类来管理相同类型的属性,这样设计使得操作属性非常方便,直接操作这些属性类就行。
UUIDGenerator
在源工程 uuid_generator.rb 的文件中,类UUIDGenerator就是用来生成UUID,但与系统生成UUID不同的是,UUIDGenerator 利用文件路径,然后MD5之后来作为UUID。其中在方法 generate_paths 可以看出拼接文件路径的逻辑,有路径的属性就直接利用 path ,没有路径的就根据属性类型按照一定规则拼接路径,然后在方法uuid_for_path中MD5一下路径。
实践
创建 Xcodeproj 工程文件,并保存(这里需要注意的是不管是这里的创建工程,还是对工程的修改最后一定要使用save保存,才会执行对文件的工程的修改)
12# def new(klass)Xcodeproj::Project.new("./JCTest.xcodeproj").save打开 Xcodeproj 文件
12# def self.open(path)project=Xcodeproj::Project.open("./JCTest.xcodeproj")创建 Group 分组,名称为 JCTestGroup,对应的路径为./JCTestGroup
1234567891011# def new_group(name, path = nil, source_tree = :group)@param source_tree : SOURCE_TREES_BY_KEY = {:absolute => '<absolute>',:group => '<group>',:project => 'SOURCE_ROOT',:built_products => 'BUILT_PRODUCTS_DIR',:developer_dir => 'DEVELOPER_DIR',:sdk_root => 'SDKROOT',}JCTestGroup=proj.main_group.new_group("JCTest","./JCTest")Group 分组添加文件引用
12# def new_reference(path, source_tree = :group)ref=JCTestGroup.new_reference("JCTest.m")创建 Target
123456789101112# def new_target(type, name, platform, deployment_target = nil, product_group = nil, language = nil)@param [Symbol] type : ":application",":framework",":dynamic_library",":static_library"@param [String] name : the name of the target product@param [Symbol] platform : ":ios" or ":osx"@param [String] deployment_target : the deployment target for the platform@param [PBXGroup] product_group : the product group, where to add to a filereference of the created target.@param [Symbol] language : ":objc" or ":swift"@return [PBXNativeTarget] the target.target = project.new_target(:application,"JCTest",:ios)下面是 new_target 方法的内部实现,可以看到初始化 Target、Product、Build phases、Frameworks,那么如果需要对 Xcodeproj 工程文件进行修改,就可以直接操作这些对象得到想到的配置:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455def self.new_target(project, type, name, platform, deployment_target,product_group, language)# ----------------- Target -------------------# 新建 target 加入到 project.targets 中target = project.new(PBXNativeTarget)project.targets << targettarget.name = nametarget.product_name = name/* 设置 product_type 类型 ,使用 Constants 中的 PRODUCT_TYPE_UTI 获取:PRODUCT_TYPE_UTI = {:application => 'com.apple.product-type.application',:framework => 'com.apple.product-type.framework',:dynamic_library => 'com.apple.product-type.library.dynamic',:static_library => 'com.apple.product-type.library.static',:bundle => 'com.apple.product-type.bundle',:octest_bundle => 'com.apple.product-type.bundle',:unit_test_bundle => 'com.apple.product-type.bundle.unit-test',:ui_test_bundle => 'com.apple.product-type.bundle.ui-testing',:app_extension => 'com.apple.product-type.app-extension',:command_line_tool => 'com.apple.product-type.tool',:watch_app => 'com.apple.product-type.application.watchapp',:watch2_app => 'com.apple.product-type.application.watchapp2',:watch_extension => 'com.apple.product-type.watchkit-extension',:watch2_extension => 'com.apple.product-type.watchkit2-extension',:tv_extension => 'com.apple.product-type.tv-app-extension',:messages_application => 'com.apple.product-type.application.messages',:messages_extension => 'com.apple.product-type.app-extension.messages',:sticker_pack => 'com.apple.product-type.app-extension.messages-sticker-pack',:xpc_service => 'com.apple.product-type.xpc-service',}.freeze*/target.product_type = Constants::PRODUCT_TYPE_UTI[type]# target 配置信息target.build_configuration_list = configuration_list(project, platform,deployment_target, type, language)# ----------------- Product -------------------product = product_group.new_product_ref_for_target(name, type)target.product_reference = product# ----------------- Build phases -------------------target.build_phases << project.new(PBXSourcesBuildPhase)target.build_phases << project.new(PBXFrameworksBuildPhase)# ----------------- Frameworks -------------------framework_name = (platform == :osx) ? 'Cocoa' : 'Foundation'target.add_system_framework(framework_name)targetendtarget 的配置信息 build_configuration_list,其实在创建 Target 的 new_target 方法中已经使用 configuration_list 方法初始化 build_configuration_list 配置信息,使用 set_setting 可以修改 buildSettings 的配置,这里的 key 与 buildSettings 的 key 是一致的:
123# def set_setting(key, value)target.build_configuration_list.set_setting('INFOPLIST_FILE',"$(SRCROOT)/JCTest/Info.plist")看一下 configuration_list 内部实现,可以看出为 section XCBuildConfiguration 初始化 project.targets 中所有的 target 的 Release 和 Debug 的 buildSettings,使用 common_build_settings 方法设置在当前 platform 和 configuration 下的 buildSettings:
12345678910111213141516171819202122232425262728293031# def self.configuration_list(project, platform = nil, deployment_target = nil, target_product_type = nil, language = nil)cl = project.new(XCConfigurationList)cl.default_configuration_is_visible = '0'cl.default_configuration_name = 'Release'release_conf = project.new(XCBuildConfiguration)release_conf.name = 'Release'release_conf.build_settings = common_build_settings(:release, platform,deployment_target, target_product_type, language)debug_conf = project.new(XCBuildConfiguration)debug_conf.name = 'Debug'debug_conf.build_settings = common_build_settings(:debug, platform,deployment_target, target_product_type, language)cl.build_configurations << release_confcl.build_configurations << debug_confproject.build_configurations.each do |configuration|next if cl.build_configurations.map(&:name).include?(configuration.name)new_config = project.new(XCBuildConfiguration)new_config.name = configuration.namenew_config.build_settings = common_build_settings(configuration.type,platform, deployment_target, target_product_type, language)cl.build_configurations << new_configendclendtarget添加相关的文件引用,这样编译的时候才能引用到:
123456# def add_file_references(file_references, compiler_flags = {})@param [Array<PBXFileReference>] file_references : the files references ofthe source files that should be added to the target.@param [String] compiler_flags : the compiler flags for the sourcefilestarget.add_file_references([ref],'-fno-objc-arc')save 保存,对 project 的修改,只有调用 save 之后才会执行
1project.save以下是利用 Xcodeproj 组件创建一个 Xcodeproj 工程的完整代码,但需要注意的是 Xcodeproj 创建的只是工程的各种配置以及文件的索引,但实体文件需要自己组织好工程目录,与填写在 Xcodeproj 组件的路径一致
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051require 'xcodeproj'#创建 JCTest.xcodeproj工程文件,并保存Xcodeproj::Project.new("./JCTest.xcodeproj").save#打开创建的JCTest.xcodeproj文件proj=Xcodeproj::Project.open("./JCTest.xcodeproj")#创建一个分组,名称为JCTest,对应的路径为./JCTestJCTestGroup=proj.main_group.new_group("JCTest","./JCTest")#给JCTest分组添加文件引用JCTestGroup.new_reference("AppDelegate.h")ref1=JCTestGroup.new_reference("AppDelegate.m")ref2=JCTestGroup.new_reference("Assets.xcassets")JCTestGroup.new_reference("Base.lproj/LaunchScreen.storyboard")JCTestGroup.new_reference("Base.lproj/Main.storyboard")JCTestGroup.new_reference("ViewController.h")ref4=JCTestGroup.new_reference("ViewController.m")#在JCTest分组下创建一个名字为Supporting Files的子分组,并给该子分组添加main和info.plist文件引用supportingGroup=JCTestGroup.new_group("Supporting Files")ref3=supportingGroup.new_reference("main.m")supportingGroup.new_reference("Info.plist")#创建target,主要的参数 type: application :dynamic_library framework :static_library 意思大家都懂的#name:target名称#platform:平台 :ios或者:osxtarget = proj.new_target(:application,"JCTest",:ios)#添加target配置信息target.build_configuration_list.set_setting('INFOPLIST_FILE', "$(SRCROOT)/JCTest/Info.plist")#target添加相关的文件引用,这样编译的时候才能引用到target.add_file_references([ref1,ref2,ref3,ref4])testGroup=proj.main_group.new_group("JCTestTests","./JCTestTests")ref4=testGroup.new_reference("JCTestTests.m")supportingGroup=testGroup.new_group("Supporting Files")supportingGroup.new_reference("Info.plist")#创建test targettestTarget = proj.new_target(:unit_test_bundle,"JCTestTests",:ios,nil,proj.products_group)testRefrence = testTarget.product_referencetestRefrence.set_explicit_file_type('wrapper.cfbundle')testRefrence.name = "JCTestTests.xctest"testTarget.add_file_references([ref4])#保存proj.save