产物路径修改
首先看一个问题:比如你是一个SDK提供商,如何为不同国家提供名为SJSDK的SDK,并且在一个工程下生成?
这个问题最简单方法就是生成多个project分别产出SJSDK即可,但是如果要求在一个工程下呢?
首先我们创建个工程,选择Framework,名字SJSDK:
创建完,比如我们在target再复制一个:
修改名字和上面一样,发现无法修改同名target,只能修改下名字:SJSDK-HK
这时候我们可以通过修改product name,创建config修改TARGETNAME,这种方式有成本的,而且资源文件放在哪,也不好说。
我们就不想通过这个方式了,可以再创建一个project,这个project也产出SJSDK,这样不需要修改什么东西。
因为target名字默认跟随project,所以把target名字改成SJSDK。
现在我们就有了两个名为SJSDK的在不同project下管理,我们需要管理这两个project,就需要创建workspace,可以通过File -> Save As Workspace...,创建完workspace把另一个project也添加进来。
现在两个project都在workspace里并且两个target名称都是SJSDK.
接下来把资源文件放在workspace同级目录下,并拖进工程。
配置编译文件,在不同project的target下的Build Phases的Compile Sources里面添加编译所需文件,直接拖过来即可。
下面就是怎么触发这两个target的编译,默认的我们只能选择一个进行编译。有的同学就会想到添加依赖Dependencies,但是这个需要注意Dependencies是有作用域的,只有相同project下的target才能添加依赖,我们现在是在不同project下。
Scheme定义了各个action使用target的集合,所以我们可以通过scheme进行操作。
创建一个名为SJSDK-Merge的scheme:
在这个scheme添加target
当前scheme就控制了两个target的编译,我们cmd + B进行编译,会报一个错:
有两个同名target都放在一个产物目录下,我不知道编译哪个。我们随便修改一个target名字再进行编译,就成功了。
现在来分析下产物路径:
workspce名称/以第一个project名称+复杂字符串作为产物路径的开始。Build。Build里面两个文件夹。 3.1Intermediates.noindex中间产物文件夹,里面文件是以project名称+.build为名称。 3.1.1config+平台名 3.1.2target名字+.build3.2Products产物目录 3.2.1config+平台名 3.2.2target产物名称
所以说产物目录之前的路径,都是固定的。有两个产物同名同目录,那么现在就看能不能想办法修改一下他们的产物目录,让他们在不同目录下就可以了,可以在产物目录前包一层目录。
那就看下xcode环境变量有没有定义产物的路径。printenv命令可以输出当前所有的环境变量,在Build Phases添加脚本,输出所有环境变量到指定终端:printenv > /dev/ttys000
全局搜索Debug-iphonesimulator看有没有以这个结尾的目录,会发现有很多,我们无法确定是哪个。可以参考cocoaPods,可以看到cocoaPods的产物目录前面是包了一层文件夹的:
可以看下cocoaPods的xcconfig文件:
看里面产物路径设置,我们也可以借鉴过来,里面的CONFIGURATION_BUILD_DIR就是产物路径。
我们分别在project中创建Config文件,产物路径都加个PROJECT_NAME:
CONFIGURATION_BUILD_DIR = ${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/${PROJECT_NAME}
再编译,就可以看到不同目录下生成相同名字的产物了。
知道CONFIGURATION_BUILD_DIR就是产物路径,也可以直接在Config写下面代码,效果是一样的。
CONFIGURATION_BUILD_DIR = $(CONFIGURATION_BUILD_DIR)/${PROJECT_NAME}
读取编译产物.hmap
使用终端配置工具DumpHeaderMap,按顺序读取编译产物.hmap文件,结果输出到指定终端。
之前用过xcode_run_cmd.sh这个脚本,新建个工程,创建Config文件,添加脚本。
中间产物目录Intermediates.noindex里面有很多.hmap文件,我们需要找到这个文件的路径,先添加脚本printenv > /dev/ttys000输出环境变量到指定终端,找到TEMP_FILE_DIR就是中间产物路径。
.hmap文件都是以dumpTest开头的,写一个路径如下:
TTY = /dev/ttys000
HMAP_PATH="${TEMP_FILE_DIR}/${PRODUCT_NAME}-all-non-framework-target-headers.hmap"
CMD =DumpHeaderMap ${HMAP_PATH}
添加脚本$SRCROOT/dumpTest/xcode_run_cmd.sh进行编译,发现报错了
没有一个很明显提示,问题出在命令上,DumpHeaderMap在xcode终端环境并没有这个环境变量,我们需要把配置的环境变量导出到xcode这个终端上,这样就可以了。
现在把所有.hmap文件路径添加上,用空格分开,使用循环语句:
TTY = /dev/ttys000
HMAP_PATH="${TEMP_FILE_DIR}/${PRODUCT_NAME}-all-non-framework-target-headers.hmap ${TEMP_FILE_DIR}/${PRODUCT_NAME}-all-target-headers.hmap ${TEMP_FILE_DIR}/${PRODUCT_NAME}-generated-files.hmap ${TEMP_FILE_DIR}/${PRODUCT_NAME}-own-target-headers.hmap ${TEMP_FILE_DIR}/${PRODUCT_NAME}-project-headers.hmap ${TEMP_FILE_DIR}/${PRODUCT_NAME}.hmap"
CMD =source ~/.bash_profile;for i in $HMAP_PATH; do DumpHeaderMap $i;done
编译,结果如下:
拷贝-Swift.h文件到指定目录
将系统生成的-Swift.h文件,拷贝到指定目录(产物目录或SRCROOT)。
创建一个Static Library工程,语言选择OC,在工程中创建一个swift文件。
在swift文件中定义一个类:
@objc
public class SJ: NSObject {
}
在OC中如果想使用这个类,需要导入,以当前产物名称+-Swift.h就可以使用了。#import "moveSwiftH-Swift.h"。
为什么要导入这个文件,这个文件又有什么内容呢?
这个文件当你工程有swift文件,并且继承自OC的,并且是public文件的时候,系统在进行预编译的时候会生成以当前project名称+-Swift.h的文件。这个文件是在编译过程中生成的,所以它在中间产物路径下。
/Users/shangjie/Library/Developer/Xcode/DerivedData/moveSwiftH-feixujjprfvcrjhizlpwniuffgws/Build/Intermediates.noindex/moveSwiftH.build/Debug-iphonesimulator/moveSwiftH.build/DerivedSources/moveSwiftH-Swift.h
这时候如果我们是SDK提供商,代码里面有swift文件,要不要把-Swift.h文件提供出去?如果这个swift文件要给别人使用的时候,那么我们就需要把-Swift.h文件提供出去。
这个文件就是把swift文件按照OC格式做了个类声明。
这个文件要提供给别人使用的话,我们就需要处理下这个文件,把这个文件拷贝到头文件目录里面才能给别人使用。
这个头文件路径是不是环境变量的值,继续添加脚本printenv > /dev/ttys000,输出所有环境变量搜索moveSwiftH-Swift.h,好像没有,那就搜索上一级文件夹名字DerivedSources,会发现有了。
接下来写脚本执行拷贝命令,首先定义拷贝后文件目录SWIFT_H_PATH="${SRCROOT}/Swift Compatibility Header/${PRODUCT_NAME}-Swift.h"。执行拷贝命令我们用ditto,原路径我们用DERIVED_FILES_DIR,整个路径为"$DERIVED_FILES_DIR/${PRODUCT_NAME}-Swift.h",最后脚本内容为:
SWIFT_H_PATH="${SRCROOT}/Swift Compatibility Header/${PRODUCT_NAME}-Swift.h"
ditto "$DERIVED_FILES_DIR/${PRODUCT_NAME}-Swift.h" "${SWIFT_H_PATH}"
编译完后,可以看到目录中生成的Swift.h文件:
pbxproj文件
工程里文件引用,路径等信息都存在pbxproj文件中,在xcodeproj右键显示包内容即可查看。
我推荐用VSCode查看,下一个插件名字:Syntax Xcode Project Data搜索pbx就出来了。这个插件可以对pbxproj文件进行格式化,这样看的话会更清晰一些。
这个文件看上去有点像
JSON,它实际上是plist的变种。
pbxproj文件包含以下几种key:
• archiveVersion:当前文件的生成版本
• objectVersion:当前文件内 objects的描述版本
• classes:占位,没有实际意义
• objects:字典,以每个object的UUID作为key,object的属性作为value
• rootObject:当前文件的根object的UUID(isa = PBXProject)
• 每个object是一个字典
• 每个object都有一个isa key,用来表明当前是什么object
• 每个object也有很多attributes key,用来表明当前object特性。以及包含的其他 object的UUID,描述object的依赖和包含关系
已知的
object:
PBXBuildFile - 项目参与编译的文件
AbstractBuildPhase
PBXBuildRule
XCBuildConfiguration
XCConfigurationList
PBXContainerItemProxy - 用来代指当前project包含的其他project
PBXFileReference
PBXGroup
PBXProject
PBXTargetDependency
PBXReferenceProxy - 当前project引用的,相同空间的其他project的文件
AbstractTarget
这些project是怎么拿到的呢?我们想cocoapods在install时候,为什么能生成一个project,是不是它知道里面所有的规则才能生成,它内置了一个工具:Xcodeproj。
里面有个constants.rb文件,内容就是对pbxproj文件的探索。
已知的
AbstractTarget
PBXNativeTarget:正常
PBXAggregateTarget:代表一组文件,占位target
PBXLegacyTarget:使用外部构建工具的生成的target
已知的
PBXGroup
XCVersionGroup:包含多个不同版本的文件(CoreData)
PBXVariantGroup:本地化文件
已知的
PBXGrAbstractBuildPhase
PBXCopyFilesBuildPhase
PBXResourcesBuildPhase
PBXSourcesBuildPhase
PBXFrameworksBuildPhase
PBXHeadersBuildPhase
PBXShellScriptBuildPhase
sourceTree
<absolute>:绝对路径
<group>:相对于所在group路径
SOURCE_ROOT:相对于工程所在目录路径
DEVELOPER_DIR:相对于DEVELOPER_DIR目录路径
BUILT_PRODUCTS_DIR:相对于产物所在目录路径
SDKROOT:相对于SDK目录所在路径
初识Podfile
使用cocoapods一定离不开Podfile文件,这个文件用ruby写的。
创建一个project工程,名字:SeePodfile。在xcodeProj同级目录创建Podfile文件,输入p 'SJ--------'。p就是打印命令,执行pod install。
可以看到输出结果,并且报了个错说没有workspace:
创建一个workspace,在Podfile中写入workspace路径。
在ruby中,workspace代表函数,后面是参数,可以用括号括起来或者空格+参数。路径写入workspace './SeePodfile.xcworkspace'或者workspace('./SeePodfile.xcworkspace'),之后再执行pod install,就发现没有报错了。
cocoapods内置了一个工具库:xcodeproj,我们可以通过这个工具去修改pbxproj文件。
首先要引入这个库:require 'xcodeproj'。ruby会去当前执行的环境内置变量$LOAD_PATH,里面保存你这个工程使用所有三方库的路径,require就是拿引入的名称和LOAD_PATH拼到一起,有这个库的路径就可以使用了。
app_project_path = './SeePodfile.xcodeproj'
project = Xcodeproj::Project.open(app_project_path)
这句话目的就是把xcodeproj里面的内容转化为xcodeproj这个工具里面的类
复制一个文件到工程目录,怎么把它添加到工程呢?首先要定义文件路径:
file_path = './SJSDKTest1.h'。
添加文件:
project.new_file(file_path)
project.save
执行pod install,就可以看到文件已经被添加到工程引用中了。
使用xcode内置工具修改pbxproj
我们在xcode添加文件,拖动文件时,xcode肯定有一些工具来帮助我们修改pbxproj文件。
xcode继承了很多动态库,在应用程序->xcode右键显示包内容,可以看到有很多framework
这些framework如果能集成到工程里,那我们就能调用这些framework了。
我们要使用这些framework的前提是我们知道这些framework具体是做什么的。
Contents -> Plugins -> Xcode3Core.ideplugin -> Contents -> Frameworks -> DevToolsCore.framework 这个framework就是用来修改pbxproj文件的。如果我们能把这个framework继承到工程中,那么就可以通过这个framework修改pbxproj文件了。
创建一个工程,选择macOS->Command Line Tool。把DevToolsCore.framework拷贝到工程SRCROOT路径下,然后把文件夹直接拖到General -> Frameworks and Libraries下:
编译成功,运行报错:
dyld通过路径'@rpath/DVTPortal.framework/Versions/A/DVTPortal'找DVTPortal的时候找不到,加载这个DVTPortal最简答的方法就是拿到它的完整路径,但是完整路径通过App控制的,所以系统提供了个rpath就是App提供的前半部分路径。在Build Settings搜索rpath,得到环境变量LD_RUNPATH_SEARCH_PATHS,这个framework在电脑中完整路径/Applications/Xcode.app/Contents/SharedFrameworks/DVTPortal.framework,此时@rpath对应/Applications/Xcode.app/Contents/SharedFrameworks/,创建一个Config,把路径赋值到环境变量上:LD_RUNPATH_SEARCH_PATHS = /Applications/Xcode.app/Contents/SharedFrameworks/,再运行:
这个报错完整路径/Applications/Xcode.app/Contents/Frameworks/libclang.dylib,修改Config
LD_RUNPATH_SEARCH_PATHS = /Applications/Xcode.app/Contents/SharedFrameworks/ /Applications/Xcode.app/Contents/Frameworks
再运行,又报错,继续:
完整路径/Applications/Xcode.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/Frameworks/DevToolsFoundation.framework,修改Config
LD_RUNPATH_SEARCH_PATHS = /Applications/Xcode.app/Contents/SharedFrameworks/ /Applications/Xcode.app/Contents/Frameworks /Applications/Xcode.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/Frameworks
路径可以加双引号也可以不加:
LD_RUNPATH_SEARCH_PATHS = "/Applications/Xcode.app/Contents/SharedFrameworks" "/Applications/Xcode.app/Contents/Frameworks" "/Applications/Xcode.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/Frameworks"
再运行,成功了!
我们把framework拖过来,执行了两步操作,一个是framework search path告诉你去哪个地方搜索frame;另一个是ld -framework告诉链接器去链接这个framework。
我们可以手动执行这个操作,在Config文件中设置
FRAMEWORK_SEARCH_PATHS = "/Applications/Xcode.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/Frameworks"
OTHER_LDFLAGS = -framework DevToolsCore
把DevToolsCore文件夹添加到工程里,导入头文件:#import "PBXProject.h",进行编译,发现报错:
因为当你把文件拖到工程里时,那文件就是在工程里面,引用的时候前面不能加其他的目录了。那就不能直接拖文件夹了,通过header search path,在Config中配置HEADER_SEARCH_PATHS = "${SRCROOT}"再编译。
因为PBXProject是在DevToolsCore下的,所以要添加目录:#import "DevToolsCore/PBXProject.h"再编译,还有错:
因为xcode内置有NSObject而这个库不行,要替换成#import <Foundation/Foundation.h>,有的代码错误直接注释即可,因为是头文件,所以没关系。
修改完后可以了,我们就可以写代码了。
projectWithFile:在PBXProject找到这个方法,参数就写要修改的xcodeproj的绝对路径。
int main(int argc, const char * argv[]) {
PBXProject *project = [PBXProject projectWithFile:@"/Users/shangjie/Desktop/工程化/未命名文件夹/PBFix/addFileTest/addFileTest.xcodeproj"];
NSLog(@"%@", project);
return 0;
}
跑一下发现崩溃了:
错误是断言失败了,在IDEInitialization.m文件中,这个我们都不用看,系统库里面的方法,我们也看不到。
那怎么办呢?只能选择站在巨人肩膀上,网上有很多插件都是大神们写好的,他们对这个肯定都有研究,我们可以参考一下。有个国外大神写的xcproj-develop,参考这个工程,我们发现这个报错是使用Xcode的IDE的API,如果要使用这个API,需要先进行初始化,初始化代码如下:
NSError *error;
BOOL initialized = IDEInitialize(1, &error);
但是IDEInitialize并不在现在开发环境中有函数声明,但是我们知道这个函数声明肯定在我们引用的某个动态库里面,我们就要告诉编译器,你不要管,在运行的时候dyld会自动找到这个函数,我们要骗编译器,骗编译器分为两部分:
- 预处理。这个比较简单,我们直接添加个函数的声明即可:
BOOL IDEInitialize(int, NSError **error); - 链接。连接器在生成可执行文件的时候是需要函数声明的。那么我们要告诉编译器,链接的时候,这个符号你不要管,到时候自然有人会管。使用
Xlinker,Xlinker后面的参数是直接传给链接器的,而不是通过clang转给链接器的。链接器有个指令-U忽略后面的符号,在Config文件中的OTHER_LDFLAGS添加OTHER_LDFLAGS = -framework DevToolsCore -Xlinker -U -Xlinker _IDEInitialize。
在运行就会提示访问文件夹了,并且看到project里面的信息:
随便拷贝一个Person.h文件到addFileTest工程的SRCROOT路径下,添加文件首先这个文件要在project的一个group下面。使用group里面的API需要先导入头文件:#import "DevToolsCore/PBXGroup.h",添加文件引用:
int main(int argc, const char * argv[]) {
NSError *error;
BOOL initialized = IDEInitialize(1, &error);
PBXProject *project = [PBXProject projectWithFile:@"/Users/shangjie/Desktop/工程化/未命名文件夹/PBFix/addFileTest/addFileTest.xcodeproj"];
[[project rootGroup] addFiles:@[@"/Users/shangjie/Desktop/工程化/未命名文件夹/PBFix/addFileTest/Person.h"] copy:NO createGroupsRecursively:NO];
[project writeToFileSystemProjectFile:YES userFile:NO checkNeedsRevert:NO];
NSLog(@"%@", project);
return 0;
}
执行代码后,文件引用就被添加上了。