我是一名iOS开发者,我是一名非计算机专业出生的程序员。
如果学习很幸苦,那就尝试无知的代价
我们公司主要以项目为主,做项目的过程中免不了需要集成第三方的 SDK,例如人脸识别、即时通讯等,第三方的 SDK 往往比较大,公司为节省 SVN 硬盘资源,不允许 SVN 提交超过 50 MB 的文件,然而这些 SDK 可能会有100MB+ 左右,这不利于管理第三方的 SDK,我们将 SDK 存放在 FTP 服务器上,这也对前线项目开发人员造成难题,无法一次性通过 pod install 成功后运行项目,需要先执行 pod install,再前往 FTP 服务器下载第三方 SDK ,再将 SDK 移动至对应的组件目录,再次执行 pod install,工程才能正常运行,这无非增加项目开发的成本,这也是本次调研 cocoapods 插件开发的原因。
cocoapods 插件开发,允许开发者可以再 pod install 的过程中 hook 其中一个生命周期,对其进行操作,这正是我想要的,那接下来我需要学习以下技术:
通过百度可以搜索到网上大部分的文章内容,写的很全。
https://www.jianshu.com/p/5889b25a85dd
gem install cocoapods-plugins
pod plugins create githooks
执行完上述操作后你会得到一个 Cocoapods Plugins 的工程目录,你可以选择 WebStorm、VSCode 来开发,在这里我使用 VSCode,安装 Ruby 插件后,开发比较流畅。
# 修改这行代码
spec.files = 'git ls-files'.split($/)
# 修改为
spec.files = Dir['lib/**/*']
sudo gem build cocoapods-githooks.gemspec
显示以下信息,编译成功
WARNING: no author specified
WARNING: open-ended dependency on rake (>= 0, development) is not recommended
if rake is semantically versioned, use:
add_development_dependency 'rake', '~> 0'
WARNING: See http://guides.rubygems.org/specification-reference/ for help
Successfully built RubyGem
Name: cocoapods-githooks
Version: 0.0.1
File: cocoapods-githooks-0.0.1.gem
编译成功后,会得到 cocoapods-githooks-0.0.1.gem 文件
sudo gem install cocoapods-githooks-0.0.1.gem
显示以下信息,安装成功
zhudezzhendeMacBook-Pro:cocoapods-githooks zhudezhen$ sudo gem install cocoapods-githooks-0.0.1.gem
Successfully installed cocoapods-githooks-0.0.1
Parsing documentation for cocoapods-githooks-0.0.1
Installing ri documentation for cocoapods-githooks-0.0.1
Done installing documentation for cocoapods-githooks after 0 seconds
1 gem installed
你可以通过以下命令,检验插件是否安装成功
pod plugins installed
在列表中找到你的插件,说明安装成功了
zhudezzhendeMacBook-Pro:TestWKWebView zhudezhen$ pod plugins installed
[!] The specification of arguments as a string has been deprecated Pod::Command::Vendors: `NAME`
[!] The specification of arguments as a string has been deprecated Pod::Command::Githooks: `NAME`
Installed CocoaPods Plugins:
- cocoapods-clean : 0.0.1
- cocoapods-deintegrate : 1.0.4
- cocoapods-ftp-vendors : 0.0.1 (pre_install hook)
- cocoapods-githooks : 0.0.1 (post_install hook)
- cocoapods-plugins : 1.0.0
- cocoapods-search : 1.0.0
- cocoapods-stats : 1.1.0 (post_install hook)
- cocoapods-trunk : 1.5.0
- cocoapods-try : 1.2.0
到此就掌握了 cocoapods 插件的开发流程,当然还没有对 pod install 进行 hook。
Ruby 语法的学习,基本都是从菜鸟教程上看的,这里不展开了
https://www.runoob.com/ruby/ruby-syntax.html
对目录 ‘lib/cocoapods_plugins.rb’ 进行修改,修改内容如下:
require 'cocoapods-githooks/command'
# 引用 cocoapods 包
require 'cocoapods'
module CocoapodsGithooks
# 注册 pod install 钩子
Pod::HooksManager.register('cocoapods-githooks', :post_install) do |context|
p "hello world!"
end
end
注意:module 的名称,来源于 cocoapods-githooks.gemspec 的 version 字段,钩子注册的名称必须和组件名称保持一致。
修改完毕后,重新编译安装,为了方便调试,每次修改内容你可以通过以下命令,重新执行
sudo gem uninstall cocoapods-githooks-0.0.1.gem && sudo gem build cocoapods-githooks.gemspec && sudo gem install cocoapods-githooks-0.0.1.gem
你需要在项目的 Podfile 文件的顶部,增加如下代码:
platform :ios, '9.3'
# 此处为新增代码
plugin 'cocoapods-githooks'
target ....
好了,我们执行 pod install 试一下
zhudezzhendeMacBook-Pro:TestWKWebView zhudezhen$ pod install
[!] The specification of arguments as a string has been deprecated Pod::Command::Vendors: `NAME`
[!] The specification of arguments as a string has been deprecated Pod::Command::Githooks: `NAME`
Analyzing dependencies
Downloading dependencies
Using Masonry (1.1.0)
Using arcface (1.2.0)
Generating Pods project
Integrating client project
"hello world!"
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 2 total pods installed.
zhudezzhendeMacBook-Pro:TestWKWebView zhudezhen$
我们看到了 "hello world!"。
Ruby 本身自带 FTP 模块,可以直接连接 FTP 服务器,这里需要注意,连接的如果是 Window 的 FTP 服务器的话,需要对参数进行 UTF-8 -> gb2312 的转换。
腾讯云文档:https://cloud.tencent.com/developer/section/1378298
Ruby 连接 FTP 模块的代码如下:
require "net/ftp"
ftpclient = Net::FTP.new
ftpclient.connect(host, port)
ftpclient.passive = true
ftpclient.login(username, password)
p ftpclient.nlst
执行上述代码后,就可以成功链接到 FTP 服务器,如果连接的是 Window 服务器,目录存在中文的话,需要进行编码转换,DirectoryName.encode("utf-8", "gb2312")
,当需要访问 FTP 指定目录时,如果是 Window 服务器,路径中存在中文的话,路径需要进行编码转换,"/我的组件库/组件1/组件2/abcd.framework".encode("gb2312", "utf-8")
可以通通过 system 获取 FTP 服务器信息
p ftpclient.system
# print Window_NT
通过 getbinaryfile 下载文件,路径问题同上。
ftpclient.getbinaryfile 文件路径
由于在 iOS 中,*.framework 静态库属于特殊的文件夹形式,因此我们需要递归下载静态库:
def self.d_f_p(ftp_key = '', path = '', rootpath = '')
# 从连接池中,获取 FTP 连接客户端
ftp = FtpClient.ftp_connect_pool[ftp_key]
if !ftp
return nil;
end
# 获取文件名
subpaths = path.split("/")
filename = subpaths[subpaths.size - 1];
# 先创建目录
subfiles = ftp.nlst(path)
# 判断是否是文件夹、还是文件
if !((subfiles.size == 1) && (subfiles[0] == "#{path}/#{filename}"))
# 如果是目录,则构建目录
buildDir = "#{rootpath}/#{filename}"
if !(File.directory? buildDir)
Dir.mkdir buildDir
end
# 获取子目录
subfiles = ftp.nlst path
# 获取目录下子文件
for sf in subfiles do
d_f_p(ftp_key, sf, buildDir + "/")
end
else
# 普通文件,直接下载
filesize = ftp.size path;
ftp.getbinaryfile(path, "#{rootpath}/#{filename}", filesize)
end
end
通过上述代码,可以自动下载 framework 静态库。
解决了以上问题后,我们开始串联整体流程,大致流程如下:
FTP 配置信息,最开始有两种思路:
我选择了后者,如果选择了前者,那将会对所有需要 FTP 的组件进行改造,而后者可以以插件的形式改造工程。
一开始我将 ftp 的信息放在 Podfile 对应组件的后面,如下:
platform :ios, '9.3'
plugin 'cocoapods-githooks'
target 'TestWKWebView' do
pod 'sm', :path => 'xxxx/sm2', :ftp => { "host" => "xxx.xxx.xxx.xxx", "port" => "xxxxx", "user" => "xxxx", "pwd" => "xxxx", "vendors" => [{ "path" => "/xxxxxx/xxxx/1.0.1~1.0.11.a/xxxx.framework", "destination" => "sm2" ,"version" => "1.0.1" }]}
end
这样配置可以在 Cocoapods Plugin 中正常获取到,并且 pod install 也可以成功,但是换成 svn 之后,这样配置在 pod install 会失败,如下:
platform :ios, '9.3'
plugin 'cocoapods-githooks'
target 'TestWKWebView' do
pod 'sm', :svn => 'svn:xxxx:xxx/xxx/xxxx/sm2', :tag => 'xxx.xx', :ftp => { "host" => "xxx.xxx.xxx.xxx", "port" => "xxxxx", "user" => "xxxx", "pwd" => "xxxx", "vendors" => [{ "path" => "/xxxxxx/xxxx/1.0.1~1.0.11.a/xxxx.framework", "destination" => "sm2" ,"version" => "1.0.1" }]}
end
这时候无论怎么操作,终究会在 pod install 最后发生失败,这让我不得不换个思路,索性采用配置文件的形式读取 FTP 信息,这样可以完全和 Podfile 隔离。
文件内容如下:
{
"arcface":{
"host":"xxx.xxx.xxx.xxx 【ftp 服务器地址】",
"port":"xxxxx 【ftp 端口号】",
"user":"xxxx 【ftp 账号】",
"pwd":"xxxx 【ftp 密码】",
"vendors":[
{
"path":"/xxxx/xxxx/iOS/1.0.1/ArcSoftFaceEngine.framework 【组件 ftp 绝对路径】",
"destination":"xxxx/xxx 【组件本地存放路径,相对于工程 Pods/】",
"version":"1.0.1【组件版本号信息】"
}
]
},
}
通过配置文件读取,pod install 成功,并且插件中也获取到了配置信息。
在 hook 的回调方法中,有一个 context 变量,很明显这是当前钩子的上下文,这时候需要理解上下文中的内容:
Cocoapoads API 在线文档:https://www.rubydoc.info/github/cocoapods/cocoapods 这一块内容,网上比较少,只能自己阅读文档来学习。
通过阅读 API,我们可以在 context 中获取工程的 podsproject、sandbox、sandboxroot、umbrellatargets。同时还了解,除了 :postinstall Hook、还有 :preinstall、:postintegrate、:sourceprovider 可以 hook,其中 :postintegrate 只有 cocoapods 1.10.0+ 以上版本支持,由于目前公司的特性,我们只能维持 cocoapods 1.4.0 版本,这带来了组件移动完毕后,工程合成的问题。每一个 hook,都有自己的 context,context 中携带的信息不一样,这里需要注意。目前为止,可能还是理解 context 中携带的数据,所以我去搜了一波“Ruby 反射”,发现在 Runtime 阶段,调用 context.methods 可以获取当前 Ruby 对象的所有方法,这对整个流程带来了关键性的进展。
流程串联完毕后,很开心,测试了,第一个试验性大型项目工程,FTP 从下载、组件移动、pod install 一切顺利,就在运行项目的时候,发现编译失败,原因是第三方静态库没有被引用,很奇怪,我不是已经组件移动到指定目录了吗?经过分析发现,pod install 其实是有流程的,我从 github 上下载 cocoapods 1.4.0 的源码后发现,post_install 会在整体工程合成完毕后执行,也就是说静态库没有被自动引用进 Pods Target 的工程配置中,造成上述问题。
接下来准备分析 pod install 流程,从源码中得到 cocoapods 会执行以下流程:
def install!
prepare
resolve_dependencies
download_dependencies
validate_targets
generate_pods_project
if installation_options.integrate_targets?
integrate_user_project
else
UI.section 'Skipping User Project Integration'
end
perform_post_install_actions
end
generatepodsproject 方法已经在构建 Pods Target 工程,而我们的钩子在 performpostinstallactions 的时候执行,所以没有被引用进工程中。那我们可以使用 :preinstall 来进行 Hook?由于 context 不一样,需要进行代码改造,改造完毕后再次执行,还是一样编译失败,而这一次的原因是,组件移动成功后,又被 download_dependencies 方法冲刷掉了。
接下来开始阅读 generatepodsproject 源码
def generate_pods_project(generator = create_generator)
UI.section 'Generating Pods project' do
generator.generate!
@pods_project = generator.project
run_podfile_post_install_hooks
generator.write
generator.share_development_pod_schemes
write_lockfiles
end
end
我发现源码中有 runpodfilepostinstallhooks 方法,这是一个钩子,接着往下看发现
# Runs the post install hooks of the installed specs and of the Podfile.
#
# @note Post install hooks run _before_ saving of project, so that they
# can alter it before it is written to the disk.
#
# @return [void]
#
def run_podfile_post_install_hooks
UI.message '- Running post install hooks' do
executed = run_podfile_post_install_hook
UI.message '- Podfile' if executed
end
end
# Runs the post install hook of the Podfile
#
# @raise Raises an informative if the hooks raises.
#
# @return [Boolean] Whether the hook was run.
#
def run_podfile_post_install_hook
podfile.post_install!(self)
rescue => e
raise Informative, 'An error occurred while processing the post-install ' \
'hook of the Podfile.' \
"\n\n#{e.message}\n\n#{e.backtrace * "\n"}"
end
这里居然执行了 podfile.postinstall , 也就是说,在工程目录中 Podfile 中,编写 postinstall Hook 就行了,最后我在工程 Podfile 文件的末尾,添加如下代码:
pre_install do |installer|
p "hello world!"
end
这里可以成功输出。
还需要解决如何在这里拿到 Cocoapods Plugins 计算的结果?在这里我通过生成 .lock 文件来解决跨代码文件通信问题,首先,我们必须要在 :pre_install 中下载组件,并将组件移动信息的写入.lock 文件中,之后我们在 Podfile 文件中的钩子,读取 .lock 文件信息,移动组件目录,最终在 Podfile 文件末尾添加如下代码:
def download_from_ftpandmove
# 如果 lock 文件不存在,则不执行
if !File.exists?("PodfileFtp.lock")
return ;
end
# 读取 json 文件
require "json"
file = File.open "PodfileFtp.lock"
vendor_plugins = JSON.load file
# 遍历组件、并移动到指定目录
for item in vendor_plugins do
from = item["from"]
to = item["to"]
# 创建目录
if !File.directory?(to)
FileUtils.mkdir_p to
end
# 拷贝移动
FileUtils.cp_r(from, to)
end
end
pre_install do |installer|
download_from_ftpandmove
end
最后重新执行 pod install 之后,工程下载自动引用组件、编译运行成功!