iOS高級(jí)強(qiáng)化--011:module & Swift庫

module(模塊):最小的代碼單元

一個(gè)module是機(jī)器代碼和數(shù)據(jù)的最小單位,可以獨(dú)立于其他代碼單位進(jìn)行鏈接

通常,module是通過編譯單個(gè)源文件生成的目標(biāo)文件。例如:當(dāng)前的test.m被編譯成目標(biāo)文件test.o時(shí),當(dāng)前的目標(biāo)文件就代表了一個(gè)module

這里有一個(gè)問題,module在調(diào)用的時(shí)候會(huì)產(chǎn)生開銷,當(dāng)使用一個(gè)靜態(tài)庫的時(shí):

@import TestStaticFramework;

如果靜態(tài)庫中包含許多.o文件。這豈不是會(huì)導(dǎo)入很多module

當(dāng)然不會(huì)。在靜態(tài)鏈接的時(shí)候,也就是靜態(tài)庫鏈接到主項(xiàng)目或者動(dòng)態(tài)庫,最終生成可執(zhí)行文件或者動(dòng)態(tài)庫時(shí),靜態(tài)鏈接器可以把多個(gè)module鏈接優(yōu)化成一個(gè),來減少本來多個(gè)module直接調(diào)用的問題

module原理

未開啟module時(shí),當(dāng)B文件導(dǎo)入A.h,C文件又導(dǎo)入了A.hB.h

  • #includeA.h會(huì)跟隨B文件和C文件編譯多次。使用#include造成C文件重復(fù)包含A.h,所以當(dāng)C文件編譯時(shí),A.h又會(huì)被編譯多次,相當(dāng)于編譯了N * M
  • #importA.h依然會(huì)跟隨B文件和C文件編譯多次。但使用#import可以避免C文件重復(fù)包含A.h,此時(shí)C文件編譯,A.h只編譯一次,相當(dāng)于編譯了N + M

開啟module時(shí),頭文件會(huì)被預(yù)先編譯成二進(jìn)制文件,并且每個(gè)頭文件只會(huì)被編譯一次。此時(shí)無論有多少文件導(dǎo)入頭文件,都不會(huì)被重復(fù)編譯,只需要執(zhí)行N次即可

Cat目錄中,有A.hB.h兩個(gè)頭文件,還有一個(gè)use.c代碼和一個(gè)module.modulemap文件。和Cat目錄平級(jí),創(chuàng)建prebuilt目錄,用來存儲(chǔ)編譯后的module緩存

打開A.h文件,寫入以下代碼:

#ifdef ENABLE_A
void a() {}
#endif

打開B.h文件,寫入以下代碼:

#import "A.h"

打開use.c文件,寫入以下代碼:

#import "B.h"
void use() {
#ifdef ENABLE_A
 a();
#endif
}
  • use.c文件中,使用了B.h,同時(shí)B.h使用了A.h

打開module.modulemap文件,寫入以下代碼:

module A {
 header "A.h"
}

module B {
 header "B.h"
 export A
}
  • module.modulemap文件的作用,它是用來描述頭文件module之間映射的關(guān)系
  • 定義了名稱為AB的兩個(gè)module
  • module A中,定義了header A.h,表示module AA.h的映射關(guān)系
  • module B中,定義了header B.h,和A同理。export A表示將B.h導(dǎo)入的A.h頭文件重新導(dǎo)出

通過clang命令,開啟module并將use.c編譯成目標(biāo)文件

clang  -fmodules -fmodule-map-file=module.modulemap -fmodules-cache-path=../prebuilt -c use.c -o use.o
  • -fmodules:允許使用module語言來表示頭文件
  • -fmodule-map-filemodule map的路徑。此參數(shù)缺失,默認(rèn)找module.modulemap文件。如果文件不存在,執(zhí)行會(huì)報(bào)錯(cuò)
  • -fmodules-cache-path:編譯后的module緩存路徑

打開prebuilt目錄,兩個(gè).pcm文件,分別對(duì)應(yīng)A.hB.h,它們就是預(yù)編譯頭文件后的產(chǎn)物

moduleXcode中是默認(rèn)開啟的

如果在Build Settings中,將Enable Modules設(shè)置為NO,導(dǎo)入頭文件將不能使用@import方式

開啟module后,項(xiàng)目中導(dǎo)入頭文件,無論使用#include#import、@import中何種方式,最終都會(huì)映射為@import方式

module解讀

查看實(shí)際開發(fā)中使用的.modulemap文件,例如:AFNetworking

打開AFNetworking.framework中的module.modulemap文件

framework module AFNetworking {
 umbrella header "AFNetworking-umbrella.h"

 export *
 module * { export * }
}
  • 定義module名稱為AFNetworking,模塊是framework
  • umbrella:可以理解為傘柄。一把雨傘的傘柄下有很多傘骨umbrella的作用是指定一個(gè)目錄,這個(gè)目錄即為傘柄,目錄下所有.h頭文件即為傘骨
  • umbrella header AFNetworking-umbrella.h:指定module AFNetworking映射AFNetworking-umbrella.h文件中所有.h頭文件
  • export **表示通配符。將AFNetworking-umbrella.h文件中,所有.h頭文件重新導(dǎo)出
  • module * { export * }:創(chuàng)建子module,使用*通配符,將AFNetworking-umbrella.h中導(dǎo)入的頭文件,按照頭文件名稱命名為子module名稱。再使用export *將子module中導(dǎo)入的頭文件重新導(dǎo)出

打開AFNetworking-umbrella.h文件

#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif

#import "AFNetworking.h"
#import "AFHTTPSessionManager.h"
#import "AFURLSessionManager.h"
#import "AFCompatibilityMacros.h"
#import "AFNetworkReachabilityManager.h"
#import "AFSecurityPolicy.h"
#import "AFURLRequestSerialization.h"
#import "AFURLResponseSerialization.h"
#import "AFAutoPurgingImageCache.h"
#import "AFImageDownloader.h"
#import "AFNetworkActivityIndicatorManager.h"
#import "UIActivityIndicatorView+AFNetworking.h"
#import "UIButton+AFNetworking.h"
#import "UIImageView+AFNetworking.h"
#import "UIKit+AFNetworking.h"
#import "UIProgressView+AFNetworking.h"
#import "UIRefreshControl+AFNetworking.h"
#import "WKWebView+AFNetworking.h"

FOUNDATION_EXPORT double AFNetworkingVersionNumber;
FOUNDATION_EXPORT const unsigned char AFNetworkingVersionString[];
  • AFNetworking-umbrella.h文件,相當(dāng)于傘柄
  • AFNetworking-umbrella.h文件中,導(dǎo)入的所有.h頭文件,相當(dāng)于傘骨

項(xiàng)目中,使用@import AFNetworking,可以.出一個(gè)子module列表,它對(duì)應(yīng)的也是傘柄下的傘骨列表

查看開源項(xiàng)目AsyncDisplayKit中的module.modulemap

打開module.modulemap文件

framework module AsyncDisplayKit {
 umbrella header "AsyncDisplayKit.h"
 
 export *
 module * {
   export *
 }
 
 explicit module ASControlNode_Subclasses {
   header "ASControlNode+Subclasses.h"
   export *
 }
 
 explicit module ASDisplayNode_Subclasses {
   header "ASDisplayNode+Subclasses.h"
   export *
 }
 
}
  • 定義module名稱為AsyncDisplayKit,模塊是framework
  • 定義傘柄AsyncDisplayKit.h
  • AsyncDisplayKit.h文件中,所有.h頭文件重新導(dǎo)出
  • 創(chuàng)建子module,使用*通配符,將AsyncDisplayKit.h中導(dǎo)入的頭文件,按照頭文件名稱命名為子module名稱。將子module中導(dǎo)入的頭文件重新導(dǎo)出
  • explicit:顯示指明子module名稱
官方文檔

更多API可查看 官方文檔

自定義module

搭建LGOCFramework項(xiàng)目

LGOCFramework是一個(gè)動(dòng)態(tài)庫項(xiàng)目,創(chuàng)建項(xiàng)目后,系統(tǒng)默認(rèn)并不提供.modulemap文件

項(xiàng)目編譯后,在LGOCFramework.framework中的Modules目錄下,會(huì)自動(dòng)生成module.modulemap文件

打開module.modulemap文件,里面存儲(chǔ)了基本的頭文件與module之間映射的關(guān)系

/* module.modulemap */

framework module LGOCFramework {
   // umbrella<目錄>
   umbrella header "LGOCFramework.h"
 
   explicit module LGTeacher {
       header "LGTeacher.h"
       export *
   }
   explicit module LGStudent {
       header "LGStudent.h"
       export *
   }
}

如果想對(duì)module進(jìn)行配置,例如:定義子module,此時(shí)需要自己創(chuàng)建modulemap文件

在項(xiàng)目LGOCFramework目錄下,創(chuàng)建LGOCFramework.modulemap文件

LGOCFramework.modulemap文件加入到項(xiàng)目中

BuildSetting中,修改Module Map File配置項(xiàng):

  • Module Map File:設(shè)置.modulemap文件路徑,填寫${SRCROOT}之后的路徑即可

打開LGOCFramework.modulemap文件,寫入以下代碼:

framework module LGOCFramework {
   umbrella header "LGOCFramework.h"
 
   explicit module LGTeacher {
       header "LGTeacher.h"
       export *
   }
   explicit module LGStudent {
       header "LGStudent.h"
       export *
   }
}
  • 定義module名稱為LGOCFramework,模塊是framework
  • 定義傘柄LGOCFramework.h
  • 顯示指明子module名稱為LGTeacher,映射LGTeacher.h,將LGTeacher.h中導(dǎo)入的頭文件重新導(dǎo)出
  • 顯示指明子module名稱為LGStudent,映射LGStudent.h,將LGStudent.h中導(dǎo)入的頭文件重新導(dǎo)出

項(xiàng)目編譯后,在LGOCFramework.framework中的Modules目錄下,生成的依然是名稱為module.modulemap的文件

由于系統(tǒng)默認(rèn)識(shí)別.modulemap文件的名稱是module.modulemap,所以自定義的LGOCFramework.modulemap文件在編譯后,名稱依然是module.modulemap,但里面的內(nèi)容已經(jīng)生效

搭建LGApp項(xiàng)目

LGApp是一個(gè)App項(xiàng)目

創(chuàng)建MulitProject.xcworkspace,加入LGOCFramework動(dòng)態(tài)庫項(xiàng)目。LGApp鏈接LGOCFramework動(dòng)態(tài)庫

打開ViewController.m文件,導(dǎo)入LGOCFramework動(dòng)態(tài)庫的頭文件,和module中的配置完全一致

至此自定義module成功

Swift庫使用OC代碼
module映射

搭建LGSwiftFramework項(xiàng)目

LGSwiftFramework是一個(gè)Swift動(dòng)態(tài)庫項(xiàng)目

打開LGOCStudent.h文件,寫入以下代碼:

#import <Foundation/Foundation.h>

@interface LGOCStudent : NSObject

- (void)speek;

@end

打開LGOCStudent.m文件,寫入以下代碼:

#import "LGOCStudent.h"

@implementation LGOCStudent

- (void)speek {
   NSLog(@"LGOCStudent--speek");
}

@end

打開LGSwiftTeacher.swift文件,寫入以下代碼:

import Foundation

@objc open class LGSwiftTeacher: NSObject {

   public func speek() {
       let s = LGOCStudent()
       s.speek()
       print("speek!")
   }

   @objc public func walk() {
       print("walk!")
   }
}

LGSwiftTeacher.swift文件中,調(diào)用了OC代碼。在日常項(xiàng)目中,使用橋接文件即可。但在Framework項(xiàng)目中,沒有橋接文件的概念,此時(shí)編譯報(bào)錯(cuò)

解決辦法:

創(chuàng)建LGSwiftFramework.modulemap文件,寫入以下代碼:

framework module LGSwiftFramework {
   umbrella "Headers"
   export *
}
  • 定義module名稱為LGSwiftFramework,模塊是framework
  • 定義傘柄Headers目錄
  • Headers目錄下所有.h頭文件重新導(dǎo)出

BuildSetting中,修改Module Map File配置項(xiàng):

Headers目錄下的.h頭文件

此時(shí)LGSwiftTeacher.swift文件中,使用的OC代碼不再報(bào)錯(cuò),項(xiàng)目編譯成功

App使用Swift庫

承接自定義module的案例

打開MulitProject.xcworkspace文件,加入LGSwiftFramework動(dòng)態(tài)庫項(xiàng)目。LGApp鏈接LGSwiftFramework動(dòng)態(tài)庫

LGApp中,打開ViewController.m文件,使用@import LGSwiftFramework導(dǎo)入頭文件,只能找到一個(gè).Swift

LGSwiftFramework項(xiàng)目在編譯時(shí),系統(tǒng)在.framework中生成的module.modulemap文件,會(huì)自動(dòng)生成以下代碼:

framework module LGSwiftFramework {
   umbrella "Headers"
   export *
}

module LGSwiftFramework.Swift {
   header "LGSwiftFramework-Swift.h"
   requires objc
}

但這種導(dǎo)入方式,無法使用LGOCStudent

解決辦法:

使用#import方式,也無法找到LGOCStudent.h頭文件

LGSwiftFramework中的.modulemap文件,將Headers目錄下所有.h文件全部重新導(dǎo)出。所以可以強(qiáng)行導(dǎo)入<LGSwiftFramework/LGOCStudent.h>,導(dǎo)入后LGOCStudent類可以正常使用

#import "ViewController.h"
#import <LGSwiftFramework/LGOCStudent.h>

@implementation ViewController

- (void)viewDidLoad {
   [super viewDidLoad];
   LGOCStudent *student=[LGOCStudent new];
}

@end

另一種解決辦法,通過.modulemap文件,暴露出LGOCStudent

打開LGSwiftFramework.modulemap文件,改為以下代碼:

framework module LGSwiftFramework {
   umbrella "Headers"
   export *
}

module LGSwiftFramework.LGOCStudent {
   header "LGOCStudent.h"
   requires objc
}

再次編譯項(xiàng)目,使用@import方式,此時(shí)可以找到LGOCStudent

導(dǎo)入LGSwiftFramework.LGOCStudent后,LGOCStudent類可以正常使用

#import "ViewController.h"
@import LGSwiftFramework.LGOCStudent;

@implementation ViewController

- (void)viewDidLoad {
   [super viewDidLoad];
   LGOCStudent *student=[LGOCStudent new];
}

@end
私有module映射

在某些情況下,是否使用特定頭文件用于區(qū)分指定庫的公共API私有API

例如:一個(gè)庫可能包含分別提供公共API私有API的頭文件LGOCStudent.hLGOCStudent_Private.h。此外,LGOCStudent_Private.h可能僅在某些版本的庫中可用,而在其他版本庫中不可用。使用統(tǒng)一的module.modulemap文件無法表達(dá)這一點(diǎn)

LGSwiftFramework項(xiàng)目

創(chuàng)建LGOCStudent_Private.h文件,寫入以下代碼:

#import <Foundation/Foundation.h>

@interface LGOCStudent_Private : NSObject

- (void)speek;

@end

創(chuàng)建LGOCStudent_Private.m文件,寫入以下代碼:

#import "LGOCStudent_Private.h"

@implementation LGOCStudent_Private

- (void)speek {
   NSLog(@"LGOCStudent_Private--speek");
}

@end

創(chuàng)建LGSwiftFramework.private.modulemap文件,寫入以下代碼:

framework module LGSwiftFramework_Private {
   module LGOCStudent {
       header "LGOCStudent_Private.h"
       export *
   }
}
  • 私有.modulemap文件的名稱,中間的.private一定要加,這個(gè)是命名規(guī)則
  • 定義module名稱為LGSwiftFramework_Private,模塊是framework
  • 定義私有module名稱,后面一定要加Private后綴,并且首字母大寫
  • 定義module名稱為LGOCStudent,映射LGOCStudent_Private.h
  • LGOCStudent_Private.h中導(dǎo)入的頭文件重新導(dǎo)出

BuildSetting中,修改Private Module Map File配置項(xiàng):

LGApp項(xiàng)目

打開ViewController.m文件,導(dǎo)入LGOCStudent.hLGOCStudent_Private.h頭文件,此時(shí)它們被徹底分開了

#import "ViewController.h"
@import LGSwiftFramework.LGOCStudent;
@import LGSwiftFramework_Private.LGOCStudent;

@implementation ViewController

- (void)viewDidLoad {
   [super viewDidLoad];

   LGOCStudent *student=[LGOCStudent new];
   LGOCStudent_Private *sp=[LGOCStudent_Private new];
}

@end
Swift靜態(tài)庫

Xcode 9之后,Swift開始?持靜態(tài)庫

Swift沒有頭?件的概念,外界如何使?Swiftpublic修飾的類和函數(shù)?

Swift庫中引?了?個(gè)全新的?件.swiftmodule

  • .swiftmodule包含序列化過的AST(抽象語法樹,Abstract Syntax Tree),也包含SILSwift中間語?,Swift Intermediate Language
Swift靜態(tài)庫合并

搭建LGSwiftA項(xiàng)目

LGSwiftA是一個(gè)Swift靜態(tài)庫項(xiàng)目

打開LGSwiftTeacher.swift文件,寫入以下代碼:

import Foundation

@objc open class LGSwiftTeacher: NSObject {

   public func speek() {
       print("speek!")
   }

   @objc public func walk() {
       print("walk!")
   }
}

搭建LGSwiftB項(xiàng)目

LGSwiftB是一個(gè)Swift靜態(tài)庫項(xiàng)目

打開LGSwiftTeacher.swift文件,寫入以下代碼:

import Foundation

@objc open class LGSwiftTeacher: NSObject {

   public func speek() {
       print("speek!")
   }

   @objc public func walk() {
       print("walk!")
   }
}

創(chuàng)建MulitProject.xcworkspace,加入LGSwiftA、LGSwiftB兩個(gè)靜態(tài)庫項(xiàng)目

創(chuàng)建Products目錄,和MuiltProject.xcworkspace平級(jí)

LGSwiftA、LGSwiftB項(xiàng)目中,選擇Build Phases,創(chuàng)建Run Script,寫入以下代碼:

cp -Rv -- "${BUILT_PRODUCTS_DIR}/" "${SOURCE_ROOT}/../Products"
  • 使用cp命令,將編譯后的.framework文件拷貝到Products目錄

編譯LGSwiftA、LGSwiftB項(xiàng)目,打開Products目錄,.framework文件已成功拷貝

使用libtool命令,合并LGSwiftALGSwiftB兩個(gè)靜態(tài)庫

libtool -static \
-o \
libLGSwiftC.a \
LGSwiftA.framework/LGSwiftA \
LGSwiftB.framework/LGSwiftB

由于LGSwiftA、LGSwiftB項(xiàng)目中,都存在了相同的LGSwiftTeacher.swift文件,使用libtool命令合并后提示警告

libtool: warning same member name (LGSwiftTeacher.o) in output file used for input files: LGSwiftA.framework/LGSwiftA(LGSwiftTeacher.o) and: LGSwiftB.framework/LGSwiftB(LGSwiftTeacher.o) due to use of basename, truncation and blank padding

使用ar -t libLGSwiftC.a命令,查看libLGSwiftC.a的文件列表

__.SYMDEF
LGSwiftA_vers.o
LGSwiftTeacher.o
LGSwiftB_vers.o
LGSwiftTeacher.o

如果是OC動(dòng)態(tài)庫,.framework中可以舍棄Modules目錄,將兩個(gè)靜態(tài)庫的頭文件拷貝到一起即可

Swift動(dòng)態(tài)庫,包含了x.swiftmodule目錄,里面的.swiftmodule文件不能舍棄,此時(shí)應(yīng)該如何處理?

解決辦法:

Products目錄下,創(chuàng)建LGSwiftC目錄,將庫文件libLGSwiftC.a拷貝到LGSwiftC目錄下

仿照Cocoapods生成三方庫的目錄結(jié)構(gòu),在LGSwiftC目錄下,創(chuàng)建Public目錄,將LGSwiftA.frameworkLGSwiftB.framework拷貝到Public目錄下

打開LGSwiftA.frameworkLGSwiftB.framework文件,將里面的庫文件、.plist文件、簽名等信息全部刪除,最終只保留HeadersModules兩個(gè)目錄

雖然生成.framework時(shí),自動(dòng)創(chuàng)建了Modules目錄。但編譯時(shí),.modulemap文件和x.swiftmodule目錄,應(yīng)該和Headers目錄平級(jí)

.modulemap文件和x.swiftmodule目錄,從Modules目錄移動(dòng)到.framework文件下,和Headers目錄平級(jí)。然后刪除Modules目錄

此時(shí)靜態(tài)庫合并完成

App使用合并后的靜態(tài)庫

搭建LGApp項(xiàng)目

LGApp是一個(gè)App項(xiàng)目

LGSwiftC目錄,拷貝到LGApp項(xiàng)目的根目錄下

libLGSwiftC.a庫文件,拖動(dòng)到項(xiàng)目中的Frameworks目錄

勾選Copy items if needed,點(diǎn)擊Finish

創(chuàng)建xcconfig文件,并配置到Tatget上,寫入以下代碼:

HEADER_SEARCH_PATHS = $(inherited) '${SRCROOT}/LGSwiftC/Public/LGSwiftA.framework/Headers'
HEADER_SEARCH_PATHS = $(inherited) '${SRCROOT}/LGSwiftC/Public/LGSwiftB.framework/Headers'
  • 指定頭文件路徑Header Search Paths

ViewController.m中,使用module方式導(dǎo)入LGSwiftA,編譯報(bào)錯(cuò)

  • 使用module方式,還需要加載modulemap文件的路徑

打開xcconfig文件,改為以下代碼:

HEADER_SEARCH_PATHS = $(inherited) '${SRCROOT}/LGSwiftC/Public/LGSwiftA.framework/Headers'
HEADER_SEARCH_PATHS = $(inherited) '${SRCROOT}/LGSwiftC/Public/LGSwiftB.framework/Headers'

OTHER_CFLAGS = $(inherited) '-fmodule-map-file=${SRCROOT}/LGSwiftC/Public/LGSwiftA.framework/module.modulemap'
OTHER_CFLAGS = $(inherited) '-fmodule-map-file=${SRCROOT}/LGSwiftC/Public/LGSwiftB.framework/module.modulemap'
  • OTHER_CFLAGS:傳遞給用來編譯C或者OC的編譯器,當(dāng)前就是clang
  • 加載modulemap文件的路徑
  • 對(duì)應(yīng)Build Setting中的配置項(xiàng)

打開ViewController.m,寫入以下代碼:

#import "ViewController.h"
@import LGSwiftA;

@implementation ViewController

- (void)viewDidLoad {
   [super viewDidLoad];
   
   LGSwiftTeacher *teacher = [LGSwiftTeacher new];
}

@end

編譯成功,Swift靜態(tài)庫中的LGSwiftTeacher類,可以在OC下正常使用

但此時(shí)還有另一個(gè)問題:

LGSwiftTest.swift中,使用import導(dǎo)入LGSwiftA,還是編譯報(bào)錯(cuò)

  • Swift中,還需要加載swiftmodule文件的路徑

打開xcconfig文件,改為以下代碼:

HEADER_SEARCH_PATHS = $(inherited) '${SRCROOT}/LGSwiftC/Public/LGSwiftA.framework/Headers'
HEADER_SEARCH_PATHS = $(inherited) '${SRCROOT}/LGSwiftC/Public/LGSwiftB.framework/Headers'

OTHER_CFLAGS = $(inherited) '-fmodule-map-file=${SRCROOT}/LGSwiftC/Public/LGSwiftA.framework/module.modulemap'
OTHER_CFLAGS = $(inherited) '-fmodule-map-file=${SRCROOT}/LGSwiftC/Public/LGSwiftB.framework/module.modulemap'

SWIFT_INCLUDE_PATHS = $(inherited) '${SRCROOT}/LGSwiftC/Public/LGSwiftA.framework'
SWIFT_INCLUDE_PATHS = $(inherited) '${SRCROOT}/LGSwiftC/Public/LGSwiftB.framework'
  • SWIFT_INCLUDE_PATHS:傳遞給SwiftC編譯器
  • 在指定路徑下查找swiftmodule文件
  • 對(duì)應(yīng)Build Setting中的配置項(xiàng)

打開LGSwiftTest.swift文件,寫入以下代碼:

import Foundation
import LGSwiftA

@objc open class LGSwiftTest: NSObject {

   public override init() {
       super.init()
       
       let t = LGSwiftTeacher()
       t.speek()
   }
}

編譯成功,Swift靜態(tài)庫中的LGSwiftTeacher類,可以在Swift下正常使用

LGSwiftA.frameworkLGSwiftB.framework兩個(gè)靜態(tài)庫中,都存在LGSwiftTeacher,有時(shí)甚至?xí)嬖陬^文件相同的情況。所以在案例中,手動(dòng)構(gòu)建的目錄結(jié)構(gòu),可以有效避免相同頭文件的沖突。并且在使用的時(shí)候,導(dǎo)入的頭文件是誰的,使用的LGSwiftTeacher對(duì)應(yīng)就是誰的

鏈接靜態(tài)庫,只要沒指定-all_load-ObjC參數(shù),默認(rèn)會(huì)使用-noall_load參數(shù)。所以在同一個(gè)文件內(nèi),即使導(dǎo)入兩個(gè)頭文件,當(dāng)鏈接一個(gè)文件找到代碼后,就不會(huì)鏈接另一個(gè),因此也不會(huì)沖突

OC映射到Swift方式

搭建OCFramework項(xiàng)目

OCFramework是一個(gè)OC動(dòng)態(tài)庫項(xiàng)目

打開LGToSwift.h文件,寫入以下代碼:

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger, LGTeacherName) {
   LGTeacherNameHank,
   LGTeacherNameCat,
};

typedef NSString * LGTeacherNameString;

extern NSString *getTeacherName(void);
extern NSString * const LGTeacherCat;
extern LGTeacherNameString const LGTeacherHank;

@interface LGToSwift : NSObject

- (nullable NSString *)teacherNameForIndex:(NSUInteger)index;

- (BOOL)changeTeacherName:(nullable NSDictionary<NSString *, id> *)options;

@end

打開LGToSwift.m文件,寫入以下代碼:

#import "LGToSwift.h"

NSString *getTeacherName(void) {
   return nil;
}

NSString * const LGTeacherCat = @"Cat";
LGTeacherNameString const LGTeacherHank = @"Hank";

@implementation LGToSwift

- (nullable NSString *)teacherNameForIndex:(NSUInteger)pageIndex {
   return nil;
}

- (BOOL)changeTeacherName:(nullable NSDictionary<NSString *, id> >*)options {
   return NO;
}

@end

搭建SwiftProject項(xiàng)目

SwiftProject是一個(gè)App項(xiàng)目

創(chuàng)建MulitProject.xcworkspace,加入OCFramework動(dòng)態(tài)庫項(xiàng)目。SwiftProject鏈接OCFramework動(dòng)態(tài)庫

ViewController.swift中,使用OCFramework動(dòng)態(tài)庫的方法,出現(xiàn)以下問題:

  • 無法對(duì)LGTeacherNameString類型的屬性賦值枚舉值
  • teacherName方法的命名,被改為teacherName(for:),但我們預(yù)期的是teacherName(forIndex:)
  • changeTeacherName方法,我們希望它作為私有方法,并以雙下劃線字符__開頭

解決辦法:

可以使用特定宏,改變映射規(guī)則

OCFramework中,打開LGToSwift.h文件,改為以下代碼:

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger, LGTeacherName) {
   LGTeacherNameHank,
   LGTeacherNameCat,
};

typedef NSString * LGTeacherNameString NS_TYPED_EXTENSIBLE_ENUM;

extern NSString *getTeacherName(void);
extern NSString * const LGTeacherCat;
extern LGTeacherNameString const LGTeacherHank;

@interface LGToSwift : NSObject

- (nullable NSString *)teacherNameForIndex:(NSUInteger)index NS_SWIFT_NAME(teacherName(forIndex:));

- (BOOL)changeTeacherName:(nullable NSDictionary<NSString *, id> *)options NS_REFINED_FOR_SWIFT;

@end
  • NS_TYPED_EXTENSIBLE_ENUM:屬性指示編譯器,使用struct(swift_wrapper(struct)屬性),通過指定NS_TYPED_ENUM宏,編譯器被指示使用enum(swift_wrapper(enum)屬性)
  • NS_SWIFT_NAME:通過指定NS_SWIFT_NAME宏,可以添加一些詳細(xì)信息以使函數(shù)清晰可見
  • NS_REFINED_FOR_SWIFT:通過指定NS_REFINED_FOR_SWIFT宏,SwiftClang Importer將做一些額外的工作,將該方法導(dǎo)入為私有方法,并以雙下劃線字符__開頭

SwiftProject中,打開ViewController.swift文件,寫入以下代碼:

import UIKit
import OCFramework

class ViewController: UIViewController {

   override func viewDidLoad() {
       super.viewDidLoad()

       let Hank: LGTeacherNameString = .hank
       
       let teacher: LGToSwift = LGToSwift()
       teacher.teacherName(forIndex: 1)
   }
}

extension LGToSwift {
   func change() -> Bool {
       return __changeTeacherName(nil)
   }
}

問題解決,OC中的方法和屬性,在Swift中使用符合預(yù)期

但另一個(gè)問題又出現(xiàn)了:

通過指定宏的方式,需要修改原有代碼。如果一個(gè)使用OC開發(fā)的SDK需要適配Swift,需要為每一個(gè)方法或?qū)傩灾付ê?,這將是工程浩大且費(fèi)時(shí)費(fèi)力的事情

解決辦法:

使用.apinotes文件,代替宏的方式

OCFramework目錄下,創(chuàng)建OCFramework.apinotes文件

OCFramework中,將OCFramework.apinotes文件加入到項(xiàng)目

  • .apinotes文件必須要放在SDK的目錄中,采用yaml格式書寫,類似JSON格式

打開OCFramework.apinotes文件,寫入以下代碼:

---
Name: OCFramework
Classes:
- Name: LGToSwift
#  SwiftName: ToSwift
 Methods:
 - Selector: "changeTeacherName:"
   Parameters:
   - Position: 0
     Nullability: O
   MethodKind: Instance
   SwiftPrivate: true
   Availability: nonswift
   AvailabilityMsg: "prefer 'deinit'"
  • changeTeacherName:方法,在Swift中設(shè)置為不可用

編譯項(xiàng)目,顯示自定義錯(cuò)誤提示:prefer 'deinit'

.apinotes文件最終會(huì)被放入編譯后的.framework

官方文檔

更多API可查看 官方文檔

總結(jié)

module(模塊):最小的代碼單元,表示頭文件與目標(biāo)文件的關(guān)系
modulemap:最小的代碼單元,表示頭文件與目標(biāo)文件的映射

定義一個(gè)module

  • export:導(dǎo)出當(dāng)前代表的頭文件使用的頭文件
  • export *:匹配目錄下所有的頭文件
  • module *:目錄下所有的頭文件都當(dāng)作一個(gè)子module
  • explicit *:顯式聲明一個(gè)module的名稱

Swift庫使用OC代碼:

  • 不能使用橋接文件
  • OC的頭文件放到modulemap
  • 使用私有modulemap更好的表達(dá)公共API私有API

Swift靜態(tài)庫合并

  • 必須保留.swiftmodule文件(Swift的頭文件)
  • 使用libtool命令,合并靜態(tài)庫本身
  • 用到的頭文件、Swift頭文件以及modulemap文件,通過目錄的形式放到一起
  • OC要用合并的靜態(tài)庫:clang: other c flags-fmodule-map-file <modulemap path>
  • Swift要用合并的靜態(tài)庫:SwiftC :other swift flags顯式告訴SwiftC <modulemap dir>

OC映射到Swift方式

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

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

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