CocoaPods缓存清理之谜

2021-02-26 16:15:43 浏览数 (2)

1. 摘要

CocoaPods的缓存你了解吗?缓存的默认存储路径在哪里?缓存怎么手动清除?又有什么机制会触发自动清理?今天小菜带大家一窥究竟。

小菜在工作中遇到了一个问题,就是负责打包的机器的 CocoaPods 缓存会莫名奇妙被全部删掉,一时不知啥原因。为了搞清楚这个问题,小菜扒了扒 CocoaPods 的代码,了解了下缓存部分处理细节。特地整理成篇分享大家。

2. CocoaPods缓存

pod install/update 的时候,缓存起到了重要的作用。有了缓存,就可以避免二次下载,特别是对于一些静态库、动态库而言,因为 x86_64 以及多个 arm 架构,动不动就几十上百兆下载,会大大提升 install/update 的速度。

2.1缓存的路径

默认 CocoaPods 的缓存路径是 /Users/YourName/Library/Caches/CocoaPods 下,我们称之为 cache_root。一起来看下 cocoapods/config.rb。

代码语言:javascript复制
# cocoapods/config.rb
DEFAULTS = {
  :verbose             => false,
  :silent              => false,
  :skip_download_cache => !ENV['COCOAPODS_SKIP_CACHE'].nil?,
  :new_version_message => ENV['COCOAPODS_SKIP_UPDATE_MESSAGE'].nil?,

  :cache_root          => Pathname.new(Dir.home)   'Library/Caches/CocoaPods',
}

我们用 tree 来直观感受下目录构成。

代码语言:javascript复制
❯ cd ~/Library/Caches/CocoaPods
❯ tree -L 5
.
├── Pods
│   ├── Release
│   │   ├── MBProgressHUD
│   │   │   └── 0.6.1-5f908
│   │   │       └── MBProgressHUD.framework
│   │   ├── MJExtension
│   │   │   └── 3.2.200-fdf9f
│   │   │       └── MJExtension.framework
│   │   ├── Masonry
│   │   │   └── 1.1.0-9f8bc
│   │   │       └── Masonry.framework
│   │   ├── SDWebImage
│   │   │   └── 5.8.100-0c2d6
│   │   │       └── SDWebImage.framework
│   │   ├── SQLiteRepairKit
│   │   │   └── 1.2.3-93a15
│   │   │       └── bin_SQLiteRepairKit_1.2.3
│   │   ├── WCDBOptimizedSQLCipher
│   │   │   └── 1.2.1-49817
│   │   │       └── bin_WCDBOptimizedSQLCipher_1.2.1
│   │   ├── YYText
│   │   │   └── 1.0.7-stable zhuanzhuan-0a2bf
│   │   │       └── bin_YYText_1.0.7-stable zhuanzhuan
│   │   └── lottie-ios
│   │       └── 2.1.8-stable zhuanzhuan-168f1
│   │           └── lottie-ios.framework
│   ├── Specs
│   │   └── Release
│   │       ├── MBProgressHUD
│   │       │   └── 0.6.podspec.json
│   │       ├── MJExtension
│   │       │   └── 3.2.podspec.json
│   │       ├── Masonry
│   │       │   └── 1.1.podspec.json
│   │       ├── SDWebImage
│   │       │   └── 5.8.podspec.json
│   │       ├── SQLiteRepairKit
│   │       │   └── 1.2.podspec.json
│   │       ├── WCDBOptimizedSQLCipher
│   │       │   └── 1.2.podspec.json
│   │       ├── YYText
│   │       │   └── 1.0.podspec.json
│   │       └── lottie-ios
│   │           └── 2.1.podspec.json
│   └── VERSION
└── search_index.json

cocoapods/downloader.rb 中,默认会将代码/二进制依赖放到 cache_root 'Pods' 下。

代码语言:javascript复制
def self.download(
      request,
      target,
      can_cache: true,
      cache_path: Config.instance.cache_root   'Pods'
    )
代码语言:javascript复制

2.2 如何寻找指定库指定版本

pod install的时候,每一个组件库都会对应一个下载请求,该下载请求 Request 自身会持有一个 spec 对象,该 spec 对象能获取到从哪个 podspec 文件读取而来。

代码语言:javascript复制
# cocoapods/downloader/request.rb
def slug(name: self.name, params: self.params, spec: self.spec)
    checksum = spec && spec.checksum && '-' << spec.checksum[0, 5]
    if released_pod?
      "Release/#{name}/#{spec.version}#{checksum}"
    else
      opts = params.to_a.sort_by(&:first).map { |k, v| "#{k}=#{v}" }.join('-')
      digest = Digest::MD5.hexdigest(opts)
      "External/#{name}/#{digest}#{checksum}"
    end
end 
代码语言:javascript复制

根据 podspec 文件算出 checksum(SHA1) 值,然后根据 pod 是否是 release 来决定不同的目录。比如在 release pod 的情况下路径拼接为Release/pod库名称/pod库版本-sha1前5位。比如 MBProgressHUD podspec的 SHA1 前5位为5f908 ,版本0.6.1,release pod,那么pod在寻找该缓存的时候便会去 Pods/Release/MBProgressHUD/0.6.1-5f908 路径下寻找。如果不是 release pod,则会根据参数计算出一个 md5 摘要,最后在External/pod库名称/md5摘要-sha1前5位这样的路径下寻找。

2.3 如何清除缓存?

代码语言:javascript复制
1. pod cache clean XXX --all
2. pod cache clean --all

这两个命令大家可能平时会用到。第一个命令用来清除指定 pod 库,指定 —all 后会清除 cache_root 下所有版本的缓存,不指定的话,如果有多个版本,则会终端出现列表提示你删除具体的版本。

第二个命令用来清除 cache_root 下所有文件,所有的 pod 库,包括 Release/External 下的代码以及 Specs 下的 spec.json 文件以及 VERSION 都会被清除掉。

2.4 缓存会在什么时机被自动清除?

终于写到本文的重点了。也是小菜在 CI 机器上观察到的一个现象:CI机器在编译的时候,报项目 Pods 目录中的依赖库某些文件找不到。

而通过源码,我们知道项目根目录 Pods 下的库依赖,其实是从缓存目录里相对应的目录 copy 过来。寻找过程本文 2.2 小节有阐述。那么是下载下来的代码就是缺失某些文件,导致缓存的时候文件就缺失了吗?

经过在打包机上多次的测试,下载下来存放在 cache_root 下的代码并没有发现缺失文件。那么会不会是其他原因呢?

小菜在 cocoapods/downloader/cache.rb 中发现了一个关键性的问题:

代码语言:javascript复制

def initialize(root)
  @root = Pathname(root)
  ensure_matching_version
end

def ensure_matching_version
  version_file = root   'VERSION'
  version = version_file.read.strip if version_file.file?

  root.rmtree if version != Pod::VERSION && root.exist?
  root.mkpath

  version_file.open('w') { |f| f << Pod::VERSION }
  
end 
代码语言:javascript复制

在 cache_root "Pods" 目录下有一个 VERSION 文件,里面存储了 cache_root 中缓存代码和 podspec.json 文件时所使用的 CocoaPods 版本。如果后来发现 CocoaPods 版本和此 VERSION 文件中的版本号不一致,则会自动清除 cache_root 下的所有文件。

所以,问题很有可能在这里。

在 CI 机器上,存在了多个 CocoPods 版本,集团的其他几个 APP 和我们用的 CocoaPods 版本还不太一致,有用1.7.4的,有用1.10.0的,通过pod _1.7.4_ install或者 pod _1.10.0_ install 是实现不同版本的 install 操作。

也就是出现了多个进程操作同一份缓存的问题。因为这个缓存并没有什么锁机制,所以这种情况就容易报Directory not empty @ dir_s_rmdir - /Users/zzqadervice/Library/Caches/CocoaPods/Pods 的问题。

2.5 如何解决?

能否不同版本自定义一个缓存目录呢?小菜猜测强大的 CocoaPods 应该提供了这种能力。但 google 了一下没发现啥有用的线索。只能再次扒源码看下。

代码语言:javascript复制
# cocoapods/config.rb

def initialize(use_user_settings = true)
  configure_with(DEFAULTS)

  unless ENV['CP_HOME_DIR'].nil?
    @cache_root = home_dir   'cache'
  end

  if use_user_settings && user_settings_file.exist?
    require 'yaml'
    user_settings = YAML.load_file(user_settings_file)
    configure_with(user_settings)
  end

  unless ENV['CP_CACHE_DIR'].nil?
    @cache_root = Pathname.new(ENV['CP_CACHE_DIR']).expand_path
  end
end 
代码语言:javascript复制

我们可以看到 cache_root 是可以自定义的,只要能进入到 ruby 的环境变量 ENV 中。

如果要全局自定义一个环境变量

1)可以在 ~/.bash_profile 中 export CP_HOME_DIR=/Users/YourName/XXX/XXX/XXX,之后 source ~/.bash_profile生效下。

2)也可以自定义 ~/.cocoapods/config.yaml 文件。

代码语言:javascript复制

  # The default settings for the configuration.
  #
  # Users can specify custom settings in `~/.cocoapods/config.yaml`.
  # An example of the contents of this file might look like:
  #
  #     ---
  #     skip_repo_update: true
  #     new_version_message: false
  #
代码语言:javascript复制

~/.cocoapods/config.yaml 被成为用户配置文件,会被 CocoaPods 读取到覆盖默认配置。

代码语言:javascript复制
# cocoapods/config.rb
def initialize(use_user_settings = true)
  configure_with(DEFAULTS)

  ···
  if use_user_settings && user_settings_file.exist?
    require 'yaml'
    user_settings = YAML.load_file(user_settings_file)
    configure_with(user_settings)
  end

  ···
end

def configure_with(values_by_key)
  return unless values_by_key
  values_by_key.each do |key, value|
    if key.to_sym == :cache_root
      value = Pathname.new(value).expand_path
    end
    instance_variable_set("@#{key}", value)
  end
end 
代码语言:javascript复制

那么我们可以在文件中配置上cache_root便可以自定义缓存根路径。

代码语言:javascript复制
cache_root: /Users/YourName/XXX/XXX/XXX
代码语言:javascript复制

但是这种全局定义一个的情况并不能解决我们机器上多个版本公用一个缓存库的问题,因为使用的还是一个缓存库,只是是自定义路径而已。

如何办?

自定义CP_HOME_DIR或CP_CACHE_DIR

我们配置下CI打包脚本如下:

CP_HOME_DIR=/Users/YourName/XXX/XXX/XXX/1.10.0 pod _1.10.0_ install

或者

CP_CACHE_DIR=/Users/YourName/XXX/XXX/XXX/1.10.0 pod _1.10.0_ install

代码语言:javascript复制
def home_dir
  @home_dir ||= Pathname.new(ENV['CP_HOME_DIR'] || '~/.cocoapods').expand_path
end
def repos_dir
  @repos_dir ||= Pathname.new(ENV['CP_REPOS_DIR'] || (home_dir   'repos')).expand_path
end
代码语言:javascript复制

CP_HOME_DIR 和 CP_CACHE_DIR 一个区别在于如果没有定义 CP_REPOS_DIR 环境变量路径,那么索引库 repos 将也会存放在自定义的 home_dir 中。

此时我们只想把缓存单独自定义下,所以使用 CP_CACHE_DIR 就可以了,索引库还是想多个 APP 进行共享的。

至此问题算是得到解决,Ruby 是一门让人编程感到快乐的语言,牛逼的 fastlane、homebrew 都是 Ruby 编写的,iOS 开发者经常接触的 CocoaPods 也由 Ruby 编写。日常在遇到问题的时候,有时候扒下源码,常常有文档之外的收获。

本文是 CocoaPods沉思录 的第一篇文章,日后有相关 CocoaPods 探讨的文章都会归并到这个话题下。欢迎大家关注。 小菜与老鸟

0 人点赞