How We Slashed iOS Build Times by 80% Using Binary Pods and CCache
Facing a 24‑million‑line codebase with over 100 third‑party pods, the team tackled slow Xcode compilation, lengthy packaging, and merge‑request bottlenecks by applying build‑setting tweaks, RAM‑disk compilation, CCache, and a custom binary‑pod workflow that automates packaging, publishing, and integration without altering source projects.
Background and Pain Points
The Youzan Retail iOS application grew to over 240,000 lines of code and more than 100 internal and third‑party pods across 15+ modules (goods, transactions, inventory, members, etc.). This rapid expansion caused three major build‑time problems:
Full clean builds took ~25 minutes.
Packaging time increased, delaying test windows.
Merge‑request compilation checks slowed, creating merge bottlenecks.
With limited hardware resources, the team needed a solution that improved build performance without changing developers' habits.
Initial Exploration
Xcode Build Optimizations
Several Xcode settings were tuned:
Architectures : In Debug mode set to armv7 to reduce the number of architectures compiled.
Build the project on a RAM‑disk to minimise disk I/O.
Increase the number of compile threads beyond the default CPU core count.
Disable dSYM generation in Debug ( DebugInformationFormat = DWARF).
Turn off Link‑Time Optimizations ( AppleLLVM → CodeGeneration → TimeOptimization = NO).
These tweaks shortened compile time but did not meet the target performance and introduced runtime trade‑offs.
CCache
CCache caches compiled objects for C/C++/Objective‑C/Objective‑C++. It cannot cache pre‑compiled headers (PCH) and does not support Swift, limiting its usefulness for the project.
Binary‑Pod Strategies
Three common binary‑pod approaches were evaluated:
Instant binary generation (Carthage‑style) : Use CocoaPods‑Binary to generate a binary after pod install. Drawback – extra latency when a binary version does not exist.
Single private source with source + binary info in the podspec : Store binary URLs in a vendor_libraries / vendor_frameworks subspec. Drawback – podspec becomes large and download/lint speed degrades.
Multiple private sources : Keep source pods in one repository and binaries in a separate static server. This approach best matches the team’s requirements.
Chosen Binary‑Pod Solution
The team selected the multi‑private‑source model and defined the following non‑intrusive requirements:
No changes to project files or Podfile; developers should not notice any difference.
Support binary distribution for both pod and non‑pod business modules.
Whitelist for component and business pods to allow selective debugging.
Fully automated pipeline: binary generation, podspec conversion, lint, and push.
High stability with early error reporting.
CocoaPods Plugin Development
A custom plugin cocoapods‑yzpodbin was created using the cocoapods‑plugin generator. The plugin provides commands to build, package, publish binaries and to modify podspecs.
pod plugins create 'cocoapods‑yzpodbin'Binary Component Architecture
Remote Services
All source code, internal pods, and third‑party pod mirrors are stored in GitLab. Jenkins jobs are triggered by GitLab webhooks:
Push events on internal/third‑party pod repos start a binary‑packaging job.
Merge events on business code trigger a job that runs git log and grep to find changed modules and rebuild only those binaries.
Local Usage
Developers invoke the cocoapods‑yzpodbin plugin, which reads a local configuration file to decide which pods are sourced from binaries and which from source code, providing a seamless, non‑intrusive experience.
Binary Package Generation
The generation process consists of four steps:
Compile the source to produce .a, .h, .bundle or .framework artifacts.
Compress the binary (using 7z) and upload it to a static server, obtaining a download URL.
Convert the original podspec to a binary podspec by updating s.source, s.vendored_libraries or s.vendored_frameworks, and adjusting header paths.
Run pod spec lint and push the binary podspec to the binary repository.
Example Xcode build commands for static libraries:
xcodebuild -project 'TargetProject' -target 'Target' ONLY_ACTIVE_ARCH=NO -sdk iphonesimulator VALID_ARCHS='i386 x86_64' ARCHS='i386 x86_64' xcodebuild -project 'TargetProject' -target 'Target' ONLY_ACTIVE_ARCH=NO -sdk iphoneos VALID_ARCHS='armv7 armv7s arm64' ARCHS='armv7 armv7s arm64'After building, lipo -create merges simulator and device binaries, then 7z a compresses the result and wput uploads it.
Podspec Conversion
Podspecs are treated as JSON. The conversion script reads the original spec, replaces the source URL, sets vendored_libraries or vendored_frameworks, and updates public headers.
# Load spec
spec = Pod::Specification.from_file specpath
# Replace source with binary URL
spec.source = { "http" => 'binary_url' }
# Set binary artifact
spec.vendored_libraries = 'MyLib.a'
# Adjust public headers
spec.public_header_files = "*.h"
# (Optional) handle subspec merging as neededBusiness Project to Pod Conversion
Business sub‑projects (e.g., Home, Goods) are compiled into static libraries, then wrapped in a generated podspec. The workflow:
Delete the original .xcodeproj reference from the workspace (using the Xcodeproj Ruby API).
Add the generated binary pod to the Podfile via a plugin hook, avoiding any manual Podfile edits.
# Example of removing unwanted sub‑projects
project.main_group.children.objects.each do |item|
if item.name && item.name.end_with?('.xcodeproj')
name_without_ext = item.name.split('.').first
unless code_targets.include?(name_without_ext)
item.remove_from_project
end
end
end
project.save('BinaryProjectName')Local Configuration
A non‑version‑controlled Ruby file defines the binary source, a flag to enable binaries, and white‑lists for component and business pods:
$BINARY_SOURCE = "xxx"
$USE_BINARY = true
$CODE_PODS = [] # component pods kept as source
$CODE_MODULES = [] # business modules kept as sourceIssues and Solutions
Source switching : Simple ordering of source lines does not guarantee correct version resolution when dependencies overlap. The plugin patches the pod resolver to rewrite dependency specs and source order during the install hook.
Pod cache buildup : Each binary build creates large caches. A pod cache clean step is added after publishing.
Shared scheme numbering : Duplicate workspaces generate numbered schemes; the plugin removes xcshareddata/xcschemes from the binary workspace to keep scheme names stable.
Swift compatibility : Since Swift 5 introduced a stable ABI, the plugin generates a module.modulemap and an umbrella header for each binary pod, making Swift static libraries usable.
Additional Features
Self‑update : After each pod install, the plugin checks a remote tag and updates itself if a newer version exists.
Statistics : The plugin prints which pods are binary vs. source and reports the data to an internal dashboard.
Results
After six months of production use, the binary‑pod service stabilised. When only one or two business modules remain in source form, full‑project compile time dropped from ~25 minutes to 3‑5 minutes, dramatically improving developer productivity.
Future Plans
Build a binary‑package testing platform for business releases.
Integrate static analysis and resource checks into the service.
Support debugging of binary pods without switching back to source code.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Youzan Coder
Official Youzan tech channel, delivering technical insights and occasional daily updates from the Youzan tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
