“踩坑”經驗分享:Swift語言落地實踐

架構師修行手冊發表於2023-12-28

來源:百度Geek說
作者 | 路濤、豔紅

導讀 
introduction
Swift 是一種適用於iOS/macOS應用開發、伺服器端的程式語言。自2014年蘋果釋出 Swift 語言以來,Swift5 實現了 ABI 穩定性、Module 穩定性和Library Evolution,與Objective-C(下文簡稱“OC”)相比,Swift 在開發效率、安全、編譯最佳化、執行效能和記憶體管理方面具有顯著優勢。(官方部落格:

百度App 已在工程和環境上支援 Swift 開發,百度搜尋大前端團隊負責搜尋服務的穩定落地,我們積極探索 Swift的應用,希望能大幅提升開發效率和靈活性、提升端使用者的搜尋體驗。然而,在實施過程中可能會遇到各種問題,例如程式碼陳舊且不支援Swift,人員對Swift掌握不夠熟練、意識不足,協作方對Swift的支援不足等。

對於其他語言來說,Swift相對年輕,我們在實踐過程中整理一些常見問題及其解決方法,希望能幫助讀者更順利地使用Swift進行程式設計,提高研發效率。


全文6947字,預計閱讀時間18分鐘。


GEEK TALK

01

Swift 適用場景

在決定是否引入Swift前,我們需要判斷場景是否適合。通常情況下,可以用OC的場景均適合使用Swift,但也有一些不太適合直接替換的場景,需要慎重,比如:

1、涉及OC動態性,頻繁在runtime時操作屬性和方法;

2、核心基礎功能,出現問題影響面較大的邏輯;

3、呼叫C++(目前Swift不能直接呼叫C++);

4、繼承不支援Swift元件的類。

此外,對使用OC比較久遠的工程,使用Swift前也應注意:

1、能在工程環境和單獨模組上支援Swift;

2、模組較多的工程,可以內外OC和Swift混編;

3、為了避免Swift Waring帶來的潛在問題,可以把SWIFT_TREAT_WARNINGS_AS_ERRORS設定為YES,這樣警告會作為錯誤,輔助程式設計師更好的規範程式碼;

4、模組Module化後,要注意維護 umbrella header 中的公開標頭檔案。

注:本文中的“元件”均指代工程中的“Target”。


GEEK TALK

02

Swift的基本用法

2.1 Swift 的字串為什麼這麼難用?

如:字串不能透過索引取字元
  • 原因:Swift認為字串是由一個個字形群集 (grapheme clusters)組成的,字形群集的大小不固定所以不能用整數去索引 (字形群集其實就是Swift中的Character(字元)類)。

  • 解決方案:如要透過下標取字元可以為String新增擴充套件在下標subscript實現透過傳入Int索引,在subscript轉為String.index獲取對應字元的方式。

2.2 try try? try! 的區別

當你進行檔案操作時,可能會遇到需要使用try、try?和try!的情況。它們在異常處理方面有所不同。

1、使用try時,如果出現異常,程式會進入異常處理流程,你可以在catch語句塊中處理這個異常。

2、使用try?時,如果發生異常,它不會進入異常處理流程,而是返回一個可選值型別。也就是說,如果出現異常,它將返回nil。

3、使用try!時,它不允許異常繼續傳播。一旦出現異常,程式會立即停止執行。

因此,在檔案操作中,你可以根據需要選擇合適的異常處理方式。在百度App中一般推薦使用try?。

2.3 public 和 open 的區別

在Swift語言中,public和open都是用於在模組中宣告需要對外界暴露的函式的關鍵字,但它們在繼承和公開程度上有所不同。

1、public關鍵字修飾的類在模組外部無法被繼承。這意味著,如果其他模組試圖繼承這個類,編譯器會報錯。這樣的限制可以保護類的完整性,但也可能限制了其在其他模組中的可重用性。

2、open關鍵字則允許任意繼承。如果一個類被open關鍵字修飾,那麼其他模組中的類可以自由地繼承這個類,不受任何限制。這樣的公開程度使得open關鍵字修飾的類在模組間的重用性和擴充套件性更加靈活。

從公開程度上來說,public的限制比open更嚴格,所以可以說public < open,即public的公開程度比open要低。

2.4 解析JSON情況

在Swift中解析JSON的情況,如果自行將JSON轉換為字典,需要涉及到型別判斷、轉換等操作,程式碼比較複雜。這時可以使用第三方庫SwiftyJSON、ObjectMapper或者系統庫JSONEncoder來簡化操作,提高開發效率。

2.5 UIView子類必須新增init?(coder decoder: NSCoder)的原因

1、這是NSCoding protocol定義的,遵守了NSCoding protocol的所有類必須繼承。只是有的情況會隱式繼承,而有的情況下需要顯示實現。

2、當我們在子類定義了指定初始化器(包括自定義和重寫父類指定初始化器),那麼必須顯示實現required init?(coder aDecoder: NSCoder),而其他情況下則會隱式繼承,我們可以不用理會。

3、當我們使用storyboard實現介面的時候,程式會呼叫這個初始化器。

4、注意要去掉fatalError,fatalError的意思是無條件停止執行並列印。

2.6 Swift類和子類的初始化

Swift的類和子類初始化涉及到兩個關鍵階段。首先,確保所有的儲存屬性被賦予初始值,然後,在例項準備使用之前,可以自定義儲存屬性的值。為了確保這兩個階段成功,實施了四步安全檢查,詳細如下:

1、在完成本類所有儲存屬性賦值之後,指定構造器才能向上代理到父類的構造器。

2、在為繼承的屬性設定新值之前,指定構造器必須向上代理呼叫父類構造器。

3、便利構造器必須先呼叫其他構造器,再為任意屬性(包括所有同類中定義的)賦新值。

4、在第一階段構造完成之前,構造器不能呼叫任何例項方法,不能讀取任何例項屬性的值,不能引用self作為一個值。

總之,類初始化必須完成的一個任務就是讓所有的儲存屬性都有初始值(optional 除外)。如果父類有指定初始化,子類必須也有指定初始化,並且必須呼叫父類的其中一個指定初始化(如果是必須初始化,就是過載),並遵循兩段式初始化的規則。一個便利初始化必須呼叫同一類中的初始化方法(可以是另一個便利初始化,也可以是指定初始化),但最終一定會呼叫到一個指定初始化。便利初始化不遵循兩段式初始化的規則,不能被子類呼叫或者過載。


GEEK TALK

03

OC與Swift的互相呼叫及跳轉

3.1 元件內Swift檔案呼叫公開OC標頭檔案

  • 將公開OC標頭檔案(如:xyz.h)新增到元件(如:ABC)umbrella header中(如:#import);

  • Swift檔案中直接呼叫公開OC標頭檔案內容。

3.2 元件內Swift檔案呼叫非公開(私有)的OC檔案

元件應該儘可能少的公開暴露標頭檔案,但Swift和OC混編不可避免使用OC非公開標頭檔案,因此我們可以採取以下措施:將Framework 中將私有標頭檔案宣告為一個私有 module(modulemap內宣告),由元件內的 Swift 原始碼 import 該私有 module 即可。

1、建立Private.modulemap檔案,以NewModule做為元件名為例,可以命名為NewModule.private.modulemap,內容為下,module後面加_Private

  • 羅列標頭檔案的形式




framework module NewModule_Private {  header "xxxxx.h"}

  • 使用根標頭檔案的形式,新增標頭檔案NewModule_Private.h







framework module NewModule_Private {  umbrella header "NewModule_Private.h"
 export *  module * { export * }}

2、在元件build settings中配置MODULEMAP_PRIVATE_FILE路徑,MODULEMAP_PRIVATE_FILE='NewModule.private.modulemap';百度App中在NewModule.boxspec中如下程式碼設定路徑;




s.xcconfig = {    'MODULEMAP_PRIVATE_FILE' => '${BOX_ROOT}/NewModule.private.modulemap'}

3、將NewModule.private.modulemap新增到工程目錄;百度App中在NewModule.boxspec中如下程式碼設定路徑;




s.refer_files = [      "NewModule.private.modulemap",]

4、將xxxxx.h設定為Private header,百度App中在NewModule.boxspec中如下程式碼設定xxxxx.h到Private header




s.private_headers = [    "Sources/xxxxx.h"  ]

5、呼叫方式




import NewModule_Privatelet objectX = xxxxx()print(objectX)
注意:
  • 新增的Private標頭檔案可能存在傳遞標頭檔案的情況,即import其他標頭檔案,也需要將傳遞的標頭檔案新增到NewModule_Private中,同時import需要使用尖括號;

  • Private Header也會暴露在framework中,所以可以約定外部元件使用Public Header,而避免使用Private Header,因為隨著業務發展和Swift&OC混編,Private Header是不穩定的。

3.3 元件內OC檔案如何呼叫Swift檔案?

  • Swift 類需要繼承 NSObject,方法前面加上@objc 標識,並且是 public 或者 open 的;

  • 引入方式 #import"

3.4 OC中的向前宣告,被Swift檔案引用該元件會報錯

如error: cannot find protocol definition for 'xxxProtocol'
  • 原因:此報錯在OC中是程式碼警告,百度App中預設情況Swift中SWIFT_TREAT_WARNINGS_AS_ERRORS 設定為 YES,導致OC中的Warning視為Error;

  • 解決方案:三選一

1、暫時設定 SWIFT_TREAT_WARNINGS_AS_ERRORS 為 NO

2、import xxxProtocol 不要向前宣告

3、使用 pragma 忽略警告

3.8 Swift怎麼用OC定義的宏?

  • 在Swift中,能直接使用定義為常量的宏,不能使用帶有方法呼叫的宏,也不能使用靜態常量。











下面這種定義為常量的宏可以使用#define APP_LANGUAGE_EN @"en" #define kNavigationBarHeight 44.0
下面帶有方法呼叫的宏不可以使用#define kScreenHeight [[UIScreen mainScreen] bounds].size.height#define kScreenWidth [[UIScreen mainScreen] bounds].size.width
下面帶有靜態常量swift不能使用,可以改成宏static NSString *const StopTabRefreshNotifyNameHtml = @"TabRefreshNotifyNameHtml";

3.6 Swift與OC泛型的混編

  • 在我分們基礎框架中,有一個使用了OC泛型的類,如:




@interface BBAXYZ<T> : NSObject <BBAXYZEventProtocol>  @property (nonatomic, weak) T page;  @end

這個泛型的使用導致無法使用Swift來繼承和開發BBAXYZ的子類。然而,這個基礎框架是業務的核心部分,因此,我們需要在未來支援Swift的開發。

  • 經過仔細觀察和分析,我們發現泛型主要被用於指定page屬性的型別。因此,我們可以考慮去掉泛型,改為提供一個返回適當型別的方法。這樣,我們就可以在Swift中順利地繼承和使用這個基礎框架。修改後的程式碼如下:




@interface BBAXYZ : NSObject <BBAXYZEventProtocol>  - (id<BBAXYZEventProtocol>)page;  @end

然後,我們可以建立一個OC類來實現這個基礎框架,並讓所有的子類繼承這個OC類並實現 page 方法,以返回適當型別的物件。這樣,我們就可以在Swift中順利地繼承和使用這個基礎框架。

例如:




@interface BBAABC : BBAXYZ  - (UIViewController<BBAXYZEventProtocol> *)page; @end

需要注意的是,雖然這樣的修改增加了輕量級的中間OC類,但它仍然實現了Swift與OC的混編,並允許我們在Swift中開發新的子類。這種方式既保證了程式碼的相容性,又使得我們可以繼續利用OC的優點。

  • 使用方式




class BBAEFG: BBAABC {    }

3.7 Swift呼叫OC介面,OC的nullability標註使用時的注意事項

問題場景:

1、OC 介面定義為 nonnull,swfit 呼叫時正常是當做不可選型別使用,這時如果 OC 介面不規範返回 nil,則出現執行時崩潰。

2、OC 介面未定義 nonnull 或 nullable,在這種情況下,編譯器會將 OC 的指標型別當成是隱式解析可選型別(例如 String!)匯入到 Swift 中。swift 呼叫時,OC介面如果返回 nil,將會因為隱式解析一個為 nil 的可選值導致執行時崩潰。

解決方式:

1、Swift 呼叫 OC 介面時,如果 OC 的介面宣告為 nonnull 或未指定 nullability 時,只有明確 OC 介面不為空的情況下才可呼叫

2、在 OC 環境下,將 nil 賦值給 nonnull 指標也沒有關係,編譯器只會產生警告。這就需要程式設計師按規範編寫 OC 程式碼,正確使用 nullability 標註,並增加執行時判空的斷言,以支援向後相容。


GEEK TALK

04

其他常見問題

4.1 Xcode編譯只提示編譯錯誤,提示資訊非常少

  • 原因:使用Swift語言開發的元件,依賴了不支援Module化的元件,導致元件都能編譯成功,但整個工程卻編譯失敗了;

  • 解決方案:二選一

1、檢查並保障所有依賴的元件都已經Module化了,如配置build settings;

2、在元件中新增Swift檔案(空檔案也行)。

4.2 由於元件開啟了Library Evolution 導致的編譯報錯

錯誤顯示:@objc' instance method in extension of subclass of 'xxxxx' requires iOS 13.0.0

這是由於元件開啟了Library Evolution導致,開關BUILD_LIBRARY_FOR_DISTRIBUTION 控制的。

一個庫開啟了Library Evolution,在依賴鏈下游的庫中將:

1、對它的類實現 @objc 子類。

2、對它的類使用 extension 實現 @objc 的方法(這在 UIKit 的 protocol 中經常會遇到)。

3、對它的類實現子類,並新增 @objc 方法,且方法中使用父類的型別作為引數。

這些功能是實現的侷限。估計是需要在 Swift 執行時有一些對應的更改,所以只在 swift 5.1 (iOS 13)執行時裡才可以執行。

除此之外,還會有一些編譯時沒有報錯,但執行時 crash 或結果不正確的情況。

百度App中預設開啟Library Evolution,一個元件關閉Library Evolution會導致二進位制存在不相容的情況,暫時無解決方案。

4.3 暴露的Private標頭檔案如果使用雙引號import,會報警告,需要修改為尖括號

如果使用import <xxxx.h>,Project下其他Target就引用不到了,如百度App中Debug模組引用此Private標頭檔案時,會報錯 not found with <angled> include, use "quotes" instead。
  • 原因:這是由於其他Target對主模組引用時預設是從當前專案下引用標頭檔案,而尖括號方式從系統庫或使用者庫中引用;

  • 決方案:其他Target將Private Header 配置到其 HEADER_SEARCH_PATHS,使用雙引號和尖括號均可。


GEEK TALK

05

總結

以上是我們在Swift開發過程中所遇到的一些常見問題及其相應的解決方案。然而,隨著我們不斷深入Swift開發這片浩渺的海洋,更多獨特的問題將會逐漸浮現。我們會持續將這些新問題以及其對應的解決方案整理併發布出來,為廣大的開發者們提供有價值的參考。歡迎大家留言探討。

來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70027824/viewspace-3001860/,如需轉載,請註明出處,否則將追究法律責任。

相關文章