最近、同社のプロジェクトでSwiftの導入が予定されていますが、現在のプロジェクトはコンポーネント化が完了し、単純な単一のウェアハウスプロジェクトではなくなったため、再構築する必要があります。私の考えと、プロジェクトをミックスし、変換するプロセスを記録させてください。
混合原理
多くのドキュメントを読んでも、混合の原理について説明しているものはほとんどありません。Swift と OC が混合できることは誰もが知っているので、ここで言語混合の基本ロジックを簡単に紹介します。
まず、コンパイルと静的リンクのプロセスを実行します。
-
プリコンパイル段階では、Bm は Ah と Bh のすべてのコピーを Bm にインポートします (このプリコンパイル方法をテキスト モデルと呼びます)。
-
コンパイル段階では、Bm は B のすべてのメソッド アドレスをシンボル テーブルにコンパイルしますが、コンパイル プロセス中に A.function への呼び出しが発生します。このとき、コンパイラは A.function に宣言があるかどうかを確認し、それは明らかです (Ah ヘッダー ファイルのコピーが来ました) が、対応する実装がないため、A.function の参照は特別なマークとしてマークされ、静的リンクが成功した後に再配置テーブルに記録されます。 、A.function の実アドレスを指すように変更されます。
-
すべての .o ファイルのシンボル テーブルのマージを完了し、再配置テーブル内のレコードに従ってシンボルのアドレスを再変更し、実際の A.function アドレスの特殊マークの置換を完了します。
プロセス全体において、生成された Ao および Bo 中間ファイルがコンパイラーが予期する形式に従って編成されている限り、静的リンカー ld をその形式に従って最終製品 (実行可能なアプリ) に組み込むことができます。したがって、A と B がどのような言語で書かれているかに関係なく、コンパイラ B 自体が対応する言語 A の宣言を認識できる限り、B のコンパイル プロセスで A.function をマークし、.o ファイルを生成できます。指定された形式。最後に、静的リンカーの助けを借りて、A.function への呼び出しが完了します。(上記からもわかるように、ヘッダファイルは実際にはシンボルマークの役割を担うだけで、残りのコンパイル部分は実装ファイル内で完結します)
以上が言語混合の基本です。
備考: JAVA のコンパイル ロジックは少し異なります。ここでは、仮想マシンを介してクラスを実際のアドレスに変換するプロセスではなく、Java からクラスへの変換についてのみ説明します。C ベースの言語とは異なり、リンカーは最終的なシンボル アドレスを見つけます。 Javaでは、コードを記述することによって作成されます ユーザーは完全修飾子を直接使用して、シンボルがどのファイルにあるかを指定するため、シンボルのアドレスは、Javaがクラスにコンパイルされた後に決定されます これは、Javaが実行しないロジックでもありますヘッダー ファイルが必要です。Java と kotlin の混合コンパイルでは、コンパイルされたバイトコード標準 Unification が混合できることを確認するだけで済みます。ファイルのコンパイルには他のファイルへの参照があるため、コンパイラは参照されたファイルをすぐにコンパイルしてクラスを生成します。 java と kotlin はバイトコードのルールを認識できるため、対応するメソッドを認識できます。呼び出してミックスを完了します。
SwiftとOCの混合
言語混合の基礎を理解した後、特定の言語、つまり Swift と OC の混合を実装してみましょう。
1. Swift は OC を呼び出します
Swift のコンパイラ swiftc には、clang コンパイラの関数が多数含まれているため、swift が OC を呼び出すとき、実際には swiftc に依存して OC ファイルの宣言を swift 宣言 (.swiftiinterface) に変換し、その後 swift が直接使用します。 Swift 構文の OC メソッド宣言。これで OC メソッド実装の呼び出しが完了します。以下の図に示すように (すべて xcode によって自動生成されます)
2. OC は迅速に呼び出します
歴史的な理由により、OC のコンパイラ Clang は Swift 言語を認識できないため、OC が Swift 宣言を認識できる場合は、swiftc コンパイラに依存して Swift 宣言を OC 宣言 (FZCache-swift.h) に変換してから、OC を直接使用する必要があります。 OC 構文の swift メソッド宣言を使用して、swift メソッド実装の呼び出しを完了します。
Swift ステートメントから OC ステートメント、または OC ステートメントから Swift ステートメントであるかどうかに関係なく、ヘッダー ファイルの検索という重要な要素が上記の 2 つのプロセスに含まれます。対応するステートメントの変換により、メソッドにアクセスできるようになります。最終的にミックスを完成させるために。
ヘッダーファイルの検索
1.テキストモデルに基づくヘッダファイル検索
就是我们常规使用的方案#import头文件,跟#include本身区别不大,除了会自动去重,但是他们处理头文件的逻辑是一样的,每次编译一个.m文件都要重新对此.m文件中的头文件引入进行复制粘贴,因此理论上时间复杂度为O(m*n),另外由于采用的是复制粘贴替换的逻辑,因此在处理一些宏定义的时候容易出错,比如可能会存在某个定义
#define nonatomic @"nonatomic"
这个宏定义可能在某个文件中是没有任何问题的,但是如果有人使用了@property (nonatomic)这样的属性的时候就会导致代码出现错误,关键由于预编译采用的是复制的方式,即便是编译器再次报错,也会让间接引入了这个宏定义声明的开发者一下子难以查找到真正的问题位置
2. clang module
基于以上的问题,苹果提出了clang module的头文件查找方案,该方案声明了一种特定的文件组织形式,以静态库为例,静态库分成两种.a和framework,clang module规定静态库必须以如下方式进行资源的组织
module使用以下方案对头文件进行访问:
@import FZCache.FZKVCache
当编译器读取到FZCache的时候就会从特定存储路径中查看是否存在有FZCache这个组件空间(也就是这里说的module),然后查询其中是否有FZKVCache的缓存产物,如果有则直接引用,如果没有就先找到FZCache.framework这个文件夹,然后进入Headers文件夹查询FZKVCache.h头文件,如果可以找到,再进入Modules文件查看是否有modulemap文件,如果有则启用module,在特定存储路径中创建一份单独的编译空间用于存放预编译缓存,否则报错,确认有modulemap文件后继续查看此描述文件中是否包含了FZKVCache.h,
framework module FZCache {
umbrella header "FZCache-umbrella.h"
export *
module * { export * }
}
module FZCache.Swift {
header "FZCache-Swift.h"
requires objc
}
#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 "FZCache.h"
#import "FZKVCache.h"
FOUNDATION_EXPORT double FZCacheVersionNumber;
FOUNDATION_EXPORT const unsigned char FZCacheVersionString[];
显然FZCache-umbrella.h头文件中是包含FZKVCache.h的,因此编译生成FZKVCache的预编译产物放入FZCache module空间中,以备下次使用。
所以启用了clang module以后,组件只需编译一次,从理论上极大的降低了编译时间。
swiftmodule可以认为是clang module的升级版,基本上逻辑大同小异,但是针对swift的有一些特定的优化,我们可以简单的把swiftmodule和modulemap对应起来。在swiftmodule文件中存储了对整个模块以及模块内部子模块的二进制描述。
由于swiftc编译器只能通过clang module和swiftmodule识别到framework的头文件(如果是本target内部的头文件其实swiftc是能直接识别出来的,比如在主仓中swift通过桥接文件写入#import oc的头文件就可以识别到这些头文件),因此如果需要在swift仓库中引入OC仓库,就必须要对OC仓库进行clang module化。
备注:xcode对#import <A/A.h>做了优化,如果确认能找到modulemap文件,则启用clang module编译,转成@import A.A,如果未能找到则转成我们普通的#include <A/A.h>文本复制替换操作
鉴于目前所有的仓库都有可能需要使用混编,因此需要对所有的组件进行clang module化。
实现方案
我们将基于cocoapods完成所有仓库的module化。开启方法有多种。
1.use_framework!
2.use_modular_headers
3.自己写脚本生成modulemap,并组织好头文件。
这里我们选用use_framework!选项,即在开启所有仓库module化的同时,将生成产物从.a转变为framework.碰到头文件报错的位置就修改引用方式解决问题。要注意的是module化具有传递性,如果A开启了module,但是A依赖的B没有开启module,编译器就会报错。
使用方式
子仓的互相调用模式:
主工程内部调用方式