iOS SDK開發系列一之Cocoapods原理,Xcode的配置,動態庫和靜態庫介紹

前言

三個月沒寫東西了,是時候總結點東西了。SDK這個東西開發其實和項目開發類似,但是項目中你不需要關注一些配置和打包的參數,或者你壓根不需要關注Framework和.a到底是怎麼鏈接配置的,因爲Cocoapods或者你拖進Xcode已經幫你自動生成配置了,趁着週末這良辰美景,翻了幾篇文章,做個總結和記錄,文章不知道會分幾篇,但是一片理論介紹,一篇Demo製作應該會有的,由於網上的Demo都是很簡單的介紹給你怎麼製作Framework或者靜態庫,沒有第三方依賴,也沒有靜態庫依賴,第二篇會做一個Demo比如又有Masonry依賴,又有微信支付.a的依賴,到時候會介紹下cocoapods打包和Xcode自己打包的區別

Xocde工程配置

workSpace,project,target,scheme

workSpace

workspace是Xcode的一種文件,用來管理工程和裏面的文件,一個workspace可以包含若干個project,甚至可以添加任何你想添加的文件。workspace提供了project和project裏面的target之間隱式和顯式依賴關係,用來管理和組織工程裏面的所有文件。workspace 是以 xcworkspace 的文件形式存在的(這點和 project 一致)。workspace 的存在是爲了解決不同 project 之間引用和調用困難的問題。同一個 workspace 下的所有 project 共用同一個編譯路徑。workspace中的工程默認都是在同一個編譯目錄下,也就是workspace的編譯目錄。由於每個工程中的文件都在workspace的編譯目錄下,所以每個工程之間的文件都是相互可以引用的。因此,如果workspace中的多個工程使用了同一個庫的時候,我們就不需要給每個工程都拷貝一個。

project

Xcode中的 project裏面包含了所有的源文件,資源文件和構建一個或者多個product的信息。project利用他們去編譯我們所需的product,也幫我們組織它們之間的關係。一個project可以包含一個或者多個target。project定義了一些基本的編譯設置,每個target都繼承了project的默認設置,就是這個參數($(inherited)),每個target可以通過重新設置target的編譯選項來定義自己的特殊編譯選項。
一個project可以單獨存在,也可以被workspace包含,這種結構就是cocoapods默認我們所看到的,也是現在開發最常用的結構,當你開啓pod的時候,會生成一個xcworkspace。

target

target定義了構造一個product所需的文件和編譯指令。一個target對應於一個product。target說白了就是告訴編譯系統要編譯的文件和編譯這些源文件的設置說明。編譯指令就是根據build settings and build phases來確定的。

Xcode右上角的側邊欄打開–> Target Membership可以查看 文件歸屬於哪個Target

target之間可以進行依賴。如果一個target的編譯需要另外一個target作爲他的輸入,那麼我們就可以說前者依賴於後者。如果這兩個target在同一個workspace裏面,Xcode可以發現他們的依賴關係,這種依賴稱之爲隱式依賴。當然你可以通過設置,明確他們的依賴關係。

  1. 你可以根據現有的產品,建立不同的Target,右鍵出來一個Duplicate,然後你修改不同的參數,可以配製出日常打包,線上,預發佈等包的配置,通過General,BuildSetting和BuildPhrase來進行定製化修改
  2. 開發SDK的時候先默認一個Target開發,然後把需要打包的文件新建一個Target進行打包,如果打包Target是後來建立的,可以先remove reference,然後再拖進來,選擇對應Target即可

scheme

scheme定義了編譯集合中的若干target,編譯時的一些設置以及要執行的測試集合。我們可以定義多個scheme,但是每次只能使用其中一個。我們可以設置scheme保存在project中還是workspace中。如果保存在project中,那麼任意包含了這個工程的workspace都可以使用。如果保存在workspace中,那麼只有這個workspace可以使用。

WorkSpace --- > Project ---> Target

靜態庫和動態庫

庫無非就是一種代碼共享的方式,根據代碼的開源情況,一種開源庫,就是我們平時Github上用的那些,另一種就是閉源庫,只暴露頭文件,看不到具體的實現,是一個已經編譯好的二進制文件,比如百度地圖SDK,微信SDK等。這個又分兩種,一種靜態庫,另一種動態庫。咱們後面的都以我們自己做的靜態庫爲主。
靜態庫存在形式
.framework 和 .a
即靜態鏈接庫,是一系列從源碼編譯得到的目標文件的集合,是你的源碼的實現所對應的二進制。鏈接時,靜態庫會被完整地複製到目標程序中,被多次使用就有多份冗餘拷貝。
在 iOS 8 之前,iOS 只支持以靜態庫的方式來使用第三方的代碼。
靜態庫的優點是,編譯完成之後,原始靜態庫實際上就沒有作用了,應用程序沒有外部依賴(因爲依賴的靜態庫已經被完整的拷貝進來),直接就可以運行。當然其缺點也很明顯,就是會使用應用程序的體積增大。

系統 靜態庫文件
Windows .lib
Linux .a
MacOS/iOS .a

動態庫存在形式
.dylib 和 .framework
一個沒有main函數的可執行文件。動態庫在鏈接時並不會被拷貝到目標程序中,目標程序中只會存儲指向動態庫的引用。等到程序運行時,動態庫纔會被真正加載進內存,這也是叫做動態庫的原因。
動態庫的優點是,不影響目標程序的體積,可以隨時對庫進行升級替換而不需要重新編譯。

系統 動態庫文件
Windows .dll
Linux .so
MacOS/iOS .dylib

在這裏插入圖片描述
靜態Framework,實際上就是.a文件 + .h頭文件 + 資源文件的集合,實際上和.a文件的本質是一樣的

MacOS/iOS裏面的動態Framework

除了上面提到的 .a 和 .dylib/.tbd 之外,Mac OS/iOS 平臺還可以使用 Framework。Framework 是一種特殊的文件夾,將庫的二進制文件,頭文件和有關的資源文件打包到一起,方便管理和分發。

系統的 framework 是存在於系統內部,而不會打包進 app 中。app 的啓動的時候會檢查所需要的動態框架是否已經加載。像 UIKit 之類的常用系統框架一般已經在內存中,就不需要再次加載,這可以保證 app 啓動速度。相比靜態庫,framework 是自包含的,你不需要關心頭文件位置等,使用起來很方便。

在 iOS 8 之前,iOS 平臺不支持使用動態 Framework,開發者可以使用的 Framework 只有蘋果自家的 UIKit.Framework,Foundation.Framework 等。這種限制可能是出於安全的考慮。換一個角度講,因爲 iOS 應用都是運行在沙盒當中,不同的程序之間不能共享代碼,同時動態下載代碼又是被蘋果明令禁止的,沒辦法發揮出動態庫的優勢,實際上動態庫也就沒有存在的必要了。

iOS 8/Xcode 6 推出之後,iOS 平臺添加了動態庫的支持,同時 Xcode 6 也原生自帶了 Framework 支持(動態和靜態都可以)。爲什麼 iOS 8 要添加動態庫的支持?唯一的理由大概就是 App Extension 的出現,可以爲一個應用創建插件。Extension 和 App 是兩個分開的可執行文件,同時需要共享代碼,這種情況下動態庫的支持就是必不可少的了。但是這種動態 Framework 和系統的 UIKit.Framework 還是有很大區別。系統的 Framework 不需要拷貝到目標程序中,我們自己做出來的 Framework 哪怕是動態的,最後也還是要拷貝到 App 沙盒中(App 和 App Extension 的 Bundle 是共享的),因此蘋果又把這種 Framework 稱爲 Embedded Framework。

由於 iOS 的沙盒機制,自己創建的 Framework 和系統 Framework 不同,App 中使用的 Framework 運行在沙盒裏,而不是系統中。每個 App 都只能用自己對應簽名的動態庫,做不到多個 App 使用一個動態庫。也就是說,如果不同的 App 使用了同一個動態庫 Framework,那該 Framework 會被分別簽名、打包和加載。所以,iOS 上我們自己創建的動態庫只能是私有的,無法將動態庫放置在除了自身沙盒以外的地方。

Xcode SearchPath選項配置

在這裏插入圖片描述

Header Search Paths 和 User Header Search Paths的區別

Header Search Paths來管理導入頭文件的路徑,這兩者的作用是一樣的,唯一的區別是在
importinclude的時候Header Search Paths會多一種方式

#import "class.h"
#import <class.h>

若在Header Search Paths中設置class的路徑後,兩種方式都可以。但是如果在User Header Search Pacth中設置後,<>的就會報錯。

<> 是從系統空間目錄,對應Header Search Paths 中搜索文件," " 從用戶空間目錄,對應User Header Search Paths中搜索。如果你把路徑加到 User Header Search Paths 中,<> 無法從系統目錄空間中找到新的路徑,從而報錯。
一般來講系統User Header Search Paths是不會有填寫的,但默認的User Header Maps 會開啓,這個Maps的開關的意思是開啓這個開關後,在本地會根據當前目錄生成一份文件名和相對路徑的映射,依靠這個映射,我們可以直接import工程裏的文件,不需要依靠header search path。也就是工程目錄下添加的文件默認都會進入映射,不需要指定Header Search Paths。直接默認import " "即可。

import "xxx.h"的路徑搜索順序:

1. USE_HEADERMAP(如果啓用,則會在映射表中查,直接跳過的header search path的配置,如果未查到,則繼續往下搜索。)
2. USER_HEADER_SEARCH_PATHS
3. HEADER_SEARCH_PATHS

import <xxx.h> 的路徑搜索順序:

1. 系統路徑
2. USER_HEADER_SEARCH_PATHS (ALWAYS_SEARCH_USER_PATHSYES,則會搜索該路徑,該變量默認是NO,並且已經被標記爲Deprecated)
3. HEADER_SEARCH_PATHS

系統路徑到底是什麼路徑?
可以在Build Phases -> Link Binary With Libraries添加一些靜態庫和動態庫依賴,然後Show In Finder查看具體路徑

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk 
這裏面有兩個System和usr兩個查找目錄

總結:所以說一般情況下你至少默認配置下,設置好Header Search Paths可以通過" "<>引用。雙引號是用於本地工程的頭文件,需要指定相對路徑,默認在Maps自動開啓映射,尖括號是全局的引用(編譯器類庫搜索路徑),想搜哪裏直接填路徑就好了或者幹錯直接寫死(不建議),其路徑由編譯器提供,如引用系統的庫。但在實際工程裏,不僅不用指定相對路徑,而且用<>也是能引用到的.

Library 和 Framework Search Paths

這兩個是搜索Library和Framework配置的路徑

$(SRCROOT) 和 $(PROJECT_DIR)

$(SRCROOT) 和 $(PROJECT_DIR) 都指xxx.xcodeproj所在的父目錄

$(inherited)

項目的Framework Search Paths添加$(inherited)參數會從WorkSpace->PROJECT -> Build Settings -> Framework Search Paths裏面的路徑會被其繼承,沒有的話不會繼承。所以一個項目裏面有多個target,使用到了同一個庫(Library或Framework)那麼爲了方便我們可以在target添加繼承參數,並且PROJECT統一中添加庫的路徑。

Other Link Flags

Other Linker Flags中加入-ObjC或者-all_load或者-force_load
這個最常見的就是-Objc,一般導入第三方的時候或者你做好了一個SDK,自己測試的時候,導入Demo工程,發現直接崩潰

selector not recognized

這裏你可以通過ar -x libxxx.a文件查看SORTED文件,默認是沒有Category被鏈接的,所有你跑起來就會找不到,從而崩潰,具體後面在分析。
-ObjC
一般這個參數足夠解決前面提到的問題,這個flag告訴鏈接器把庫中定義的Objective-C類和Category都加載進來。這樣編譯之後的app會變大,因爲加載了很多不必要的文件而導致可執行文件變大。但是如果靜態庫中有類和category的話只有加入這個flag纔行,但是Objc也不是萬能的,當靜態庫中只有分類而沒有類的時候,Objc就失效了,這就需要使用-all_load或者-force_load了。
-all_load
-all_load會強制鏈接器把目標文件都加載進來,即使沒有objc代碼。但是這個參數也有一個弊端,那就是你使用了不止一個靜態庫文件,那麼你很有可能會遇到ld: duplicate symbol錯誤,因爲不同的庫文件裏面可能會有相同的目標文件 這裏會有兩種方法解決 1:用命令行就行拆包. 2:就是用下面的這個參數
-force_load
這個flag所做的事情跟-all_load其實是一樣的,只是-force_load需要指定要進行全部加載的庫文件的路徑,這樣的話,你就只是完全加載了一個庫文件,不影響其餘庫文件的按需加載 .
注意:有時候做了SDK,弄了個Demo測試SDK,用Pod引入第三方依賴,但是怎麼就報錯第三方找不到呢,注意看Other Link Flags這個參數,是否有-l的引用,或者$(inherited)時候有配置,不然是不會被link到的
在這裏插入圖片描述

Cocoapods原理

  1. 避免直接導入文件的原始方式,方便後續代碼升級 (版本升級)
  2. 簡化、自動化集成流程,避免不必要的配置 (ObjC問題和Search Path配置)
  3. 自動處理庫的依賴關係 (Dependency依賴)
  4. 簡化開發者發佈代碼流程 (Pod Package,發佈到倉庫Spec管理)

最簡單的靜態庫製作驗證Other Link Flags問題

.a.Framework的區別上面介紹了,我們直接做.a

打開Xcode
File
New
Project
Cocoa Touch Static Library

然後你隨便加個Categroy驗證下-objc這個加載,現在暫時不管什麼編譯架構的問題,直接按住,Command + B,跑起來,MKJFirstStaticLibrary 項目下現在有兩個類,一個是MKJFirstStaticLibrary,一個MKJFirstStaticLibrary+Extension,你就能得到一個.a文件。

What’s Fuck .a 文件????
這貨就是一個壓縮文件而已,壓縮了所.o目標文件的集合。

ar -x libMKJFirstStaticLibrary.a

在這裏插入圖片描述
這裏默認編譯的時候已經在 Build Phases Copy Files添加了我們需要暴露的頭文件。
再新建一個Project殼(mainProject工程),把.a和.h文件拖進去,運行我們剛纔寫的代碼,這個時候你就能看到經典的問題。

-[MKJFirstStaticLibrary sayEx]: unrecognized selector sent to instance 0x6000036dbbe0
2019-03-24 14:36:58.252513+0800 OtherLinkFlags[35181:326759] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MKJFirstStaticLibrary sayEx]: unrecognized selector sent to instance 0x6000036dbbe0'

首先,我們知道.a其實是編譯好的目標文件的集合,因此問題出現在鏈接這一步,而非編譯。OC在使用靜態庫的時候,需要知道哪些文件需要鏈接進來,它依據的就是上面ar -x解壓出來的__.SYMDEF SORTED文件。
這個文件不會包含所有的 .o 目標文件,而只是包含了定義了類的目標文件。我們可以執行cat __.SYMDEF\ SORTED 來驗證一下,你會看到其中並沒有拓展類的信息。這樣一來,MKJFirstStaticLibrary+Extension雖然存在,但是不被鏈接到最終的可執行文件中,從而導致了找不到方法的錯誤。
解決上述問題的方法是調用者在 Build Settings 中找到 other linker flag,並寫上-ObjC 選項,這個選項會鏈接所有的目標文件。然而根據文檔描述,如果靜態庫只有分類,而沒有類, 即使加了-ObjC 選項也會報錯,應該使用 -force_load 參數。這幾個參數上面有介紹。
因此這些瑣碎的東西Cocoapods給我們默認加上了,但是如果你自己做SDK自己搞得話總歸會遇到那些本該就遇到的坑,雖然現在別人幫你做了,但是出來混,總是要還的。。。所以,瞭解下原理很有必要。

總結:

上面我們的做法是把編譯好的靜態庫拖進去,這樣明顯就有了顯示依賴,那Cocopods爲何就不需要?打開你的Cocoapods項目,你會注意到下面幾個問題:

  1. 主工程中沒有導入第三方庫的代碼或靜態庫
  2. 主工程不顯式的依賴各個第三方庫,但是引用了 libPods-項目名.a 這個 Cocoapods 庫,如果是Framework就是Pods_項目名.framework,你可以看下Link Binary With Librarys
  3. 不需要手動編譯第三方庫,直接運行主工程即可,隱式指定了編譯順序

以上的做法就是cocoapods的做法,但是你必須開啓xcworkSpace來打開工程。

做個簡單的Demo模擬下Cocoapods引用結構

1.新建一個主工程mainProject
2.然後在主工程下新建一個Pods文件,在裏面新建Project,用靜態庫做Target
結構如下,mianProject是編寫的主工程,Pods裏面是靜態庫的Target
在這裏插入圖片描述
其中Pods,FirstLibrary和SecondLibrary是三個不同的Target,其中Pods什麼都不做,只是依賴其他兩個,這個就是Cocoapods項目下的做法,先看下Cocoapods的圖解
在這裏插入圖片描述
這裏是靜態庫,那麼Cocoapods會給我們創建一個總的靜態庫Pods-項目名規則命名,該庫什麼都不做,只是依賴其他第三方庫,然後我們在看看主工程要做什麼。
在這裏插入圖片描述
首先主工程下面我們需要在Link Binary With Librarys裏面添加那個主的靜態庫,Pods-xxx.a,然後再Other Link Flag裏面告訴Xcode我們需要在Xcode裏面引用哪些靜態庫和Framework。
這樣,我們跑主工程的時候,編譯的時候會先去依賴Pods-xxx.a,然後這個總的Pods會把依賴的其他第三方靜態庫編譯好,這樣在最後鏈接的時候都能找到,並且成功鏈接成可執行文件。

3.模擬Cocoapods
Pods下面新建一個工程,放入三個靜態庫,Pod作爲總的依賴其他兩個
在這裏插入圖片描述
這個就是Pods子工程了,理論上Pods靜態庫是給外部引用的,那麼內部它依賴的其他第三方庫,因此按照Cocoapods,需要給這個Pods設置Header Search PathLibrary Search Path

// 設置成recursive 默認不會遞歸查找 Header Search Path
"${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/include"

// Library Search Path 設置
"${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/FirstLibrary"
"${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SecondLibrary"

這幾個參數是參考Cocoapods的,環境變量的值可以參考如下,一個簡單的靜態庫編譯後.a文件的目錄就是按這個查找的
Xcode環境變量詳細羅列
第三步總結

  1. 新建一個文件夾創建靜態庫Projects
  2. 創建多個靜態庫Target,統一創建一個Pods來管理,什麼都不做,只設置依賴
  3. Project–>Pods Target下設置Header Search Path 和 Library Search Path
  4. 第三步在後續測試中竟然可以不設置,親測Cocoapods項目下刪掉這兩個也可以,但是前提是你在主工程要配置好,所以這裏的兩個Search有點看不懂了,具體的理論介紹可以看上面,但是這麼可能是在同一個workSpace下默認已經能找到了。

4.主工程配置

Target
Build Phases
Link Binary With Librares
Pods.a
Target
Build Setting
Other Link Flags
-Objc
-lFirstLibrary
-lSecondLibrary

第一條分支告訴Xcode Link的時候需要依賴Pods,這個Pods自己的依賴會優先被編譯出來。
第二條分支告訴Xcode實際需要使用的靜態庫,-l "xxxxxx"
然後在主工程import <頭文件>測試子工程下面的兩個靜態庫
在這裏插入圖片描述
以上做完之後,如果你沒有合理的設置Other Link Flags就會報錯,如果少了-ObjC就會報上面介紹的找不到方法的那個錯誤,如果鏈接錯誤或者路徑錯了,就會報如下
在這裏插入圖片描述
一般只要設置好Link的參數,要麼是Search Path,要麼是Other Link Flags,否則就會有Linker方面的錯誤。

下一篇傳送門

以上介紹了基本的一些參數配置,下一篇詳細介紹下Cocoapods的配置以及Xcode環境變量的介紹
Demo地址

參考如下:
Objc中Import的介紹
Cocoapods
Xcode Help
<> 和 ""的搜索路徑

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章