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.
- through
instance_method
Access to the oldpod
methods - with
define_method
To do the reloading - In order to
old_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
-
Check whether the master switch prebuild_all corresponds to all_binary! In the statement.
-
Check the pod method for the optional argument :binary and update should_prebuild
-
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
- check
PrebuildSandbox
Does the following existManifest.lock
Return false if binary Pod was not installed successfully - perform
prebuild_pods_changes
Get POD changes - perform
exsited_framework_pod_names
Check to see if there is a precompiled framework - 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.
- through
prebuild_pods_changes
å’Œexsited_framework_pod_names
getroot_names_to_update
- Then use the
fast_get_targets_for_pod_name
Find the corresponding Taregets - call
recursive_dependent_targets
Take the targets dependency map out, merge it into targets and repeat it - Filter targets that have been precompiled and are marked as
:binary => false
The 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_name
Is 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.
- 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
- 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.
- Delete source code downloaded from PreBuild Framework and generated Target support files.
- Empty the source_files configuration in the Prebuild spec and replace it with the vender framework.
- 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@import
To 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…