如何打造一個讓人愉快的框架
文/OneV's Den
這是我在今年 1 月 10 日 @Swift 開發者大會 上演講的文字稿。相關的視頻還在制作中,沒有到現場的朋友可以通過這個文字稿了解到這個 session 的內容。
雖然我的工作是程序員,但是最近半年其實我的主要干的事兒是養了一個小孩。 所以這半年來可以說沒有積累到什么技術,反而是積累了不少養小孩的心得。 當知道了有這么次會議可以分享這半年來的心得的時候,我毫不猶豫地選定了主題。那就是
如何打造一個讓人愉快的小孩
但考慮到這是一次開發者會議...當我把這個想法和題目提交給大會的時候,被殘酷地拒絕了。考慮到我們是一次開發者大會,所以我需要找一些更合適的主題。其實如果你對自己的代碼有感情的話,我們開發和維護的項目或者框架就如同自己的孩子一般這也是我所能找到的兩者的共同點。所以,我將原來擬定的主題換了兩個字:
如何打造一個讓人愉快的框架
在正式開始前,我想先給大家分享一個故事。我們那兒的 iOS 開發小組里有一個叫做武田君的人,他的代碼寫得不錯,做事也非常嚴謹,可以說是楷模般的員工。但是他有一個致命的弱點 -- 喜歡自己發明輪子。他出于本能地抗拒在代碼中使用第三方框架,所以接到開發任務以后他一般都要花比其他小伙伴更多的時間才能完成。
武田君其實在各個方面都有建樹...比如
- 網絡請求
- 模型解析
- 導航效果
- 視圖動畫 ...
不過雖然造了很多輪子,但是代碼的重用比較糟糕,耦合嚴重。在新項目中使用的話,只能復制粘貼,然后針對項目修修補補。因為承擔的任務總是沒有辦法完成,他一直是項目 deadline 的決定者,在日本這種社會,壓力可想而知。就在我這次回國之前,武田君來向我借了一本我本科時候最喜歡的書。就是這本:
我有時候就想,到底是什么讓一個開發者面臨如此大的精神壓力,我們有什么辦法來緩解這種壓力。在我們有限的開發生涯中,應該如何有效利用時間來做一些更有價值的事情。
顯然,我們不可能一天建成羅馬,也不可能一個人建成羅馬。我們需要一些方法把自己和別人寫的代碼組織起來,高效地利用,并以此為基礎構建軟件。這就涉及到使用和維護框架。如何利用框架迅速構建應用,以及在開發和發布一個框架的時候應該注意一些什么,這是我今天想講的主題。當然,為了讓大家安心和專注于今天的內容,而不是掛念武田君的命運,特此聲明:
以上故事純屬虛構,如有雷同實屬巧合
使用框架
在了解如何制作框架之前,先讓我們看看如何使用框架。可以說,如果你想成為一個框架的提供者,首先你必須是一個優秀的使用者。
在 iOS 開發的早期,使用框架其實并不是一件讓人愉悅的事情。可能有幾年經驗的開發者都有這樣的體會,那就是:
忘不了那些年,被手動引用和
.a
文件所支配的恐懼
其實恐懼源于未知,回想一下,當我們剛接觸軟件開發的時候,懵懵懂懂地引用了一個靜態庫,然后面對一排排編譯器報錯時候手足無措的絕望。但是當我們了解了靜態庫的話,我們就能克服這種恐懼了。
什么是靜態庫 (Static Library)
所謂靜態庫,或者說 .a 文件,就是一系列從源碼編譯的目標文件的集合。它是你的源碼的實現所對應的二進制。配合上公共的 .h 文件,我們可以獲取到 .a 中暴露的方法或者成員等。在最后編譯 app 的時候 .a 將被鏈接到最終的可執行文件中,之后每次都隨著 app 的可執行二進制文件一同加載,你不能控制加載的方式和時機,所以稱為靜態庫。
在 iOS 8 之前,iOS 只支持以靜態庫的方式來使用第三方的代碼。
什么是動態框架 (Dynamic Framework)
與靜態相對應的當然是動態。我們每天使用的 iOS 系統的框架是以 .framework 結尾的,它們就是動態框架。
Framework 其實是一個 bundle,或者說是一個特殊的文件夾。系統的 framework 是存在于系統內部,而不會打包進 app 中。app 的啟動的時候會檢查所需要的動態框架是否已經加載。像 UIKit 之類的常用系統框架一般已經在內存中,就不需要再次加載,這可以保證 app 啟動速度。相比靜態庫,framework 是自包含的,你不需要關心頭文件位置等,使用起來很方便。
Universal Framework
iOS 8 之前也有一些第三方庫提供 .framework 文件,但是它們實質上都是靜態庫,只不過通過一些方法進行了包裝,相比傳統的 .a 要好用一些。像是原來的 Dropbox 和 非死book 等都使用這種方法來提供 SDK。不過因為已經脫離時代,所以在此略過不說。有興趣和需要的朋友可以參看一下這里和這里。
Library v.s. Framework
對比靜態庫和動態框架,后者是有不少優勢的。
首先,靜態庫不能包含像 xib 文件,圖片這樣的資源文件,其他開發者必須將它們復制到 app 的 main bundle 中才能使用,維護和更新非常困難;而 framework 則可以將資源文件包含在自己的 bundle 中。 其次,靜態庫必須打包到二進制文件中,這在以前的 iOS 開發中不是很大的問題。但是隨著 iOS 擴展(比如通知中心擴展或者 Action 擴展)開發的出現,你現在可能需要將同一個 .a 包含在 app 本體以及擴展的二進制文件中,這是不必要的重復。
最后,靜態庫只能隨應用 binary 一起加載,而動態框架加載到內存后就不需要再次加載,二次啟動速度加快。另外,使用時也可以控制加載時機。
動態框架有非常多的優點,但是遺憾的是以前 Apple 不允許第三方框架使用動態方式,而只有系統框架可以通過動態方式加載。
很多時候我們都想問,Apple,憑什么?
好吧,這種事也不是一次兩次了...不過好消息是:。
Cocoa Touch Framework
Apple 從 iOS 8 開始允許開發者有條件地創建和使用動態框架,這種框架叫做 Cocoa Touch Framework。
雖然同樣是動態框架,但是和系統 framework 不同,app 中的使用的 Cocoa Touch Framework 在打包和提交 app 時會被放到 app bundle 中,運行在沙盒里,而不是系統中。也就是說,不同的 app 就算使用了同樣的 framework,但還是會有多份的框架被分別簽名,打包和加載。
Cocoa Touch Framework 的推出主要是為了解決兩個問題:首先是應對剛才提到的從 iOS 8 開始的擴展開發。其次是因為 Swift,在 Swift 開源之前,它是不支持編譯為靜態庫的。雖然在開源后有編譯為靜態庫的可能性,但是因為 Binary Interface 未確定,現在也還無法實用。這些問題會在 Swift 3 中將被解決,但這至少要等到今年下半年了。
現在,Swift runtime 不在系統中,而是打包在各個 app 里的。所以如果要使用 Swift 靜態框架,由于 ABI 不兼容,所以我們將不得不在靜態包中再包含一次 runtime,可能導致同一個 app 包中包括多個版本的運行時,暫時是不可取的。
包和依賴管理
在使用框架的時候,用一些包管理和依賴管理工具可以簡化使用流程。其中現在使用最廣泛的應該是 [CocoaPods](http://cocoapods.org](http://cocoapods.org)。
CocoaPods 是一個已經有五年歷史的 ruby 程序,可以幫助獲取和管理依賴框架。
CocoaPods 的主要原理是框架的提供者通過編寫合適的 PodSpec 文件來提供框架的基本信息,包括倉庫地址,需要編譯的文件,依賴等用戶使用 Podfile 文件指定想要使用的框架,CocoaPods 會創建一個新的工程來管理這些框架和它們的依賴,并把所有這些框架編譯到成一個靜態的 libPod.a。然后新建一個 workspace 包含你原來的項目和這個新的框架項目,最后在原來的項目中使用這個 libPods.a
這是一種“侵入式”的集成方式,它會修改你的項目配置和結構。
本來 CocoaPods 已經準備在前年發布 1.0 版本,但是 Swift 和動態框架的橫空出世打亂了這個計劃。因為必須提供對這兩者的支持。不過最近 1.0.0 的 beta 已經公布,相信這個歷時五年的項目將在最近很快迎來正式發布。
從 0.36.0 開始,可以通過在 Podfile 中添加 use_frameworks!
來編譯 CocoaTouch Framework,也就是動態框架。
因為現在 Swift 的代碼只能被編譯為動態框架,所以如果你使用的依賴中包含 Swift 代碼,又想使用 CocoaPods 來管理的話,必須選擇開啟這個選項。
use_frameworks!
會把項目的依賴全部改為 framework。也就是說這是一個 none or all 的更改。你無法指定某幾個框架編譯為動態,某幾個編譯為靜態。我們可以這么理解:假設 Pod A 是動態框架,Pod B 是靜態,Pod A 依賴 Pod B。要是 app 也依賴 Pod B:那么要么 Pod A 在 link 的時候找不到 Pod B 的符號,要么 A 和 app 都包含 B,都是無解的情況。
使用 CocoaPods 很簡單,用 Podfile 來描述你需要使用和依賴哪些框架,然后執行 pod install 就可以了。下面是一個典型的 Podfile 的結構。
# Podfile platform :ios, '8.0' use_frameworks! target 'MyApp' do pod 'AFNetworking', '~> 2.6' pod 'ORStackView', '~> 3.0' pod 'SwiftyJSON', '~> 2.3' end
-
$ pod install
Carthage 是另外的一個選擇,它是在 Cocoa Touch Framework 和 Swift 發布后出現的專門針對 Framework 進行的包管理工具。
Carthage 相比 CocoaPods,采用的是完全不同的一條道路。Carthage 只支持動態框架,它僅負責將項目 clone 到本地并將對應的 Cocoa Framework target 進行構建。之后你需要自行將構建好的 framework 添加到項目中。和 CocoaPods 需要提交和維護框架信息不同,Carthage 是去中心化的它直接從 git 倉庫獲取項目,而不需要依靠 podspec 類似的文件來管理。
使用上來說,Carthage 和 CocoaPods 類似之處在于也通過一個文件 Cartfile
來指定依賴關系。
# Cartfile github "ReactiveCocoa/ReactiveCocoa" github "onevcat/Kingfisher" ~> 1.8 github "https://enterprise.local/hello/repo.git"
-
$ carthage update
在使用 Framework 的時候,我們需要將用到的框架 Embedded Binary 的方式鏈接到希望的 App target 中。
隨著上個月 Swift 開源,有了新的可能的選項,那就是 Swift Package Manager。這可能是未來的包管理方式,但是現在暫時不支持 iOS 和 tvOS (也就是說 UIKit 并不支持)。
Package Manager 實際上做的事情和 Carthage 相似,不過是通過 llbuild
(low level build system)的跨平臺編譯工具將 Swift 編譯為 .a 靜態庫。
這個項目很新,從去年 11 月才開始。不過因為是 Apple 官方支持,所以今后很可能會集成到 Xcode 工具鏈中,成為項目的標配,非常值得期待。但是現在暫時還無法用于應用開發。
創建框架
作為框架的用戶你可能知道這些就能夠很好地使用各個框架了。但是如果你想要創建一個框架的話,還遠遠不夠。接下來我們說一說如何創建一個框架。
Xcode 為我們準備了 framework target 的模板,直接創建這個 target,就可以開始編寫框架了。
添加源文件,編寫代碼,編譯,完成,就是這么簡單。
app 開發所得到產品直接面向最終用戶;而框架開發得到的是一個中間產品,它面向的是其他開發者。對于一款 app,我們更注重使用各種手段來保證用戶體驗,最終目的是解決用戶使用的問題。而框架的側重點與 app 稍有不同,像是集成上的便利程度,使用上是否方便,升級的兼容等都需要考慮。雖然框架的開發和 app 的開發有不少不同,但是也有不少共通的規則和需要遵循的思維方式。
API 設計
最小化原則
基于框架開發的特點,相較于 app 開發,需要更著重地考慮 API 的設計。你標記為 public 的內容將是框架使用者能看到的內容。提供什么樣的 API 在很大程度上決定了其他的開發者會如何使用你的框架。
在 API 設計的時候,從原則上來說,我們一開始可以提供盡可能少的接口來完成必要的任務,這有助于在框架初期控制框架的復雜程度。 之后隨著逐步的開發和框架使用場景的擴展,我們可以添加公共接口或者將原來的 internal 或者 private 接口標記為 public 供外界使用。
// Do this public func mustMethod () { ... } func onlyUsedInFramework () { ... }private func onlyUsedInFile () { ... }
-
// Don't do this public func mustMethod () { ... }public func onlyUsedInFramework () { ... }public func onlyUsedInFile () { ... }
命名考慮
在決定了 public 接口以后,我們很快就會迎來編程界的最難的問題之一,命名。
在 Objective-C 時代 Cocoa 開發的類型或者方法名稱就以一個長字著稱,Swift 時代保留了這個光榮傳統。Swift 程序的命名應該盡量表意清晰,不用奇怪的縮寫。在 Cocoa 的世界里,精確比簡短更有吸引力。
幾個例子,相比于簡單的 remove
,removeAt
更能表達出從一個集合類型中移除元素的方式。而remove
可能導致誤解,是移除特定的 int 還是從某個 index 移除?
// Do this public mutating func removeAt (position: Index) -> Element
-
// Don't do this public mutating func remove (i: Int) -> Element // <- index or element?
同樣,recursivelyFetch
表達了遞歸地獲取,而 fetch
很可能被理解為僅獲取當前輸入。
// Do this public func recursivelyFetch (urls: [(String, Range<Version>)]) throws -> [T]
-
// Don't do this public func fetch (urls: [(String, Range<Version>)]) throws -> [T] // <- how?
另外需要注意方法名應該是動詞或者動詞短語開頭,而屬性名應該是名詞。當遇到沖突時,(比如這里的 displayName,既可以是名字也可以是動詞)應該特別注意屬性和方法的上下文造成的理解不同。更好的方式是避免名動皆可的詞語,比如把 displayName 換為 screenName,就不會產生歧義了。
public var displayName: Stringpublic var screenName: String // <- Better
-
// Don't do this public func displayName () -> String // <- noun or verb? Why returning `String`?
在命名 API 時一個有用的訣竅是為你的 API 寫文檔。如果你用一句話無法將一個方法的內容表述清楚的話,這往往就意味著 API 的名字有改進的余地。好的 API 設計可以讓有經驗的開發者猜得八九不離十,看文檔更多地只是為了確認細節。一個 API 如果能做到不需要看文檔就能被使用,那么它肯定是成功的。
關于 API 的命名,Apple 官方給出了一個很詳細的指南 (Swift API Design Guidelines),相信每個開發者的必讀內容。遵守這個準則,和其他開發者一道,用約定俗稱的方式來進行編程和交流,這對提高框架質量非常,非常,非常重要(重要的事情要說三遍,如果你在我的演講中只能記住一頁的話,我希望是這一頁。如果你還沒有看過這個指南,建議去看一看,只需要花十分鐘時間。)
優先測試,測試驅動開發
你應該是你自己寫的框架的第一個用戶,最簡單的使用你自己的框架的方式就是編寫測試。據我所知,在 app 開發中,很多時候單元測試被忽視了。但是在框架開發中,這是很重要的一個環節。可能沒有人會敢使用沒有測試的框架。除了保證功能正確以外,通過測試,你可以發現框架中設計不合理的地方,并在第一時間進行改善。
為框架編寫測試的方式和為 app 測試類似, Swift 2 開始可以使用 @testable 來把框架引入到測試 module。這樣的話可以調用 internal 方法。
不過對于框架來說,理論上只測試 public 就夠了。但是我個人推薦使用 testable,來對一些重要的 internal 的方法也進行測試。這可以提高開發和交付時的信心。
// In Test Target import XCTest @testable import YourFrameworkclass FrameworkTypeTests: XCTestCase { // ... }
開發時的選擇
命名沖突
在 Objective-C 中的 static library 里一個常見問題是同樣的符號在鏈接時會導致沖突。
Swift 中我們可以通過 module 來提供類似命名空間隔離,從而避免符號沖突。但是在對系統已有的類添加 extension 的時候還是需要特別注意命名的問題。
// F1.framework extension UIImage { public method () { print ("F1") } } // F2.framework extension UIImage { public method () { print ("F2") } }
比如在框架 F1 和 F2 中我們都對 UIImage 定義了 method 方法,分別就輸出自己來自哪個框架。
如果我們需要在同一個文件里的話引入的話:
// app import F1 import F2 UIImage () .method ()// Ambiguous use of 'method ()'
在 app 中的一個文件里同時 import F1 和 F2,就會產生編譯錯誤,因為 F1 和 F2 都為同一個類型 UIImage 定義了 method,編譯器無法確定使用哪個方法。
當然因為有 import 控制,在使用的時候注意一下源文件的劃分,避免同時 import F1 和 F2,似乎就能解決這個問題。
// app import F1 UIImage () .method ()// 輸出 F2 (結果不確定)
確實,只 import F1 的話,編譯錯誤沒有了,但是運行的時候有可能看到雖然 import 的是 F1,但是實際上調用到的是 F2 中的方法。
這是因為雖然有命名空間隔離,但 NSObject 的 extension 實際上還是依賴于 Objective-C runtime 的,這兩個框架都在 app 啟動時候被加載,運行時究竟調用了哪個方法是和加載順序相關的,并不確定。
這種問題可以實際遇到的話,會非常難調試。
所以我們開發框架時的選擇,對于已存在類型的 extension
,必須添加前綴, 這和以前我們寫 Objective-C 的 Category 的時候的原則是一樣的。
上面的例子里,在開發的時候,不應該寫這樣的代碼,而應該加上合適的前綴,以減少沖突的可能性。
// Do this// F1.framework extension UIImage { public f1_method () { print ("F1") } }// F2.framework extension UIImage { public f2_method () { print ("F2") } }
資源 bundle
剛才提到過,framework 的一大優勢是可以在自己的 bundle 中包含資源文件。在使用時,不需要關心框架的用戶的環境,直接訪問自己的類型的 bundle 就可以獲取框架內的資源。
let bundle = NSBundle (forClass: ClassInFramework.self) let path = bundle.pathForResource ("resource", ofType: "png")
發布框架
最后說說發布和維護一個框架。辛苦制作的框架的最終目的其實就是讓別人使用,一個沒有人用的框架可以說是沒有價值的。
如果你想讓更多的人知道你的框架,那拋開各種愛國感情和個人喜好,可以說 iOS 或者 Swift 開發者的發布目的地只有一個,那就是 GitHub。
當然在像是開源中國或者 CSDN 這樣的代碼托管服務上發布也是很好的選擇,但是不可否認的現狀是只有在 GitHub 上你才能很方便地和全世界其他地方的開發者分享和交流你的代碼。
選擇依賴工具
關于發布,另外一個重要的問題,一般你需要選擇支持一個或多個依賴管理工具。
CocoaPods
剛才也提到,CocoaPods 用 podspec 文件來描述項目信息,使用 CocoaPods 提供的命令行工具可以創建一個 podspec 模板,我們要做的就是按照項目的情況編輯這個文件。 比如這里列出了一個 podspec 的基本結構,可以看到包含了很多項目信息。關于更詳細的用法,可以參看 CocoaPods 的文檔。
pod spec create MyFramework
-
Pod::Spec.new do |s| s.name = "MyFramework" s.version = "1.0.2" s.summary = "My first framework" s.description = <<-DESC It's my first framework. DESC s.ios.deployment_target = "8.0" s.source = { :git => "https://github.com/onevcat/myframework.git", :tag => s.version } s.source_files = "Class/*.{h,swift}" s.public_header_files = ["MyFramework/MyFramework.h"] end
提交到 CocoaPods 也很簡單,使用它們的命令行工具來檢查 podspec 語法和項目是否正常編譯,最后推送 podspec 到 CocoaPods 的主倉庫就可以了。
# 打 tag git tag 1.0.2 && git push origin --tags # podspec 文法檢查 pod spec lint MyFramework.podspec # 提交到 CocoaPods 中心倉庫 pod trunk push MyFramework.podspec
Carthage
另一個應該考慮盡量支持的是 Carthage,因為它的用戶數量也不可小覷。 支持 Carthage 比 CocoaPods 要簡單很多,你需要做的只是保證你的框架 target 能正確編譯,然后在 Manage Scheme 里把這個 target 標記為 Shared 就行了。
Swift Package Manager
Swift Package Manager 暫時還不能用于 iOS 項目的依賴管理,但是對于那些并不依賴 iOS 平臺的框架來說,現在就可以開始支持 Swift Package Manager 了。
Swift Package Manager 按照文件夾組織來確定模塊,你需要把你的代碼放到項目根目錄下的 Sources 文件夾里。
然后在根目錄下創建 Package.swift 文件來定義 package 信息。這就是一個普通的 swift 源碼文件,你需要做的是在里面定義一個 package 成員,為它指定名字和依賴關系等等。Package Manager 命令將根據這個文件和文件夾的層次來構建你的框架。
// Package.swift import PackageDescription let package = Package ( name: "MyKit", dependencies: [ .Package (url: "https://github.com/onevcat/anotherPacakge.git", majorVersion: 1) ] )
版本管理
在發布時另外一個需要特別注意的是版本。在 Podfile 或者 Cartfile 中指定依賴版本的時候我們可以看到類似這樣的小飄箭頭的符號,這代表版本兼容。比如兼容 2.6.1 表示高于 2.6.1 的 2.6.x 版本都可以使用,而 2.7 或以上不行;同理,如果兼容 2.6 的話,2.6,2.7,2.8 等等這些版本都是兼容的,而 3.0 不行。當然也可以使用 >= 或者是 = 這些符號。
# Podfile pod 'AFNetworking', '~> 2.6.1' # 2.6.x 兼容 (2.6.1, 2.6.2, 2.6.9 等,不包含 2.7) # Podfile pod 'AFNetworking', '~> 2.6' # 2.x 兼容 (2.6.1, 2.7, 2.8 等,不包含 3.0) # Cartfile github "Mantle/Mantle" >= 1.1 # 大于等于 1.1 (1.1,1.1.4, 1.3, 2.1 等)
Semantic Versioning 和版本兼容
那什么叫版本兼容呢?我們看到的這套版本管理的方法叫做 Semantic Versioning。它一般通過三個數字來定義版本。
x(major).y (minor).z (patch)
- major - 公共 API 改動或者刪減
- minor - 新添加了公共 API
- patch - bug 修正等
0. x.y
只遵守最后一條
major 的更改表示用戶必須修改自己的代碼才能繼續使用框架;minor 表示框架添加了新的 API,但是現有用戶不需要修改代碼可以保持原有行為不變;而 patch 則代表 API 沒有改變,僅只是內部修正。
在這個約定下,同樣的 major 版本號就意味著用戶不需要修改現有代碼就能繼續使用這個框架,所以這是使用最廣的一個依賴方式,在這個兼容保證下,用戶可以自由升級 minor 版本號。
但是有一個例外,那就是還沒有正式到達 1.0.0 版本號的框架。 這種框架代表還在早期開發,沒有正式發布,API 還在調整中,開發者只需要遵守 patch 的規則,也就是說 0.1.1 和 0.1.2 只有小的修正。但是 0.2 和 0.1 是可以完全不兼容。如果你正在使用一個未正式發布的框架的時候,需要小心這一點。
框架的版本應該和 git 的 tag 對應,這可以和大多數版本管理工具兼容一般來說用戶會默認你的框架時遵循 Semantic Versioning 和兼容規則。
我們在設置版本的時候可能會注意到 Info.plist 中的 Version 和 Build 這兩個值。雖然 CocoaPods 或者 Carthage 這樣的包管理系統并不是使用 Info.plist 里的內容來確定依賴關系,但是我們最好還是保持這里的版本號和 git tag 的一致性。
當我們編譯框架項目的時候,會在頭文件或者 module map 里看到這樣的定義。 框架的用戶想要在運行時知道所使用的框架的版本號的話,使用這些屬性會十分方便。這在做框架版本遷移的時候可能會有用。所以作為開發者,也應該維護這兩個值來幫助我們確定框架版本。
// MyFramework.h//! Project version string for MyFramework. FOUNDATION_EXPORT const unsigned char MyFrameworkVersionString[]; // 1.8.3//! Project version number for MyFramework. FOUNDATION_EXPORT double MyFrameworkVersionNumber; // 347// Exported module map//! Project version number for MyFramework. public var MyFrameworkVersionNumber: Double// 并沒有導出 MyFrameworkVersionString
持續集成
在框架開發中,一個優秀的持續集成環境是至關重要的。CI 可以保證潛在的貢獻者在有保障的情況下對代碼進行修改,減小了框架的維護壓力。大部分 CI 環境對于開源項目都是免費的,得益于此,我們可以利用這個星球上最優秀的 CI 來確保我們的代碼正常工作。
就 iOS 或者 OSX 開發來說,Travis CI, CircleCI, Coveralls,Codecov 等都是很好的選擇。
開發總是有趣的,但是發布一般都很無聊。因為發布流程每次都一樣,非常機械。無非就是跑測試,打 tag,上傳代碼,寫 release log,更新 podspec 等等。雖然簡單,但是費時費力,容易出錯。對于這種情景,自動化流程顯然是最好的選擇。而相比于自己寫發布腳本,在 Cocoa 社區我們有更好的工具,那就是 fastlane。
fastlane 是一系列 Cocoa 開發的工具的集合,包括跑測試,打包 app,自動截圖,管理 iTunes Connect 等等。
不單單是 app 開發,在框架開發中,我們也可以利用到 fastlane 里很多很方便的命令。
使用 fastlane 做持續發布很簡單,建立自己的合適的 Fastfile 文件,然后把你想做什么寫進去就好了。比如這里是一個簡單的 Fastfile 的例子:
# Fastfile desc "Release new version" lane :release do |options| target_version = options[:version] raise "The version is missed." if target_version.nil? ensure_git_branch # 確認 master 分支 ensure_git_status_clean # 確認沒有未提交的文件 scan # 運行測試 sync_build_number_to_git # 將 build 號設為 git commit 數 increment_version_number (version_number: target_version) # 設置版本號 version_bump_podspec (path: "Kingfisher.podspec", version_number: target_version) # 更新 podspec git_commit_all (message: "Bump version to #{target_version}") # 提交版本號修改 add_git_tag tag: target_version # 設置 tag push_to_git_remote # 推送到 git 倉庫 pod_push # 提交到 CocoaPods end $ fastlane release version:1.8.4
AFNetworking 在 3.0 版本開始加入了 fastlane 做自動集成和發布,可以說把開源項目的 CI 做到了極致。在這里強烈推薦大家有空可以看一看這個項目,除了使用 fastlane 簡化流程以外,這個項目里還介紹了一些發布框架時的最佳實踐。
我們能不能創造出像 AFNetworking 這樣優秀的框架呢?一個優秀的框架包含哪些要求?
創建一個優秀的框架
一個優秀的框架必定包含這些特性:詳盡的文檔說明,可以指導后來開發者或者協作者迅速上手的注釋,
完善的測試保證功能正確以及不發生退化,簡短易讀可維護的代碼,可以讓使用者了解版本變化的更新日志,對于 issue 的解答等等。
我們知道在科技界或者說 IT 界會有很多喜歡跑分的朋友。其實跑分這個事情可以辯證來看,它有其有意義的一面。跑分高的不一定優秀,但是優秀的跑分一般一定都會高。
不止在硬件類的產品,其實在框架開發中我們其實也可以做類似的跑分來檢驗我們的框架質量如何。
那就是 CocoaPods Quality,它是一個給開源框架打分的索引類的項目,會按照項目的受歡迎程度和完整度,并基于我們上面說的這些標準來對項目質量進行評判。
對于框架使用者來說,這可以成為一個選擇框架時的重要參考,分數越高基本可以確定可能遇到的坑會越少。
而對于框架的開發者來說,努力提高這個分數的同時,代碼和框架質量肯定也得到了提高,這是一個自我完善的良好途徑。在遇到功能類似的框架,我們也可以說“不服?跑個分”
可能的問題
最后想和大家探討一下在框架開發中幾個比較常見和需要特別注意的問題。
首先是兼容性的保證這里的兼容性不是 API 的兼容性,而是邏輯上的兼容性。 最可能出現問題的地方就是在不同版本中對數據持久化部分的處理是否兼容, 包括數據庫和 Key-archiving。比如在新版本中添加了一個屬性,如何從老版本中進行遷移如果處理不當,很可能就造成嚴重錯誤甚至 crash。
另一個問題是重復的依賴。Swift 運行時還沒有包含在設備中,如果對于框架,將EMBEDDED_CONTENT_CONTAINS_SWIFT
設為 YES
的話,Swift 運行庫將會被復制到框架中,這不是我們想見到的。在框架開發中這個 flag 一定是 NO,我們應該在 app 的 target 中進行設置。另外,可能你的框架會依賴其他框架,不要在項目中通過 copy file 把依賴的框架 copy 到框架 target 中,而是應該通過 Podfile 和 Cartfile 來解決依賴問題。
在決定框架依賴的時候,可能遇到的最大的問題就是不同框架的依賴可能無法兼容。
比如說一個 app 同時依賴了框架 A 和框架 B,而這兩個框架又都依賴另一個框架 C。如果 A 中指定了兼容 1.1.2 而 B 中指定的是精確的 1.6.1 的話,app 的依賴關系就無法兼容了。
在框架開發中,如果我們依賴了別的框架,就必須考慮和其他框架及應用的兼容。 為了避免這種依賴無法滿足的情況,我們最好盡量選擇最寬松的依賴關系。
一般情況下我們沒有必要限定依賴的版本,如果被依賴的框架遵守我們上面提到的版本管理的規則的話,我們并沒有必要去選擇固定某一個版本,而應該盡可能放寬依賴限制以避免無法兼容。
如果在使用框架中遇到這樣的情況的話,去向依賴版本較舊的框架的維護者提 issue 或者發 pull request 會是好選擇。
有一些開發者表示在轉向使用 Framework 以后遇到首次應用加載速度變長的問題 (參考 1,參考 2。
社區討論和探索結果表明可能是 Dynamic linker 在驗證證書的時候的問題。 這個時間和 app 中 dynamic framework 的數量為 n2 時間復雜度。不過現在大家發現這可能是 Apple 在證書管理上的一個 bug,應該是只發生在開發階段。可能現在比較安全的做法是控制使用的框架數量在合理范圍之內,就我們公司的產品來說,并沒有在生產環境遇到這個問題。如果你在 app 開發中遇到類似情況,這算是一個小提醒。
最后,因為現在 Swift 現在 Binary Interface 還沒有穩定,不論是框架還是應用項目中所有的 Swift 代碼都必須用同樣版本的編譯器進行編譯。就是說,每當 Swift 版本升級,原來 build 過的 framework 需要重新構建否則無法通過編譯。對框架開發者來說,保持使用最新 release 版本的編譯器來發布框架就不會有大的問題。
在 Swift 3.0 以后語言的二進制接口將會穩定,屆時 Swift 也將被集成到 iOS 系統中。也就是說到今年下半年的話這個問題就將不再存在。
從今天開始開發框架
做一個小的總結。現在這個時機對于中國的 Cocoa 開發者來說是非常好的時代,GitHub 中國用戶很多,國內 iOS 開發圈子大家的分享精神和新東西的傳播速度也非常快。可以說,我們中國開發者正在離這個世界的中心舞臺越來越近,只要出現好東西的話,應該很快就能得到國內開發者的關注,繼而登上 GitHub Trending 頁面被世界所知。不要說五年,可能在兩年之前,這都是難以想象的。
Write the code, change the world.
Swift 是隨著這句口號誕生的,而現在開發者改變這個世界的力度可以說是前所未有的。
對于國內的開發者來說,我們真的應該希望少一些像 MingGeJS 這樣的東西,而多一些能幫助這個世界的項目,以認真的態度多寫一些有意義的代碼,回饋開源社區,這于人于己都是一件好事。
希望中國的開發者能夠在 Swift 這個新時代創造出更多世界級的框架,讓這些框架能幫助全球的開發者一起構建更優秀的軟件。
作者介紹
嗨,我是王巍 (@onevcat),一名來自中國的 iOS / Unity 開發者。現居日本,就職于 LINE。正在修行,探求創意之源。
Swifter.tips - 我維護的 Swift 使用技巧分享網站,每周三更新,歡迎訪問
來自: onevcat.com