背景
雖然做iOS開發的過程中使用過 Cocoapods, 但是對里面的細節了解其實不算太多,直到這兩年做織女項目時,通過對Cocoapods進行Qt支持改造才開始深入了解部分細節,這個過程中,網上沒有找到太多相關資料,本文就簡單介紹下我對Cocoapods提供的插件機制的一個簡單了解,希望能給大家帶來一些幫助。
Ruby Open Classes
在此之前,我們簡單看下 Ruby Open Classes ,這部分是為未接觸過Ruby的同學准備的,熟悉的同學可以直接略過。
在Ruby中,類永遠是開放的,你總是可以將新的方法加入到已有的類中,除了你自己的代碼中,還可以用在標准庫和內置類中,這個特性被稱為Ruby Open Classes。下面我們通過一個示例簡單看下。
首先,我們自定義一個類Human,放在human.rb文件中:
class Human
def greeting
puts "hello everybody"
end
def hungry
puts "I am hungry"
end
end
接着,我們新增一個main.rb:
require_relative 'human'
john = Human.new
john.greeting
# hello everybody
john.hungry
# I am hungry
之后,我們在main.rb中重新定義hungry方法,
class Human
def hungry
puts "I could eat a horse"
end
end
john.hungry
# I could eat a horse
可以看到,這里在我們新增hungry方法之后,所有的Human類的實例均調用我們的新實現了,即使是已經創建好的實例,這里故意放到兩個文件中是想說明這個特性是可以跨文件甚至跨模塊的,對Ruby內置方法的替換也是可以的(謹慎使用)
puts "hello".size
class String
def size
puts "goodbye"
end
end
# 5
# goodbye
puts "hello".size
這個特性是十分強大的,讓我們可以很容易的對三方模塊進行擴展,也是Cocoapods的插件體系所依賴的基礎。
流程分析
Cocoapods的插件體系整體流程還是比較清晰的,下面我們就來逐步看下。
CLAide
首先,Cocoapods 提供了一個便捷的命令行工具庫 CLAide,這個庫包含很多功能,例如,一套命令基類,一套插件加載機制等。
Command基類
Command基類在lib/claide/command.rb中,這里提供了大量基礎功能,包括 run 、 options、 help等等。
首先,當我們每次執行 pod xxx 命令時候,會執行 bin目錄下的可執行文件pod。
require 'cocoapods'
if profile_filename = ENV['PROFILE']
# 忽略不相關內容...
else
Pod::Command.run(ARGV)
end
這里實際上是 Pod 模塊從CLAide繼承了子類Command < CLAide::Command,我們執行Pod命令時候,就會調用
def self.run(argv)
help! 'You cannot run CocoaPods as root.' if Process.uid == 0
verify_minimum_git_version!
verify_xcode_license_approved!
super(argv)
ensure
UI.print_warnings
end
實際上只是擴展了一些檢測git版本、xcode證書等,真正核心部分還是調用的CLAide的實現:
def self.run(argv = [])
plugin_prefixes.each do |plugin_prefix|
PluginManager.load_plugins(plugin_prefix)
end
argv = ARGV.coerce(argv)
command = parse(argv)
ANSI.disabled = !command.ansi_output?
unless command.handle_root_options(argv)
command.validate!
command.run
end
rescue Object => exception
handle_exception(command, exception)
end
可以看到這里真正執行命令之前會遍歷所有的插件前綴,並進行插件加載,回過頭來再查看 cocoapods/command.rb 會發現,這里指定了約定的插件前綴
self.plugin_prefixes = %w(claide cocoapods)
可以看到這里的插件分為兩種,我們目前只關心文件名為cocoapods前綴的插件。
PluginManager
我們深入PluginManager的具體實現看下,
def self.load_plugins(plugin_prefix)
loaded_plugins[plugin_prefix] ||=
plugin_gems_for_prefix(plugin_prefix).map do |spec, paths|
spec if safe_activate_and_require(spec, paths)
end.compact
end
def self.plugin_gems_for_prefix(prefix)
glob = "#{prefix}_plugin#{Gem.suffix_pattern}"
Gem::Specification.latest_specs(true).map do |spec|
matches = spec.matches_for_glob(glob)
[spec, matches] unless matches.empty?
end.compact
end
def self.safe_activate_and_require(spec, paths)
spec.activate
paths.each { |path| require(path) }
true
# 不相關代碼略去
# ...
end
為了減小篇幅,這里只貼了核心相關代碼,整體的流程大致是:
- 調用
PluginManager.load_plugins並傳入插件前綴 PluginManager.plugin_gems_for_prefix對插件名進行處理,取出我們需要加載的文件,例如cocoapods前綴在這里會轉換為所有包含cocoapods_plugin.rb的gem spec 信息及文件信息,例如~/cocoapods-qt/lib/cocoapods_plugin.rb- 調用
PluginManager.safe_activate_and_require進行對應的 gem spec 檢驗並對每個文件進行加載
至此,基本的插件加載流程大致梳理清楚了。
實操
下面我們看下如何自己擴展一個插件,關於這部分,Cocoapods其實也基本已經幫我們做了很多事情了,主要是 cocoapods-plugins, 它提供了一個插件創建的完整生命周期,包括新增、發布、檢索等。
Cocoapods-plugins
執行 pod plugins create cocoapods-test 之后,發現自動幫我們創建了一個gem工程,其中的 lib 文件夾下果然存在了一個 cocoapods_plugin.rb 文件,整體的目錄結構如下:
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── cocoapods-test.gemspec
├── lib
│ ├── cocoapods-test
│ │ ├── command
│ │ │ └── test.rb
│ │ ├── command.rb
│ │ └── gem_version.rb
│ ├── cocoapods-test.rb
│ └── **cocoapods_plugin.rb**
└── spec
├── command
│ └── test_spec.rb
└── spec_helper.rb
這里最核心的就是cocoapods_plugin.rb ,我們前面分析過,執行pod命令時候會主動加載所有cocoapods_plugin.rb文件,那么只要我們將需要擴展的類加到這里面,執行命令時候就會生效。
class Test < Command
self.summary = 'Short description of cocoapods-test.'
self.description = <<-DESC
Longer description of cocoapods-test.
DESC
self.arguments = 'NAME'
def initialize(argv)
@name = argv.shift_argument
super
end
def validate!
super
help! 'A Pod name is required.' unless @name
end
def run
UI.puts "Add your implementation for the cocoapods-test plugin in #{__FILE__}"
end
end
可以看到這里其實只是新增了一個 Test 命令,並加了一些描述信息。為了讓我們的擴展能生效,我們可以通過幾種方式,
- 本地gem源碼依賴
- 安裝gem產物
為了更貼近實際生產發布流程,這里我們采用第二種方式。
首先,我們編譯生成gem產物,
gem build cocoapods-test.gemspec
其次,本地安裝
gem install ~/CocoapodsQt/cocoapods-test/cocoapods-test-0.0.1.gem --local
此時,我們再執行 pod 命令

可以看到我們擴展的命令已經生效,接下來就可以開始愉快的coding了。
結語
至此,我們對Cocoapods的整體插件流程應該有了一個比較清晰的認識了,希望能給大家帶來一些幫助。
