introduce

Cocoapods-binary was first discovered in Cocoapods’ Blog: Pre-compiling Dependencies. Although unofficial production, but is a domestic programmer’s work, medium original introduction: Pod precompiled fool solution:

A CocoaPods plugin to integrate pods in form of prebuilt frameworks, not source code, by adding just one flag in podfile. Speed up compiling dramatically.

In simple terms, Cocoapods-Binary switches on and off during Pod Insatll to precompile the library, generate the framework, and integrate it into the project automatically.

The whole precompilation work is divided into three stages:

  • Binary Pod installation
  • The pre-compilation of binary Pod
  • Binary Pod integration

Binary Pod installation

Binary Pod installation starts with pre_install hook.

When we execute pod Install from the command line, CocoaPods executes several of the methods in the 👆 figure in turn. Cocoapods-binary’s pre_install is the logic inserted during the prepare phase.

Here pre_install is different from Podfile pre_install and intercepts as follows:

Pod::HooksManager.register('cocoapods-binary'.:pre_install) do |installer_context|.end
Copy the code

Register the Pre_install hook to download binary Pods using CocoaPods’ HooksManager. The process is divided into two steps:

Environmental audit

The environment check starts by marking the global is_prebuild_stage to prevent repeated pre_install entries.

if Pod.is_prebuild_stage
  next
end
Copy the code

Then check if your podfile has use_framework set! .

podfile = installer_context.podfile
podfile.target_definition_list.each do |target_definition|
    next if target_definition.prebuild_framework_pod_names.empty?
    if not target_definition.uses_frameworks?
        STDERR.puts "[!]  Cocoapods-binary requires `use_frameworks! `".red
        exit
    end
end
Copy the code

Cocoapods-binary needs to export the package as a framework. To learn more about why CocoaPods uses the Framework, click here.

Download and install Binary Pod

Binary pods are marked in the podfile as :binary => true.

Hook related methods are required to precompile status checks before installation and reset them after installation.

Pod.is_prebuild_stage = true
Pod::Podfile::DSL.enable_prebuild_patch true
Pod::Installer.force_disable_integration true
Pod::Config.force_disable_write_lockfile true
Pod::Installer.disable_install_complete_message true.Reset 👆 5 variables to false upon success
Pod::UserInterface.warnings = [] # clean the warning in the prebuild step, it's duplicated.
Copy the code

Initialize binary_installer:

update = nil
repo_update = nil
include ObjectSpace
ObjectSpace.each_object(Pod::Installer) { |installer|
    update = installer.update
    repo_update = installer.repo_update
}

standard_sandbox = installer_context.sandbox
prebuild_sandbox = Pod::PrebuildSandbox.from_standard_sandbox(standard_sandbox)
prebuild_podfile = Pod::Podfile.from_Ruby(podfile.defined_in_file)
lockfile = installer_context.lockfile
binary_installer = Pod::Installer.new(prebuild_sandbox, prebuild_podfile, lockfile)

# install ...
Copy the code

Installer initialization needs: prebuild_sandbox, prebuild_podfile, lockfile.

Prebuild_sandbox manages the directory in Pods/_Prebuild, which is a subclass of Sandbox. Sandbox manages the /Pods directory for CocoaPods.

Lockfile and prebuild_podfile are read from podfile.lock and podfile in the project directory, respectively.

install

if binary_installer.have_exact_prebuild_cache? && !update
    binary_installer.install_when_cache_hit!
else
    binary_installer.update = update
    binary_installer.repo_update = repo_update
    binary_installer.install!
end
Copy the code

When the cache hits and no POD needs to be updated, install_when_CACHE_hit! Is executed. Otherwise start the binary Pod download, which will be placed under Pods/_Prebuild.

Precompile environment control

Talk about the five environment switches above, defined in Feature_switches. Rb.

is_prebuild_stag

Use to indicate whether binary install is currently in progress.

class_attr_accessor :is_prebuild_stage

def class_attr_accessor(symbol)
    self.class.send(:attr_accessor, symbol)
end
Copy the code

Attr_accessor is Ruby’s Access method for instance. Objc’s @perperty generates getters and setters automatically. The author uses Ruby’s dynamic call to add an extension to class_attr_accessor by sending symbol to attr_accessor.

enable_prebuild_patch

Used to filter out pods that need to be precompiled. Default is false.

Cocoapods-binary’s instructions mention two ways to set up precompilation:

  • Optional parameters for a single specific POD::binary => true
  • Global parameters before all targets:all_binary!

Enable_prebuild_patch is the logic used to implement these two variables. Enable_prebuild_patch is set to true only in binary install. Pods that do not require precompilation are ignored. The implementation is as follows:

class Podfile
    module DSL        
        @@enable_prebuild_patch = false
        def self.enable_prebuild_patch(value)
            @@enable_prebuild_patch = value
        end

        old_method = instance_method(:pod)
        define_method(:pod) do |name, *args|
            if !@@enable_prebuild_patch
                old_method.bind(self).(name, *args)
                return
            end
            # --- patch content ---.end
    end
end
Copy the code

Binary => true is added to every pod, so we need to hook the pod implementation first to get options.

  1. throughinstance_methodAccess to the oldpodmethods
  2. withdefine_methodTo do the reloading
  3. In order toold_method.bind(self).(name, *args)Complete the call to the original logic.

Ruby Method Swizzling trilogy 😂, there are many subsequent hook operations using this Method.

Patch Content logic:

should_prebuild = Pod::Podfile::DSL.prebuild_all
local = false

options = args.last
ifoptions.is_a? (Hash)andoptions[Pod::Prebuild.keyword] ! =nil
    should_prebuild = options[Pod::Prebuild.keyword]
    local = (options[:path] != nil)
end

if should_prebuild and (not local)
    old_method.bind(self).(name, *args)
end
Copy the code
  1. Check whether the master switch prebuild_all corresponds to all_binary! In the statement.

  2. Check the pod method for the optional argument :binary and update should_prebuild

  3. Only pod methods with should_prebuild = true should follow the original logic, false should be ignored

The author uses this combination to create one-click all_binary! And a single :binary personalization.

force_disable_integration

a force disable option for integral

After normal CocoaPods installation, integrate_user_project is executed to integrate the user’s project:

  • Create an Xcode workspace and integrate all the targets into the new workspace
  • Raises warnings about Podfile empty project dependencies and whether xcConfig is overridden by the existing XCConfig dependencies.

Here the synthesis step is forced to be skipped after being intercepted by force_DISABle_Integration.

disable_install_complete_message

a option to disable install complete message

After install, print_post_install_message is executed to print the various collected warnings. Here again, the hook force skips.

force_disable_write_lockfile

option to disable write lockfiles

Normal Pod Install generates the podfile.lock file to save the last Pods dependency configuration. We in the precompiled by replacing lockfile_path will lock files saved to the Pods / _Prebuild/Manifest. Lock. TMP.

class Config
    @@force_disable_write_lockfile = false
    def self.force_disable_write_lockfile(value)
        @@force_disable_write_lockfile = value
    end
    
    old_method = instance_method(:lockfile_path)
    define_method(:lockfile_path) do 
        if @@force_disable_write_lockfile
            return PrebuildSandbox.from_standard_sanbox_path(sandbox_root).root + 'Manifest.lock.tmp'
        else
            return old_method.bind(self). ()end
    end
end
Copy the code

The pre-compilation of Binary Pod

Cocoapods-binary will check to see if there is a pre-compiled binary package before downloading the binary pod. If there is no cache, the cocoapods-binary will download and pre-compile the Binary pod.

A precompiled cache query

The cache query method is have_exact_prebuild_cache? As we mentioned earlier, look at the implementation:

def have_exact_prebuild_cache?
    return false if local_manifest == nil # step 1
    # step 2
    changes = prebuild_pods_changes
    added = changes.added
    changed = changes.changed 
    unchanged = changes.unchanged
    deleted = changes.deleted 
    
    exsited_framework_pod_names = sandbox.exsited_framework_pod_names
    missing = unchanged.select do |pod_name|
        not exsited_framework_pod_names.include? (pod_name)end

    needed = (added + changed + deleted + missing)
    return needed.empty?
end
Copy the code
  1. checkPrebuildSandboxDoes the following existManifest.lockReturn false if binary Pod was not installed successfully
  2. performprebuild_pods_changesGet POD changes
  3. performexsited_framework_pod_namesCheck to see if there is a precompiled framework
  4. Determine whether a cache exists based on the results of the previous two steps

Precompiled mainfest check

def local_manifest 
    if not @local_manifest_inited
        @local_manifest_inited = true
        raise "This method should be call before generate project" unless self.analysis_result == nil
        @local_manifest = self.sandbox.manifest
    end
    @local_manifest
end
Copy the code

Local_manifest is a Ruby method whose function is described above. What is the manifest.lock file? Details: objc. IO

This is a copy of the Podfile. Lock that gets created every time you run pod install. If you’ve ever seen the error Sandbox is not in sync with the podfile. lock, it’s because this file is no longer the same as the podfile. lock.

Since the Pods directory is not necessarily added to the project’s version control, use Mainfest.Lock to ensure that the engineer can accurately update the corresponding pod before running the project. This can lead to build failures and other problems.

Precompiled PODS change check

Then check if there are any pods that need to be updated with prebuild_pods_changes:

def prebuild_pods_changes
    return nil if local_manifest.nil?
    if @prebuild_pods_changes.nil?
        changes = local_manifest.detect_changes_with_podfile(podfile)
        @prebuild_pods_changes = Analyzer::SpecsState.new(changes)
        # save the chagnes info for later stage
        Pod::Prebuild::Passer.prebuild_pods_changes = @prebuild_pods_changes 
    end
    @prebuild_pods_changes
end
Copy the code

The first line of the method also checks local_manifest because it will be called in multiple places, so a judgment is added here as well. The core relies on Cocoapods-core’s Detect_Changes_With_Podfile to get pods that need to be updated, as described below:

Analyzes the Pod::Lockfile and detects any changes applied to the Podfile since the last installation.

It checks the following states for each pod:

  • added: Pods that weren’t present in the Podfile.
  • changed: Pods that were present in the Podfile but changed:
    • Pods whose version is not compatible anymore with Podfile,
    • Pods that changed their external options.
  • removed: Pods that were removed form the Podfile.
  • unchanged: Pods that are still compatible with Podfile.

Finally, look for exsited_framework_pod_names from the Unchanged to see if there is a precompiled framework.

Precompiled framework cache check

Precompiled framework checks that exsited_framework_pod_names are mapped from exsited_framework_name_pairs.

def exsited_framework_pod_names
    exsited_framework_name_pairs.map {|pair| pair[1]}.uniq
end
Copy the code

exsited_framework_name_pairs

def pod_name_for_target_folder(target_folder_path)
    name = Pathname.new(target_folder_path).children.find do |child|
        child.to_s.end_with? ".pod_name"
    end
    name = name.basename(".pod_name").to_s unless name.nil?
    name ||= Pathname.new(target_folder_path).basename.to_s # for compatibility with older version
end

# Array<[target_name, pod_name]>
def exsited_framework_name_pairs
    return [] unless generate_framework_path.exist?
    generate_framework_path.children().map do |framework_path|
        if framework_path.directory? && (not framework_path.children.empty?)
            [framework_path.basename.to_s,  pod_name_for_target_folder(framework_path)]
        else
            nil
        end
    end.reject(&:nil?).uniq
end
Copy the code

Although the logic of this method is simple, it is one of the core logic.

It eventually call pod_name_for_target_folder to check the Pods / _Prebuild GeneratedFrameworks directory whether exist in the corresponding framework with the pod_name as at the end of the file, To indicate whether the framework has been precompiled. It’s just an empty file.

Target to compile

Once you’ve downloaded the Pods via pre_install, you’re ready to start compiling.

old_method2 = instance_method(:run_plugins_post_install_hooks)
define_method(:run_plugins_post_install_hooks) do 
    old_method2.bind(self). ()if Pod::is_prebuild_stage
        self.prebuild_frameworks!
    end
end
Copy the code

To ensure prebuild_frameworks! In the last step, the author did not add the post_install of the plugins via HooksManager but simply Override its calling method.

For install hooks, CocoaPods provides two types: Podfile hooks and Plugin hooks.

The hooks provided in the Podfile are separate methods that execute at different times.

prebuild_frameworks!

The method is longer, I will not paste the complete code, summarized as follows:

Step 1: Obtain targets to be updated

The acquisition logic of targets is similar to that of needed in a precompiled cache check.

  1. throughprebuild_pods_changes 和 exsited_framework_pod_namesgetroot_names_to_update
  2. Then use thefast_get_targets_for_pod_nameFind the corresponding Taregets
  3. callrecursive_dependent_targetsTake the targets dependency map out, merge it into targets and repeat it
  4. Filter targets that have been precompiled and are marked as:binary => falseThe target.

Note that if the pod target was originally a binary package in the form of.a +.h, it will be filtered directly.

Step 2: Finish compiling the POD target, save the resource file, and write to.pod_nameIs the tag file at the end.

The core logic is to complete the build package through the XcodeBuild command, and finally output the generated binary package and dSYM to the GeneratedFrameworks directory. It should be noted that the Binary package generated by iOS platform contains the binaries of simulator and real machine, which are respectively packaged and then merged into a Fat Binary through Libo.

The complete build code is in build_framework.rb.

  1. For the Static Framework, you need to manually copy the related resources back after compiling. Therefore, save the corresponding path first.
path_objects = resources.map do |path|
    object = Prebuild::Passer::ResourcePath.new
    object.real_file_path = framework_path + File.basename(path)
    object.target_file_path = path.gsub('${PODS_ROOT}', standard_sandbox_path.to_s) if path.start_with? '${PODS_ROOT}'
    object.target_file_path = path.gsub("${PODS_CONFIGURATION_BUILD_DIR}", standard_sandbox_path.to_s) if path.start_with? "${PODS_CONFIGURATION_BUILD_DIR}"
    object
end
Prebuild::Passer.resources_to_copy_for_static_framework[target.name] = path_objects
Copy the code
  1. If the Pod includes the Vendored Library and vendered Framework, it will be copied back after build.

Step 3: Get rid of unwanted files and Pods

Attention! After install is complete, only the manifest.lock and GeneratedFrameworks directories are saved, and any source downloaded to the _Prebuild directory is cleaned up.

Binary Pod integration

The final integration was intercepted using the Ruby Method Swizzling trilogy in normal Pod Install mode.

resolve dependencies

Resolve_dependencies in Cocoapods analyzes podfile dependencies by creating an Analyzer.

This hook is used to clean up and modify the corresponding data in prebuild specs.

  1. Delete source code downloaded from PreBuild Framework and generated Target support files.
  2. Empty the source_files configuration in the Prebuild spec and replace it with the vender framework.
  3. Clear resource_bundles in the PreBuild spec.

The simplified logic is as follows:

old_method2 = instance_method(:resolve_dependencies)
define_method(:resolve_dependencies) do
		
    self.remove_target_files_if_needed # 1
   
    old_method2.bind(self). ()self.validate_every_pod_only_have_one_form

    cache = []
    specs = self.analysis_result.specifications
    prebuilt_specs = (specs.select do |spec|
        self.prebuild_pod_names.include? spec.root.name
    end)

    prebuilt_specs.each do |spec|
		  # 2
        targets = Pod.fast_get_targets_for_pod_name(spec.root.name, self.pod_targets, cache)
        targets.each do |target|
            framework_file_path = target.framework_name
            framework_file_path = target.name + "/" + framework_file_path if targets.count > 1
            add_vendered_framework(spec, target.platform.name.to_s, framework_file_path)
        end
        
        empty_source_files(spec)
			
        # 3
        if spec.attributes_hash["resource_bundles"]
            bundle_names = spec.attributes_hash["resource_bundles"].keys
            spec.attributes_hash["resource_bundles"] = nil 
            spec.attributes_hash["resources"] ||= []
            spec.attributes_hash["resources"] += bundle_names.map{|n| n+".bundle"}
        end

        # to avoid the warning of missing license
        spec.attributes_hash["license"] = {}
    end
end
Copy the code

There are some answers to these three steps.

The first cleanup step is performed before the original logic is executed to avoid the generated old Targets file triggering the warning of file modification.

Call original and execute validATE_every_pod_only_have_one_form again. This has no side effects, but the purpose is to avoid the odd case where some pod appears in source form in other targets.

Cocoapods-binary has one limitation mentioned in target_checker:

A POD can correspond to only one target.

The reason for this is that in step 2 we embedded the precompiled static Framework in the project as vender Framework and cleared the source_files configuration on all platforms.

The third step is to clean up the resource bundle target to avoid duplicate resource bundle copy.

Because in Pod Install, Xcode generates bundle targets for us if podSpec specifies resource_bundles. We already generated and copied it in the Static Framework, so we need to clean it up to avoid duplication.

download dependencies

This step hooks one of the methods triggered in the download_dependencies execution: install_source_of_pod

old_method = instance_method(:install_source_of_pod)
define_method(:install_source_of_pod) do |pod_name|

    # original logic ...
    if self.prebuild_pod_names.include? pod_name pod_installer.install_for_prebuild! (self.sandbox)
    else
        pod_installer.install!
    end
    # original logic ...
end
Copy the code

The purpose here is to skip the binary pod download and complete the Symbol link operation. The simplified logic is as follows:

def install_for_prebuild!(standard_sanbox)
    return if standard_sanbox.local? self.name

    prebuild_sandbox = Pod::PrebuildSandbox.from_standard_sandbox(standard_sanbox)
    target_names = prebuild_sandbox.existed_target_names_for_pod_name(self.name)
    
    target_names.each do |name|

        real_file_folder = prebuild_sandbox.framework_folder_path_for_target_name(name)
                
        target_folder = standard_sanbox.pod_dir(self.name)
        if target_names.count > 1 
            target_folder += real_file_folder.basename
        end
        target_folder.rmtree if target_folder.exist?
        target_folder.mkpath


        walk(real_file_folder) do |child|
            source = child
            # only make symlink to file and `.framework` folder
            if child.directory? and [".framework".".dSYM"].include? child.extname
                mirror_with_symlink(source, real_file_folder, target_folder)
                next false  # return false means don't go deeper
            elsif child.file?
                mirror_with_symlink(source, real_file_folder, target_folder)
                next true
            else
                next true
            end
        end

        # symbol link copy resource for static framework
        hash = Prebuild::Passer.resources_to_copy_for_static_framework || {}
        
        path_objects = hash[name]
        ifpath_objects ! =nil
            path_objects.each do |object|
                make_link(object.real_file_path, object.target_file_path)
            end
        end
    end # of for each 
end # of method
Copy the code

The core is the walk method, which iterates through the files in each static Framework directory and the.framework folder as a reference to the corresponding files in the Pods directory.

EmbedFrameworksScript

This step is to solve a problem that arises in the embeded framework after symbol link. Embedded Framework is a code sharing solution proposed by Apple after iOS 8 to solve the problem of code sharing between the host App and the Extensions. Detailed medium article;

old_method = instance_method(:script)
define_method(:script) do

    script = old_method.bind(self).()
    patch = <<-SH.strip_heredoc
        #! /bin/sh
        old_read_link=`which readlink`
        readlink () {
            path=`$old_read_link $1`;
            if [ $(echo "$path" | cut -c 1-1) = '/' ]; then
                echo $path;
            else
                echo "`dirname $1`/$path";
            fi
        }
    SH
    script = script.gsub "rsync --delete", "rsync --copy-links --delete"    
    patch + script
end
Copy the code

The framework file in the POD Target folder was changed to symblink for the relative path and EmbedFrameworksScript was used to read the path through readlink. It didn’t handle the relative path well and needed to be rewritten here.

Rewriting is essentially changing a relative path to an absolute path.

The flow chart

Finally, the flow chart of combing:

Results contrast

After introducing the basic modules into cocoapods-Binary, let’s look at the Pods file structure:

The _Prebuild directory holds a complete copy of the Pods source code, while the additional GeneratedFrameworks cache the pre-compiled binary file and the dSYM symbol table. In the final integration phase, after the symbol link is replaced, the source code will be deleted and pointed to binary.

conclusion

There are a number of limitations to using this solution:

  • Since CocoaPods has changed the framework generation logic in version 1.7 and later to not copy bundles to the framework, you need to fix the Pod environment to 1.6.2.
  • To support binary, the header ref needs to be changed to#import <>or@importTo conform to moduler standards;
  • A unified development environment is required. If the project supports Swift, the compiled products of different compiler have compatibility problems with Swift version.
  • The resulting binary size is a bit larger than when using the source code. It is not recommended to upload the binary to the Store eventually.
  • Ignore Pods folder is recommended, otherwise there will be a lot of changes during source code and binary switch, which will increase git burden;
  • If you need to debug, you need to switch back to the source code, or use dSYM mapping to locate method pairs.

The overall feeling is a very good idea, suitable for small and medium-sized projects with few people. Once the project depends on more libraries, it may not be suitable, too restrictive, and the requirements for development and environment consistency are high.

The next section is the core prebuild logic. Here’s a brain map to fill in the blanks:

! [structure] (gitee.com/looseyi/blo…