架構心得

2019-07月底跳槽,從事的工作內容是基礎平臺內容,主要是基礎工具和 SDK 的封裝;工程化 cli 落地、研發管理、靜態代碼掃描等。雖然以前寫代碼也是站在封裝、複用、聚合等出發點寫代碼,但是還是和真正寫 SDK 注意點有很多不同,這也是爲什麼寫這篇文章總結的原因。

一些注意點

  • 當你開發某個功能的時候,輕易不要使用第三方的庫。爲什麼?因爲你難以確保業務方是否也在使用這個庫,可能庫在使用了,但是版本號不一致,就會造成 api 內部實現可能不一樣,造成功能不符合預期或者一些神奇的 Bug。

  • 假如你遇到上面的情況,你出於某種原因不得不使用某個第三方,但是你又必須考慮調用者的工程可能也加入了該庫。解決方案大體有3種。1、推進業務方不要使用離散的功能三方庫,比如 AFNetWorkging 不要自己引入,而是引入基礎平臺方封裝好的網絡功能庫;2、自己將引入的第三方網絡庫選取主要用到的功能去自己實現掉。我們首先要自己這個第三方做了什麼事情,提供了哪些功能,其中哪些功能是我們會使用到的,那麼我們可以借鑑源代碼,自己去做類似的事情,然後一個精簡版的 AFNetWorking 就出來了;3、將第三方庫的類名稱、方法名稱、Block、宏…都給更換名稱(一開始想到找到一定的規則用自動化腳本去做,發現這樣子不可能處理全部的 case,程序員自己腦子都想不全所有的 case,所以代碼實現根本不可能;)一番操作下來發現還是人工手動操作效果最好

  • 當你寫某個功能的時候,你封裝的 SDK 對於提供某個能力,項目以組件化的形式開展,所以你對外暴露的地方在於 Router 文件中, Router 負責解析 url,最後調用 [target performSelector withObject],然後在 target 對象內部真正去實現某個功能,Router 一定只做最簡單的事情,也就是 url parse,尋找 target,執行 performSelector。target 暴露某個接口,也許接口內部實現也很複雜,需要依賴其他幾個 api 或者其他幾個類的 api。所以 api 也就是函數需要做到單一原則。可能某個大的能力需要幾個能力的聚合,這個大的函數內部依靠幾個單獨的函數邏輯才實現某個能力。可能由於版本迭代,你需要將之前不對外暴露的能力也要暴露出去,所以做好函數的單一功能非常重要,可拓展性強、易測試。

  • 一定要寫好 Unit Test。這樣子不斷版本迭代,對於 UT,輸入恆定,輸出恆定,這樣內部實現如何變動不需要關心,只需要判斷恆定輸入,恆定輸出就足夠了。(針對每個函數單一原則的基礎上也是滿足 UT)

  • 在做 SDK 的時候,對於一些方法或者函數的返回值,儘量要做到 iOS 和 Android 端的輸出值的數據類型一致,除非某些特殊情況,無法保證一致的輸出。

  • 當你想寫宏定義的時候應該先判斷下是否存在,因爲工程中很可能已經存在一個同名的宏。

    #ifndef Hi
      #define Hi @"Hello, nice to meet you"
    #endif
    
  • 避免重複宏定義
    因爲宏定義可以多次,但是一個工程中有可能因爲命名太規範了,大家不小心會爲一個功能起一個同名的宏定義,所以我們在宏定義的時候需要做判斷,不然多個同名宏定義,最後的功能會根據文件編譯順序決定,最後的宏定義才生效。

    #ifndef CM_IS_CLASS
      #define CM_IS_CLASS(obj,cls) [obj isKindOfClass:[cls class]]
    #endif
    
  • 對於你的某個 SDK,你在爲某個方法、某個類、某個宏定義命名的時候需要注意選擇合適的前綴
    比如。你的某個項目是在做監控,SDK 的名字叫做 Prism-Client。那麼你的類名稱、類方法名稱、宏定義、分類名稱、分類方法名稱等都需要合適且統一的前綴,一般選取 前3個字母組合。當前的項目叫做 PCT。類前面加 PCT,類裏面的方法不加前綴。分類名稱加前綴 PCT,分類裏面的方法前面加前綴,小寫的 pct。
    普通類的方法不加前綴是因爲普通類已經通過類名的唯一性確定了方法的唯一。
    分類裏面方法加前綴是因爲分類的方法在工程裏面這個類都可以訪問。所以要在方法前面區分

    // 安全的數據獲取方法
    #ifndef PCT_SAFE_STRING
        #define PCT_SAFE_STRING(x) (x) != nil ? (x) : @""
    #endif
    
    NSData+PCTAES.h
    - (NSData *)pct_AES128EncryptWithKey:(NSString *)key gIv:(NSString *)Iv;
    
    PCTRequestFactory.h
    + (void)fetchUploadConfigurationWithRequestURL:(NSString *)requestUrlString
                                                  params:(NSDictionary *)params
                                                 success:(void (^)(PRCConfigurationModel*model))success
                                                 failure:(void (^)(NSError *error))failure;
    
  • 一般來說如果你的某個文件代碼中高頻率的使用宏,且宏裏面是做一些運算,建議使用內聯函數代替,因爲內聯函數效率高,且在編譯階段可以檢查錯誤。函數的調用順序底層是出入棧的過程,Frame Pointer、Stack Pointer。一個棧保存當前函數的局部變量、參數、返回地址。所以不同函數的調用會效率有影響,如果高頻使用的函數建議用內聯函數。
    內聯函數和宏的區別
    優點相比於函數

    • inline 函數避免了普通函數的,在彙編時必須調用 call 的缺點:取消了函數的參數壓棧,減少了調用的開銷,提高效率.所以執行速度確比一般函數的執行速度要快
    • 集成了宏的優點,使用時直接用代碼替換(像宏一樣)
      優點相比於宏
    • 避免了宏的缺點:需要預編譯.因爲 inline 內聯函數也是函數,不需要預編譯
    • 編譯器在調用一個內聯函數時,會首先檢查它的參數的類型,保證調用正確。然後進行一系列的相關檢查,就像對待任何一個真正的函數一樣。這樣就消除了它的隱患和侷限性
    • 可以使用所在類的保護成員及私有成員。
      注意事項
    • 內聯函數只是我們向編譯器提供的申請,編譯器不一定採取inline形式調用函數
    • 內聯函數不能承載大量的代碼.如果內聯函數的函數體過大,編譯器會自動放棄內聯
    • 內聯函數內不允許使用循環語句或開關語句
    • 內聯函數的定義須在調用之前
    • Objective-C 中內聯函數用 NS_INLINE ,等價於 static inline。且內聯函數的命名需要注意,在該模塊內的內聯函數需要加前綴。
    NS_INLINE NSString * PCTGetTableNameFromType(PCTLogTableType type){
        if (type == PCTLogTableTypeMeta) {
            return PRC_LOG_TABLE_META;
        }
        if (type == PCTLogTableTypePayload) {
            return PRC_LOG_TABLE_PAYLOAD;
        }
        return @"";
    }
    
  • 什麼情況下用統跳(路由能力)?
    技術 SDK 的話,因爲可能依賴非常多的其他技術 SDK 所以會比較難梳理出一個需要暴露的能力,非常難抽象
    業務 SDK 很清楚需要暴露哪些能力。所以我們一般將業務 SDK 提供統跳能力,技術 SDK 不提供

  • 基礎平臺組做什麼?怎麼做?
    業務線的同學一般做的事情就是在操作 UI,手機屏幕很小,要做的事情也會比較單一,可能就是單擊某個按鈕然後多線程異步去處理某個邏輯(網絡、數據庫、File等),然後異步回調裏面回調主線程去更新 UI。所以做的事情的廣度不一樣。基礎平臺組做的事情一般來說脫離獨立的 UI,換句話說就是焦點不在於 UI,而在於整個的架構邏輯,比如一個數據上報 SDK。它考慮的事情不是 UI 怎麼用,而是數據來源是什麼,我設計的接口需要暴露什麼信息,數據如何高效存儲、數據如何校驗、數據如何高效及時上報。

    假如我做的數據上報 SDK 可以上報 APM 監控數據、同時也開放能力給業務線使用,業務線自己將感興趣的數據並寫入保存,保證不丟失的情況下如何高效上報。因爲數據實時上報,所以需要考慮上傳的網絡環境、Wi-Fi 環境和 4G 環境下的邏輯不一樣的、數據聚合組裝成自定義報文並上報、一個自然天內數據上傳需要做流量限制等等、App 版本升級一些數據可能會失去意義、當然存儲的數據也存在時效性。種種這些東西就是在開發前需要考慮清楚的。所以基礎平臺做事情基本是 設計思考時間:編碼時間 = 7:3

    爲什麼?假設你一個需求,預期10天時間;前期架構設計、類的設計、Uint Test 設計估計7天,到時候編碼開發2天完成。

    這麼做的好處很多,比如:

    1. 除非是非常優秀,不然腦子想的再前面到真正開發的時候發現有出入,coding 完發現和前期方案設計不一樣。所以建議用流程圖、UML圖、技術架構圖、UT 也一樣,設計個表格,這樣等到時候編碼也就是 coding 的工作了,將圖翻譯成代碼
    2. 後期和別人討論或者溝通或者 CTO 進行 code review 的時候不需要一行行看代碼。你將相關的架構圖、流程圖、UML 圖給他看看。他再看看一些關鍵邏輯的 UT,保證輸入輸出正確,一般來說這樣就夠了
    3. 軟件項目管理也一樣,制定進度表、確定干係人、kick-of meeting 等、定期碰頭
  • 一般來說不要在 load 方法裏面做非本類的事情。
    一般來說,不應該在當前類的 load 方法裏面寫和其他類有關係的代碼,除非非做不可。

    + (void)load
    {
        NSLog(@"%zd", [AFNetworkReachabilityManager sharedManager].networkReachabilityStatus);
    }
    

    之前在做一個類 PCTRequestFactory 用來管理網絡相關的邏輯。需要判斷網絡狀態,我們都知道 AFNetWorking 第一次判斷網絡狀態得到的是 AFNetworkReachabilityStatusUnknown。而我的邏輯需要 SDK 啓動的時候判斷網絡狀態,然後去上報數據。所以剛開始 AFNetworkReachabilityStatusUnknown 顯然不能上報 Crash 數據,所以想着是將第一次的網絡狀態獲取放到 load 方法裏。這樣是沒問題的,可以拿到網絡狀態,但是我們知道 load 是類加載的時候調用的,打開 Xcode 看到 Build Phases 裏面 Link BiBinary With Libraries 這個裏面的庫的順序決定了裏面的類加載順序。我們知道 Pod 的原理是在 Podfile 裏面描述的 pod 庫依賴,然後會按照字典序(首字母排序去)引入,所以 AFNetWorking 這個肯定早,所以會成功的。但是萬一是人工手動去引入或者修改庫的位置,則在 PCTRequestFactory 裏面的 load 方法執行的時候不一定可以保證 AFNetworkReachabilityManager 已經加載好。所以將 load 邏輯移動到 init 裏面。

    另外,load 方法一般只做和本類有關係的邏輯,比如 hook 方法。

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