通过cocoapods将源码引入到工程中,有两种方式:远程网络下载、本地导入。
一、远程网络下载
大致流程:通过podspec找到三方库的远程地址,然后将完整代码下载到本地指定目录,然后按照podspec的配置进行无用资源的清理,最后将清理之后的文件拷贝到Pods文件夹目录下。
在Cocoapods内部有专门的清理工具,可以指定哪些文件可以留住,哪些文件需要清理。
举个例子,在github上搜索AFNetWorking,然后下载工程,下载到本地之后,文件目录如下:
然后我通过pod导入的方式将AFNetWorking导入进工程中,存在工程中的AFNetWorking资源文件目录如下:
前后对比一下就能看到,通过Cocoapods远程导入三方库之后,三方库中的无用资源文件就都会被清理掉,只会留下有用的文件。
前面说到了,Cocoapods会按照podspec的配置进行无用资源的清理,至于清理哪些资源、保留哪些资源,cocoapods是有其默认的一套规则的。那么我们是否可以手动调整这个规则呢?如果可以的话,又是通过什么字段进行调整的呢?
我们在github上面查找Realm,然后下载下来,找到Realm.podspec文件打开,找到preserve_paths参数,如下:
如果有一些文件不想被cocoapods自动清理,可以将文件名加入到preserve_paths参数中。
实际上,在podspec文件中,有好多的参数我们都不是太了解,这个时候很多人的第一反应就是去Google,这是一个了解的途径,但绝对不是最佳途径,本人也不建议去Google上面盲查。
podspec文件是cocoapods的自定义DSL(领域特定语言,Domain Specific Language)。
但是与其他的DSL不同的是,podspec并没有词法分析、语法分析、语法树等解析,其本质就是在Pod::Spec这个类型里面定义了各种方法,然后在podspec文件中就可以进行各种调用了,如下图所示:
podspec、podfile里面的这些方法的详细解析,其实是可以在cocoapods工程中找到的。打开github搜索Cocoapods,进入仓库主页面,如下:
然后依次进入Core/lib/cocoapods-core/podfile/dsl.rb,或者Core/lib/cocoapods-core/specification/dsl.rb,如下:
在podspec、podfile文件中使用到的各种参数,都可以在这里面找到详细的解释和使用示例。所以,以后再遇到podspec或者podfile中某字段不明白怎么使用的时候,不要再去Google了,直接在官方文档中去找对应的解释最靠谱。
现在我们搜索一下preserve_paths字段,结果如下:
这里既有对应的解释说明,也有对应的示例。
通过注释可以知道:
①Cocoapods默认会移除所有与preserve_paths中的模式不匹配的文件;
②preserve_paths字段中的参数值是一个承载正则表达式的数组;
③在三方库文件下载成功之后,所有不需要被移除的文件都要写在preserve_paths字段中。
二、本地导入
本地导入就是通过path来指定三方库的本地路径,然后直接使用本地的资源文件。
这种方式的一大特点就是,不会将三方库的文件资源代码拷贝到Pods文件夹里面。
我调整podfile文件如下:
代码语言:javascript复制target 'Norman' do
pod 'AFNetworking', :path => '../AFNetworking-master'
pod 'Realm'
end
AFNetworking通过本地导入,Realm通过远程网络加载导入。pod install之后,文件路径如下:
可以看到,通过本地导入的AFNetworking是存放在Development Pods文件夹下面,而通过Show in finder查看可知,存放在Development Pods文件夹下面的所有文件都是直接引用的本地源文件,并未将相关代码资源拷贝一份到项目工程中;
而通过网络远程导入的Realm是存放在Pods文件夹中,通过Show in finder查看可知,存放在Pods文件夹中的所有文件都是物理实打实的存放在Pods文件夹中的,也就是说,会将相关的代码资源下载拷贝到Pods文件夹下面。
也许你并不了解path参数的使用,此时就可以打开podfile的dsl.rb,然后搜索path,这样就可以找到对应的注释和使用示例了~
现在我调整一下podfile文件如下(将Realm由远程引用改为本地引用):
代码语言:javascript复制target 'Norman' do
pod 'AFNetworking', :path => '../AFNetworking-master'
pod 'Realm', :path => '../realm-swift-master'
end
然后pod install,成功之后文件格式如下:
可以看到,AFNetworking和Realm都是通过path来本地导入的,导入之后是存放在Development Pods文件夹下面。
此时运行一下,报错了:
代码语言:javascript复制Duplicate interface definition for class 'RLMThreadSafeReference'
初步猜测,该问题的原因十有八九是:在某文件中重复引用了RLMThreadSafeReference类。那么是在哪个文件中重复引用的呢?我打开错误的详细信息:
可以看到,是在RLMThreadSafeReference.mm中重复引用的,因此我可以进一步猜测,肯定是有两个相同的头文件导入到了同一个.m文件中。
接下来我来到RLMThreadSafeReference.mm文件,对该文件进行编译预处理:
然后在预编译结果文件中搜索RLMThreadSafeReference字段,可以找到两个声明:
在同一个.m文件中声明了两次@interface,那么肯定会报这个错误的。导致两次声明的原因如下:
我们这里使用的RLMThreadSafeReference源码是通过链接的方式引用进来的,编译器在编译.m的时候,会在RLMThreadSafeReference.mm文件所在的目录下去搜索对应的.h文件,在这里会找到RLMThreadSafeReference.h文件。
与此同时,cocoapods又给配置了一个.h文件的搜索路径:
对该.h文件Show In Finder,发现在include文件夹下又多了一个RLMThreadSafeReference.h文件,如下:
RLMThreadSafeReference.h文件的这两个路径,与RLMThreadSafeReference.mm预编译文件中的两个相同声明中对应的地址是一样的,如下:
这里可能会有一个疑问,#import引入的方式不是可以避免多次引入的吗?那为什么这里还是重复引入了呢?
这是因为,#import引入的方式是通过路径进行判断的,也就是说,必须是相同路径的头文件才能避免被重复引入。而这里重复引入的RLMThreadSafeReference.h是存在两个不同的路径下,所以是可以重复引入的。
这里顺嘴提一句,RLMThreadSafeReference.m之所以重复引入了两次RLMThreadSafeReference.h,就是自己引入一次,然后再通过其他的引入的文件间接引入了一次。
导致这个现象的原因就在于,Realm.podspec里面有一个prepare_command参数,如下:
我们去Pod Specification的dsl.rb文件中去搜索prepare_command,结果如下:
可以看到,prepare_command的作用就是【pod一被下载就会执行这里面指定的脚本】。
prepare_command里面制定了setup-cocoapods.sh脚本,而setup-cocoapods.sh脚本如下:
可以看到,该脚本的作用就是将source_root/Realm路径下的所有的.h、.hpp文件都拷贝到source_root/include路径下(这跟我们上面的分析是吻合的),而这些拷贝的头文件不会被cocoapods内置的清理工具自动清理(只有通过podspec导入的三方库才会自动清理相关资源文件)。
现在在source_root/Realm路径下和source_root/include路径下有两份相同的头文件了,而#import引入头文件时的去重功能只针对相同路径下的头文件而言,这里是两个不同路径下的头文件,这就是导致头文件重复导入的原因。
三、cocoapods-generate插件介绍
在前面的文章中,我介绍过一个CocoaPods的内置终端工具xcodeproj,我们可以通过该内置工具调用相关API来创建一个工程,并且往里面添加文件。但是这些API都是手动调用的,我们现在想一想,在遗忘的业务开发过程中,有没有遇到过cocoapods自动帮我们创建工程并导入三方库的情况呢?
我们在写好一个私有库之后,需要将私有库推送到远程的索引库,推送之前需要先对私有库做一个验证,指令如下:
代码语言:javascript复制pod lib lint --verbose --allow-warnings --no-clean --sources='https://cdn.cocoapods.org,https://code.yalla.live/common/archives/specs.git'
该命令实际上做的事情是:
按照podspec文件中的形式,从git上面将代码拉下来,对每一个支持的平台都创建一个对应的App,模拟真实的自己创建的三方库引入场景,然后进行编译,编译成功之后就验证通过;编译不成功就验证不通过。
这说明Cocoapods是内置了自动创建工程并导入三方库的工具流的,那么有没有一些实现了【自动创建工程并导入三方库】功能的三方工具可以使用呢?答案是有的,cocoapods-generate(https://github.com/square/cocoapods-generate)就是。
cocoapods-generate插件可以辅助我们很轻松的按照podspec配置信息生成一个workspace工程。
但是我并不推荐大家使用cocoapods-generate插件,它应对99%的工作场景是没有问题的,很多头部公司在生成Xcode工程的时候都是使用了该插件,但是有一些特殊的业务场景它处理不了。
cocoapods-generate插件,是按照本地导入的方式将三方库的源码导入到工程中的,而按照这种方式的话,通过上面的介绍,我们就知道了有可能会导致头文件重复导入,这种情况就比较棘手了。因为原则上我们在做组件二进制的时候是不会去调整三方库的任何内容的,而如果不调整的话就不能解决【头文件重复导入】的问题。这就是我不推荐使用cocoapods-generate插件的原因。
四、cocoapods-project-gen工具介绍
前面介绍了通过cocoapods将源码引入到工程中的两种方式,以及不推荐使用cocoapods-generate插件的原因,接下来我们顺着上面的话题往下聊。
现在我们知道,当通过本地导入的方式去引入Realm库的时候,会报头文件重复导入的错误。如果要解决该错误,势必要调整Realm库的原来的资源文件配置,但是我们的原则就是不动三方库的一分一毫,因为你不知道动了之后会引发怎么样的连锁反应,所以就需要保持100%原样。既然不能动三方库原本的资源和配置,那么我们还有一种方式可以将Realm库引入,那就是通过远程导入的方式,但是我又不想为了Realm这一个库去放弃本地引入的方式,因为绝大部分库其实通过本地导入是没有问题的,这个时候该怎么办呢?
方案就是不要使用cocoapods-generate插件来自动创建工程并导入三方库了,而是换一个插件,推荐大家使用cocoapods-project-gen(https://github.com/Cat1237/cocoapods-project-gen)。
在介绍cocoapods-project-gen工具之前,我们先来聊聊cocoapods的lint验证。前面我们提到,在写好一个私有库之后,需要将私有库推送到远程的索引库,推送之前需要先对私有库做一个验证,指令如下:
代码语言:javascript复制pod lib lint --verbose --allow-warnings --no-clean --sources='https://cdn.cocoapods.org,https://code.yalla.live/common/archives/specs.git'
我们使用VSCode打开在github上面下载的cocoapods源码,找到lint指令的实现:
可以看到,lint命令的实现中,最主要的就是使用了Validator类。
我们点开validate函数,查看其实现:
然后点开perform_extensive_analysis函数:
红框内就是验证的主体流程:
1,create_app_project
创建一个App工程。
可以看到,这里面主要是通过Xcodeproj::Project.new这个API在某路径下创建一个工程。validation_dir这个路径是验证的时候生成工程的临时路径,如下:
代码语言:javascript复制 # @return [Pathname] the temporary directory used by the linter.
#
def validation_dir
@validation_dir ||= Pathname(Dir.mktmpdir(['CocoaPods-Lint-', "-#{spec.name}"]))
end
如果我将validation_dir进行调整,调整为我自己自定义的一个路径,那么就会将工程生成到我自定义的目录下了。
2,download_pod
下载pod
创建好了工程之后,需要创建podfile文件,这里是通过podfile_from_spec函数生成podfile文件,如下:
通过上面红框中的内容我们可以知道,cocoapods官方提供的lint校验函数在生成工程的时候,也是严格区分了本地加载和远程加载两种方式,二者只能选其一。而这两种导入方式,我们前面也已经分析了,各有利弊。所以说,通过cocoapods官方提供的lint指令并不能够解决Realm的本地导入的时候创建工程重复导入的问题。
而我们这里讲的cocoapods-project-gen工具实际上也是按照lint的思路,但是在lint的基础上做了调整改动。cocoapods-project-gen工具会将三方库的源码拷贝到Pods目录下,然后执行clean操作将无用的资源文件进行清理。也就是说,执行的是网络导入的那一套流程,但是源码不需要在网络下载,而是直接加载的本地源码。
3,check_file_patterns
4,install_pod
pod install
5,validate_swift_version
验证Swift版本
6,add_app_project_import
7,validate_vendored_dynamic_frameworks
8,build_pod
可以看到,这里实际上是调用了xcodebuild命令进行build。
9,test_pod
以上。