iOS技术积累

不管生活有多不容易,都要守住自己的那一份优雅。

组件化-二进制方案

为什么要做二进制

随着APP业务规模越来越大,代码量的不断增多,再加上团队迭代节奏变快,开发的痛点越来越明显—编译耗时较长。目前我司代码量约40w行左右, 业务开发人员每次从git仓库拉代码都会让Xcode重新编译.不同开发机性能也不一,快的可以8min编译完,慢的则达到20min,极大的影响了开发效率。
另外一种需求是我们的代码对于不同的团队有不同的权限管理。我可能不希望直接发布我的源码,而是通过二进制和文档的方式提供给业务方。

二进制需求

既然我们想做二进制,那么我们要考虑到我们如何做,如何来衡量做出来的二进制方案的成效。
参考网上二进制方案和自己公司内业务开发的痛点我们列了如下的需求。

  1. 我需要平滑的过度到二进制方案,不能对业务团队有大的影响

  2. 对于二进制库,编译虽然快了,但是出现错误无法调试,我需要更快捷的切换为源码形式

  3. 可以复用原来的组件仓库,不希望把二进制到处复制

  4. 二进制的生成有更方便的流程

  5. 我希望我的调用方不需要有奇怪的调用方式,如 '2.2.1 Binary'

  6. 虽然Podfile是ruby代码但是很多业务方的同学不懂,不希望出现ruby逻辑,增大接入难度

  7. 照例使用旧的pod install或者pod update命令,不希望有其他别的操作。

  8. 组件提供者不希望维护多份Podspec文件

     结合前面几篇讲解组件化-动态库的文章。我又加了一条
  9. 二进制是静态库,最好是静态framework。

二进制实施方案

公司的库无非就2种:

  1. 第三方库
  2. 自己开发的私有库
    下面分别讲解

私有库二进制方案

如何制作静态库

首先我们肯定是有源码才有的二进制。前面的需求也提到了组件维护者需要很容易的得到二进制库。首先想到的就是写脚本,但是脚本肯定依赖于模板工程,然后配置源文件,配置资源文件,配置依赖。又是工作量~~。所幸,CocoaPods已经有一个插件cocoapods-packager帮我们提供这个功能。
安装很简单执行下面命令

 gem install cocoapods-packager

那么我们前面提到的配置源文件,配置依赖,配置资源文件,其实有一个东西 我们已经做过了。那就是我们对外发布的PodSpec文件。
那么打包一个库也很简单

➜  0.0.4 git:(master) ✗ pod package --help
Usage:

    $ pod package NAME [SOURCE]

      Package a podspec into a static library.

Options:

    --force                                                         Overwrite existing
                                                                    files.
    --no-mangle                                                     Do not mangle
                                                                    symbols of
                                                                    depedendant Pods. 
    --embedded                                                      Generate embedded
                                                                    frameworks.
    --library                                                       Generate static
                                                                    libraries.
    --dynamic                                                       Generate dynamic
                                                                    framework.
    --bundle-identifier                                             Bundle identifier
                                                                    for dynamic
                                                                    framework
    --exclude-deps                                                  Exclude symbols
                                                                    from dependencies.
    --configuration                                                 Build the
                                                                    specified
                                                                    configuration
                                                                    (e.g. Debug).
                                                                    Defaults to
                                                                    Release
    --subspecs                                                      Only include the
                                                                    given subspecs
    --spec-sources=private,https://github.com/CocoaPods/Specs.git   The sources to
                                                                    pull dependant
                                                                    pods from
                                                                    (defaults to
                                                                    https://github.com/CocoaPods/Specs.git)

以下是一个简单的demo

pod package LJStability.podspec --embedded 

接下来将这个二进制文件放在我们的源码仓库即可

发布我们的二进制

我们解决了如何快速制作二进制库,那么如何发布呢?
网上很多方案是在podspec写判断条件,然后通过特殊的install/update方式去使用。在我们这边的实践情况来看,这套方案有几个弊端。

  1. 使用这些库让业务方改变了使用pod的方式
  2. 切换源码还是二进制是全局的,我无法做到只切换某个组件为源码形式。
  3. CocoaPods自身会给我们的install/update做缓存,本来是好事,但是现在变成了负担,我们要频繁的清理缓存,造成了下次install/update缓慢。

那么我们能不能一个PodSpec发布多个子pod呢,当然是支持的,CocoaPods官方文档介绍了subspec这个东西。这个subspec原本是做更细程度的模块划分,并且这些模块间是不能有互相依赖的(可以单向),换个想法既然都互相依赖了那就说明这些模块是无法做到更详细的拆解的就不适合拆分做subspec。关于Subspec做的比较好的有2个库 AFNetworking.podspecSDWebImage.podspec
那么我们可以指定做2个subspec一个叫做framework一个叫做source,指定默认的为framework即可,那么我在使用的时候有下面两种引入方法

pod 'Test' # 这会默认导入二进制库
pod 'Test/source' 这会导入源码

下面是podspec书写

Pod::Spec.new do |s|
s.name             = "LJStability"
s.version          = "0.0.5"
s.summary          = "链家网防Crash组件"
s.description      = "链家网防Crash组件支持7种Crash防护 \
1. unrecoginzed Selector Crash \
2. KVO Crash \
3. Container Crash \
4. NSNotification Crash \
5. NSNull Crash \ 
6. NSTimer Crash \
7. 野指针 Crash  "


s.license          = {:type => 'MIT', :file => 'LICENSE'}
s.homepage         = 'xx.git'
s.author           = { "author" => "xx@lianjia.com" }
s.source           = { :git => "http://git.xx.git", :commit => "" }

s.platform              = :ios, '7.0'
s.ios.deployment_target = '7.0'


  s.default_subspec = 'framework'
  s.subspec 'source' do |source|
    source.source_files = 'LJStability/LJStability/Classes/**/*'

  end

  s.subspec 'framework' do |framework|

    framework.ios.vendored_frameworks = 'LJStability/Pod/*.framework'
  end




# dependencys
s.dependency 'Crashlytics'
s.dependency 'JRSwizzle'


end



通过上面的方案我们解决了前面所说的三个问题。业务方正常使用install/update即可,我可以动态切换部分组件到源码形式,我也不用总是频繁的清理pod的缓存。

如何lint

我们为了保证将要发布的podspec是正确的,会有lint(校验)这个步骤

➜  ~ pod spec lint --help
Usage:

    $ pod spec lint [NAME.podspec|DIRECTORY|http://PATH/NAME.podspec ...]

      Validates `NAME.podspec`. If a `DIRECTORY` is provided, it validates the podspec
      files found, including subfolders. In case the argument is omitted, it defaults
      to the current working dir.

Options:

    --quick                                           Lint skips checks that would
                                                      require to download and build
                                                      the spec
    --allow-warnings                                  Lint validates even if warnings
                                                      are present
    --subspec=NAME                                    Lint validates only the given
                                                      subspec
    --no-subspecs                                     Lint skips validation of
                                                      subspecs
    --no-clean                                        Lint leaves the build directory
                                                      intact for inspection
    --fail-fast                                       Lint stops on the first failing
                                                      platform or subspec
    --use-libraries                                   Lint uses static libraries to
                                                      install the spec
    --sources=https://github.com/artsy/Specs,master   The sources from which to pull
                                                      dependent pods (defaults to
                                                      https://github.com/CocoaPods/Specs.git).
                                                      Multiple sources must be
                                                      comma-delimited.
    --private                                         Lint skips checks that apply
                                                      only to public specs
    --swift-version=VERSION                           The SWIFT_VERSION that should be
                                                      used to lint the spec. This
                                                      takes precedence over a
                                                      .swift-version file.
    --skip-import-validation                          Lint skips validating that the
                                                      pod can be imported
    --silent                                          Show nothing
    --verbose                                         Show more debugging information
    --no-ansi                                         Show output without ANSI codes
    --help   

之前我们校验一个podspec文件是否书写正确可能只是简单的加上文件名即可,不过有了subspec我们就要关心这些参数了 ,如果使用了上文说的工作方式,那么我们就要分别校验2个subspec了,并且由于我们制作的是静态库,需要加上--use-libraries 默认情况下使用的是use_framework!模式。

业务方更进一步的需求

本来到了这里,我们的教程基本结束了,但是业务方提了新需求,上文的截图中我们看到,我们发布的源码是没有文件夹组织结构的,业务方同学说,我能不能更好的组织下文件夹结构,这样方便我调试。
这里的解决办法其实也很简单,将subspec分为2大部分,1是private的subspec用于组织文件夹结构,2是public的subspec如 framework和source用于供调用方使用。
以下是一个例子

Pod::SPod::Spec.new do |s|
s.name             = "LJStability"
s.version          = "0.0.5"
s.summary          = "链家网防Crash组件"
s.description      = "链家网防Crash组件支持7种Crash防护 \
1. unrecoginzed Selector Crash \
2. KVO Crash \
3. Container Crash \
4. NSNotification Crash \
5. NSNull Crash \ 
6. NSTimer Crash \
7. 野指针 Crash  "


s.license          = {:type => 'MIT', :file => 'LICENSE'}
s.homepage         = 'xx.git'
s.author           = { "author" => "xx@lianjia.com" }
s.source           = { :git => "http://git.xx.git", :commit => "" }

s.platform              = :ios, '7.0'
s.ios.deployment_target = '7.0'



  s.default_subspec = 'framework'

#public
  s.subspec 'source' do |source|


    source.dependency 'LJStability/SDK'
    source.dependency 'LJStability/FoundationContainer'
    source.dependency 'LJStability/KVO'
    source.dependency 'LJStability/Notification'
    source.dependency 'LJStability/NSNull'
    source.dependency 'LJStability/NSTimer'
    source.dependency 'LJStability/SmartKit'
    source.dependency 'LJStability/DanglingPointerStability'
    source.dependency 'LJStability/Record'

  end

  s.subspec 'framework' do |framework|

    framework.ios.vendored_frameworks = 'Pod/*.framework'
  end


#  private
  s.subspec 'SDK' do |sdk|
    sdk.source_files = 'LJStability/Classes/*'
    sdk.requires_arc = true
  end
  s.subspec 'FoundationContainer' do |foundationContainer|
    foundationContainer.requires_arc = true
  end
  s.subspec 'KVO' do |kvo|

    kvo.requires_arc = true
  end
  s.subspec 'Notification' do |notification|
    notification.requires_arc = true
  end
  s.subspec 'NSNull' do |nSNull|

    nSNull.requires_arc = true
  end
  s.subspec 'NSTimer' do |nSTimer|

    nSTimer.requires_arc = true
  end
  s.subspec 'SmartKit' do |smartKit|

    smartKit.requires_arc = true
  end
  s.subspec 'DanglingPointerStability' do |danglingPointerStability|

  end
  s.subspec 'Record' do |record|
    record.requires_arc = true
  end





      # dependencys
      s.dependency 'Crashlytics'
      s.dependency 'JRSwizzle'


  end

上面的关于公司内部文件我删除了,以上部分只是一份参考。
调用方如图

后续优化

至此,我们的私有库组件二进制的方案已经演示完毕。
这里我要总结下我的看法 :
CocoaPods只是一个工具,我们在使用的时候要么把这个工具用的熟练到极致,要么我们修改这个工具创造出更适合的工具。
强烈建议学下ruby,自己去定制CocoaPods
不过我们上面的教程虽然给业务方解决了问题,但是其中还是有很多人工成本在里面的,人工编写podspec文件,人工打包静态库,人工lint podspec。这些都是重复性且没有技术含量的工作。
所以我们后续优化的点有以下:

  1. 根据规范自动的生成podspec文件
  2. 自动的打包静态库
  3. 更新podspec文件的多个subspec
  4. 自动校验podspec的文件是否正确

第三方库二进制方案

关于第三方库,由于podspec文件并不在我们控制,所以我们不能自己去发布并且修改podspec,网上有的方案是使用prepare_command。文后有参考链接这里不做讨论了。
但是可以预见的随着公司规模的扩大,我们最好的做法就是对于第三方代码和podspec文件完全做镜像。我们自己控制这些库,也就不存在第三方库一说了。

参考

  1. CocoaPods组件平滑二进制化解决方案 ​​​​
  2. Pod 预编译,减少不必要的生命浪费
  3. I have a pod, I have a carthage, En...
  4. 谈谈CocoaPods组件二进制化方案

评论卡

已有 9 条评论

  1. George

    假设JRSwizzle也是私有库,如果需要调试JRSwizzle的源码,那么在Podfile中就是这样: pod 'JRSwizzle/source'。这时LJStability依赖的实际是JRSwizzle/framework,那么就会有冲突,这块你那边是怎么解决的呢?

    George2017-08-09 15:12回复
    1. 南栀倾寒

      实际上想做二进制并且很方便的切换到源码调试是一件非常难的事,国内有的公司是去依赖画 也就是虽然写了依赖 但是修改了CocoaPods 在安装的时候并不安装依赖的顺序去安装

      南栀倾寒2017-08-09 15:21回复
      1. George

        确实是一件很困难的事,你这边用的什么方法呢?

        George2017-08-09 15:30回复
        1. 南栀倾寒

          就是去依赖那种 各种扩展CocoaPods

          南栀倾寒2017-08-09 15:39回复
    2. wanyakun

      调试组件指定source,其他组件组件其实依赖的如果是framework,这时候在其实在Pod中source和framework是并存的,相当于两个subspec都显式书写了。这时候如果调试,其实是走源码的。所谓的冲突指的是?

      wanyakun2017-09-05 17:30回复
      1. 南栀倾寒

        你那里测试的是相当于Framework不生效?
        我记得我之前测试的适合如果同时出现了Framework和source其实会duplicate symbols

        南栀倾寒2017-09-05 17:48回复
        1. wanyakun

          同时出现framework和source时xcconfig中FRAMEWORK_SEARCH_PATHS为"$PODS_CONFIGURATION_BUILD_DIR/XXX(组件名字)",指定framework的时候FRAMEWORK_SEARCH_PATHS为"${PODS_ROOT}/xxx(framework所在目录)",所以并不会出现duplicate symbols,我目前测试的是这样,你可以再测下。

          wanyakun2017-09-06 10:11回复
          1. 妖妖

            确实会出现楼上这种duplicate symbols问题,不知道怎么能解决这样的问题?

            妖妖2019-08-15 16:08
  2. fei

    多个静态framework之间如果依赖同样的库的话会冲突吧?

    fei2018-05-09 16:20回复