27 KiB
Ruby
为了 iOS 工程化开展,自己最近开始了 Ruby 的学习,本篇博文就用来记录 Ruby 的学习心得和体验。本文作为切入点,展开聊聊原理、组件、脚本
一. Ruby VS Python
- Python 的解析器实现更成熟,第三方库质量高。但是 Ruby 包管理更简单、方便。
- Python 的应用领域广泛。而Ruby目前主要局限在在 Web 领域与精致项目。
- Python语法简单,Ruby更强大、灵活
二. Ruby 语法
1. 注释
单行注释
# 单行注释
puts "Hello, ruby!"
多行注释
=begin
多行注释:第1行
多行注释:第2行
多行注释:第3行
=end
print("Hello world!\n")
2. 打印
-
puts:打印后自动换行
-
print:打印后不会自动换行
-
另外如果打印内容携带变量格式的话,必须用双引号。比如
name = "@FantasticLBP" puts "hello, #{name}!" puts 'hello,#{name}!' # 输出 hello, @FantasticLBP! hello,#{name}! -
如果要直接 shell,则需要用 ``
puts `ruby --version` # 输出 ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.x86_64-darwin21]
3. 万物皆对象
puts 3.class
puts 'FantasticLBP'.class
puts nil.class
puts true.class
# 输出
Integer
String
NilClass
TrueClass
4. Symbol
- Ruby 是一个强大的面向对象脚本语言,一切皆是对象
- 在 Ruby 中 Symbol 表示“名字”,比如字符串的名字,标识符的名字。
- 创建一个 Symbol 对象的方法是在名字或者字符串前面加上冒号:
- 在 Ruby 中每一个对象都有唯一的对象标识符(Object Identifier)
- 对于 Symbol 对象,一个名字唯一确定一个 Symbol 对象
- Ruby 内部一直在使用 Symbol,Ruby内部也存在符号表
- Symbol 本质上是一个数字,这个数字和创建 Symbol 的名字形成一对一的映射;而String 对象是一个重量级的 用C结构体表示的家伙,因此使用 Symbol 和 String 的开销相差很大。
- 符号表是一个全局数据结构,它存放了所有 Symbol 的(数字ID,名字)。 Ruby 不会从中删除 Symbol ,因此 当你创建一个 Symbol 对象后,它将一直存在,直到程序结束。
三. 安装篇
1. rvm & rbenv
- rvm & rbenv 是一种命令行工具,可让您轻松地安装,管理和使用多个Ruby环境。
- 这两个工具本质都是 PATH 上做手脚,一个在执行前,一个在执行中
- 如果你不需要维护特定版本的 Ruby 项目,那么只需要装一个比较新的 Ruby 版本 就行了。
brew install ruby
2. gem
- 与大多数的编程语言一样,Ruby 也受益于海量的第三方代码库
- 这些代码库大部分都以 Gem 形式发布。 RubyGems 是设计用来帮助创建,分享和安装 这些代码库的
gem search -r/-f <gem>gem install <gem> --version <num>gem list有没有发现 gem search、install、list 和 cocoapods 的 pod 指令一样
3. Bundler
Bundler 能够跟踪并安装所需的特定版本的 gem,以此来为 Ruby 项目提供一致的运行环境

source 'https://rubygems.org' gem 'rails', '4.1.0.rc2'
gem ‘rack-cache'
gem 'nokogiri', '~> 1.6.1'
- 读取 Gemfile:Bundler 首先会读取当前目录下的 Gemfile 文件,解析其中声明的所有依赖项及其版本约束
- 解析依赖关系:
- 分析每个 gem 的版本要求,确定满足所有约束的最佳版本组合
- 处理依赖的依赖(传递依赖),确保整个依赖树的兼容性
- 检查本地缓存:
- 查看本地是否已缓存所需版本的 gem
- 如果有,直接使用本地缓存,跳过下载步骤
- 从源下载 gem:
- 对于本地没有的 gem,从 source 'https://rubygems.org' 指定的源下载
- 默认源是 RubyGems 官方仓库,国内用户可能需要切换到镜像源以提高速度
- 安装 gem 到项目目录:
- 默认情况下,gem 会被安装到项目根目录下的 vendor/bundle 目录
- 这种方式可以避免污染系统级的 gem 安装,实现项目间的依赖隔离
- 生成 Gemfile.lock:
- 安装完成后,Bundler 会生成 Gemfile.lock 文件
- 该文件记录了实际安装的每个 gem 的精确版本,确保团队协作或部署时使用完全相同的依赖版本
思考:怎么样,是不是发现 Ruby Bundler 工作流程和 iOS Cocoapods 的工作过程一致?是的,iOS Cocoapods 的设计就是参考 Ruby Bundler 的设计。甚至连 pod search、install、list API 设计也和 gem 一致
四. Cocoapods
1. cocoapods-binary
- cocoapods-binary 通过开关,在 pod insatll 的过程中进行 library 的预编译,生成 framework,并自动集成到项目中。
- 整个预编译工作分成了三个阶段来完成:
- binary pod 的安装
- binary pod 的预编译
- binary pod 的集成
2. Hook
- pre_install:Pod 下载之后,但在安装之前可以有时机对 Pod 进行任何更改
- post_install:当所需依赖安装完成,但此时生成的 XcodeProj 项目在写入磁盘前,我们可以对其进行最后的更改
3. Cocoapods 工程拆解
1. cocoapods.gemspec 文件
# encoding: UTF-8
require File.expand_path('../lib/cocoapods/gem_version', __FILE__)
require 'date'
Gem::Specification.new do |s|
s.name = "cocoapods"
s.version = Pod::VERSION
s.date = Date.today
s.license = "MIT"
s.email = ["eloy.de.enige@gmail.com", "fabiopelosin@gmail.com", "kyle@fuller.li", "segiddins@segiddins.me"]
s.homepage = "https://github.com/CocoaPods/CocoaPods"
s.authors = ["Eloy Duran", "Fabio Pelosin", "Kyle Fuller", "Samuel Giddins"]
s.summary = "The Cocoa library package manager."
s.description = "CocoaPods manages library dependencies for your Xcode project.\n\n" \
"You specify the dependencies for your project in one easy text file. " \
"CocoaPods resolves dependencies between libraries, fetches source " \
"code for the dependencies, and creates and maintains an Xcode " \
"workspace to build your project.\n\n" \
"Ultimately, the goal is to improve discoverability of, and engagement " \
"in, third party open-source libraries, by creating a more centralized " \
"ecosystem."
s.files = Dir["lib/**/*.rb"] + %w{ bin/pod bin/sandbox-pod README.md LICENSE CHANGELOG.md }
s.executables = %w{ pod sandbox-pod }
s.require_paths = %w{ lib }
# Link with the version of CocoaPods-Core
s.add_runtime_dependency 'cocoapods-core', "= #{Pod::VERSION}"
s.add_runtime_dependency 'claide', '>= 1.0.2', '< 2.0'
s.add_runtime_dependency 'cocoapods-deintegrate', '>= 1.0.3', '< 2.0'
s.add_runtime_dependency 'cocoapods-downloader', '>= 2.1', '< 3.0'
s.add_runtime_dependency 'cocoapods-plugins', '>= 1.0.0', '< 2.0'
s.add_runtime_dependency 'cocoapods-search', '>= 1.0.0', '< 2.0'
s.add_runtime_dependency 'cocoapods-trunk', '>= 1.6.0', '< 2.0'
s.add_runtime_dependency 'cocoapods-try', '>= 1.1.0', '< 2.0'
s.add_runtime_dependency 'molinillo', '~> 0.8.0'
s.add_runtime_dependency 'xcodeproj', '>= 1.27.0', '< 2.0'
s.add_runtime_dependency 'colored2', '~> 3.1'
s.add_runtime_dependency 'escape', '~> 0.0.4'
s.add_runtime_dependency 'fourflusher', '>= 2.3.0', '< 3.0'
s.add_runtime_dependency 'gh_inspector', '~> 1.0'
s.add_runtime_dependency 'nap', '~> 1.0'
s.add_runtime_dependency 'ruby-macho', '~> 4.1.0'
s.add_runtime_dependency 'addressable', '~> 2.8'
s.add_development_dependency 'bacon', '~> 1.1'
s.add_development_dependency 'bundler', '~> 2.0'
s.add_development_dependency 'rake', '~> 12.3'
s.required_ruby_version = '>= 2.6'
end
cocoapods.gemspec 作为 Cocoapods 工程的配置文件,类似 iOS 组件库的 Podspec 文件一样。
Cocospods 工程本身就是一个 Ruby gem,所以 cocoapods.gemspec 用于描述这个 gem 包的元数据,包括作者、版本、描述信息,包括一些导入的文件。
也声明了该 gem 包含的源代码文件、资源文件,以及它所依赖的其他 Ruby gem(比如 xcodeProj 等)和版本要求,确保安装时能正确解析依赖关系。
2. cocoapods-core
-
CocoaPods 核心模块,用来支持:
-
Pod::specification(podspec)
-
Pod::Podfile(Podfile)
-
Pod::Source(Spec repo)
-
-
cocoapods-deintergrate: 用于从项目中删除和取消集成 CocoaPods,指令为
pod deintegrate -
Xcodeproj:
来操作 Xcode 项目的创建和编辑等。同时支持 Xcode 项目的脚本管理和 libraries 构建,以及 Xcode 工作空间(.xcworkspace) 和配置文件 .xcconfig 的管理
-
cocospods-downloader:用于下载和管理引入的源码
-
cocoapods-plugins: 插件管理功能
-
cocoapods-try:可以快速体验该 pod 的 Demo 项目
-
CLAide:命令行解释器
-
ruby-macho:一个用于检查和修改 Mach-O 文件的 Ruby 库
3. Podfile
Podfile 是一个文件,以 DSL 来描述依赖关系,用于描述项目所需要的第三方库。
4. VSCode 调试 Cocoapods
- 新创建文件夹
RubyDemos - 从 git clone Cocoapods 源码到本地目录
- 进入到 Cocoapods 文件夹,将分支切换到和本机安全的 pod 版本一致的分支,指令为: git checkout
pod --version RubyDemos根目录下创建一个 Xcode iOS 工程,并为其编写 Podfile 文件。目的是为了调试 CocoapodsRubyDemos根目录下创建一个 Gemfile 文件。内容如下:
source 'https://rubygems.org'
gem 'cocoapods', path: './Cocoapods' # 指向本地源码
gem 'debug', '~> 1.9.0' # 调试用
- 终端执行
bundle install指令 - 此时,项目文件夹为:
.
├── CocoaPods
├── Demos
├── Gemfile
├── Gemfile.lock
└── StaticLibConflictsDemo
- 用 VSCode 打开工程。进入 Run and Debug 面板 → 点击 create a launch.json file → 选择 Ruby → 选 Debug Local File。
- 修改 launch.json 内容。
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug CocoaPods",
"type": "rdbg", // 必须为 rdbg(debug 工具类型)
"request": "launch",
"script": "${workspaceFolder}/CocoaPods/bin/pod", // 源码中的 pod 入口
"args": ["install", "--verbose"], // 执行 pod install
"cwd": "${workspaceFolder}/StaticLibConflictsDemo", // 测试工程目录(Podfile 所在目录)
"useBundler": true, // 强制通过 bundle 执行
"askParameters": false // 关闭参数询问(避免干扰)
}
]
}
接下来就可以愉快的调试了。 说明:
- pod 的每个指令,分别对应 Cocoapods 工程中一个代码文件

- 同时根据观察,发现
target do的代码比 pre_install、post_install 执行更早。所以我们可以做一些脚本化的操作。 比如下面,增加了一段自定义的脚本
五、体验核心依赖库能力
1. ruby-macho
1. 操作 Mach-O 文件
读取 Mach-O 文件,并用操作对象的方式去读区、增加、删除信息。
分别对 Mach-O 增加了一个 LC_RPATH 类型的 Load Command
lc_rpath = MachO::LoadCommands::LoadCommand.create(:LC_RPATH, 'test_rpath')
file_exec.add_command lc_rpath
对 Mach-O 文件中删除了类型为 LC_LINKER_OPTION 的 Load Command
2. 操作动态库
ruby-macho 还可以读取动态库信息,下面演示几个能力:
-
读取动态库所依赖的动态库信息
# 打印出当前 Mach-O 文件使用的所有动态库 macho_dylibs = MachO::Tools.dylibs(macho_filepath) macho_dylibs.each do | dylib | puts dylib end -
修改动态库的 id
# 修改动态库的 id MachO::Tools.change_dylib_id(macho_copy_filepath, 'test_selfdefined_dylib')修改后在终端用指令 objdump --macho --private-headers ./macho/libAFNetworking_copy.dylib | grep 'LC_ID_DYLIB' -A 5 验证效果,如下图所示:
也可以直接查看动态库的 id
original_dylib_id = MachO::MachOFile.new(macho_filepath) copyed_dylib_id = MachO::MachOFile.new(macho_copy_filepath) # 也可以直接查看动态库的 id puts "before change dylib id: #{original_dylib_id.dylib_id}" puts "after change dylib id: #{copyed_dylib_id.dylib_id}" -
修改动态库 rpath
MachO::Tools.change_rpath(macho_copy_filepath, '@loader_path/Frameworks', '@loader_path/Frameworks/FantasicLBP')
3. 合并动态库到胖二进制
ruby-macho 有很多丰富的 API,基本上开发阶段所遇到的问题,都有现成的 API 解决。比如二进制指令集的合并
filenames = [dylib_merged_filepath, dylib_arm_filepath]
# # 第一个参数为合并之后的动态库名称,第二个参数为需要合并的一堆动态库
MachO::Tools.merge_machos(dylib_merged_filepath, *filenames)
合并后用 otool -f ./macho/libAFNetworking_merged.dylib 指令查看指令集
2. Xcodeproj
通过 xcodeproject 路径构建 xcodeproj 对象 app_project = Xcodeproj::Project.new(app_project_path)
通过 xcworkspace 路径构建 xcworkspace 对象 app_workspace = Xcodeproj::Workspace.new_from_xcworkspace(app_workspace_project_path)
然后通过对象的方式访问 xcworkspace 的信息。比如 schemes
app_workspace.schemes.each do | scheme |
puts scheme
end
也可以针对特定的 target 修改 xcconfig
# 修改 targets 中第一个对应 configurations 为指定的 xcconfig 文件
configuration_path = File.dirname(__FILE__) + '/xcodeproject/AFNetworkingMock/xcodeproj-testing.debug.xcconfig'
# 转换为 xcode 的 file
xc_file = app_project.new_file(configuration_path)
app_project.targets.first.build_configurations.first.base_configuration_reference = xc_file
效果如下:
也可以对特定的 target 修改 buildSetting 中的信息,比如 bundle id
# 修改 target 的 bundle id
app_project.targets.each do | target |
target.build_configurations.each do | config |
config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.github.FantasticLBP.AFNetworkingMock' if config.name == 'Debug'
end
end
六、自定义 cocoapods 插件
1. 自定义 gem 库
1. 初始化创建
输入指令 bundle gem cocoapods-hmap 自定义一个名为 cocoapods-hmap 的 gem 库
2. 工程结构说明
得到的工程结构是:
.
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│ ├── console
│ └── setup
├── cocoapods-hmap.gemspec
├── lib
│ └── cocoapods
│ ├── hmap
│ │ └── version.rb
│ └── hmap.rb
└── sig
└── cocoapods
└── hmap.rbs
说明:
-
cocoapods-hmap.gemspec:
- gem 的核心配置文件,定义了 gem 的名称、版本、作者、依赖、描述、文件包含规则等关键信息。
- 用于打包和发布 gem 到 RubyGems 仓库,是 gem 工程的 “身份证
-
Rakefile:
- 定义自动化任务(如测试、打包、发布等),通过
rake <任务名>执行(类似Makefile)。 - 常见任务:
rake spec(运行测试)、rake build(打包 gem)、rake release(发布到 RubyGems)
- 定义自动化任务(如测试、打包、发布等),通过
-
Gemfile:
- 定义 gem 开发 / 测试阶段的依赖(如测试框架
rspec、打包工具等),类似前端的package.json。 - 通过
bundle install安装依赖,依赖版本由Gemfile.lock(自动生成)锁定
- 定义 gem 开发 / 测试阶段的依赖(如测试框架
-
**源代码目录:lib/ **
-
gem 的核心功能代码存放目录,Ruby 会自动加载该目录下的文件
-
lib/cocoapods/hmap.rb:gem 的主入口文件,定义了CocoaPods::Hmap模块的核心逻辑,是功能实现的主要载体(如与 CocoaPods 集成的逻辑、头文件映射相关功能等) -
lib/cocoapods/hmap/version.rb: 单独存放版本号的文件,通常定义CocoaPods::Hmap::VERSION常量,便于统一管理版本(在gemspec中会引用该常量)。
-
3. 改造并 run 起来
-
修改 gemspec 文件中带有 url、uri 的字段,开发阶段可以修改为任何一个 url 字符串
-
修改自带的工程结构,比如
cocoapods/hmap改为cocoapods-hmap,将对应的文件也移动位置 -
我们预期的效果是:在终端输入 pod hmap 就可以将工程中的静态库 Header Search Path 传统查找模式改为 hmap 文件配置模式。所以需要的的步骤就是在 bin 目录下创建 hmap.rb,然后通过 bin 目录下的代码调用 lib 目录下的 HMap.rb 能力。
-
为此,需要:
-
在 lib 目录下创建 HMap.rb 文件
require_relative "version" module CocoapodsHmap class HMap def initialize puts "Cocoapods HMap initialized" end def self.run puts "Running Cocoapods HMap..." end end end -
在 bin 目录下创建 hmap.rb 文件
#!/usr/bin/env ruby require "bundler/setup" require "cocoapods-hmap/hmap" # 打印携带的参数 puts ARGV CocoapodsHmap::HMap.run
-
-
为了在 VSCode 中测试,需要在 Gemfile 中添加一行 gem 'debug', '~> 1.9.0' # 调试用
-
工程根目录创建
.vscode文件夹,创建 launch.json 文件。内容如下:{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "rdbg", "name": "Debug current file with rdbg", "request": "launch", "script": "${workspaceFolder}/bin/hmap", "args": ["hmap"], // pod 命令的参数 "askParameters": true, "cwd": "${workspaceFolder}", // pod 执行命令的路径 }, { "type": "rdbg", "name": "Attach with rdbg", "request": "attach" } ] }
VSCode 中运行效果如下:
4. 如何将自定义的指令加入到 cocoapods 中
-
在 Command 目录下创建
hmap文件,不带任何拓展名。rake 处理后,最后会变为/Users/unix_kernel/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/cocoapods-hmap-0.1.0/bin/hmap: Ruby script text executable, ASCII text -
在 lib 目录下,创建
cocoapods-hmap文件夹。- 在
cocoapods-hmap文件夹内创建command文件夹,在command文件夹内创建hmap.rb文件 - class 继承自 Pod::Command,并实现相关方法。比如 initialize、validate!、run
- 在
-
在 lib 目录下,创建
cocoapods-plugin.rb文件- 暴露继承自 Pod::Command 的 hmap command
调试运行后的效果如下:
类名小写,和类文件关联起来
比如 install.rb 中,类名为 class Install ,内部会记录为 {"install": "install.rb"}
5. 打包安装到本地
在终端项目目录下,执行指令 rake install:local ,主要用于在本地构建并安装当前开发的 gem 包,方便开发者进行本地测试和调试。
一开始有报错,如下图所示。按照提示修改 cocoapods-hmap.gemspec 中的配置,然后就可以成功安装了。然后输入 gem list 查看:
输入: gem info cocoapods-hmap 查看安装信息
就目前的功能进行测试:
| 条件 | 预期 | 结果 |
|---|---|---|
| 随便一个工程目录,没有 Podfile 文件 | 执行 pod hmap 会报错 |
符合预期 |
| 存在 Podfile 文件的目录 | 正常执行 run 方法里面的逻辑(打印逻辑) | 符合预期 |
6. Hook 能力
1. post_install
-
修改插件入口文件 (cocoapods_plugin.rb)
先在
lib/cocoapods-plugin.rb中注册插件和对应的 hook 能力。利用 API:Pod::HooksManager.register('cocoapods-hmap', :post_install),其文档说明如下:register(plugin_name, hook_name, &block)
Definitions: [hooks_manager.rb](vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html)
Registers a block for the hook with the given name.
@param [String] plugin_name The name of the plugin the hook comes from.
@param [Symbol] hook_name The name of the notification.
@param [Proc] block The block.
Pod::HooksManager.register('cocoapods-hmap', :post_install) do |context, options| argv = CLAide::ARGV.new([]) # 创建一个空的参数数组 command = Pod::Command::HMap.new(argv) command.run_post_install(context, options) end -
完善 HMap 命令类
修改
lib/cocoapods-hmap/command/hmap.rb,增加run_post_install方法module Pod class Command class HMap < Command // ... # Post-install 钩子执行的方法 def run_post_install(context, options = {}) puts "[Cocoapods hmap] Running HMap command in post_install hook..." end end end end -
测试配置
修改测试项目的 Podfile 文件,声明 plugin 'cocoapods-hmap'
platform :ios, '9.0' plugin 'cocoapods-hmap' post_install do | installer | puts "Self defined post_install hook" end target 'StaticLibConflictsDemo' do # Comment the next line if you don't want to use dynamic frameworks # AFNetworking 以静态库的形式被依赖 pod 'AFNetworking' # 脚本化 script_phase :name => 'Run Self-defined Script', :script => "echo 'This is a self-defined script phase'", :input_files => [], :execution_position => :after_compile end -
修改
cocoapods-hmap工程的 VSCode 的 launch.json 文件因为 cocoapods-hmap 工程和 iOS Pods 工程不在一个目录,所以可以在 args 的第二个参数设置为测试工程路径
{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "rdbg", "name": "Debug current file with rdbg", "request": "launch", // "script": "${workspaceFolder}/bin/hmap", "script": " /Users/unix_kernel/.rbenv/versions/3.2.2/bin/pod", "args": [ "install", "--project-directory=${workspaceFolder}/../StaticLibConflictsDemo" ], // pod 命令的参数 "askParameters": true, "cwd": "${workspaceFolder}", // pod 执行命令的路径 }, { "type": "rdbg", "name": "Attach with rdbg", "request": "attach" } ] } -
测试:存在2种方法
2. pre_install
2. CocoaPods 插件系统设计
CocoaPods 通过严格的目录结构约定来加载插件:
lib/
├── cocoapods-plugin.rb # 插件主入口文件(必需)
└── cocoapods-hmap/ # 插件命名空间目录
└── command/ # 命令目录
└── hmap.rb # 命令实现文件(必需)
1. 自动加载机制
CocoaPods 启动时会自动执行以下操作:
- 扫描已安装的 gem
- 查找所有以
cocoapods-为前缀的 gem - 加载这些 gem 中的
lib/cocoapods-plugin.rb文件 - 通过该文件加载插件功能
2. 关键文件
-
插件入口文件 (
lib/cocoapods-plugin.rb)require 'cocoapods-hmap/command/hmap' -
命令实现文件 (lib/cocoapods-hmap/command/hmap.rb)


