iOS SDK開發二三事

来源:https://www.cnblogs.com/simplepp/archive/2020/04/23/12760769.html
-Advertisement-
Play Games

引子 無意間,看到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大小不可以超過XXKB:【其他】
  • .......

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

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

3、公開介面設計

  • SDK使用者們關註介面是夠好用,設計好介面,讓你的SDK使用體驗加分;

  • 介面功能儘量職責單一,介面需要的參數不要太多;如果參數多,可以使用Model將業務參數封裝下;提供Model的default實現;介面名,參數名使用駝峰命名,最好見名知義;

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

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

4、代理和狀態碼

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

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-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函數執行了;

5、Method Swizzling

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

推薦

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • concat(exp1,exp2) 把exp1和exp2拼接在一起,通常用在百分比中 select concat('abc','ba') from dual abcba select concat('100','%') from dual 100% select concat(100,'%') fr ...
  • Ceil(value) 函數返回大於等於指定值(value)的最小整數,取整,沒有四捨五入這一說法 select Ceil(103.46) from dual 104 select Ceil(103.46) from dual 104 ...
  • floor(value) 函數返回小於或等於指定值(value)的最小整數,取整,沒有四捨五入這一說法 select floor(103.56) from dual 103 select floor(103.46) from dual 103 ...
  • trunc(exp1) trunc(exp1)和Round(exp1,exp2)類似,只不過trunc()不指定截取的小數位數進行處理,只取到整數位,不做捨去處理 select trunc(123.1234) from dual 123 select trunc(123.9234) from dua ...
  • Round(exp1,exp2)函數具有四捨五入的功能,分為以下兩種情況: 1.exp2數為非負 四捨五入的位數從小數點後開始計數,小數點後|exp2|位,看後一位,進本位,後面捨去 select Round(125.455,0) from dual 125 select Round(125.455 ...
  • 一、 1.我們設置主活動的框架,把主活動的屏幕分成兩個碎片,各顯示子活動的界面​。 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android ...
  • 目錄:andorid jar/庫源碼解析 EventBus: 用於不同Activity,Service等之間傳遞消息(數據)。 A:onCreate定義 EventBus.getDefault().register(this); onDestroy定義 EventBus.getDefault().u ...
  • 前言 本篇作為開篇,會大體上說明,需要解讀源碼的,類庫,或者jar。 序 原本,類庫和jar的系列準備寫到逆向系列課程的,但是那個東西,在寫了兩篇,就沒有後續了,現在也不知道從哪裡開始了, 只能等後期想好了,再開篇單獨寫吧。 目錄: EventBus、Dagger、okhttp、retrofit、b ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...