原文地址 ,此简书只作备份,强烈推荐原文,毕竟格式比简书好看,还清晰php
去年,链家网iOS端,以前因为全部的业务端代码都是混乱管理,形成开发有不少痛点
没法单测
团队成员提交代码冲突机率大
CI配合效果差
功能性代码多端没法复用
单仓库代码量大
编译时间长
等等痛点,领导和组内屡次沟通开始着手组件化开发,但愿能改进这些开发中的痛点,成立组件化团队。 组件化的方案大同小异,基础性代码封装私有库,业务组件交互交由中间件负责,项目依赖工具用 iOS项目事实上的标准CocoaPods
前期的基础性组件拆分都较为顺利,从依赖树的叶子节点开发是最合适的方案。 随着组件抽离的愈来愈多,私有库的依赖体系也愈来愈复杂,慢慢过渡到了业务组件。业务组件用了Swift的第三方组件,用了Swift库的同窗都知道必须加上use_frameworks!
,这个标记是说Pod管理的依赖所有编译为动态库
,而后呢咱们的不少组件又依赖了诸如百度地图,微信分享等静态库
,因而我在执行pod install
报了一个没有遇见过的错误html
[!] The 'Pods-LJA_Example' target has transitive dependencies that include static binaries: (/Users/nero/Desktop/Static_Dynamic/Componment/Example/Pods/libWeChatSDK/libWeChatSDK.a)
复制代码
这就尴尬了,因而一阵疯狂的搜索 google stackoverflow 等,然而并无什么卵用,并且上面催得急,根本没时间处理这些
小问题
业务重构是最主要的,以致于咱们的业务组件没有作到独立仓库拆分。 直到最近终于找到了解决办法:( 主要是本身的功力不够深厚linux
首先静态库和动态库都是以二进制提供代码复用的代码库ios
.a
.dll(windows)
.dylib(mac)
so(linux)
静态库: 连接时会被完整的复制到可执行文件中,因此若是两个程序都用了某个静态库,那么每一个二进制可执行文件里面其实都含有这份静态库的代码 动态库: 连接时不复制,在程序启动后用dyld加载,而后再决议符号,因此理论上动态库只用存在一份,好多个程序均可以动态连接到这个动态库上面,达到了节省内存(不是磁盘是内存中只有一份动态库),还有另一个好处,因为动态库并不绑定到可执行程序上,因此咱们想升级这个动态库就很容易,windows 和linux上面通常插件和模块机制都是这样实现的。git
But咱们的苹果爸爸在iOS平台上规定不容许存在动态库,而且全部的IPA都须要通过苹果爸爸的私钥加密后才能用,基本你用了动态库也会由于签名不对没法加载,(越狱和非APP store除外)。因而就把开发者本身开发动态库掐死在幻想中。 直到有一天,苹果爸爸的iOS升级到了8,iOS出现了APP Extension
,swift
编程语言也诞生了,因为iOS 主APP须要和Extension共享代码,Swift语言的机制也只能有动态库,因而苹果爸爸尴尬了,不过这难不倒咱们的苹果爸爸,毕竟我是爸爸,规则是我来定,我想怎样就怎样,因而提出了一个概念Embedded Framework
,这种动态库容许APP
和 APP Extension
共享代码,可是这份动态库的生命被限定在一个APP进程内。简单点能够理解为 被阉割的动态库。程序员
若是你把某个本身开发的动态库(系统的不算,毕竟苹果是爸爸)放在了Linked Frameworks and Libraries
里面,程序一启动就会报Reason: Image Not Found
,你只能把它放在Embeded Binaries
里面才能正常使用, 看图: github
简单点,说话的方式简单点~~编程
上面的介绍貌似有点抽象啊 套用在美团技术分享大会上的话就是: 静态库: 一堆目标文件(.o/.obj)的打包体(并不是二进制文件) 动态库: 一个没有main函数的可执行文件json
这里咱们来复习下C语言的基本功,编译和连接 编译:将咱们的源代码文件编译为目标文件 连接:将咱们的各类目标文件加上一些第三方库,和系统库连接为可执行文件。 因为某个目标文件的符号(能够理解为变量,函数等)可能来自其余目标文件,其实连接这一步最主要的操做就是 决议符号的地址。swift
因而连接加装载就有了不一样的状况
而后组合起来就是2*2 = 4了
程序员的自我修养
一书既然有2种库,那么依赖关系又是2*2喽
第一种 静态库互相依赖,这种状况很是常见,制做静态库的时候只须要有被依赖的静态库头文件在就能编译出来。可是这就意味者你要收到告诉使用者你的依赖关系 幸运的是 CocoaPod
就是这样作的 第二种动态库依赖动态库,两个动态库是相互隔离的具备隔离性
,可是制做的静态库的时候须要被依赖动态库参与连接,可是具体的符号决议交给dyld
来作。 第三种,静态库依赖动态库,也很常见,静态库制做的时候也须要动态库参与连接,可是符号的决议交给dyld来作。 第四种,动态库依赖静态库,这种状况就有点特殊了。首先咱们设想动态库编译的时候须要静态库参与编译,可是静态库交由dyld来作符号决议,but 这和咱们前面说的就矛盾了啊。静态库本质是一堆.o的打包体,首先并非二进制可执行文件,再者你没法保证主程序把静态库参与连接共同生成二进制可执行文件。这就尴尬了。 怎么办? 目前的编译器的解决办法是,首先我没法保证主程序是否包含静态库,再者静态库也没法被dyld
加载,那么我直接把你静态库的.o偷过来,共同组成一个新的二进制。也被称作吸附性
那么我有多份动态库都依赖一样的静态库,这就尴尬了,每一个动态库为了保证本身的正确性会把静态库吸附进来。而后两个库包含了一样的静态库,因而问题就出现了。 看到这里想必前面出现的错误你已经能猜出来了把~_~
后面再详细解释
先来个总结 可执⾏⽂件(主程序或者动态库)在构建的连接阶段
target-对于一个产物(app,.a ,.framework) project-一个项目包含多个target workspace:一个包含多个target schema: 指定了一个产物是按照何种的依赖关系,编译-连接到最终的一个产物
这么多年,Apple的博客和文档也就告诉了咱们什么是静态库 什么是动态库,如何制做等。可是并无给咱们提供一系列的依赖管理工具。因此CocoaPods成了事实上的标准。 一般CocoaPods管理的工程结构以下:
• CocoaPods
+ App.workspace
+ App.project
• Pods.project
• pod target => .a
复制代码
那么当咱们按下CMD+B的时候,整个项目按照先编译被依赖Pod,而后依赖其余Pod的Pod也被构建出来,最终全部的组件被编译为一个lib-Pods-XXXAPP.a
被添加进项目进去。资源经过CocoaPods提供的脚本也一并被复制进去。想了解CocoaPods作了什么的读者能够参看后面的连接
这么多理论功底的创建,相信咱们已经能分析出来以前pod install
的缘由了。就是用了use_framework
那么咱们的全部Pod都会以动态库(Embeded Framework)的形式去构建,因而那些非开源的库(如 百度地图,微信分享)若是被多个Pod依赖(组件化开发中太常见了)因而被吸附到动态库里面,因此CocoaPod直接就不让咱们install成功。由于你如今的依赖管理就是错误的。 在听取美团叶樉老师分享的时候 他们的出发点是由于要绕过苹果爸爸在iOS9如下对__text段60M的限制使用了动态库方案,咱们是由于某些swift库必需要用到(历史遗留缘由)动态库。美团的作法是摘除依赖关系,自定义CocoaPods(开源的原本就是用着不爽我就改)。可是我是个小菜鸡啊。我也不会ruby(之后会学的),可是叶樉老师给我提了别的idea。 前面咱们知道 动态库和动态库是隔离性
,动态库依赖静态库具备吸附性
,那么咱们能够自定义一个动态库把百度地图这种静态库吸附进来。对外总体呈现的是动态库特性。其余的组件依赖咱们自定义的动态库,因为隔离性
的存在,不会出现问题。
1 建立动态库项目这里以wx举例
2 按照微信的官方文档。添加依赖库(我是由于pod install巨慢 因此我直接拽进来了)
3 将wx的PublicHeader暴露出来,注意因为我并无使用到wx相关API因此连接器帮咱们连接动态库 的时候可能并不会把wx静态库吸附进来。咱们手动在build Setting的other link flags加上-all_load
标记
4.在Schema里面跳转编译配置为Release ,而且选择全部的CPU架构
5 而后选择模拟器或者Generic iOS Device运行编译就会生成对应版本的Framework了。
6.可是为了保证开发者使用的时候是真机模拟器都能正常使用,咱们须要合并不一样架构 这里在Build Phases
里添加如下脚本,真机和模拟器都Build一遍以后就会在工程目录下生成Products文件夹,
if [ "${ACTION}" = "build" ]
then
INSTALL_DIR=${SRCROOT}/Products/${PROJECT_NAME}.framework
DEVICE_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework
SIMULATOR_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi
mkdir -p "${INSTALL_DIR}"
cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
#ditto "${DEVICE_DIR}/Headers" "${INSTALL_DIR}/Headers"
lipo -create "${DEVICE_DIR}/${PROJECT_NAME}" "${SIMULATOR_DIR}/${PROJECT_NAME}" -output "${INSTALL_DIR}/${PROJECT_NAME}"
open "${DEVICE_DIR}"
open "${SRCROOT}/Products"
fi
复制代码
因而咱们有了咱们本身的私有动态库LJWXSDK,那么咱们来验证咱们以前的问题 首先指定一个LJWXSDK.podspec这里我直接传到了个人Github上面
#
# Be sure to run `pod lib lint LJPod.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'LJWXSDK'
s.version = '0.1.0'
s.summary = 'A short description of LJWXSDK.'
s.description = <<-DESC TODO: Add long description of the pod here. DESC
s.homepage = 'https://github.com/ValiantCat/LJWXSDK'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'ValiantCat' => '519224747@qq.com' }
s.source = { :http => 'http://onk2m6gtu.bkt.clouddn.com/LJWXSDK.framework.zip' }
s.ios.deployment_target = '8.0'
s.default_subspec = 'zip'
s.subspec 'zip' do |zip|
puts '-------------------------------------------------------------------'
puts 'Notice:LJWXSDK is zip now'
puts '-------------------------------------------------------------------'
zip.ios.vendored_frameworks = '*.framework'
end
end
复制代码
注意上面我是把二进制压缩丢进了七牛的oss文件存储。毕竟免费还快。
而后经过pod lib create建立了一个pod用来验证以前咱们的传递性依赖问题, 文件夹结构以下
.
├── Example
│ ├── LJA
│ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ ├── Images.xcassets
│ │ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ ├── LJA-Info.plist
│ │ ├── LJA-Prefix.pch
│ │ ├── LJAppDelegate.h
│ │ ├── LJAppDelegate.m
│ │ ├── LJViewController.h
│ │ ├── LJViewController.m
│ │ ├── en.lproj
│ │ │ └── InfoPlist.strings
│ │ └── main.m
│ ├── LJA.xcodeproj
│ ├── LJA.xcworkspace
│ ├── Podfile
│ ├── Podfile.lock
│ ├── Pods
│ │ ├── Headers
│ │ ├── LJWXSDK
│ │ │ └── LJWXSDK.framework
│ │ │ ├── Headers
│ │ │ │ ├── LJWXSDK.h
│ │ │ │ ├── WXApi.h
│ │ │ │ ├── WXApiObject.h
│ │ │ │ └── WechatAuthSDK.h
│ │ │ ├── Info.plist
│ │ │ ├── LJWXSDK
│ │ │ ├── Modules
│ │ │ │ └── module.modulemap
│ │ │ ├── _CodeSignature
│ │ │ │ └── CodeResources
│ │ │ └── read_me.txt
│ │ ├── Local\ Podspecs
│ │ │ ├── LJA.podspec.json
│ │ │ ├── LJB.podspec.json
│ │ │ └── LJWXSDK.podspec.json
│ │ ├── Manifest.lock
│ │ ├── Pods.xcodeproj
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace
│ │ ├── Target\ Support\ Files
│ │ │ ├── LJA
│ │ │ │ ├── Info.plist
│ │ │ │ ├── LJA-dummy.m
│ │ │ │ ├── LJA-prefix.pch
│ │ │ │ ├── LJA-umbrella.h
│ │ │ │ ├── LJA.modulemap
│ │ │ │ └── LJA.xcconfig
│ │ │ ├── LJB
│ │ │ │ ├── Info.plist
│ │ │ │ ├── LJB-dummy.m
│ │ │ │ ├── LJB-prefix.pch
│ │ │ │ ├── LJB-umbrella.h
│ │ │ │ ├── LJB.modulemap
│ │ │ │ └── LJB.xcconfig
│ │ │ ├── Pods-LJA_Example
│ │ │ │ ├── Info.plist
│ │ │ │ ├── Pods-LJA_Example-acknowledgements.markdown
│ │ │ │ ├── Pods-LJA_Example-acknowledgements.plist
│ │ │ │ ├── Pods-LJA_Example-dummy.m
│ │ │ │ ├── Pods-LJA_Example-frameworks.sh
│ │ │ │ ├── Pods-LJA_Example-resources.sh
│ │ │ │ ├── Pods-LJA_Example-umbrella.h
│ │ │ │ ├── Pods-LJA_Example.debug.xcconfig
│ │ │ │ ├── Pods-LJA_Example.modulemap
│ │ │ │ └── Pods-LJA_Example.release.xcconfig
│ │ │ └── Pods-LJA_Tests
│ │ │ ├── Info.plist
│ │ │ ├── Pods-LJA_Tests-acknowledgements.markdown
│ │ │ ├── Pods-LJA_Tests-acknowledgements.plist
│ │ │ ├── Pods-LJA_Tests-dummy.m
│ │ │ ├── Pods-LJA_Tests-frameworks.sh
│ │ │ ├── Pods-LJA_Tests-resources.sh
│ │ │ ├── Pods-LJA_Tests-umbrella.h
│ │ │ ├── Pods-LJA_Tests.debug.xcconfig
│ │ │ ├── Pods-LJA_Tests.modulemap
│ │ │ └── Pods-LJA_Tests.release.xcconfig
│ │ └── libWeChatSDK
│ │ ├── README.md
│ │ ├── WXApi.h
│ │ ├── WXApiObject.h
│ │ ├── WechatAuthSDK.h
│ │ └── libWeChatSDK.a
├── LICENSE
├── LJA
│ ├── Assets
│ └── Classes
│ └── LJA.m
├── LJA.podspec
├── LJB
│ ├── Assets
│ └── Classes
│ └── LJB.m
├── LJB.podspec
├── README.md
└── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj
复制代码
测试工程我也丢在7牛上面。下载测试便可 编译运行。完美。咱们又能够愉快的和swift第三方库配合使用。 不少人可能会问 诸如百度地图 微信这种sdk为何官方不支持动态库版(所说的都是embeded Framework),猜想是为了兼容更低iOS7版本吧 不少人会以为麻烦的要死。首先每一个公司多多少少都有历史包袱,麻烦也要作,再者这是一次对基本功的补充,即使大家没有用到,可是为了学习,这篇教程所作的也值得你尝试一次。
上述解决了咱们一开始遇到的问题。but既然动态库和静态库压根就不一回事,因此里面仍是有不少细节值得咱们去了解的。
首先咱们以前记得若是一个动态库加在LinkedFrameworksand Libraies
程序启动就会报ImageNotFound,若是放在EmbededBinaries
里面就能够。这是为何呢。咱们拿MacoView来看下两种状况下可执行文件的细节
其中@rpth这个路径表示的位置能够查看Xcode 中的连接路径问题 这样咱们就知道了其实加在EmbededBinaries
里面的东西其实会被复制一份到xx.app里面,因此这个名字起得仍是不错的直译就是嵌入的框架
形成这个的主要缘由是Swift的运行时库(不等同于OC的runtime概念),因为Swift的ABI不稳定,静态库会致使最终的目标程序中包含重复的运行库,相关能够看下最后的参考文章SwiftInFlux#static-libraries。等到咱们的SwiftABI稳定以后,咱们的静态库支持可能就又会出现了。固然也可能不出,Swift伴随诞生的SPM(Swift,Package Manager),可能有更好的官方的
包依赖管理工具。让咱们期待吧。
既然加了Swift的第三方库以后就须要在Podfile
里面加上use_framework!
那么CocoaPods就会帮咱们生成动态库,可是奇怪的是,咱们并无在主工程的embeded binaries
看到这个动态库,这又是什么鬼。实际上是CocoaPods使用脚本帮咱们加进去了。脚本位置在主工程的 build Phase
下的 Emded Pods frameworks
"${SRCROOT}/Pods/Target Support Files/Pods-LJA_Example/Pods-LJA_Example-frameworks.sh"
复制代码
.
├── Headers
│ ├── LJWXSDK.h
│ ├── WXApi.h
│ ├── WXApiObject.h
│ └── WechatAuthSDK.h
├── Info.plist
├── LJWXSDK
├── Modules
│ └── module.modulemap
└── _CodeSignature
└── CodeResources
复制代码
不带 main的二进制文件了
,.o的打包体@class,@protocol
不说了就是声明一个类,并不导入。 #import <>, #import""
是增强版的#include<>,#include""
防止重复导入的。 #import<>
: 经过 build setting里面中的 header Search Path里面去找
#import"" :
第一步先搜索user Header search Path 再搜索 header search Path 。因此对咱们的framework来讲,CocoaPod
帮咱们加到了 Header search Path 目前2种导入方式都是能够支持的。 上面的导入方式都带了 某个framework的路径 <XX/xx.h> "xx/xx.h" ,咱们在开发本身主工程的时候会发现咱们导入主工程其余类是不须要导入前缀的。 这又是怎么回事。 看下面的配置
你们都知道iOS7以后多了@import,这又是什么鬼。 简单理解这个方式叫作Module导入,好处就是使用了@import以后不须要在project setting手动添加 framework,系统会自动加载,并且效率更高。 最主要的是swift也只能这样用。 导入的时候系统会查找若是有模块同名的文件就会导入这个文件。若是没有CocoaPods帮咱们生成一个module-umbrela.hl
文件,而后就是导入的这个文件。
回过头来看咱们的framework的结构 里面有个Modules
文件夹,里面有个文件module.modulemap
framework module LJWXSDK {
umbrella header "LJWXSDK.h"
export *
module * { export * }
}
复制代码
咱们能够看到其实被暴露的header就是这个文件,以前我在按照#import "/"
的时候有个警告
@import
,而且
#import"/"
也不会报warning 更多关于
umbrella Header
参看文后参考
首先咱们来看常见的资源文件:主要分为图片和其余类资源那么加载图片和加载其余资源都是怎么作的? 1: [UIimage imageNamed:]
2: [NSbundle bundleForclass[XXX class]]
其实方式1去本质就是去mainBundle
去拿资源,方式2从XXX
所在的框架里面去拿。 前面也说道framework只是资源的打包方式,本质上是有两种的。 咱们这个framework若是本质是静态库,那么无需改变使用方式,资源最终都会打包到Main Bundle
里面 若是咱们这个framework本质是动态库,那么咱们的资源就发生了变化,资源就会被存放在 framework里面。因此咱们须要使[NSbundle bundleForclass[XXX class]]
。须要注意的是不少人为了简单,下意识的使用self class
传递,可是有可能这个self实例
不在资源所属的framework。因此会出现资源加载 失败。必定要谨慎使用。
程序员的自我修养,连接,装载 和库 iOS里的动态库和静态库 Systems Programming: What is the exact difference between Dynamic loading and dynamic linking? CocoaPods 都作了什么? Dynamic Linking of Imported Functions in Mach-O OS里的导入头文件 iOS - Umbrella Header在framework中的应用 SwiftInFlux#static-libraries iOS里的导入头文件 iOS - Umbrella Header在framework中的应用 @import vs #import - iOS 7