Objective-C Swift 混編的模塊二進制化 1:基礎知識

Objective-C 與 Swift 混編

Objective-C 與 Swift 混編在使用上主要依賴兩個頭文件:ProjectName-Bridging-Header.h 和 ProjectName-Swift.h。html

對於 Swift 調用 Objective-C,在 ProjectName-Bridging-Header.h 中 import 要使用的 Objective-C 頭文件。git

對於 Objective-C 調用 Swift,須要編譯過程當中生成的 ProjectName-Swift.h 文件,此文件會將 Objective-C 須要使用的 Swift 類轉成 Objective-C 格式的 .h 文件。下面就是一段例子:github

Build Pipeline

當 Objective-C 與 Swift 進行混編時,編譯的過程(Pipeline)是:objective-c

  • 首先編譯 Swift Module。預編譯 Bridging Header 後,再編譯 Swift 源文件。
  • Swift 編譯完成後,生成 ProjectName-Swift.h 的頭文件供 Objective-C 使用。
  • 最後編譯 Objective-C 源文件。

Circle Reference

在開發過程當中,有時候會遇到 ’ProjectName-Swift.h’ file not found 錯誤,而 ProjectName-Swift.h 又是在編譯時動態生成的,當出現這樣的錯誤時就比較迷惑。下面就分析一下出現該錯誤的緣由。算法

@objc (TTLesson)
class lesson: NSObject {
}
複製代碼

假設有一個 Swift 類 Lesson,Objective-C 的 TTLessonViewController 中使用了該類,所以須要引入 MixedProj-Swift.h 頭文件。編程

// TTLessonViewController.h
#import "MixedProj-Swift.h"

@interface TTLessonViewController: UIViewController
@property (nonatomic, strong) TTLesson *lesson;
@end
複製代碼

若是 TTLessonViewController 又要在 Swift 中使用,須要將 TTLessonViewController.h 加入到 MixedProj-Bridging-Header.h 中swift

// MixedProj-Bridging-Header.h
#import "TTLessonViewController.h"
複製代碼

此時編譯就會出現 MixedProj-Swift.h file not found 的錯誤。從新回顧一下混編時的 Pipeline:xcode

  • 首先編譯 Swift,須要先處理 MixedProj-Bridging-Header.h。
  • 在處理 MixedProj-Bridging-Header.h 時,裏面的 TTLessonViewController.h 中引用了 MixedProj-Swift.h 頭文件。
  • 此時因爲 Swift 還沒編譯,所以 MixedProj-Swift.h 頭文件並無生成,因此出現 MixedProj-Swift.h 找不到的錯誤。

解決辦法就是不顯式的 import 頭文件,而是使用前置聲明(Forward Declaration),打破 Circle Reference。bash

// TTLessonViewController.h
@class TTLesson;

@interface TTLessonViewController: UIViewController
@property (nonatomic, strong) TTLesson *lesson;
@end
複製代碼

語言混編的一些思考

筆者在瞭解 Objective-C 與 Swift 混編時一直在思考語音爲何能混編,語言究竟是如何混編的?筆者瞭解的也很少,這裏只是談談本身的一些思考,拋磚引玉。服務器

對於語言混編,不妨嘗試着迴歸到程序本質的角度進行看待:

程序 = 數據結構 + 算法 / 數據 + 邏輯

在機器碼層面,全部編程語言都轉化成了機器指令:

  • 數據:內存訪問,一個基地址 Base + 偏移 Offset,讀取 Size 長度的內存。
  • 邏輯:用符號標記的一段機器碼,跳轉到符號標記的地址。

那語言想要混編,就須要在數據和邏輯之間創建鏈接

  • 數據:要麼內存佈局相同,能夠直接用;要麼互相瞭解轉換規則可以進行轉換;要麼使用通用格式進行通訊。
  • 邏輯:符號可以匹配上或者互相識別,方法調用的 Call Convenience 是相同的,可以互相跳轉。

最後發散一下,既然是創建鏈接,那麼經過 TCP/HTTP/IPC 等方式創建的通訊能夠當作更廣義上的混編。例如客戶端以 REST API 的方式向服務器端請求數據也能夠當作客戶端語言 Objective-C/Swift/Kotlin/Java 與服務器語言 Java/Go/Python 的混編,只是兩端之間使用 URL(邏輯)+ JSON(數據)這種通用格式/協議的方式創建起了鏈接。

Clang Module

Module 是 WWDC 2013 就引進的技術,在 Xcode 的 Build Setting 中能看到 Enable Modules (C and Objective-C) 的設置項,在 OTHER_CFLAGS 中也看到了 -fmodule-map-file=「xxx.modulemap」,可是一直對其不太瞭解,一項技術的出現老是爲了解決某些痛點,那 Module 是爲了處理哪些問題呢?

Module 解決什麼問題?

之前在 C/C++/Objective-C 中,源文件中引入的頭文件在編譯時須要進行展開,預處理宏等相關操做。這樣的方式存在如下幾種問題:

Header Fragile

因爲頭文件是一塊兒展開後再統一處理宏,當宏重名時,頭文件引入的順序就會致使不同的結果,而且宏只是簡單粗暴的文本替換,也存在宏污染的可能。例如,AppDelegate.m 中定義了一個 readonly 的宏,與 Objective-C 中屬性的關鍵字衝突了。

// iAd/ADBannerView.h
@interface ADBannerView : UIView
@property (nonatomic, readonly) ADAdtype type;
@end

// AppDelegate.m
#define readonly 0x01
#import <iAd/iAd.h>

@implementation AppDelegate
// ....
@end
複製代碼

因爲是文本替換,預處理完以後,ADBannerView 的 type 屬性中,readonly 被替換成了 0x01,致使編譯錯誤,而且很難定位到問題根源。

// AppDelegate.m
#define readonly 0x01
@interface ADBannerView : UIView
@property (nonatomic, 0x01) ADAdtype type;
@end

@implementation AppDelegate
// ....
@end
複製代碼

Compile Time

因爲每一個源文件中的頭文件都須要展開、預處理,假設有 M 個源文件,N 個頭文件,須要 M * N 的編譯時間。

爲了優化這個問題,Objective-C 引入了預編譯頭文件(Pre-Compiled Header),將公用的頭文件放入預編譯頭文件中預先進行編譯,而後在真正編譯工程時再將預先編譯好的產物加入到全部待編譯的 Source 中去,來加快編譯速度。

可是 PCH 文件會致使裏面的頭文件是全局可見的,至關於變成了全局依賴,可能會致使 Namespace Pollution。而且當 PCH 文件變成龐大時,仍是會致使預編譯頭文件的時間變長。

Link & Use

在使用框架時,須要 import 正確的頭文件,否則可能致使編譯問題。另外,僅僅 import 頭文件是不夠的,須要在設置中連接對應的庫,不是很方便。

Module 是什麼?

framework module UIKit {
	umbrella header "UIKit.h"
	module * {export *}
	link framework "UIKit"
}
複製代碼

Modules 至關於將框架進行了封裝,看一下上面 UIKit 的 modulemap 文件,Module 在 modulemap 中定義了框架的三大核心要素:

  • umbrella header 描述主要頭文件:UIKit.h
  • module 描述須要導出的子 Module:所有導出
  • link 描述須要連接的 Framework:UIKit

在實際編譯時,加入到一個用來存放已編譯添加過的 Modules 列表中。若是在編譯的文件引用到某個 Modules 的話,將首先在這個列表內查找,找到的話說明已經被加載過則直接使用已有的,若是沒有找到,則把引用的頭文件編譯後加入到這個表中。

所謂 umbrella header,就是 includes all of the headers in its directory,一個包含框架內全部須要開放頭文件的 Wrapper 頭文件。爲何叫」雨傘「呢?筆者猜想是「雨傘」能比較形象的描述這種頭文件的做用。能夠想象一下雨傘☂️的形狀和做用,最頂部的那個尖就像把全部東西集中起來,雨傘也將傘下的細節給遮蓋起來了。

@import ModuleName;
@import ModuleName.SubmoduleName;
複製代碼

經過 @import 使用 Module,而且 Module 支持 Submodule。若是在 Build Settings 中將 Enable Modules (C and Objective-C) 打開,不須要改變代碼,即便使用的是舊的 #import 方式,編譯器也會在編譯的時候自動地把可能的地方換成 Modules 的寫法去編譯的。

總之,Module 有以下特色:

  • 單獨編譯處理,宏不會互相影響
  • 編譯時間從 M * N 變成了 M + N
  • 自動連接相關庫

下一篇,將講一下如何將一個 Objective-C 與 Swift 混編的代碼,從依賴混亂不完備的 Git Submodule,一步步的抽成 Development Pod,最後變成二進制的過程與遇到的坑。

Article by Joe Shang

參考:

相關文章
相關標籤/搜索