iOS CocoaPods组件平滑二进制化解决方案及详细教程二之subspecs篇

这篇文章主要想介绍以下几个部分:

  • subspecs基本概念
  • 实际应用的好处
  • 如何对含有subspecs的CocoaPods库进行二进制化

什么是CocoaPods的subspecs

来一个直观点的。顺便为自己的YTXAnimations做个广告。

在Podfile中,它是这样的:

pod 'YTXAnimations', '~> 1.2.4', :subspecs => ["AnimateCSS", "Transformer"]

在App中Pods/YTXAnimations文件目录下它是这样的:


subspecsdemodict

在CocoaPods项目开发时是这样的:


subspecsdemodev

在podspec里是这样的:

    YTXAnimateCSS   = { :spec_name => "AnimateCSS" }
    YTXCSShake   = { :spec_name => "CSShake" }
    YTXMagicCSS   = { :spec_name => "MagicCSS" }
    
    $animations = [YTXAnimateCSS, YTXCSShake, YTXMagicCSS]
    
    $animations.each do |sync_spec|
        s.subspec sync_spec[:spec_name] do |ss|
            
            specname = sync_spec[:spec_name]
            
            sources = ["Pod/Classes/UIView+YTX#{specname}.*", "Pod/Classes/YTXAnimationsUtil.{h,m}"]
            
            ss.source_files = sources

            if sync_spec[:dependency]
                sync_spec[:dependency].each do |dep|
                    ss.dependency dep[:name], dep[:version]
                end
            end
            
        end
    end
    
    s.subspec "Transformer" do |ss|
      ss.source_files = ["Pod/Classes/YTXGooeyCircleLayer.{h,m}", "Pod/Classes/YTXCountDownShowLayer.{h,m}"]
    end

==在一个podspec里我可以定义它的subspecs,给使用方提供了一种灵活的方式去获取相关源码,而不是全部源码。subspec之间也可以有依赖关系,依赖其他第三方库等。==

如果是这样的用的话,就是全量。

pod 'YTXAnimations', '~> 1.2.4'

也有不少在github第三方库用了subspecs。比如:ARAnalytics

谈谈作用:subspecs这种模式特别适合组件化开发。

比如有两个业务team A和B。他们各自维护一个业务组件A和业务组件B。原则上业务组件A和业务组件B之间不能相互依赖。但是很多时候组件A需要调用组件B的功能,接受组件B的回调。架构的时候我们会使用依赖协议或者依赖下沉等等方式去除他们之间的耦合。

但问题是我们还是需要集成在一起调试的。

一般做法就是在业务组件A的Example项目的Podfile中,加上依赖业务组件B:

target 'TestBusinessAExampleApp' do
  pod 'BusinessA', :path => "../"
  pod 'BusinessB', '~>1.2.1'
end

然后在Example App中串联起A和B,以达到调试的目的。

组件B作为一个业务肯定是很庞大的,所以编译慢。二进制化可以解决这个问题。作为Team A的人,我不需要关注组件B是否太大编译慢,依赖等问题。

举个例子,比如外卖和电影,外卖会送电影票。

很容易想到!业务组件A只依赖业务组件B的部分。组件B应该把这部分其实对外的内容尽量做成一个subspec或者正常结构划分,划分成依赖其中几个subspec。这样业务组件A需要关心的事就更少了。当发生问题,Team A不得已想要查看业务组件B的源代码以查看是否问题出在了业务组件B的时候,Team A的人员面对的不是整个业务组件B的业务源代码,而是部分其实对外的源代码。缩小依赖的二进制文件大小或源代码数量也是有显而易见的好处的。

嗯,调试源代码,我们应该:

  • 删除Pods目录
  • pod cache clean --all
  • IS_SOURCE=1 pod install

嗯,这已经在公司内部达成了一致,使用IS_SOURCE。

现在的业务组件A的Example项目的Podfile中应该变成了这样:

target 'TestBusinessAExampleApp' do
  pod 'BusinessA', :path => "../"
  pod 'BusinessB', '~>1.2.1' , :subspecs => ["SomeBusinessXXX"]
end

进一步的如果有个业务组件C要和业务组件B打交道,它的Example项目的Podfile应该这样写:

target 'TestBusinessCExampleApp' do
  pod 'BusinessC', :path => "../"
  pod 'BusinessB', '~>1.2.1' , :subspecs => ["SomeBusinessTTT"]
end

在主项目App中的Podfile是这样写:

target 'DaMeiTuanApp' do
  pod 'BusinessA', '~>3.0.5'
  pod 'BusinessB', '~>1.2.1'
  pod 'BusinessC', '~>2.2.0'
end

下面开始说说subspec如何二进制化,如何在podspec中定义

==如果没有特别说明,没有讲到细节的内容或方式都应该在教程一里==

在教程一里面提到有subspecs的CocoaPods的组件二进制化方案,说了两个方案。最后选择的方案:是对每一个subspec都做份二进制并保持它们之间依赖的相互关系。

为什么不使用全集?也就是把所有源码都变成.a呢?

  • 使用方的Podfile就需要改写了。
  • 使用方本来希望只用AnimateCSS和Transformer,现在不得不把CSShake和MagicCSS也包含进来了。

接下来以实际项目YTXUtilCategory作为例子来讲解。

方案就是:是对每一个subspec都做份二进制并保持它们之间依赖的相互关系。

YTXUtilCategory是我们的一个提供公共通用方法和Category的类。在二进制化之前它大概是长这个样子的:


subspecsutilcategorydemo1

二进制化之前它的podspec是这样的:

Pod::Spec.new do |s|
  .......
  _all_names = []
  
  _GTMBase64         = { :spec_name => "GTMBase64",        :source_files => ['Pod/Classes/GTMBase64/GTM*.{h,m}'              ] }

  _UIColor           = { :spec_name => "UIColor",          :source_files => ['Pod/Classes/UIColor/UIColor+*.{h,m}'          ] }
  _UIView            = { :spec_name => "UIView",           :source_files => ['Pod/Classes/UIView/UIView+*.{h,m}'           ] }
  _UIImage           = { :spec_name => "UIImage",          :source_files => ['Pod/Classes/UIImage/UIImage+*.{h,m}'          ] }
  _UIDevice          = { :spec_name => "UIDevice",         :source_files => ['Pod/Classes/UIDevice/UIDevice+*.{h,m}'         ] }
  _UITableView       = { :spec_name => "UITableView",      :source_files => ['Pod/Classes/UITableView/UITableView+*.{h,m}'      ] }
  _UIViewController  = { :spec_name => "UIViewController", :source_files => ['Pod/Classes/UIViewController/UIViewController+*.{h,m}' ] }
  _UIButton          = { :spec_name => "UIButton",         :source_files => ['Pod/Classes/UIButton/UIButton+*.{h,m}'           ] }

  _NSURL             = { :spec_name => "NSURL",            :source_files => ['Pod/Classes/NSUR/NSURL+*.{h,m}'            ] }
  _NSArray           = { :spec_name => "NSArray",          :source_files => ['Pod/Classes/NSArray/NSArray+*.{h,m}'          ] }
  _NSDictionary      = { :spec_name => "NSDictionary",     :source_files => ['Pod/Classes/NSDictionary/NSDictionary+*.{h,m}'     ] }
  _NSDate            = { :spec_name => "NSDate",           :source_files => ['Pod/Classes/NSDate/NSDate+*.{h,m}'           ] ,     :dependency => [{:name => "DateTools",    :version => "~> 1.0"    }] }

  _NSString          = { :spec_name => "NSString",         :source_files => ['Pod/Classes/NSString/NSString+*.{h,m}'         ],
    :sub_dependency => [_GTMBase64] }
  
  _Util              = { :spec_name => "Util",             :source_files => ['Pod/Classes/Util/*.{h,m}'         ]}
  
  _FoundationAll     = { :spec_name => "FoundationAll",    :sub_dependency => [_NSString, _NSURL, _NSDate, _NSArray, _NSDictionary    ] }
  
  _UIAll             = { :spec_name => "UIAll",            :sub_dependency => [_UIColor, _UIView, _UIImage, _UIButton, _UIDevice, _UITableView, _UIViewController        ] }

  _all_subspec = [_GTMBase64, _UIColor, _UIView, _UIImage, _UIDevice, _UITableView, _UIViewController, _UIButton, _NSURL, _NSDate, _NSArray, _NSString, _NSDictionary, _Util, _FoundationAll, _UIAll]

  _all_subspec.each do |spec|
      s.subspec spec[:spec_name] do |ss|

          specname = spec[:spec_name]

          _all_names << specname
          if spec[:source_files]
              ss.source_files = spec[:source_files]
          end

          if spec[:sub_dependency]
              spec[:sub_dependency].each do |dep|
                  ss.dependency "YTXUtilCategory/#{dep[:spec_name]}"
              end
          end

          if spec[:dependency]
              spec[:dependency].each do |dep|
                  ss.dependency dep[:name], dep[:version]
              end
          end

      end
  end

  
  spec_names = _all_names[0...-1].join(", ") + " 和 " + _all_names[-1]
  
  s.description = "拆分了这些subspec:#{spec_names}"
  
end

通过分析代码可以知道:

  • 有这些subspecs:GTMBase64, UIColor, UIView, UIImage, UIDevice, UITableView, UIViewController, UIButton, NSURL, NSDate, NSArray, NSString, NSDictionary, Util, FoundationAll, UIAll
  • NSDate依赖第三方DateTools。
  • NSString依赖兄弟GTMBase64。
  • FoundationAll依赖兄弟subspecs,自己没什么内容,或者说提供一个灵活的方式一口气纳入所有相关Foundation内容。
  • UIAll依赖兄弟subspecs,和FoundationAll想要做的是一样的。

相信各位读者看了这个podspec也就知道怎么创建自己的subspec了。或者看看ARAnalytics的podspec。

在App中的Podflie是这样用的:

pod 'YTXUtilCategory','~> 1.2.0'

pod 'YTXUtilCategory','~> 1.2.0', :subspecs => ["UIColor", "FoundationALL"]

从分析的结果来看我应该创建这些target:


subspecsdictbinaryall

在Example/Podfile中根据target的名字增加以下内容并pod install:

target 'YTXUtilCategoryGTMBase64Binary' do

end

.........省略

target 'YTXUtilCategoryNSDateBinary' do
  pod 'DateTools', '~> 1.0'
end

target 'YTXUtilCategoryUtilBinary' do

end

注意YTXUtilCategoryNSDateBinary,把它的第三方依赖加上。版本和podspec里描写的一致。

在根目录增加两个shell脚本。

buildbinary.sh和教程一的基本一致,只是改了第一行:

#获得第一个参数
PROJECT_NAME=$1
# 编译工程
BINARY_NAME="${PROJECT_NAME}Binary"

buildallbinary.sh

pushd "$(dirname "$0")" > /dev/null
SCRIPT_DIR=$(pwd -L)
popd > /dev/null
#也可以写个for循环
./buildbinary.sh YTXUtilCategoryGTMBase64
......省略
./buildbinary.sh YTXUtilCategoryUtil

执行

./buildallbinary.sh

得到结果:
![subspecsbinarybuildresult] (https://cloud.githubusercontent.com/assets/2350193/16981721/6ec1af32-4e9e-11e6-9c4a-37e6018ebf5c.png)

更改podspec内容为:

Pod::Spec.new do |s|
  ......省略
  _all_names = []

  _GTMBase64         = { :spec_name => "GTMBase64"}

  _UIColor           = { :spec_name => "UIColor"}
  _UIView            = { :spec_name => "UIView"}
  _UIImage           = { :spec_name => "UIImage"}
  _UIDevice          = { :spec_name => "UIDevice"}
  _UITableView       = { :spec_name => "UITableView"}
  _UIViewController  = { :spec_name => "UIViewController"}
  _UIButton          = { :spec_name => "UIButton"}

  _NSURL             = { :spec_name => "NSURL"}
  _NSArray           = { :spec_name => "NSArray"}
  _NSDictionary      = { :spec_name => "NSDictionary"}
  _NSDate            = { :spec_name => "NSDate", :dependency => [{:name => "DateTools",    :version => "~> 1.0"    }] }

  _NSString          = { :spec_name => "NSString", :sub_dependency => [_GTMBase64] }
  
  _Util              = { :spec_name => "Util"}
  
  _temp = [_GTMBase64, _UIColor, _UIView, _UIImage, _UIDevice, _UITableView, _UIViewController, _UIButton, _NSURL, _NSArray, _NSDictionary, _NSDate, _NSString, _Util]

  puts '-------------------------------------------------------------------'
  if ENV['IS_SOURCE']
    puts '-------------------------------------------------------------------'
    puts "Notice:#{s.name} is source now"
    puts '-------------------------------------------------------------------'

    _temp.each do |spec|
      spec[:source_files]=["Pod/Classes/#{spec[:spec_name]}/*.{h,m}"]
    end

  else
    puts '-------------------------------------------------------------------'
    puts "Notice:#{s.name} is binary now"
    puts '-------------------------------------------------------------------'
        
    _temp.each do |spec|
        spec[:source_files]=["Pod/Products/#{s.name}#{spec[:spec_name]}/include/**"]
        spec[:public_header_files]=["Pod/Products/#{s.name}#{spec[:spec_name]}/include/*.h"]
        spec[:vendored_libraries]=["Pod/Products/#{s.name}#{spec[:spec_name]}/lib/*.a"]
    end
  end
  
  _FoundationAll     = { :spec_name => "FoundationAll",    :sub_dependency => [_NSString, _NSURL, _NSDate, _NSArray, _NSDictionary    ] }
  
  _UIAll             = { :spec_name => "UIAll",            :sub_dependency => [_UIColor, _UIView, _UIImage, _UIButton, _UIDevice, _UITableView, _UIViewController        ] }
  
  _all_subspec = [_GTMBase64, _UIColor, _UIView, _UIImage, _UIDevice, _UITableView, _UIViewController, _UIButton, _NSURL, _NSDate, _NSArray, _NSString, _NSDictionary, _Util, _FoundationAll, _UIAll]
  
  _all_subspec.each do |spec|
      s.subspec spec[:spec_name] do |ss|
          
          specname = spec[:spec_name]
          
          _all_names << specname
          if spec[:source_files]
              ss.source_files = spec[:source_files]
          end

          if spec[:public_header_files]
              ss.public_header_files = spec[:public_header_files]
          end

          if spec[:vendored_libraries]
              ss.ios.vendored_libraries = spec[:vendored_libraries]
          end
          
          if spec[:sub_dependency]
              spec[:sub_dependency].each do |dep|
                  ss.dependency "YTXUtilCategory/#{dep[:spec_name]}"
              end
          end
          
          if spec[:dependency]
              spec[:dependency].each do |dep|
                  ss.dependency dep[:name], dep[:version]
              end
          end
          
      end
  end
  
  spec_names = _all_names[0...-1].join(", ") + " 和 " + _all_names[-1]
  
  s.description = "拆分了这些subspec:#{spec_names}"
  
end

难点其实在于podspec如何写,如何描述subspec之间的关系,如何拆分subspec。

假如a.h和b.h都用到了c.h,而a.h隶属于subspec A,而b.h隶属于subspec B。那你应该做一个subspec C其中包含c.h。而A和B都依赖C。

要避免a.h依赖b.h,b.h依赖a.h这种循环依赖的问题。

到此为止,含有subspec的CocoaPods库就这么简单的完成了。

后记,在做subspec二进制化遇到的问题

_all_sync.each do |sync_spec|
  ...
  ss.prefix_header_contents = "#define YTX_#{specname.upcase}_EXISTS 1"
  ... 
end

注意ss. ss.prefix_header_contents这段。加了一个宏。然后在这里会用到:

#import "YTXRestfulModel.h"

#ifdef YTX_USERDEFAULTSTORAGESYNC_EXISTS
#import "YTXRestfulModelUserDefaultStorageSync.h"
#endif

#ifdef YTX_AFNETWORKINGREMOTESYNC_EXISTS
#import "AFNetworkingRemoteSync.h"
#endif

#ifdef YTX_YTXREQUESTREMOTESYNC_EXISTS
#import "YTXRestfulModelYTXRequestRemoteSync.h"
#endif

#ifdef YTX_FMDBSYNC_EXISTS
#import "YTXRestfulModelFMDBSync.h"
#import "NSValue+YTXRestfulModelFMDBSync.h"
#endif

在源码的情况下,如果我想要用YTXRequestRemote这个subspec,那么引入YTXRequestRemote时会自带宏YTX_YTXREQUESTREMOTESYNC_EXISTS,YTXRestfulModel在编译时会根据宏引入相关头文件。

问题来了,宏都是预编译的。当我编译出二进制时,内容已经决定了。这样就丧失了subspec的动态性了。所以关键的问题在于当初设计的时候没有考虑好。

希望大家看到这个例子后,避免将来遇到相思的问题。目前没有想到好的解决方案,所以这个库并没有二进制化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容