14、iOS強化 --- Module與Swift庫(OC方法轉(zhuǎn)Swift)

Module(模塊)

  • Module(模塊) : 最小的代碼單元。
    一個Module是機器代碼和數(shù)據(jù)的最小單位,可以獨立于其他代碼單位進(jìn)行鏈接。通常,Module是通過編譯單個源文件生成的目標(biāo)文件。
    例如:當(dāng)前的test.m被編譯成目標(biāo)文件test.o,當(dāng)前的目標(biāo)文件就代表一個Module。但是有一個問題,Module在調(diào)用的時候會產(chǎn)生開銷,比如我們在使用一個靜態(tài)庫的時候可以這樣使用:
@import TestStaticFramework;

這個靜態(tài)庫中可能包含了許多的.o文件。豈不是要導(dǎo)入很多的Module?
并不需要,在靜態(tài)鏈接的時候,也就是靜態(tài)庫鏈接到主項目或者動態(tài)庫,最后生成可執(zhí)行文件或者動態(tài)庫時,靜態(tài)鏈接器可以把多個Module鏈接優(yōu)化成一個,來減少原本多個Module直接調(diào)用的問題。

  • module.modulemap 用來描述頭文件module之間的映射關(guān)系。
    下面使我們經(jīng)常用到的AFNetworking產(chǎn)生的.modulemap文件:
    image.png

    那么.modulemap里面的這些代碼又是什么意思呢?
/// 聲明一個module A,它映射的頭文件是 A.h
module A {
  header "A.h"
}

/// 聲明一個module B,它映射的頭文件是 B.h
module B {
  header "B.h"  
  /// 導(dǎo)出 A,這里假設(shè) "B.h" 映入了 "A.h";
  /// 那么 導(dǎo)出 的意思就是將"B.h"引入的其他的"頭文件" 也暴露出來。 
  export A
}

---
通常我們看到的`module`里面是 {export *} ,如上面的`AFNetworking.modulemap`
"*" 是通配符,意思是所有引入的`頭文件`,全部 導(dǎo)出。

我們來讀一下AFNetworking.modulemap

/// framework module 名稱 AFNetworking
framework module AFNetworking {
  /// umbrella <目錄> 傘柄  <目錄>/.h
  /// AFNetworking-umbrella.h 傘柄 
  /// AFNetworking-umbrella.h/.h 傘骨(里面引入的所有的其他頭文件)
  umbrella header "AFNetworking-umbrella.h"

  /// 重新導(dǎo)出
  export *
  /// module: 子module*
  module * { export * }
}

image.png

如果要顯示指明子module的名稱,要加上explicit關(guān)鍵字:

/// 假設(shè)在 `AFNetworking.modulemap` 中顯示指明 `子module` 
framework module AFNetworking {
  /// umbrella <目錄> 傘柄  <目錄>/.h
  /// AFNetworking-umbrella.h 傘柄 
  /// AFNetworking-umbrella.h/.h 傘骨(里面引入的所有的其他頭文件)
  umbrella header "AFNetworking-umbrella.h"

  /// 重新導(dǎo)出
  export *
  /// module: 子module*
  module * { export * }

  /// 假設(shè) `子module` 叫 `SubAFN`
  explicit module SubAFN {
    header "SubAFN.h"
    export *
  }
}
  • 我們的Xcode是自動開啟Module
    所有開發(fā)中我們引入頭文件的三種形式:
    #include#import 、@import 最終都會被轉(zhuǎn)換成@import。
    image.png
  • Module到底有什么用呢?
    在我們傳統(tǒng)的#include引入頭文件,并且沒有開啟Module的情況下。頭文件被引入多少次就要被編譯所少次。比如:
    A.h,此時被use.cuse_1.c引入,那么此時A.h就要被編譯兩次。

此時Module的好處就提現(xiàn)出來了。它會預(yù)先把A.h編譯成二進(jìn)制文件,那么后面不管有多少個文件使用到A.h,只有一個A.h的二進(jìn)制文件(除非A.h被改動)。
下面我們來演示一下:
1、module.modulemap:

module A {
  header "A.h"
}

module B {
  header "B.h"
  export A
}

2、終端指令:

# -fmodules:允許使用module語言來表示頭文件
# -fmodule-map-file:module map的路徑。如不指明默認(rèn)module.modulemap
# -fmodules-cache-path:編譯后的module緩存路徑

clang  -fmodules -fmodule-map-file=module.modulemap -fmodules-cache-path=../prebuilt -c use.c -o use.o

3、B.h 引用 A.h, use.c 引用 B.h:

image.png

可以看到生成了兩個二級制文件,這就是我們頭文件預(yù)先編譯的產(chǎn)物:

image.png


Swift庫

  • 我們都知道,在平常的開發(fā)中,swiftOC代碼的混編,我們都是使用橋接文件(Bridging-header)。
    但是在Framework中,是不允許使用橋接文件的。因此:我們不能使用橋接文件的方式進(jìn)行混編 Objective-C 代碼的引用,需要用 Swift Module 進(jìn)行模塊間的引用。
    i :首先創(chuàng)建自己的.modulemap文件:

    image.png

    ii :在FrameworkBuild Settings 里面配置Module Map File:
    image.png

    iii :此時已經(jīng)可以在swift文件中使用OC的代碼了(同時OC文件中使用swift代碼也是一樣的):
    image.png

  • 這樣就引出了另外一個問題:如果我不想將YSStudent暴露到Framework之外怎么辦?
    這個時候我們就可以引入.private.modulemap文件

framework module YSSwiftFramework_Private {
    module YSOCStudent {
        header "YSOCStudent.h"
        export *
    }
}
  • 注意:①_Private首字母大寫,② 文件名中要有.private。
    然后配置一下Build Settings
    image.png
  • 注意:這里雖然配置了.private.modulemap文件,但并不是真正的隱藏。我們通過模塊訪問,依然是可以訪問到的:
@import YSSwiftFramework_Private.YSOCStudent;

這樣做雖然做不到完全的隱藏,但是可以達(dá)到提醒使用者的作用,告訴用戶這是一個私有庫,不要隨便使用。

這里還有另外一種方法可以達(dá)到上面的要求:swiftOC遵守同一個協(xié)議,通過協(xié)議來調(diào)用OC的代碼,從而做到隱藏OC代碼的效果。


Swift Module

  • Xcode 9 之后,Swift 開始支持靜態(tài)庫。
    Swift 沒有頭文件的概念,那么我們外界要使用 Swift中用 public修飾的函數(shù)該怎么辦呢?
    Swift庫中引入了一個全新的文件.swiftmodule。
    .swiftmodule包含序列化過的AST(抽象語法樹,Abstract Syntax Tree),也包含STL(Swift 中間語言,Swift Intermediate Language)。
    比如我們剛剛的工程中,Xcode就給我們自動生成了.swiftmodule文件。

我們要怎么用Swift Module呢?下面我們通過swift靜態(tài)庫來看一下。


Swift 靜態(tài)庫的合并

  • 1、首先我們創(chuàng)建SwiftA & SwiftB 兩個靜態(tài)庫,并且兩個靜態(tài)庫中都包含YSSwiftTeacher,名字和函數(shù)一模一樣(埋點,看看后面會不會報錯):
    image.png
  • 我們將生成的庫文件拷貝出來,做一下合并:
libtool -static SwiftA SwiftB -o libSwiftC.a

此時會有一個警告,提示我們合并的兩個庫文件中有相同的文件。

image.png

這就是我們使用libtool的好處,我們之前講過可以使用ar來合并靜態(tài)庫,但是使用ar的情況下,先解壓再合并,可能發(fā)生文件的替換。
我們來查看下當(dāng)前靜態(tài)庫里面有哪些.o文件:
image.png

可以看到,兩個庫里面的同名.o文件都在,并沒有產(chǎn)生替換。

  • 2、接下來我們將.framework里面原先的簽名文件,配置文件刪除,只留下HeadersModules。因為我們需要的是合并后的靜態(tài)庫,所有原先的簽名和配置沒有用了。
    image.png
  • 3、將Modules里面的文件,提到和``Headers一個等級,這樣做是為了后續(xù)SwiftC編譯器能夠找到對應(yīng)的Module文件。要不然可能會找不到。
    image.png
  • 4、將我們合并后的靜態(tài)庫libSwiftC.a拖到工程里面,并且配置xcconfig文件
    image.png
HEADER_SEARCH_PATHS = $(inherited) "SwiftC/SwiftA.framework/Headers" "SwiftC/SwiftB.framework/Headers"

// OTHER_CFLAGS: 傳遞給 用來編譯C或者OC的編譯器,當(dāng)前就是clang
OTHER_CFLAGS="-fmodule-map-file=${SRCROOT}/SwiftC/SwiftA.framework/module.modulemap" "-fmodule-map-file=${SRCROOT}/SwiftC/SwiftB.framework/module.modulemap"

// SWIFT_INCLUDE_PATHS: 傳遞給SwiftC編譯器,告訴它去下面的路徑查找module.file
SWIFT_INCLUDE_PATHS="${SRCROOT}/SwiftC/SwiftA.framework" "${SRCROOT}/SwiftC/SwiftB.framework"

這里強調(diào)一下: 配置OTHER_CFLAGS是為了給OC代碼用;配置SWIFT_INCLUDE_PATHS是為了給Swift代碼用。

image.png

  • 5、編譯運行我們發(fā)現(xiàn)兩點不同:
    iOC文件中同時使用AB沒有問題:
    image.png

    iiSwift文件中不允許這樣使用:
    image.png

這也與兩門語言的特性有關(guān)系,OC是運行時語言,我們已經(jīng)告訴編譯器,頭文件的地址,所以只要運行時能夠找到對應(yīng)的符號就不會報錯。但是Swift就不一樣,在編譯的時候檢測到可能存在的隱患就會報錯。

另外,不管是靜態(tài)庫還是動態(tài)庫的合并,大家盡量用不同的文件夾隔開要合并的庫的庫文件,這樣可以預(yù)防Header里面有相同的文件(也就是我們上面的埋點)。


Swift配置

在我們?nèi)粘5拈_發(fā)過程中,Swift去使用OC的一些方法的時候,Swift會進(jìn)行一些優(yōu)化。
比如:

  • 函數(shù) :
/// OC 定義
- (void)ysOCFuncationWithValue:(NSString *)value WithKey:(NSString *)key;
/// Swift 使用
let obj = YSObject.init()
obj.ysOCFuncation(withValue: "123", withKey: "key")

/**************此時我們?nèi)绻胍?guī)范一下Swift中的函數(shù)名可以這樣*****************/
- (void)ysOCFuncationWithValue:(NSString *)value WithKey:(NSString *)key
    NS_SWIFT_NAME(ysAction(key:value:));
/// Swift 使用
let obj = YSObject.init()
obj.ysAction(key: "key", value: "123")

如果想要定義私有方法:

// NS_REFINED_FOR_SWIFT從現(xiàn)在開始,Swift的Clang Importer將做一些額外的工作并將該方法導(dǎo)入為私有方法,并以雙下劃線字符開頭__,例如:
//- (BOOL)changeTeacherName:(nullable NSDictionary<NSString *, id> *)options;
- (BOOL)changeTeacherName:(nullable NSDictionary<NSString *, id> *)options
    NS_REFINED_FOR_SWIFT;
  • 上面的寫法雖然可行,但是也存在一些弊端。如果我們要大批量的去修改已經(jīng)穩(wěn)定的OC的庫的時候,就會是一個繁重的工作。
  • 此時我們可以使用.apinotes文件來驚醒修改。
    命名規(guī)則:SDK名稱.apinotes。并且該文件一定要放到SDK的目錄里面去。
Name: OCFramework // SDK 名稱
Classes:
- Name: LGToSwift // 類名
  SwiftName: ToSwift // 在Swift中的名稱
  Methods: // 方法
  - Selector: "changeTeacherName:" // 方法名
    Parameters:
    - Position: 0
      Nullability: O
    MethodKind: Instance
    SwiftPrivate: true
    Availability: nonswift // 設(shè)置在Swift中不能用
    AvailabilityMsg: "這個不能用" // 提示
  - Selector: "initWithName:"
    MethodKind: Instance
    DesignatedInit: true

image.png

這個時候我們就可以結(jié)合腳本批量修改。
另外還有很多的用法,可以參考:API Notes

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容