最近公司项目准备引进swift,因为现在工程已经完结了组件化不再是简略的单仓工程,所以需求进行混编改造。下面记载一下自己对工程进行混编改造的思考以及进程。

混编原理

看了许多文档,比较罕见讲混编原理的,这儿简略介绍一下言语进行混编的基本逻辑,因为咱们都知道swift和OC能够混编,可是他们为啥能混编呢?

工程swift与OC混编改造

首先简略的过一下编译以及静态链接的流程:

  1. 预编译阶段,B.m将引进A.h以及B.h悉数copy到B.m(这种预编译办法咱们称它为文本模型)

  2. 编译阶段,B.m将B自身的办法地址悉数编入符号表,可是编译的进程会碰到对A.function的调用,这个时分编译器检查A.function是否有声明,明确有(跟着A.h头文件copy过来的),可是并没有相应的完结,因而会将A.function的引证做一个特别符号,并在重定位表中记载,等待静态链接成功后修改为指向A.function实在地点的地址

  3. 完结一切.o文件的符号表的合并,依据重定位表中的记载需求从头修改符号的地址,完结实在的A.function地址的特别符号替换。

整个进程中只需生成的A.o以及B.o中间文件是按照编译器预期的格局安排的,那么静态链接器ld的就能依据格局缝合成终究产品:可运转的app。因而无论A和B是用什么言语编写的,只需编译器B自身能够辨认出对应的言语A的声明,那么就能够在B的编译进程中符号A.function,并产出规矩格局的.o文件。终究在静态链接器的协助下完结对A.function的调用。(由上也能够看出头文件其实仅仅承当了符号符号的作用,其他的编译部分都是在完结文件中完结的)

工程swift与OC混编改造

以上便是言语混编的基础。

备注:JAVA的编译逻辑稍有不同,这儿只评论java转class,不评论class经过虚拟机变成实在地址的进程,java不像c系言语相同由链接器寻找终究符号地址,他们是由写代码的人直接运用全限定词指定符号在哪个文件,因而java编译成class以后符号地址就确定了,这也是java不需求头文件的逻辑,java与kotlin混编只需求保证编译出来的字节码标准一致即可混编,因为编译某个文件对其他文件有引证,编译器会马上对引证文件进行编译生成class,因为java和kotlin都能够辨认出字节码的规矩,因而能够辨认出相应的办法调用然后完结混编

swift与OC混编

了解了言语混编的基础之后咱们再来落实到具体的言语:swift与OC的混编。

1. swift调用OC

swift的编译器swiftc包含了clang编译器的大量的功用,因而swift调用OC,其实便是依靠于swiftc将OC文件的声明转成swift的声明(.swiftiinterface),然后swift直接运用swift语法的OC办法声明,完结了对OC办法完结的调用。如下图所示(都是xcode主动生成的)

工程swift与OC混编改造

工程swift与OC混编改造

2. OC调用swift

因为历史原因,OC的编译器clang是无法辨认swift言语的,因而要想让OC能够辨认swift的声明需求依靠于swiftc编译器将swift声明转成OC声明(FZCache-swift.h),然后OC直接运用OC语法的swift办法声明,完结对swift办法完结的调用。

工程swift与OC混编改造

工程swift与OC混编改造

不管是swift声明转OC声明,仍是OC声明转swift声明,以上两个进程中都暗含了一个关键的要素:头文件的查找,只要找到相应的声明转换后的头文件的位置,才干拜访到办法声明。才干终究完结混编。

头文件查找

1. 根据文本模型的头文件查找

便是咱们常规运用的计划#import头文件,跟#include自身差异不大,除了会主动去重,可是他们处理头文件的逻辑是相同的,每次编译一个.m文件都要从头对此.m文件中的头文件引进进行复制粘贴,因而理论上时间复杂度为O(m*n),另外因为采用的是复制粘贴替换的逻辑,因而在处理一些宏界说的时分容易犯错,比方可能会存在某个界说

#define nonatomic @"nonatomic"

这个宏界说可能在某个文件中是没有任何问题的,可是假如有人运用了@property (nonatomic)这样的属性的时分就会导致代码出现错误,关键因为预编译采用的是复制的办法,即便是编译器再次报错,也会让间接引进了这个宏界说声明的开发者一下子难以查找到真正的问题位置

2. clang module

根据以上的问题,苹果提出了clang module的头文件查找计划,该计划声明晰一种特定的文件安排形式,以静态库为例,静态库分成两种.a和framework,clang module规矩静态库有必要以如下办法进行资源的安排

工程swift与OC混编改造

工程swift与OC混编改造

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文件中存储了对整个模块以及模块内部子模块的二进制描绘。

工程swift与OC混编改造

因为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,编译器就会报错。

运用办法

子仓的互相调用形式:

工程swift与OC混编改造

主工程内部调用办法

工程swift与OC混编改造