把玩CocoaPods post_install 和 pre_install

在日常iOS开发中,对CocoaPods使用最长见得形式如下:

platform :ios, '9.0'

target 'TestCocoaPods' do
    pod 'CFYNavigationBarTransition', '1.2.2'
    pod 'SDWebImage', '4.4.2'
end

但有时候我们想在pod install/update时做一些除了第三方库安装以外的事情,比如关闭所有target的Bitcode功能。这时就要用到CocoaPods中的钩子(Hooks),关于钩子(Hooks)的官方介绍在这里:CocoaPods Hooks

一、初识Hooks

现在使用Hooks来实现上文说的"关闭所有target的Bitcode功能",Podfile如下:

platform :ios, '9.0'

target 'TestCocoaPods' do
    pod 'CFYNavigationBarTransition', '1.2.2'
    pod 'SDWebImage', '4.4.2'
end

# 实现post_install Hooks
post_install do |installer|
  # 1. 遍历项目中所有target
  installer.pods_project.targets.each do |target|
    # 2. 遍历build_configurations
    target.build_configurations.each do |config|
      # 3. 修改build_settings中的ENABLE_BITCODE
      config.build_settings['ENABLE_BITCODE'] = 'NO'
    end
  end
end

在上面的Podfile使用了一个 "post_install" Hooks,这个Hooks允许你在生成的Xcode project写入硬盘或者其他你想执行的操作前做最后的改动。

对CocoaPods稍微有了解的话,应该知道CocoaPods是用Ruby开发的,其实Podfile就是一个Ruby代码文件,从Ruby的角度来看"post_install"这个Hooks,其实它就是一个Ruby中的Block,如果对Ruby Block没有了解,可以类比到OC中的Block。
post_install block接收一个"installer"参数,通过对"installer"修改来完成我们想要执行的特殊操作。

还有另一个Hooks叫做"pre_install",它的作用是允许你在Pods被下载后但是还未安装前对Pods做一些改变。写法和post_install一样,这里不再赘述。

二、初探Hooks

在Ruby语言中,万物皆为对象。上文提到CocoaPods的两个Hooks是Ruby中的Block,并且都接收一个"installer"参数。installer就是对象,那么就来看看这个installer对象的属性和方法信息,继续探索Hooks。

如果对Ruby语言熟悉的话,可以直接阅读CocoaPods源码来深入研究installer对象。但我是个Ruby小白,相信很多读者也是Ruby小白,那么我们就用小白的方式来研究一下installer对象。

先来捋一捋思路:

  1. Podfile是Ruby代码文件,那么我们可以在里面写Ruby代码;
  2. installer是对象;
  3. 可以通过写简单的Ruby代码来打印对象的属性和方法;

再来一点Ruby简单知识:

  • 每个Ruby对象都有"public_methods"方法,这个方法返回对象公开方法名列表;
  • 每个Ruby对象都有"instance_variables"方法,这个方法返回对象的属性名列表;
  • 每个Ruby对象都有"instance.instance_variable_get"方法,调用这个方法并传入属性名,就可以得到属性名称对应的对象;
  • Array类型类似OC中的NSArray;Hash类型类似OC中的NSDictionary;Array和Hash对象可以使用each方法来遍历;
  • puts 是ruby中的打印方法

总结来说:在Podfile文件里写一下简单的Ruby代码来打印installer对象的属性和方法。Podfile代码如下:

platform :ios, '9.0'

target 'TestCocoaPods' do
    pod 'CFYNavigationBarTransition', '1.2.2'
    pod 'SDWebImage', '4.4.2'
end

post_install do |installer|
    # puts 为在终端打印方法
    puts "##### post_install start #####"

    # 为了打印的日志方便查看,使用╟符号修饰
    puts "╟ installer"
    # 获取属性名称列表,并遍历
    installer.instance_variables.each do |variableName|
        # 打印属性名称
        puts "  ╟ #{variableName}"
    end

    puts "  ╟ installer.public_methods"
    # 获取方法名称列表,并遍历
    installer.public_methods.each do |method|
        # 打印方法名称
        puts "    ┣ #{method}"
    end
    puts "##### post_install end #####"
end

接下来运行pod install我们会看到如下输出:

##### post_install start #####
╟ installer
  ╟ @sandbox
  ╟ @podfile
  ╟ @lockfile
  ╟ @use_default_plugins
  ╟ @has_dependencies
  ╟ @repo_update
  ╟ @update
  ╟ @installation_options
  ╟ @analysis_result
  ╟ @aggregate_targets
  ╟ @installed_specs
  ╟ @pod_installers
  ╟ @pods_project
  ╟ installer.public_methods
    ┣ update
    ┣ sandbox
    ┣ podfile
    ┣ lockfile
    ┣ repo_update?
    ┣ repo_update
    ┣ repo_update=
    ┣ update=
    ┣ install!
    ┣ pods_project
    ┣ has_dependencies
    ┣ has_dependencies?
    ┣ use_default_plugins
    ┣ use_default_plugins?
    ┣ prepare
    ┣ resolve_dependencies
    ┣ download_dependencies
    ┣ installation_options
    ┣ aggregate_targets
    ┣ pod_targets
    ┣ analysis_result
    ┣ names_of_pods_to_install
    ┣ installed_specs
    ┣ has_dependencies=
    ┣ development_pod_targets
    ┣ use_default_plugins=
    ┣ installed_specs=
    ┣ installation_options=
    ┣ config
    ┣ suppress_warnings
    ┣ instance_variables
    ┣ instance_variable_set
    ┣ instance_variable_get
    ┣ public_methods
##### post_install end #####

在这里贴出的installer.public_methods只是一部分,因为太多,只贴出主要的。
从输出中能看到一些熟悉的身影:

  • @podfile 是否对应Podfile文件?
  • @lockfile 是否对应Podfile.lock文件?
  • @pods_project 上面已经见过了,修改bitcode属性就是用了它。

下我们来印证一下上面的猜想,这两个属性"@podfile"、"@lockfile"是不是对应文件,我们可以继续打印这两个属性,Podfile代码如下:

platform :ios, '9.0'

target 'TestCocoaPods' do
    pod 'CFYNavigationBarTransition', '1.2.2'
    pod 'SDWebImage', '4.4.2'
end

post_install do |installer|
    # puts 为在控制台打印方法
    puts "##### post_install start #####"

    # 为了打印的日志方便查看,使用╟符号修饰
    puts "╟ podfile"
    # 打印podfile属性列表
    installer.podfile.instance_variables.each do |variableName|
        # 遍历属性并打印
        puts "  ╟ #{variableName}"
    end

    puts "╟ lockfile"
    # 打印lockfile属性列表
    installer.lockfile.instance_variables.each do |variableName|
        # 遍历属性并打印
        puts "  ╟ #{variableName}"
    end

    # 暂时只看属性,不打印方法列表,因为暂时只看属性就够了
    puts "##### post_install end #####"
end

可以看到如下打印:

##### post_install start #####
╟ podfile
  ╟ @defined_in_file
  ╟ @internal_hash
  ╟ @root_target_definitions
  ╟ @current_target_definition
  ╟ @pre_install_callback
  ╟ @post_install_callback
╟ lockfile
  ╟ @internal_data
  ╟ @defined_in_file
  ╟ @external_sources_data
  ╟ @dependencies
  ╟ @pod_names
  ╟ @pod_versions
##### post_install end #####

通过观察打印中出属性,可以看到一些关键点:

  • podfile中的属性pre_install_callback、post_install_callback和Podfile中的pre_install和post_install好像是一一对应的。
  • lockfile中的属性dependencies、pod_versions和Podfile.lock文件中的DEPENDENCIES:、COCOAPODS:好像是一一对应的。

这里使用了"好像"一词,因为通过现在打印的信息还不能100%确定是否一一对应。下面我们继续探索。

三、深探Hooks

上面使用一下简单的Ruby代码进行了一些简单的探索,想要继续打印属性中的属性中的属性,如果用上面方法会被累死,上面的方式有两个缺点:

  1. 每次要打印一个属性,都需要手动调整Podfile,不能一次性打印多层级属性。
  2. public_methods会打印出对象都有的公共方法,影响我们观察。

针对以上缺点,改进一下:

  1. 定义一个方法,将对象传给方法,就可以打印对象的属性和方法信息,并通过instance_variable_get方法取出对象的属性对象,递归调用方法自己将对象的属性传入,继续打印下一层级属性,直到没有属性列表或者超出设置的最大层级为止。
  2. 定义一个类TempClass, TempClass的对象会自带公共属性列表,然后再在TempClass里定义一些要过滤的实例方法。通过TempClass的实例的public_methods方法就可以取出想要过滤的方法列表,在打印对象的方法列表前,就可以进行过滤。

根据以上思路,Podfile是这样:

# 临时的Class,用来提取Class公共的方法列表和要手动过滤方法列表
class TempClass
    # 定义一下需要过滤的方法
    def initialize
    end

    def to_yaml
    end

    def +(other)
    end

    def -(other)
    end

    def *(other)
    end

    def /(other)
    end

    def -@
    end
end
# 这里创建了全局变量,来存储TempClass对象的方法列表
# 这个全局变量是为了后面做方法列表滤用。
$classPublicMethos = TempClass.new().public_methods

# 定义一个方法,打印对象属性和方法列表,并递归打印对象的属性
# param: instance 要打印的对象
# param: name 对象的名称
# param: currentLayer 当前对象距离顶层的层级
# param: maxLayer 要打印的最大的层级
def putsInstanceVariables(instance,name,currentLayer=0,maxLayer=3)
    # 当前层级是否在最大层级内
    if currentLayer < maxLayer
        if instance.nil?
            # instance是空值
            # 一个字符串乘以一个�整数,就是对应整数个字符串拼接
            # ' '*2 结果为 '    '
            # "#{' '*(currentLayer+1)}╟ "作用就是方便查看层级,使用VSCode编辑器就可以折叠对应的层级来查看
            puts "#{' '*(currentLayer+1)}╟ #{name} : nil"
        elsif instance.instance_of? Numeric
            # instance是数字
            puts "#{' '*(currentLayer+1)}╟ #{name} : #{instance}"
        elsif instance.instance_of? TrueClass
            # instance是ture值
            puts "#{' '*(currentLayer+1)}╟ #{name} : true"
        elsif instance.instance_of? FalseClass
            # instance是false值
            puts "#{' '*(currentLayer+1)}╟ #{name} : false"
        elsif instance.instance_of? Pathname
            # instance是路径
            puts "#{' '*(currentLayer+1)}╟ #{name} : #{instance.to_s}"
        elsif instance.instance_of? Array
            # instance为数组对象

            puts "#{' '*(currentLayer+1)}╟ #{name} : Array(Length: #{instance.length})"
            # 遍历数组
            instance.each_index do |index|
                item = instance.at(index)
                # 递归调用,打印数组中的对象,名称为index,层级+1
                putsInstanceVariables item, "#{index}", currentLayer+1
            end
        elsif instance.instance_of? Hash
            # instance为Hash对象,为<Key,Value>形式的集合

            puts "#{' '*(currentLayer+1)}╟ #{name} : Hash(Length: #{instance.length})"

            # 遍历Hash,取出key,value
            instance.each do |key,value|
                # 递归调用,打印Hash中的对象,名称为key,层级+1
                putsInstanceVariables value, "#{key}", currentLayer+1
            end

        else
            # instance为普通对象

            puts "#{' '*(currentLayer+1)}╟ #{name} : #{instance.to_s}"

            # 遍历对象所有属性名称
            instance.instance_variables.each do |variableName|
                # 根据名称获取属性
                variable = instance.instance_variable_get(variableName)
                # 递归调用,打印属性对象信息,名称为属性名,层级+1
                putsInstanceVariables variable, variableName, currentLayer+1
            end

            # 过滤掉通用方法
            # 数组使用"-"号就可以进行过滤
            publicMethods = (instance.public_methods - $classPublicMethos)
            # 打印公开方法
            puts "#{' '*(currentLayer+2)}╟ public_methods : Array(Length: #{publicMethods.length})"
            # 过滤Class都有的公共的方法
            publicMethods.each do |method|
                puts "#{' '*(currentLayer+3)}┣ #{method}"
            end

        end
    end
end

platform :ios, '9.0'

target 'TestCocoaPods' do
    pod 'CFYNavigationBarTransition', '1.2.2'
    pod 'SDWebImage', '4.4.2'
end

pre_install do |installer|
    puts ""
    puts ""
    puts "##### pre_install start #####"

    # 打印installer信息
    putsInstanceVariables installer, "installer"

    puts "##### pre_install end #####"
    puts ""
    puts ""
end

post_install do |installer|
    puts ""
    puts ""
    puts "##### post_install start #####"

    # 打印installer信息
    putsInstanceVariables installer, "installer"

    puts "##### post_install end #####"
    puts ""
    puts ""
end

因为这样会打印很多信息,直接在终端里查看很不方便,所以将输出存到文件里,然后使用VSCode查看会很方便,VSCode可以折叠对应的层级。所以要这样执行pod install > PodInstallLog
pod install完成以后,就可以打开PodInstallLog来查看输出信息,里面会有很全面的installer的信息。

上面所说的podfile属性和Podfile文件是否一一对应,通过这次的打印就可以很明了的看出来,如下图:


PodInstallLog
Podfile

两个Hooks的installer参数里包含了pod运行的很多信息,巧妙利用这些信息可以做很多事情来提高我们的开发效率。

笔者是一个Ruby小白,就看了一个小时的Ruby教程,上述方法可能很笨拙,如果你有更好的方法, 欢迎留言指点。

本文只做抛转引玉,这里只是对podfile属性进行了一些探索,如果想对其他属性进行探索,可以使用上面的对象打印方法,把对象打印出来观察。希望这篇文章对你把玩CocoaPods有一些帮助。

推荐阅读更多精彩内容