CocoaPods對三方庫的管理探究
CocoaPods是iOS開發中經常被用到的第三方庫管理工具,我們有必要深入了解一下它對項目產生了什么影響,以及它是如何工作的。
使用pod安裝三方庫
我們新建一個不帶測試模塊的名為FFDemo的Swift項目,它的目錄結構是這樣的
├── FFDemo
│?? ├── AppDelegate.swift
│?? ├── Assets.xcassets
│?? ├── Base.lproj
│?? ├── Info.plist
│?? ├── SceneDelegate.swift
│?? └── ViewController.swift
└── FFDemo.xcodeproj
├── project.pbxproj
├── project.xcworkspace
└── xcuserdata
然后我們執行pod init
創建一個Podfile模板,在里面引入這兩個三方庫:
target 'FFDemo' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for FFDemo
pod 'MJRefresh', '~> 3.5.0'
pod 'Moya'
end
成功執行pod install
之后我們就將這兩個庫引入到了項目,這時項目目錄變成了這樣:
├── FFDemo
│?? ├── AppDelegate.swift
│?? ├── Assets.xcassets
│?? ├── Base.lproj
│?? ├── Info.plist
│?? ├── SceneDelegate.swift
│?? └── ViewController.swift
├── FFDemo.xcodeproj
│?? ├── project.pbxproj
│?? ├── project.xcworkspace
│?? └── xcuserdata
├── FFDemo.xcworkspace
│?? └── contents.xcworkspacedata
├── Podfile
├── Podfile.lock
└── Pods
├── Alamofire
├── Headers
├── Local\ Podspecs
├── MJRefresh
├── Manifest.lock
├── Moya
├── Pods.xcodeproj
└── Target\ Support\ Files
從目錄看,除了pod init引入了Podfile,其余三部分內容:FFDemo.xcworkspace、Podfile.lock、Pods目錄都是由pod install之后生成的。我們下面重點講下這三部分內容。
CocoaPods安裝的內容
xcworkspace文件
該文件下包含一個叫contents.xcworkspacedata
的文件,它的內容是這樣的:
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:FFDemo.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
使用xml
格式將依賴包含在
xcworkspace是一個項目容器,當有多個project需要相互依賴時可以用xcworkspace將它們組織起來。pod在首次安裝三方庫時會生成一個叫Pods.xcodeproj
的project管理三方庫,然后將該project和主項目的project通過workspace進行關聯。這樣我們就可以在主工程里引入三方庫了,而且三方庫由Pods.xcodeproj統一管理,不會對我們原項目產生任何干擾。
Podfile.lock
Podfile.lock文件的內容是這樣的:
PODS:
- Alamofire (5.3.0)
- MJRefresh (3.5.0)
- Moya (14.0.0):
- Moya/Core (= 14.0.0)
- Moya/Core (14.0.0):
- Alamofire (~> 5.0)
DEPENDENCIES:
- MJRefresh (~> 3.5.0)
- Moya
SPEC REPOS:
trunk:
- Alamofire
- MJRefresh
- Moya
SPEC CHECKSUMS:
Alamofire: 2c792affbdc2f18016e08fdbcacd60aebe1ba593
MJRefresh: 6afc955813966afb08305477dd7a0d9ad5e79a16
Moya: 5b45dacb75adb009f97fde91c204c1e565d31916
PODFILE CHECKSUM: 073f3d6d9f03e6a76838ca3719df48ae6cc01450
COCOAPODS: 1.9.3
因為Podfile文件里可以不指定版本號,而版本信息又很重要,于是就有了Podfile.lock,它里面記錄完整的版本信息和依賴關系。它的內容包含以下幾大塊
PODS
PODS
是指當前引用庫的具體版本號,可以發現我們并沒有引入Alamofire,但在PODS里確有它。這是因為Moya中依賴了它,Moya里定義了一個subspec叫Core,這是Moya/Core寫法的由來。pod是通過各個庫的podspec文件找到對應依賴的,這里可以簡單看下Moya的部分podspeec文件內容Moya.podspec:
Pod::Spec.new do |s|
s.default_subspecs = "Core"
s.subspec "Core" do |ss|
ss.source_files = "Sources/Moya/", "Sources/Moya/Plugins/"
ss.dependency "Alamofire", "~> 5.0"
ss.framework = "Foundation"
end
end
DEPENDENCIES
DEPENDENCIES為pod庫的描述信息,這里內容是同Podfile里的寫法。因為我們指定了MJRefresh的版本號,并沒有指定Moya的版本號,所以這里內容也是一樣的。
SPEC REPOS
這里描述的是倉庫信息,即安裝了哪些三方庫,他們來自于哪個倉庫。
trunk是共有倉庫的名稱,它的地址是https://github.com/CocoaPods/Specs.git
,外部使用的三方庫大都來自于這里。通常我們還會依賴一些公司內部的私有庫,私有庫的信息也會顯示在這里。
SPEC CHECKSUM
這里描述的是各個三方庫的校驗和,校驗和的算法是對當前安裝版本的三方庫的podspec文件求SHA1。比如MJRefresh的校驗和:6afc955813966afb08305477dd7a0d9ad5e79a16
。我們安裝的MJRefresh的版本為3.5.0,它在本地的podspec文件路徑為:~/.cocoapods/repos/trunk/Specs/0/f/b/MJRefresh/3.5.0/MJRefresh.podspec.json
。
這個路徑可以通過在安裝庫時增加--verbose
參數在輸出日志里查看。我們對該文件內容通過openssl求sha1摘要:
$ pod ipc spec ~/.cocoapods/repos/trunk/Specs/0/f/b/MJRefresh/3.5.0/MJRefresh.podspec.json | openssl sha1
$ 6afc955813966afb08305477dd7a0d9ad5e79a16
因為是對podspec.json內容求sha1,所以只要內容發生一點變化,得出的校驗和就將大不相同,而這也是校驗和設計的目的:podspec文件發生變化意味著版本信息發生了變化,就需要重新同步代碼。
大家可能注意到了,我們通常制作私有pod,控制配置信息的文件是podspec格式的,為什么本地文件變成了json格式?
這是因為json格式兼容性更高也更容易批量處理,官方Spec倉庫的所有庫配置文件都是被轉成json格式的。在我們制作私有庫的時候是可以直接以podspec的格式推到遠程倉庫的,但后續解析文件時pod內部檢索還是會把它轉成json格式。上面的命令是包含了podsepc轉json的命令的,轉json命令如下:
$ pod ipc spec ModuleName.podspec
PODFILE CHECKSUM
這個校驗和是針對Podfile內容的校驗和,如果Podfile內容改變了,該值也會跟著改變。計算方法為:
$ openssl sha1 filePath/Podfile
COCOAPODS: 1.9.3
這個代表當前使用的CocoaPod版本號,遠程版本管理應該要保證大家使用的pod版本號一致。
Pods
Manifest.lock
Manifest.lock是Podfile.lock的副本,它是在Pods目錄里面。它的作用是這樣的,我們通常是不把Pods文件放到版本管理里面,而把Podfile.lock放到版本管理里面。這時對于拉取代碼之后是否需要更新pod,就可以通過對比本地的Manifest.lock和遠程Podfile.lock是否相同即可。
Targets Support Files
Pods安裝的依賴是這樣的組織形式
一個Pods的Project下面有三個Targets,其中三個是安裝的依賴庫,最后一個Pods-FFDemo是關聯三個庫的Framework,也即是Pods這個Project的Targets。
Pods-Demo Framework
先看這個Demo的Framework,它會被用于工程項目的引用依賴
這個庫不會被打進包里,因為Do Not Embed
代表并不是包含的關系。
這個工程下的配置文件有這些:
許可協議文件
兩個以acknowledgements命名的文件是用于管理pod庫的許可協議,即三方庫必須帶有的LICENSE
文件,這也是為什么我們在制作pod時會要求我們指定軟件協議。
Framework文件 這里還包含了用于管理Module的modulemap和umbrella.h文件。modulemap是對Module的聲明文件,制作Framework我們總是需要該文件,它的內容如下:
framework module Pods_FFDemo {
umbrella header "Pods-FFDemo-umbrella.h"
export *
module * { export * }
}
其指向了一個umbrella的頭文件,這是制作Framework必須的頭文件,modulemap和umbrella.h會在創建Module時自動生成,不建議手動修改其關系。
dummy.m文件
這其實是一個空的.m文件
#import <Foundation/Foundation.h>
@interface PodsDummy_Pods_FFDemo : NSObject
@end
@implementation PodsDummy_Pods_FFDemo
@end
那為什么要有這個東西呢,包括所有的三方庫的包里也會包含一個dummy文件。我在stackoverflow找到了一個解釋:Xcode的編譯是依賴.m文件的,如果一個庫里沒有.m文件,將不會被編譯,為了防止這種情況就會在每個庫里增加一個空的.m文件。
xcconfig文件
xcconfig文件是Build Setting配置項的文件形式,它的優先級大于Xcode內的Build Setting??匆粋€pod生成的debug模式下的xcconfig文件。
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh" "${PODS_CONFIGURATION_BUILD_DIR}/Moya"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire/Alamofire.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/MJRefresh/MJRefresh.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Moya/Moya.framework/Headers"
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
OTHER_LDFLAGS = $(inherited) -framework "Alamofire" -framework "CFNetwork" -framework "Foundation" -framework "MJRefresh" -framework "Moya"
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
PODS_BUILD_DIR = ${BUILD_DIR}
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
PODS_ROOT = ${SRCROOT}/Pods
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
xcconfig還有個作用是設置參數,比如我們比較熟悉的PODS_ROOT=${SRCROOT}/PODS
,它代表項目根目錄下的PODS文件目錄。另外兩項用于幫助我們在項目中查找三方庫的FRAMEWORK_SEARCH_PATHS
和HEADER_SEARCH_PATHS
也是在改文件內部定義的,這些配置會體現到Build Settings里面:
三方庫的Framework
各個三方庫也都有一些配置文件,他們文件格式基本一致,上圖是Moya的配置文件。Moya的xcconfig文件里有一行這個:
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Alamofire"
用于告訴Moya在引用Alamofire時應該去哪里找這個依賴。
Build Phases
這里是設置編譯階段配置的地方,當首次pod install成功之后,這里會多幾個[CP]開頭的配置項(CP即CocoaPods縮寫),它們都是由CocoPods添加的腳本內容,執行順序從上到下。
New System Build
在講編譯腳本之前簡單說下New Build System。
New Build System是Xcode10之后蘋果推出的新的構建系統,新的構建系統對編譯流程的優化做了很多工作,雖然到Xcode12仍兼容舊版的Legacy Build System,但其已經被標記為移除,我們的項目和庫都應該使用新版的構建系統進行構建。和新的構建系統隨之而來的是在運行腳本時增加的輸入輸出列表。
這是為了控制是否每次編譯都需要執行對應腳本,input和output文件可以是單個文件形式,如果文件過多可以放到格式為xcfilelist
的文件列表里。
如果沒有提供input和output,則每次構建都會運行該腳本。如果提供了,則會在以前從未運行過、某個輸入文件被更改或某個輸出文件丟失的情況下再次運行。
注意這些是構建腳本的默認邏輯,Xcode還提供了Run Scripts的自定義行為,默認勾選項:Based on dependency analysis,即代表上述邏輯。如果提供了輸入輸出還需要每次運行,關閉該選項即可。
[CP] Check Pods Manifest.lock
該腳本位于較上方,如果沒有Dependencies,開始編譯就會執行該腳本,它的內容如下:
diff "${PODS_PODFILE_DIR_PATH}/Podfile.lock" "${PODS_ROOT}/Manifest.lock" > /dev/null
if [ $? != 0 ] ; then
# print error to STDERR
echo "error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation." >&2
exit 1
fi
# This output is used by Xcode 'outputs' to avoid re-running this script phase.
echo "SUCCESS" > "${SCRIPT_OUTPUT_FILE_0}"
作用是比較Podfile.lock
和Manifest.lock
文件是否相同,如果不同就輸出錯誤信息:error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.
,并執行退出,這會導致后續項目報錯,無法繼續編譯。
該錯誤較常見,出現于拉取遠端代碼,遠端pod依賴于本地不一致的情況。這時我們可以根據提示,執行pod install
命令,根據Podfile及遠端Podfile.lock生成新的Manifest.lock文件。
[CP] Copy Pods Resources
這個一般在以靜態庫引入的三方庫切里面包含資源的話會添加該腳本,其作用是將三方庫的資源文件拷貝至項目中。
它的完成是通過運行以下腳本進行的:
"${PODS_ROOT}/Target Support Files/Pods-FFDemo/Pods-FFDemo-resources.sh"
Pods-FFDemo-resources.sh文件在Pods目錄內,該腳本內有個關鍵函數install_resource
:
install_resource()
{
if [[ "$1" = /* ]] ; then
RESOURCE_PATH="$1"
else
RESOURCE_PATH="${PODS_ROOT}/$1"
fi
if [[ ! -e "$RESOURCE_PATH" ]] ; then
cat << EOM
error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script.
EOM
exit 1
fi
case $RESOURCE_PATH in
*.storyboard)
ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS}
;;
*.xib)
ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS}
;;
*.framework)
echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true
mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
;;
*.xcassets)
ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH"
XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE")
;;
*)
echo "$RESOURCE_PATH" || true
echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY"
;;
esac
}
刪除了一部分日志內容,其內部主要是一個switch語句,根據資源文件的類型進行不同的同步操作。這里重點說下幾種重要格式文件的處理方式。
storyboard和xib格式
這兩項資源文件是需要編譯處理的,利用ibtool命令分別轉成sotryboardc和nib格式。
xcassets格式
這里的圖片最終會被打包到Assets.car供程序使用,需要使用actool。
Bundle、plist、png等資源
其他類的資源是會走到switch語句最后出口,進行資源路徑賦值給$RESOURCES_TO_COPY
,在后面的代碼中通過rsync
命令,將資源同步到構建包的目錄。
該腳本會打印很多日志,在使用CocoaPods時如果遇到資源相關的問題都可以遵循錯誤日志來這里推測定位錯誤原因。
[CP] Embed Pods Frameworks
該處腳本是直接運行Pods-FFDemo-frameworks.sh
。
"${PODS_ROOT}/Target Support Files/Pods-FFDemo/Pods-FFDemo-frameworks.sh"
可能你還記得上面說的pod會把多個庫的依賴做成一個合并的庫,但該庫是以依賴的形式引入主工程,但是程序的運行時需要這些庫,我們打包時就需要將各個庫Embed到項目里,而做這個工作的就是該腳本。
# Copies and strips a vendored framework
install_framework()
{
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}"
# other code...
# Strip invalid architectures so "fat" simulator / device frameworks work on device
if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then
strip_invalid_archs "$binary"
fi
# Resign the code if required by the build settings to avoid unstable apps
code_sign_if_enabled "${destination}/$(basename "$1")"
}
腳本內容主要是調用install_framework
函數,將framework內容同步到構建包里。在該函數里還有幾個關鍵方法,strip_invalid_archs
用于去除無用架構,code_sign_if_enabled
用于framwork簽名。
溫馨提示
登錄后才可以操作哦,去登錄