引子 無意間,看到5年前,Android大佬子勰寫的,關於SDK開發方面的文章(SDK那些事(總綱)), 不由得喚起自己開發iOS SDK的回憶;本文簡單總結下自己開發SDK方面的經驗; SDK(Software Development Kit)可以最大程度實現代碼和功能的復用,為業務開發提供一個非 ...
引子
-
無意間,看到5年前,Android大佬子勰寫的,關於SDK開發方面的文章(SDK那些事(總綱)), 不由得喚起自己開發iOS SDK的回憶;本文簡單總結下自己開發SDK方面的經驗;
-
SDK(Software Development Kit)可以最大程度實現代碼和功能的復用,為業務開發提供一個非常好的支持;這裡的業務可以是內部業務,也可以是外部業務;
-
簡單來說,所謂SDK開發,本質是服務提供;不僅需要寫好代碼,還要完善代碼之外的事情,任重道遠
一、準備
1、清晰解決的問題和要求
一般而言,新起一個SDK必然有其深刻的業務背景;研發同學對SDK要解決的問題和SDK的特殊要求,瞭解地越詳細越好;常見的要求有:
- 禁止採集用戶信息【安全方面】
- 必須對SDK使用者鑒權【安全方面】
- 核心代碼必須混淆【安全方面】
- 不可以有調試日誌,不可以監控上報【安全方面】
- 持久化的敏感數據要加密;【安全方面】
- SDK大小不可以超過
XX
KB:【其他】 - .......
2、選擇合適的開發語言
- 大多數情況下,選擇Objective-C開發就對了,不僅能接入Swift開發的項目,還能接入Objective-C開發的項目;
- 當然並非絕對,具體根據業務情況決定;
3、選擇合適的技術方案
- 網路請求使用第三方框架,還是直接利用iOS的API;
- JSON轉Model、Model序列化和反序列選擇哪個第三方框架(性能,框架大小方面考慮)
- 持久化存儲選擇SQLite、NSUserDefaults 或是 Keychain;
- UI佈局使用 frame、autolayout(Masonry框架) or flexbox(YogaKit框架)
- MVVM or MVC 架構模式選擇;
- ...
4、確立基本代碼規範
SDK可能長期維護 或 多人開發,確立好基本代碼規範,能保障SDK的代碼質量;這些規範本質上是一些共識和約束,如:
- 命名規範:屬性、變數、方法等均採用小寫字母開頭的駝峰命名;類名使用大寫字母開頭的駝峰命名;
- 註釋要求:對外暴露的類、方便和變數要有註釋,解釋其功能;關鍵代碼要有註釋;
- 簡潔要求:無用的代碼選擇直接刪除,不要註釋;無用的資源及時清除;
- ....
二、SDK的主體設計
1、多模塊設計
-
SDK中可能包含不同的模塊功能,而不同的業務方需要的模塊可能不同;對SDK中模塊進行拆分,保證業務方儘可能引入的是他們需要的代碼;
-
一般使用Cocoapods創建Pod庫的,在
podspec
中定義好模塊,為每個模塊清晰定義好包含的代碼和資源,以及外部依賴(靜態庫 or 靜態庫);這樣可以將模塊之間實現代碼和資源的物理隔離; -
關於Cocoapods創建Pod庫更多細節可以參考Cocoapods使用小記,至於是公有Pod還是私有Pod根據實際情況定;
創建公有Pod庫或者私有Pod庫原理是一樣的;不一樣的是:兩者的版本索引查詢方式不一樣,公有庫的podspec由CocoaPods/Specs管理,而內部私有使用的pod庫需要公司內部建立一個倉庫來管理podspec
2、SDK目錄層級
-
Pod庫中,代碼放在
Classes
目錄下,圖片資源放在Assets
目錄下; -
Classes
目錄按模塊劃分第一級目錄,如AModule
、BModule
、CModule
等,其中每個模塊Code再劃分二級目錄,如ModuleService、View、Controller、Model、API等;具體的代碼文件存放在這些二級目錄中;其中ModuleService
中代碼是要對外暴露的,其他預期外部不可見; -
資源方面,也按模塊細節;主要的資源是圖片資源,在
Assets
目錄下新加AModule.xcassets
、BModule.xcassets
、CModule.xcassets
等;
3、公開介面設計
-
SDK使用者們關註介面是夠好用,設計好介面,讓你的SDK使用體驗加分;
-
介面功能儘量職責單一,介面需要的參數不要太多;如果參數多,可以使用Model將業務參數封裝下;提供Model的
default實現
;介面名,參數名使用駝峰命名,最好見名知義; -
介面中每個參數類型要明確,不要出現
id
、NSDictionary
這樣的類型;避免業務隨意傳參,增加SDK內部對參數校驗難度;也減少業務方對參數的困惑; -
介面內第一件事情是要做參數校驗,不符合預期情況,Debug模式直接Assert,及早把問題拋出;Release模式要記錄到日誌並上報,提前返回,避免後續出現迷惑問題,增加排查問題成本;
4、代理和狀態碼
- 除了暴露介面,SDK還可能暴露代理(Delegate)方法 和 狀態碼;
- 代理(Delegate)是需要業務方根據需要實現的,代理(Delegate)中可選實現方法加上
@optional
關鍵字,沒有的話預設要實現;協議和協議中具體方法的作用要增加上註釋; - 狀態碼設計上,需要註意兩點:
- SDK中定義的狀態碼使用枚舉(
NS_ENUM
)定義,對應的每個值增加註釋; - 如果SDK依賴了其他SDK,這些SDK的狀態碼最好不要透傳給業務方,中間增加轉化;比如微信SDK中
WXErrCode
不要透傳給業務方,封裝一下,對外暴露的是我們的狀態碼;
- SDK中定義的狀態碼使用枚舉(
5、SDK的版本號
- SDK都要有明確的版本號,一般版本號分三段:主版本、特性版本、修正版本,如5.6.1;其中主版本號用於大版本的發佈,特性版本主要用於更新迭代,修正版本號主要用於bug修複。
- SDK內部的埋點、監控,網路請求等都要攜帶SDK的版本號,這些版本號對定位SDK問題非常重要;
- SDK對外一般是二進位的形式提供,發佈的SDK需要帶上版本號信息;至少要保證三個地方的SDK版本信息是一致的;git倉庫的tag版本號、
podspec
中的version
、代碼中的版本號;可以實現個腳本,在編譯時候,統一修改這三處的版本號信息;
6、SDK安全需求
- 目前遇到的,有關SDK安全方面要求有:
- 核心代碼的混淆;包括字元串常量、類名和方法名方面的混淆;
- 敏感數據需要加密存儲,禁止明文傳輸等
- 對SDK使用者鑒權的要求,"非法"App不得使用,非法調用者必Crash;
- 此外,有不常見的”安全訴求“:App內不得有任何調試信息,埋點、採集用戶數據等行為;不得使用帶來安全隱患的API,比如打開WKWebview跨域開關這個就不被允許;
- 代碼混淆後,混淆前後的符號間映射一定要保留,否則線上問題堆棧信息中,出現的SDK混淆後的符號會讓定位問題非常迷惑;可以做成轉化工具,導入堆棧符號信息,輸出混淆前正常的符號信息;
三、對業務的支持
1、提供SDK文檔
- SDK開發好後,對外還要提供相關文檔,包括但不限於:SDK功能介紹、SDK接入辦法 和 SDK具體使用、SDK 更新日誌記錄 以及 SDK問題記錄;
- 基於專業角度,建議提供SDK概況文檔,包括但不限於:接入的業務情況,優化記錄,SDK的依賴庫信息,二進位 和 資源大小信息等;有些業務對SDK大小敏感,有些關註SDK的穩定性...;
- 業務方在接入SDK前,是從接觸SDK文檔開始,好的文檔能幫助SDK更好地被接受,後續更好開展工作;
2、提供Demo
- 只有文檔還不夠的,一定要提供Demo;Demo有兩個好處:
- 幫助業務方結合文檔更好瞭解SDK的接入和使用;
- 幫助測試及時驗證SDK升級後帶來的影響;
- Demo中不僅有SDK引入辦法、使用辦法;還可以寫一些簡單UI,幫助展示SDK功能;每次SDK升級,都通過Demo自動出包,提供給測試人員和產品去驗證(功能驗證和設備相容性驗證);
- SDK的自動化測試這塊,暫未嘗試;但在是Demo中,定義對SDK介面的單元測試是必要的;單元測試要關註:非法傳參case,非主線程調用case。
3、規範SDK開發流程
-
開發SDK和開發完整項目一樣,要有需求評審、技術評審、排期,開發,自測,提測、測試驗收等環節 ;不同功能在不同feature分支上開發,每個feature功能驗證通過後可以合入主幹分支;合入主幹後,由主幹分支發佈版本;
-
發佈的SDK使用二進位的形式;SDK使用二進位形式,不僅能提示項目項目編譯速度,也能保護好源碼;如果業務方需要SDK源碼,需要向SDK負責人申請許可權;
4、幫助業務排查問題
- SDK開發者經常會收到業務方幫助排查問題的訴求;畢竟,SDK對業務方是一個黑盒,很多業務上涉及到SDK問題,需要SDK開發者幫助排查,耗時耗力;
- 為了更好幫助業務排查問題,推出三件套:SDK核心鏈路監控埋點 + 重要日誌信息持久化並上報 + 調試日誌信息可視化;前兩者是固化在於SDK中的;而調試日誌信息是交給業務方,由他們靈活處理;
- 業務方拿到調試日誌信息,可以將其輸出到控制台,也可以輸出到App的日誌可視化工具中;鑒於業務方可能沒有,可以提供一個輕量級的日誌可視化工具。
- 必須註意的是,SDK中的調試日誌信息禁止帶到線上,SDK的Release版本不可以帶這些信息。
5、溝通Plus
- 重要內容要有記錄:SDK會被多個宿主App接入,不同的App環境不同,SDK可能遇到很多問題,積極幫助解決後,記錄下來,作為後續宿主App使用SDK的重要參考;
- 建立SDK和業務溝通機制;及時同步SDK最新信息;SDK的bugfix版本,要及時同步,並幫助業務升級,儘量減少損失;
- 為SDK增加代碼Reviewer:SDK重大升級,最好involve主要業務方的研發進行技術評審和
Code Review
;其實這對業務方是個很高的要求,需要業務方至少有一個人對SDK有比較全面的瞭解;
四、SDK實現中的註意事項
1、註意多線程使用
- 不要在主線程執行耗時操作,可以將他們交給子線程;
- 控制好併發數量,GCD併發隊列並不會去管理最大併發數,無限制提交任務給併發隊列,會給性能帶來問題。可以適當控制併發數量,防止線程爆炸;具體可見 iOS實錄16:GCD小結之控制最大併發數
- 遇到必須要在主線程執行的任務;先判斷當前是否在主線程,不在的話,可以通過GCD將任務放到主線程隊列執行;此外,還可以加斷言,Debug下,非主線程執行執行拋異常,Crash,幫助及時發現問題;
- 儘可能使用輕量級的鎖,可以使用信號量;自旋鎖性能非常好,但是有優先順序反轉的問題,謹慎使用;
2、使用緩存要剋制
- 無論是記憶體緩存和磁碟緩存都要有清除策略(可以LRU)和使用大小限制;
- 如果記憶體緩存沒有清除策略和使用大小限制,會導致記憶體使用無限制增長,最後可能會導致OOM問題;此外,當收到記憶體警告時候,記憶體緩存要及時清除,否則可能引起OOM問題,直接破壞用戶體驗;
- 如果磁碟緩存沒有清除策略和使用大小限制,會導致磁碟空間濫用,對App整體體驗都不好,而且後續清理成本比較高;
- 持久化在磁碟文件中數據,不要在App啟動時去讀取,可以懶載入;不要對數據量和文件讀取性能報僥幸,隨著SDK的迭代,那些數據可能不斷變大,也可能在低端機器上文件讀取性能比較差,偶現幾十ms甚至幾百ms的耗時,直接拖累啟動速度;
3、記憶體使用要註意
- 儘量優化記憶體使用,四個原則:減少大塊記憶體使用、降低記憶體峰值、避免記憶體泄露和處理記憶體警告;
- 具體到記憶體使用中技巧:
- 合理使用
autorealsepool
,降低記憶體峰值,避免 OOM - 復用大記憶體對象,如UITableViewCell對象;懶載入大的記憶體對象
- 用
NSCache
代替NSMutableDictionary
,使用NSPurgableData
代替NSData
; weak strong dance
來解決 Block 中的迴圈引用,代理(delegate)使用weak
修飾;- CoreFoundation對象、CoreGraphics對象、還有C/C++的記憶體分配需要管理好,有malloc()和calloc()就要有free;
- ....
- 合理使用
- 瞭解記憶體方面知識可以看:iOS記憶體二三事
4、使用單例要註意
- 有些SDK通過單例對象提供服務;因為,單例對象只有一個對象,不僅可以節約開銷,還可以保證App中多業務操作SDK中同一個服務對象;
- 但是定義單例要註意;平常App開發中,寫單例不怎麼嚴謹,提供個讓外部訪問的類方法,如
+ (instancetype)sharedInstance
,內部使用dispatch_once
保證alloc和init只執行一次,這種是粗髮式單例
,並不能保證絕對單例; - 在SDK中,一定要保證外部訪問到的是單例對象;除了提供讓外部訪問的類方法,還要重寫
+(instancetype)allocWithZone
、-(instancetype)copyWithZone
和-(instancetype)mutableCopyWithZone
方法,保證永遠都只會分配一次記憶體空間,實現真正的單例; - iOS中,一個對象有且只能有一個代理;如果你的單例對象有需要業務方實現的代理方法,根據實際情況判斷,是否需要實現多代理;
5、註意巨集定義和條件編譯
- 編譯器前端(如Clang)在編譯源碼時,首先要做預處理(preprocessor),如頭文件引入,巨集替換,註釋處理,條件編譯(#ifdef) 等;
- 一般地,SDK中會使用到條件編譯;在源碼情況下,可以根據不同的條件,編譯不同的源碼;但是SDK使用二進位的形式對外提供的話,在二進位化時就已經根據XX條件編譯好了;因此,二進位的SDK不會能跟隨宿主App編譯條件變化了;
- 使用巨集可以提高程式的通用性和易讀性,減少不一致性,減少輸入錯誤和便於修改。但是如果巨集對應實現,需要根據條件編譯來區分不同的行為;在二進位編譯時候,巨集定位的行為會被確定下來;
- 巨集定義有個小細節,用
do{...}while(0)
構造的巨集定義不會受到大括弧、分號等的影響,非常建議使用。
五、SDK實現中的小技巧
1、weak symbol
- 簡介:symbol預設都是strong的,但是可以增加
__attribute__ ((weak))
屬性將其變成weak symbol;weak symbol在鏈接時候比較特殊:- strong symbol必須有實現,否則會報錯;
- 不可以存在兩個同名的strong symbol
- strong symbol可以覆蓋weak symbol的實現
- SDK可以用weak symbol提供預設實現,然後業務中利用strong symbol把業務實現註入進來,以此來實現依賴註入;
weak symbol 不做標準方案推薦; 遇到要臨時適配某些業務的特殊case,時間緊急情況下,可以"劍走偏鋒";
2、預定義符號
-
簡介:有些公開功能使用巨集定義的函數形式,可以在函數中帶上
__FILE__
、__LINE__
和__FUNCTION__
這些C語言中預定義符;這樣可以在發生問題時,更好找到使用巨集函數的位置,demo如下:#define AddFunc(a,b) do { \ addFuncImp(a,b, __FILE__,__LINE__,__FUNCTION__); \ } while(0)
-
有人問過:為什麼不使用
[NSThread callStackSymbols]
獲取當前線程堆棧信息,豈不是更好;不使用有3方面考慮:- 只是為了獲取調用代碼位置;瞭解到SDK調用位置,排查問題能高效地多;
[NSThread callStackSymbols]
捕獲堆棧信息在符號裁剪情況下,主模塊中的是記憶體地址信息,而不是符號信息;__FILE__
、__LINE__
和__FUNCTION__
的成本更低,性能更好;
3、section()
函數
-
簡介:
section()
函數是Clang提供的,可以讀寫二進位段;實際應用中,在編譯階段將一些確定的常量寫入數據段(__DATA段
),併在運行期根據需要讀取出來;可以利用此能力實現延遲載入; -
在阿裡的iOS的BeeHive有類似的使用,如下:
#define BeeHiveMod(name) \ char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name""; #define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
used
關鍵詞是告訴編譯器,Release下不優化,必須保留這個符號;否則Release
下。鏈接器會優化掉沒有引用的符號;
4、pre-main和after_main
-
簡介:App啟動耗時一般統計
pre-main
階段(T1),和main()
函數之後到didFinishLaunchingWithOptions
方法執行完這段(T2)階段;SDK中可以利用__attribute__ ((constructor))
、__attribute__((destructor))
這兩個函數屬性在pre-main
和after_main
時機做一些事情; -
使用這兩個屬性定義函數示範如下:
__attribute__((constructor)) void before_main_xxxx() { //can do something } __attribute__((destructor)) void after_main_xxxx() { //can do something }
-
需要說明的是:在
pre-main
和after_main
時機,千萬不要做耗時操作;在SDK(二進位形式)中使用比較隱蔽,一般情況下,業務方很難想到或註意到;如果在pre-main時機做了耗時的事情,宿主App啟動體驗就不太好了;dyld載入過程分四步:
Load dylibs image
、Rebase/Bind image
、Objc setup
和initializers
;其中+load()
在__attribute__((constructor))
之前,他們都在initializers
階段內完成;initializers
之後就是main
函數執行了;
5、Method Swizzling
- 簡介:
Method Swizzling
是Objective-C中運行時特性之一,本質是在運行時交換方法實現(IMP);SDK有時候需要Method Swizzling
利用hook一些系統(Objective-C
)方法; - 需要
Method Swizzling
的話,推薦使用RSSwizzle,他是線程安全的Method Swizzling方案,優勢是:不需要在+load()
中實現方法交換 而且是 線程安全的;