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
  • createPods.xcodeprojengineering
  • 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:

  • generatePods.xcodeprojengineering
  • 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