If you have any iOS development experience, you’ve probably used CocoaPods, and those of you who know CI and CD know Fastlane. Both of these third-party libraries, which are handy for iOS development, are written in Ruby. Why?
Leaving this topic aside, let’s take a look at how CocoaPods and Fastlane are used. The first is CocoaPods. Every project that uses CocoaPods has a Podfile:
source 'https://github.com/CocoaPods/Specs.git'
target 'Demo' do
pod 'Mantle'.'~ > 1.5.1'
pod 'SDWebImage'.'~ > 3.7.1'
pod 'BlocksKit'.'~ > 2.2.5'
pod 'SSKeychain'.'~ > 1.2.3'
pod 'UMengAnalytics'.'~ > 3.1.8'
pod 'UMengFeedback'.'~ > 1.4.2'
pod 'Masonry'.'~ > 0.5.3'
pod 'AFNetworking'.'~ > against 2.4.1'
pod 'Aspects'.'~ > 1.4.1'
endCopy the code
This is an example of using Podfile to define a dependency, but the Podfile description of a constraint actually looks like this:
source('https://github.com/CocoaPods/Specs.git')
target('Demo') do
pod('Mantle'.'~ > 1.5.1')...endCopy the code
Ruby code can omit parentheses when calling methods.
The constraints described in the Podfile are shorthand for code that can be parsed as Ruby code.
The code Fastfile in Fastlane is similar:
lane :beta do
increment_build_number
cocoapods
match
testflight
sh "./customScript.sh"
slack
endCopy the code
Scripting with descriptive “code” is hard for anyone who hasn’t touched or worked with Ruby to believe that the above text is code.
Ruby overview
Before introducing CocoaPods, let’s take a quick look at some of Ruby’s features. I tend to use the word elegant when preaching to people around me (manual smile).
In addition to being elegant, Ruby’s syntax is expressive and flexible enough to quickly meet our needs. Here are some features in Ruby.
Everything is an object
In many languages, such as Java, numbers and other primitive types are not objects. In Ruby, all elements, including primitive types, are objects and there is no concept of operators. The so-called 1 + 1 is just the syntactic sugar of 1.
Thanks to the concept of everything as an object, in Ruby you can send a methods message to any object and introspect at runtime, so EVERY time I forget a method, I use methods directly to “look up documentation” :
2.3.1:003 > 1.methods
=> [: %.: &.: *.: +.: -.: /.: <.: >.: ^.: |.: ~.: - the @.: * *.: < = >.: < <.: > >.: < =.: > =.: = =.: = = =.: [].:inspect.:size.:succ.:to_s.:to_f.:div.:divmod.:fdiv.:modulo.:abs.:magnitude.:zero?.:odd?.:even?.:bit_length.:to_int.:to_i.:next.:upto.:chr.:ord.:integer?.:floor.:ceil.:round.:truncate.:downto.:times.:pred.:to_r.:numerator.:denominator.:rationalize.:gcd.:lcm.:gcdlcm.: + @.:eql?.:singleton_method_added.:coerce.:i.:remainder.:real?.:nonzero?.:step.:positive?.:negative?.:quo.:arg.:rectangular.:rect.:polar.:real.:imaginary.:imag.:abs2.:angle.:phase.:conjugate.:conj.:to_c.:between?.:instance_of?.:public_send.:instance_variable_get.:instance_variable_set.:instance_variable_defined?.:remove_instance_variable.:private_methods.:kind_of?.:instance_variables.:tap.:is_a?.:extend.:define_singleton_method.:to_enum.:enum_for.: = ~.:! ~,:respond_to?.:freeze.:display.:send.:object_id.:method.:public_method.:singleton_method.:nil?.:hash.:class.:singleton_class.:clone.:dup.:itself.:taint.:tainted?.:untaint.:untrust.:trust.:untrusted?.:methods.:protected_methods.:frozen?.:public_methods.:singleton_methods.:! .:! =,:__send__.:equal?.:instance_eval.:instance_exec.:__id__]Copy the code
Calling methods to object 1 here, for example, returns all methods it can respond to.
Not only does everything object reduce type inconsistencies in the language and eliminate the boundary between basic data types and objects; This concept also simplifies the composition of the language, so that Ruby has only objects and methods, which also reduces the complexity of understanding the language:
- Use objects to store state
- Objects communicate with each other through methods
block
Ruby’s support for the functional programming paradigm is through blocks, which are somewhat different from blocks in Objective-C.
First, a block in Ruby is also an object. All blocks are instances of Proc classes, i.e. all blocks are first-class and can be passed as arguments and returned.
def twice(&proc)
2.times { proc.call() } if proc
end
def twice
2.times { yield } if block_given?
endCopy the code
Yield will call an external block, block_given? Used to determine whether the current method passed a block.
When this method is called, it looks like this:
twice do
puts "Hello"
endCopy the code
eval
The last feature to mention is Eval, which dates back decades to Lisp. Eval is a method that executes strings as code, meaning that eval blurs the boundary between code and data.
> eval "1 plus 2 times 3"= >7Copy the code
With the eval method, we have a much more dynamic ability to use strings to change control flow and execute code at run time. You don’t have to manually parse the input and generate a syntax tree.
Parse the Podfile manually
With a brief understanding of the Ruby language, we can start writing a simple script to parse podfiles.
Here, we take a very simple Podfile as an example, using a Ruby script to resolve the dependencies specified in the Podfile:
source 'http://source.git'
platform :ios.'8.0'
target 'Demo' do
pod 'AFNetworking'
pod 'SDWebImage'
pod 'Masonry'
pod "Typeset"
pod 'BlocksKit'
pod 'Mantle'
pod 'IQKeyboardManager'
pod 'IQDropDownTextField'
endCopy the code
Because source, platform, target, and pod are all methods, here we need to build a context that contains the above methods:
# eval_pod.rb
$hash_value = {}
def source(url)
end
def target(target)
end
def platform(platform, version)
end
def pod(pod)
endCopy the code
A global variable hash_value is used to store the dependencies specified in the Podfile, and a skeleton Podfile parsing script is built; Instead of trying to refine the implementation details of these methods, let’s try reading the contents of the Podfile and executing them to see if there’s any problem.
Add these lines at the bottom of the eval_pod.rb file:
content = File.read './Podfile'
eval content
p $hash_valueCopy the code
This reads the contents of the Podfile, executes the contents as a string, and finally prints the value of hash_value.
$ ruby eval_pod.rbCopy the code
Running this Ruby code produces no output, but it does not report any errors, so we can refine these methods:
def source(url)
$hash_value['source'] = url
end
def target(target)
targets = $hash_value['targets']
targets = [] if targets == nil
targets << target
$hash_value['targets'] = targets
yield if block_given?
end
def platform(platform, version)
end
def pod(pod)
pods = $hash_value['pods']
pods = [] if pods == nil
pods << pod
$hash_value['pods'] = pods
endCopy the code
After adding the implementation of these methods, running the script again will get the dependency information in the Podfile, but the implementation here is very simple and many cases are not handled:
$ ruby eval_pod.rb
{"source"=>"http://source.git", "targets"=>["Demo"], "pods"=>["AFNetworking", "SDWebImage", "Masonry", "Typeset", "BlocksKit", "Mantle", "IQKeyboardManager", "IQDropDownTextField"]}Copy the code
Podfile parsing in CocoaPods is pretty much the same as the implementation here, so it’s time to move on to CocoaPods implementation.
The realization of the CocoaPods
After a brief introduction to Ruby’s syntax and how to parse podfiles, let’s take a closer look at how CocoaPods manages dependencies for iOS projects, and what pod Install does.
The Pod install process
What does the pod install command actually do? First, in CocoaPods, all commands are sent from the Command class to the corresponding class, and the class that actually executes pod Install is Install:
module Pod
class Command
class Install < Command
def runverify_podfile_exists! installer = installer_for_config installer.repo_update = repo_update? (:default= >false)
installer.update = false
installer.install!
end
end
end
endCopy the code
This will fetch an instance of the Installer from config and execute install! The installer has an update property, and this is the biggest difference between Pod Install and update, where the latter disregards the existing podfile. lock file and re-analyzes the dependency:
module Pod
class Command
class Update < Command
def run. installer = installer_for_config installer.repo_update = repo_update? (:default= >true)
installer.update = true
installer.install!
end
end
end
endCopy the code
Podfile analytical
Parsing dependencies in Podfiles is similar to the manual parsing of Podfiles, which is done by cocoapods-core, and is already done in Installer_for_config:
def installer_for_config
Installer.new(config.sandbox, config.podfile, config.lockfile)
endCopy the code
This method takes an instance of the Podfile class from config.podfile:
def podfile
@podfile ||= Podfile.from_file(podfile_path) if podfile_path
endCopy the code
The podfile. from_file class method is defined in cocoapods-core and is used to analyze dependencies defined in podfiles. This method selects a different call path depending on the type of the Podfile:
Podfile.from_file
`-- Podfile.from_ruby |-- File.open `-- evalCopy the code
The From_Ruby class method reads the data from the file just as we did in the Podfile parsing method earlier, and then uses eval to execute the contents of the file directly as Ruby code.
def self.from_ruby(path, contents = nil)
contents ||= File.open(path, 'r:utf-8', &:read)
podfile = Podfile.new(path) do
begin
eval(contents, nil, path.to_s)
rescue Exception => e
message = "Invalid `#{path.basename}` file: #{e.message}"
raise DSLError.new(message, path, e, contents)
end
end
podfile
endCopy the code
At the top of the Podfile class, we use Ruby’s Mixin syntax to blend in the context needed for code execution in the Podfile:
include Pod::Podfile::DSLCopy the code
All the methods you see in Podfile are defined under the DSL module:
module Pod
class Podfile
module DSL
def pod(name = nil, *requirements) end
def target(name, options = nil) end
def platform(name, target = nil) end
def inhibit_all_warnings! end
def use_frameworks!(flag = true) end
def source(source) end.end
end
endCopy the code
This module defines many of the methods used in Podfile. When executing code in the eval file, the methods in this module are executed. Here’s a quick look at the implementation of a few of these methods, such as the source method:
def source(source)
hash_sources = get_hash_value('sources') || []
hash_sources << source
set_hash_value('sources', hash_sources.uniq)
endCopy the code
This method adds the new source to the existing source array and then updates the value of the original sources.
Slightly more complicated is the target method:
def target(name, options = nil)
if options
raise Informative, "Unsupported options `#{options}` for " \
"target `#{name}`."
end
parent = current_target_definition
definition = TargetDefinition.new(name, parent)
self.current_target_definition = definition
yield if block_given?
ensure
self.current_target_definition = parent
endCopy the code
This method creates an instance of the TargetDefinition class and sets the target_DEFINITION of the current environment system to the instance just created. In this way, any subsequent dependencies defined using POD will be populated with the current TargetDefinition:
def pod(name = nil, *requirements)
unless name
raise StandardError, 'A dependency requires a name.'
end
current_target_definition.store_pod(name, *requirements)
endCopy the code
When the POD method is called, store_pod is executed to store the dependencies in the Dependencies array in the current target:
def store_pod(name, *requirements)
return if parse_subspecs(name, requirements)
parse_inhibit_warnings(name, requirements)
parse_configuration_whitelist(name, requirements)
ifrequirements && ! requirements.empty? pod = { name => requirements }else
pod = name
end
get_hash_value('dependencies', []) << pod
nil
endCopy the code
To summarize, CocoaPods parses podfiles in much the same way that we manually parsed podfiles in the previous section. Build a context containing methods, and then execute eval directly on the contents of the file as code. This makes parsing a Podfile very easy, as long as the data in the Podfile is compliant with the specification.
Procedure for installing dependencies
The contents of the Podfile parsed are converted to an instance of the Podfile class, and the Installer instance method install! This information is used to install the dependencies of the current project, and the whole process of installing dependencies has about four parts:
- Resolve dependencies in podfiles
- Download the dependent
- create
Pods.xcodeproj
engineering - Integrated workspace
def install!
resolve_dependencies
download_dependencies
generate_pods_project
integrate_user_project
endCopy the code
In the resolve_dependencies call to the install method above, which creates an instance of the Analyzer class, you’ll see some very familiar strings:
def resolve_dependencies
analyzer = create_analyzer
plugin_sources = run_source_provider_hooks
analyzer.sources.insert(0, *plugin_sources)
UI.section 'Updating local specs repositories' do
analyzer.update_repositories
end if repo_update?
UI.section 'Analyzing dependencies' do
analyze(analyzer)
validate_build_configurations
clean_sandbox
end
endCopy the code
Updating local Specs Repositories and Analyzing Dependencies, which are common in CocoaPods, are output to the terminal from here. This method is responsible for Updating all local PodSpec files. Dependencies in the current Podfile are also parsed:
def analyze(analyzer = create_analyzer)
analyzer.update = update
@analysis_result = analyzer.analyze
@aggregate_targets = analyzer.result.targets
endCopy the code
The analyzer.analyze method eventually calls Resolver’s instance method resolve:
def resolve
dependencies = podfile.target_definition_list.flat_map do |target|
target.dependencies.each do |dep|
@platforms_by_dependency[dep].push(target.platform).uniq! if target.platform
end
end
@activated = Molinillo::Resolver.new(self.self).resolve(dependencies, locked_dependencies)
specs_by_target
rescue Molinillo::ResolverError => e
handle_resolver_error(e)
endCopy the code
Molinillo::Resolver is the class used to resolve dependencies.
Resolve Dependencies
CocoaPods uses a dependency resolution algorithm called Milinillo to resolve dependencies declared in podfiles; However, I could not find any other information about this algorithm on Google, and assumed that it was created by CocoaPods to solve the dependency relationship in iOS.
At the heart of Milinillo’s algorithm are Backtracking and forward check, which track two states in the stack (dependencies and possibilities).
I do not want to get into the analysis of the algorithm execution process here. If you are interested, you can take a look at the ARCHITECTURE. Md file in the warehouse, which explains the working principle of Milinillo algorithm in detail and gives a detailed introduction to its functional execution process.
The Molinillo::Resolver method returns a dependency graph that looks something like this:
Molinillo::DependencyGraph:[
Molinillo::DependencyGraph::Vertex:AFNetworking(#<Pod::Specification name="AFNetworking">),
Molinillo::DependencyGraph::Vertex:SDWebImage(#<Pod::Specification name="SDWebImage">),
Molinillo::DependencyGraph::Vertex:Masonry(#<Pod::Specification name="Masonry">),
Molinillo::DependencyGraph::Vertex:Typeset(#<Pod::Specification name="Typeset">),
Molinillo::DependencyGraph::Vertex:CCTabBarController(#<Pod::Specification name="CCTabBarController">),
Molinillo::DependencyGraph::Vertex:BlocksKit(#<Pod::Specification name="BlocksKit">),
Molinillo::DependencyGraph::Vertex:Mantle(#<Pod::Specification name="Mantle">),
...
]Copy the code
This dependency diagram is made up of an array of nodes. After CocoaPods gets hold of this dependency diagram, it groups all the specifications by Target in specs_by_target:
{
#<Pod::Podfile::TargetDefinition label=Pods>=>[],
#<Pod::Podfile::TargetDefinition label=Pods-Demo>=>[
#<Pod::Specification name="AFNetworking">,
#<Pod::Specification name="AFNetworking/NSURLSession">,
#<Pod::Specification name="AFNetworking/Reachability">,
#<Pod::Specification name="AFNetworking/Security">,
#<Pod::Specification name="AFNetworking/Serialization">,
#<Pod::Specification name="AFNetworking/UIKit">,
#<Pod::Specification name="BlocksKit/Core">,
#<Pod::Specification name="BlocksKit/DynamicDelegate">,
#<Pod::Specification name="BlocksKit/MessageUI">,
#<Pod::Specification name="BlocksKit/UIKit">,
#<Pod::Specification name="CCTabBarController">,
#<Pod::Specification name="CategoryCluster">,
...
]
}Copy the code
These specifications contain all of the third-party frameworks that the current project relies on, including names, versions, sources, etc., for downloading dependencies.
Download the dependent
After the dependency resolution returns a set of Specification objects, it’s time for the second part of Pod Install to download the dependencies:
def install_pod_sources
@installed_specs = []
pods_to_install = sandbox_state.added | sandbox_state.changed
title_options = { :verbose_prefix= >'-> '.green }
root_specs.sort_by(&:name).each do |spec|
if pods_to_install.include? (spec.name)if sandbox_state.changed.include? (spec.name) && sandbox.manifest previous = sandbox.manifest.version(spec.name) title ="Installing #{spec.name} #{spec.version} (was #{previous})"
else
title = "Installing #{spec}"
end
UI.titled_section(title.green, title_options) do
install_source_of_pod(spec.name)
end
else
UI.titled_section("Using #{spec}", title_options) do
create_pod_installer(spec.name)
end
end
end
endCopy the code
You’ll see more familiar prompts in this method. CocoaPods uses a sandbox to store data for existing dependencies, and when updating existing dependencies, it displays different prompts depending on the state of the dependency:
-> Using AFNetworking (3.1.0)
-> Using AKPickerView (0.2.7)
-> Using BlocksKit (2.2.5) was (2.2.4)
-> Installing MBProgressHUD (1.0.0)...Copy the code
Although there are three hints here, CocoaPods only calls two separate methods based on the state:
install_source_of_pod
create_pod_installer
The create_pod_Installer method simply creates an instance of PodSourceInstaller and adds it to the Pod_installers array. Since the dependent version has not changed, there is no need to download it again. The other method’s install_source_of_POD call stack is very large:
installer.install_source_of_pod
|-- create_pod_installer
| `-- PodSourceInstaller.new
`-- podSourceInstaller.install!
`-- download_source `-- Downloader.download
`-- Downloader.download_request `-- Downloader.download_source
|-- Downloader.for_target
| |-- Downloader.class_for_options
| `-- Git/HTTP/Mercurial/Subversion.new |-- Git/HTTP/Mercurial/Subversion.download `-- Git/HTTP/Mercurial/Subversion.download!
`-- Git.cloneCopy the code
Download_source executes another CocoaPods component cocoapods-Download method at the end of the call stack:
def self.download_source(target, params)
FileUtils.rm_rf(target)
downloader = Downloader.for_target(target, params)
downloader.download
target.mkpath
if downloader.options_specific?
params
else
downloader.checkout_options
end
endCopy the code
The for_target method creates a downloader depending on the source, because dependencies can be downloaded over different protocols or methods, such as Git/HTTP/SVN, etc. The component cocoapods-downloader uses a different method to download dependencies based on the dependency parameter options in the Podfile.
Most rely on will be downloaded to the ~ / Library/Caches/CocoaPods/Pods/Release/this folder, and then copied to the project from the directory. / the Pods, it also completes the whole CocoaPods download process.
Generate the Pods. Xcodeproj
CocoaPods has successfully downloaded all dependencies into the current project through the component cocoapods-downloader, where all dependencies are packaged into Pods.xcodeProj:
def generate_pods_project(generator = create_generator)
UI.section 'Generating Pods project' dogenerator.generate! @pods_project = generator.project run_podfile_post_install_hooks generator.write generator.share_development_pod_schemes write_lockfilesend
endCopy the code
Generate_pods_project executes the PodsProjectGenerator instance method generate! :
def generate!
prepare
install_file_references
install_libraries
set_target_dependencies
endCopy the code
This method does a few things:
- generate
Pods.xcodeproj
engineering - Add files in dependencies to the project
- Add Library from dependencies to the project
- Set Target Dependencies
All of this comes from CocoaPods’ Xcodeproj component, which works with groups and files in an Xcode project. We all know that most changes to Xcode projects are made to a file called project.pbxProj, and Xcodeproj is a third-party library developed by the CocoaPods team to manipulate this file.
Generate the workspace
This last section is somewhat similar to the process of generating pods.xcodeproj, using the class UserProjectIntegrator and calling the method integrate! The Target required by the integration project begins:
def integrate!
create_workspace
integrate_user_targets
warn_about_xcconfig_overrides
save_projects
endCopy the code
For this part of the code, I don’t want to expand into details. I will briefly introduce what the code does here. First, Xcodeproj::Workspace will create a Workspace, and then we will get all Target instances to integrate, and call their integrate! Methods:
def integrate!
UI.section(integration_message) do
XCConfigIntegrator.integrate(target, native_targets)
add_pods_library
add_embed_frameworks_script_phase
remove_embed_frameworks_script_phase_from_embedded_targets
add_copy_resources_script_phase
add_check_manifest_lock_script_phase
end
endCopy the code
Method add each Target to the project, use Xcodeproj to modify Settings like Copy Resource Script Phrase, save project. Pbxproj, and the whole Pod install process is over.
conclusion
Pod Install is different from pod Update. Every time a pod install or update is executed, the podfile. lock file is generated or modified. The former does not modify podfile. lock to display the specified version, while the latter ignores the contents of the file and tries to update all pods to the latest version.
Although CocoaPods projects a lot of code, the logic of the code is very clear and the process of managing and downloading dependencies is intuitive and logical.
other
Making Repo: iOS – Source Code – Analyze
Follow: Draveness dead simple
Source: draveness.me/cocoapods