iOS SDK開發二三事

引子html

  • 無心間,看到5年前,Android大佬子勰寫的,關於SDK開發方面的文章(SDK那些事(總綱)), 不禁得喚起本身開發iOS SDK的回憶;本文簡單總結下本身開發SDK方面的經驗;前端

  • SDK(Software Development Kit)能夠最大程度實現代碼和功能的複用,爲業務開發提供一個很是好的支持;這裏的業務能夠是內部業務,也能夠是外部業務;android

  • 簡單來講,所謂SDK開發,本質是服務提供;不只須要寫好代碼,還要完善代碼以外的事情,任重道遠git

1、準備

一、清晰解決的問題和要求

​ 通常而言,新起一個SDK必然有其深入的業務背景;研發同窗對SDK要解決的問題和SDK的特殊要求,瞭解地越詳細越好;常見的要求有:github

  • 禁止採集用戶信息【安全方面】
  • 必須對SDK使用者鑑權【安全方面】
  • 核心代碼必須混淆【安全方面】
  • 不能夠有調試日誌,不能夠監控上報【安全方面】
  • 持久化的敏感數據要加密;【安全方面】
  • SDK大小不能夠超過XXKB:【其餘】
  • .......

二、選擇合適的開發語言

  • 大多數狀況下,選擇Objective-C開發就對了,不只能接入Swift開發的項目,還能接入Objective-C開發的項目;
  • 固然並不是絕對,具體根據業務狀況決定;

三、選擇合適的技術方案

  • 網絡請求使用第三方框架,仍是直接利用iOS的API;
  • JSON轉Model、Model序列化和反序列選擇哪一個第三方框架(性能,框架大小方面考慮)
  • 持久化存儲選擇SQLite、NSUserDefaults 或是 Keychain;
  • UI佈局使用 frame、autolayout(Masonry框架) or flexbox(YogaKit框架)
  • MVVM or MVC 架構模式選擇;
  • ...

四、確立基本代碼規範

​ SDK可能長期維護 或 多人開發,確立好基本代碼規範,能保障SDK的代碼質量;這些規範本質上是一些共識和約束,如:面試

  • 命名規範:屬性、變量、方法等均採用小寫字母開頭的駝峯命名;類名使用大寫字母開頭的駝峯命名;
  • 註釋要求:對外暴露的類、方便和變量要有註釋,解釋其功能;關鍵代碼要有註釋;
  • 簡潔要求:無用的代碼選擇直接刪除,不要註釋;無用的資源及時清除;
  • ....

2、SDK的主體設計

一、多模塊設計

  • SDK中可能包含不一樣的模塊功能,而不一樣的業務方須要的模塊可能不一樣;對SDK中模塊進行拆分,保證業務方儘量引入的是他們須要的代碼;跨域

  • 通常使用Cocoapods建立Pod庫的,在podspec中定義好模塊,爲每一個模塊清晰定義好包含的代碼和資源,以及外部依賴(靜態庫 or 靜態庫);這樣能夠將模塊之間實現代碼和資源的物理隔離;緩存

  • 關於Cocoapods建立Pod庫更多細節能夠參考Cocoapods使用小記,至因而公有Pod仍是私有Pod根據實際狀況定;安全

    建立公有Pod庫或者私有Pod庫原理是同樣的;不同的是:二者的版本索引查詢方式不同,公有庫的podspec由CocoaPods/Specs管理,而內部私有使用的pod庫須要公司內部創建一個倉庫來管理podspec微信

二、SDK目錄層級

  • Pod庫中,代碼放在Classes目錄下,圖片資源放在Assets目錄下;

  • Classes目錄按模塊劃分第一級目錄,如AModuleBModuleCModule等,其中每一個模塊Code再劃分二級目錄,如ModuleService、View、Controller、Model、API等;具體的代碼文件存放在這些二級目錄中;其中ModuleService中代碼是要對外暴露的,其餘預期外部不可見;

  • 資源方面,也按模塊細節;主要的資源是圖片資源,在Assets目錄下新加AModule.xcassetsBModule.xcassetsCModule.xcassets等;

三、公開接口設計

  • SDK使用者們關注接口是夠好用,設計好接口,讓你的SDK使用體驗加分;

  • 接口功能儘可能職責單一,接口須要的參數不要太多;若是參數多,可使用Model將業務參數封裝下;提供Model的default實現;接口名,參數名使用駝峯命名,最好見名知義;

  • 接口中每一個參數類型要明確,不要出現idNSDictionary這樣的類型;避免業務隨意傳參,增長SDK內部對參數校驗難度;也減小業務方對參數的困惑;

  • 接口內第一件事情是要作參數校驗,不符合預期狀況,Debug模式直接Assert,及早把問題拋出;Release模式要記錄到日誌並上報,提早返回,避免後續出現迷惑問題,增長排查問題成本;

四、代理和狀態碼

  • 除了暴露接口,SDK還可能暴露代理(Delegate)方法 和 狀態碼;
  • 代理(Delegate)是須要業務方根據須要實現的,代理(Delegate)中可選實現方法加上@optional關鍵字,沒有的話默認要實現;協議和協議中具體方法的做用要增長上註釋;
  • 狀態碼設計上,須要注意兩點:
    • SDK中定義的狀態碼使用枚舉(NS_ENUM)定義,對應的每一個值增長註釋;
    • 若是SDK依賴了其餘SDK,這些SDK的狀態碼最好不要透傳給業務方,中間增長轉化;好比微信SDK中WXErrCode不要透傳給業務方,封裝一下,對外暴露的是咱們的狀態碼;

五、SDK的版本號

  • SDK都要有明確的版本號,通常版本號分三段:主版本、特性版本、修正版本,如5.6.1;其中主版本號用於大版本的發佈,特性版本主要用於更新迭代,修正版本號主要用於bug修復。
  • SDK內部的埋點、監控,網絡請求等都要攜帶SDK的版本號,這些版本號對定位SDK問題很是重要;
  • SDK對外通常是二進制的形式提供,發佈的SDK須要帶上版本號信息;至少要保證三個地方的SDK版本信息是一致的;git倉庫的tag版本號、podspec中的version、代碼中的版本號;能夠實現個腳本,在編譯時候,統一修改這三處的版本號信息;

六、SDK安全需求

  • 目前遇到的,有關SDK安全方面要求有:
    • 核心代碼的混淆;包括字符串常量、類名和方法名方面的混淆;
    • 敏感數據須要加密存儲,禁止明文傳輸等
    • 對SDK使用者鑑權的要求,"非法"App不得使用,非法調用者必Crash;
  • 此外,有不常見的」安全訴求「:App內不得有任何調試信息,埋點、採集用戶數據等行爲;不得使用帶來安全隱患的API,好比打開WKWebview跨域開關這個就不被容許;
  • 代碼混淆後,混淆先後的符號間映射必定要保留,不然線上問題堆棧信息中,出現的SDK混淆後的符號會讓定位問題很是迷惑;能夠作成轉化工具,導入堆棧符號信息,輸出混淆前正常的符號信息;

3、對業務的支持

一、提供SDK文檔

  • SDK開發好後,對外還要提供相關文檔,包括但不限於:SDK功能介紹、SDK接入辦法 和 SDK具體使用、SDK 更新日誌記錄 以及 SDK問題記錄;
  • 基於專業角度,建議提供SDK概況文檔,包括但不限於:接入的業務狀況,優化記錄,SDK的依賴庫信息,二進制 和 資源大小信息等;有些業務對SDK大小敏感,有些關注SDK的穩定性...;
  • 業務方在接入SDK前,是從接觸SDK文檔開始,好的文檔能幫助SDK更好地被接受,後續更好開展工做;

二、提供Demo

  • 只有文檔還不夠的,必定要提供Demo;Demo有兩個好處:
    • 幫助業務方結合文檔更好了解SDK的接入和使用;
    • 幫助測試及時驗證SDK升級後帶來的影響;
  • Demo中不只有SDK引入辦法、使用辦法;還能夠寫一些簡單UI,幫助展現SDK功能;每次SDK升級,都經過Demo自動出包,提供給測試人員和產品去驗證(功能驗證和設備兼容性驗證);
  • SDK的自動化測試這塊,暫何嘗試;但在是Demo中,定義對SDK接口的單元測試是必要的;單元測試要關注:非法傳參case,非主線程調用case。

三、規範SDK開發流程

  • 開發SDK和開發完整項目同樣,要有需求評審、技術評審、排期,開發,自測,提測、測試驗收等環節 ;不一樣功能在不一樣feature分支上開發,每一個feature功能驗證經過後能夠合入主幹分支;合入主幹後,由主幹分支發佈版本;

  • 發佈的SDK使用二進制的形式;SDK使用二進制形式,不只能提示項目項目編譯速度,也能保護好源碼;若是業務方須要SDK源碼,須要向SDK負責人申請權限;

四、幫助業務排查問題

  • SDK開發者常常會收到業務方幫助排查問題的訴求;畢竟,SDK對業務方是一個黑盒,不少業務上涉及到SDK問題,須要SDK開發者幫助排查,耗時耗力;
  • 爲了更好幫助業務排查問題,推出三件套:SDK核心鏈路監控埋點 + 重要日誌信息持久化並上報 + 調試日誌信息可視化;前二者是固化在於SDK中的;而調試日誌信息是交給業務方,由他們靈活處理;
  • 業務方拿到調試日誌信息,能夠將其輸出到控制檯,也能夠輸出到App的日誌可視化工具中;鑑於業務方可能沒有,能夠提供一個輕量級的日誌可視化工具。
  • 必須注意的是,SDK中的調試日誌信息禁止帶到線上,SDK的Release版本不能夠帶這些信息。

五、溝通Plus

  • 重要內容要有記錄:SDK會被多個宿主App接入,不一樣的App環境不一樣,SDK可能遇到不少問題,積極幫助解決後,記錄下來,做爲後續宿主App使用SDK的重要參考;
  • 創建SDK和業務溝通機制;及時同步SDK最新信息;SDK的bugfix版本,要及時同步,並幫助業務升級,儘可能減小損失;
  • 爲SDK增長代碼Reviewer:SDK重大升級,最好involve主要業務方的研發進行技術評審和Code Review;其實這對業務方是個很高的要求,須要業務方至少有一我的對SDK有比較全面的瞭解;

4、SDK實現中的注意事項

一、注意多線程使用

  • 不要在主線程執行耗時操做,能夠將他們交給子線程;
  • 控制好併發數量,GCD併發隊列並不會去管理最大併發數,無限制提交任務給併發隊列,會給性能帶來問題。能夠適當控制併發數量,防止線程爆炸;具體可見 iOS實錄16:GCD小結之控制最大併發數
  • 遇到必需要在主線程執行的任務;先判斷當前是否在主線程,不在的話,能夠經過GCD將任務放到主線程隊列執行;此外,還能夠加斷言,Debug下,非主線程執行執行拋異常,Crash,幫助及時發現問題;
  • 儘量使用輕量級的鎖,可使用信號量;自旋鎖性能很是好,可是有優先級反轉的問題,謹慎使用;

二、使用緩存要剋制

  • 不管是內存緩存和磁盤緩存都要有清除策略(能夠LRU)和使用大小限制;
  • 若是內存緩存沒有清除策略和使用大小限制,會致使內存使用無限制增加,最後可能會致使OOM問題;此外,當收到內存警告時候,內存緩存要及時清除,不然可能引發OOM問題,直接破壞用戶體驗;
  • 若是磁盤緩存沒有清除策略和使用大小限制,會致使磁盤空間濫用,對App總體體驗都很差,並且後續清理成本比較高;
  • 持久化在磁盤文件中數據,不要在App啓動時去讀取,能夠懶加載;不要對數據量和文件讀取性能報僥倖,隨着SDK的迭代,那些數據可能不斷變大,也可能在低端機器上文件讀取性能比較差,偶現幾十ms甚至幾百ms的耗時,直接拖累啓動速度;

三、內存使用要注意

  • 儘可能優化內存使用,四個原則:減小大塊內存使用、下降內存峯值、避免內存泄露和處理內存警告;
  • 具體到內存使用中技巧:
    • 合理使用autorealsepool,下降內存峯值,避免 OOM
    • 複用大內存對象,如UITableViewCell對象;懶加載大的內存對象
    • 用 NSCache 代替 NSMutableDictionary,使用NSPurgableData 代替NSData
    • weak strong dance 來解決 Block 中的循環引用,代理(delegate)使用weak修飾;
    • CoreFoundation對象、CoreGraphics對象、還有C/C++的內存分配須要管理好,有malloc()和calloc()就要有free;
    • ....
  • 瞭解內存方面知識能夠看:iOS內存二三事

四、使用單例要注意

  • 有些SDK經過單例對象提供服務;由於,單例對象只有一個對象,不只能夠節約開銷,還能夠保證App中多業務操做SDK中同一個服務對象;
  • 可是定義單例要注意;日常App開發中,寫單例不怎麼嚴謹,提供個讓外部訪問的類方法,如+ (instancetype)sharedInstance,內部使用dispatch_once保證alloc和init只執行一次,這種是粗髮式單例,並不能保證絕對單例;
  • 在SDK中,必定要保證外部訪問到的是單例對象;除了提供讓外部訪問的類方法,還要重寫+(instancetype)allocWithZone-(instancetype)copyWithZone-(instancetype)mutableCopyWithZone方法,保證永遠都只會分配一次內存空間,實現真正的單例;
  • iOS中,一個對象有且只能有一個代理;若是你的單例對象有須要業務方實現的代理方法,根據實際狀況判斷,是否須要實現多代理;

五、注意宏定義和條件編譯

  • 編譯器前端(如Clang)在編譯源碼時,首先要作預處理(preprocessor),如頭文件引入,宏替換,註釋處理,條件編譯(#ifdef) 等;
  • 通常地,SDK中會使用到條件編譯;在源碼狀況下,能夠根據不一樣的條件,編譯不一樣的源碼;可是SDK使用二進制的形式對外提供的話,在二進制化時就已經根據XX條件編譯好了;所以,二進制的SDK不會能跟隨宿主App編譯條件變化了;
  • 使用宏能夠提升程序的通用性和易讀性,減小不一致性,減小輸入錯誤和便於修改。可是若是宏對應實現,須要根據條件編譯來區分不一樣的行爲;在二進制編譯時候,宏定位的行爲會被肯定下來;
  • 宏定義有個小細節,用do{...}while(0)構造的宏定義不會受到大括號、分號等的影響,很是建議使用。

5、SDK實現中的小技巧

一、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,時間緊急狀況下,能夠"劍走偏鋒";

二、預約義符號

  • 簡介:有些公開功能使用宏定義的函數形式,能夠在函數中帶上__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__的成本更低,性能更好;

三、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下。連接器會優化掉沒有引用的符號;

四、pre-main和after_main

  • 簡介:App啓動耗時通常統計pre-main階段(T1),和main()函數以後到didFinishLaunchingWithOptions方法執行完這段(T2)階段;SDK中能夠利用__attribute__ ((constructor))__attribute__((destructor))這兩個函數屬性在pre-mainafter_main時機作一些事情;

  • 使用這兩個屬性定義函數示範以下:

    __attribute__((constructor)) void before_main_xxxx() { //can do something } __attribute__((destructor)) void after_main_xxxx() { //can do something }
  • 須要說明的是:在pre-mainafter_main時機,千萬不要作耗時操做;在SDK(二進制形式)中使用比較隱蔽,通常狀況下,業務方很難想到或注意到;若是在pre-main時機作了耗時的事情,宿主App啓動體驗就不太好了;

    dyld加載過程分四步:Load dylibs imageRebase/Bind imageObjc setupinitializers;其中+load()__attribute__((constructor))以前,他們都在initializers階段內完成;initializers以後就是main函數執行了;

五、Method Swizzling

  • 簡介:Method Swizzling是Objective-C中運行時特性之一,本質是在運行時交換方法實現(IMP);SDK有時候須要Method Swizzling利用hook一些系統(Objective-C)方法;
  • 須要Method Swizzling的話,推薦使用RSSwizzle,他是線程安全的Method Swizzling方案,優點是:不須要在+load()中實現方法交換 並且是 線程安全的;

推薦👇:

  • 020 持續更新,精品小圈子每日都有新內容,乾貨濃度極高。

  • 結實人脈、討論技術 你想要的這裏都有!

  • 搶先入羣,跑贏同齡人!(入羣無需任何費用)

  • (直接搜索羣號:789143298,快速入羣)
  • 點擊此處,與iOS開發大牛一塊兒交流學習

申請即送:

  • BAT大廠面試題、獨家面試工具包,

  • 資料免費領取,包括 數據結構、底層進階、圖形視覺、音視頻、架構設計、逆向安防、RxSwift、flutter,

     

參考文章

如何編寫一個(不)受歡迎的 iOS SDK

聽阿里雲工程師談談如何開發一個優秀的SDK

做者:南華Coder
連接:https://juejin.im/post/5e9adea16fb9a03c364f216e

相關文章
相關標籤/搜索