Swift与Objective-C混编
前言
本文是笔者在解决混编时的一些记录,有些东西可能已经发生了变化。而且由于只是随手记录,写的比较乱,各位看官见谅~~~
笔者负责的业务是以
pod模块的形式存在于工程中的,所以以下调研的方案只针对于pod中的混编场景,在MM主工程混编几乎是无缝,没什么可说的。。。推荐大家浏览下
CocoaPods(podfile & podspec)的API,没几个,花费不了几分钟,但是却能帮助大家少踩很多的坑,一本万利~
前期方案
暂时采用折中方案,把Swift独立成一个pod,然后业务pod再引用Swift pod。目的是减少依赖,避免引用不规范的repo。
如果打算在同一
pod中混编,只要把你依赖的库都支持module即可,而且需要修改一下你们引用外部repo头文件的形式,比如#import "SDWebImage.h"改为#import <SDWebImage/SDWebImage.h>
在MM主工程中创建个新的
Swift文件(空文件即可),让Xcode自动生成一个bridge-header,目的是营造一个Swift环境(之前一直想不修改主工程而只在pod中营造,但是很遗憾,最后以失败告终);由于混编
pod中依赖的repo需要支持module,但是MM中的pod水平参差不齐,大部分都没有支持module,这就限制我们在业务pod中混编时编译失败。而让pod一下子都支持module是一个不太现实的要求,所以我们暂时采用了一种折中的方案;把
Swift单独放一个pod中去,让Swift尽量少的依赖其他repo,然后业务pod再依赖Swift repo来调用Swift代码;
踩坑记录
pod中不支持bridging-header,所以混编pod中要想引用OC类,pod需要支持module混编的Swift库需要打成framework形式才可以编译成功,比如RxCocoa、PromiseKit- 限于苹果本身机制和现有二进制方案实现问题,不支持
:modular_headers => true,所以使用:modular_headers => true时临时需要添加参数:use_source_code => true,切换为代码编译; 报错如下图Swift与OC混编的pod所依赖的库需要改为动态库,比如ZDFlexLayout内部为Swift与OC混编的,依赖了Yoga,需要把Yoga编为动态库。
实际操作
跨模块引用时需要把要暴露给外部的类或者函数的访问权限设置为
public,并标记为@objc,同时需要继承自NSObject类pod中引用其他pod都是通过@import语法Swift依赖的repo需要module化,有3种方式:
i: 在
podfile中让所有的repo开启modular:use_modular_headers!ii: 只给某几个repo开启modular,举个例子:
pod 'SDWebImage', :modular_headers => trueiii: 让repo自己开启module支持,需要在podspec中修改下设置:
spec.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }, 这个设置不管你开不开启modular开关,都会自动创建module如果
podspec中不设置DEFINES_MODULE=true,默认是不会生成module的,哪怕你在podspec中设置了module_map也不行,除非你在podfile中手动开启modular_hear你自己的modulemap才会生效如果你手动创建了
modulemap就不要设置DEFINES_MODULE=true了,因为笔者发现开启DEFINES_MODULE后它还会自己再生成一份xxx-umbraller文件。推荐让
CocoaPods帮我们创建modulemap,如非你特别懂modulemap,不建议自己手动创建。只需要把
Swift用到的OC类放到umbrella中(后面说控制方法)
同一混编pod内OC调用Swift
在头文件中引入 #import <module-name>-Swift.h",然后就可以调用Swift类了
同一混编pod内Swift调用OC
同一pod中,把oc类引用放入umbrella中(默认就有了),然后需要这个文件能被找到。
一种方式是修改此文件的
membership为public,目的是为了把它移到public header中去(静态库形式pod中的文件默认都是project的,动态库形式的pod才会区分public、private、project)修改起来成本比较高,不推荐
第二种方式是把这个文件的路径包含搜索路径中,可以通过设置
podspec中的spec.header_dir参数header_dir可以是任意名字, 笔者一般会设置为./,即当前文件夹这个选项也不是万能的,你会发现就算设置了这个选项,也会出现报错的问题。建议业务方的pod如无必要,把类都放到
private_header_files中,减少umbrella中的头文件数量。如下设置
静态库中的
import需要是全路径的,而动态库中的搜索路径会被flatten,所以动态库不会出现此问题
不过这里有点需要注意的是,设置 header_dir 后需要同时设置 module_name,否则 modulename 默认会取 header_dir 的值。。。
其实官方文档上都有提到,惭愧
解惑
为什么能够混编?
能够互相调用的类都需要继承
NSObject
Swift中的类和Objective-C中的类底层元数据(class metadata)是共用的
// objc4-818.2
// objc-runtime-new.h
typedef struct objc_class *Class;
typedef struct objc_object *id;
struct objc_object {
private:
isa_t isa;
// ...
};
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
// ...
};
struct swift_class_t : objc_class {
uint32_t flags;
uint32_t instanceAddressOffset;
uint32_t instanceSize;
uint16_t instanceAlignMask;
uint16_t reserved;
uint32_t classSize;
uint32_t classAddressOffset;
void *description;
// ...
void *baseAddress() {
return (void *)((uint8_t *)this - classAddressOffset);
}
};1. XCode9 & Cocopoads 1.5 之后,不是已经支持把Swift编译为静态库了吗,为什么会报错呢?
第三方库对于把混编pod编译为静态库支持的不好,这不是苹果的锅,而是三方库未进行及时的适配,像Kingfisher、RxCocoa都有问题
这两个库笔者已经提了
pr来解决这个问题,现已合入主分支,从RxCocoa 6.1.0、Kingfisher 6.1.0开始都已支持编译为静态库;
2. 为什么改为动态库就可以正常编译通过了?
静态库需要使用绝对路径引用,而动态库强制把头文件平铺了,所以动态库能引到,静态库引不到
可以自己验证一下,改成 #import "<module-name>/xxxx.h" 之后你再编译一下
3. 为什么设置 header_dir 编译就不报错了?
默认情况下使用的是普通的header ,设置header_dir之后,pod会以header_dir为名称创建一个文件夹,然后把所有public出来的头文件引用放里面,umbrella引用头文件的时候其实指向的都是这里;
见源码
4. pod中没有bridging-header为什么Swift还能引用Objective-C类?
pod中umbrella文件其实就相当于是主工程中的bridging-header。
5. 为什么Xcode生成的hmap对我们的项目并没起到什么作用?
上面已经提到静态库的形式下我们的类文件的membership是project,hmap生成的是#import "xx.h"形式的引用路径的cache,而我们通常引用库文件的方式为#import <A/B.h>,这就导致我们的引用根本就没办法命中hmap中的映射缓存(pcm),所以最终还是会走search_path的查找逻辑。而且由于Xcode中的USE_HEADERMAP设置默认是开启的,Xcode在编译期还会自动创建对我们用处不大的hmap,而这个过程间接拖慢了我们的编译速度。
6. public、private、project区别
Public: The interface is finalized and meant to be used by your product’s clients. A public header is included in the product as readable source code without restriction.
Private: The interface isn’t intended for your clients or it’s in early stages of development. A private header is included in the product, but it’s marked “private”. Thus the symbols are visible to all clients, but clients should understand that they’re not supposed to use them.
Project: The interface is for use only by implementation files in the current project. A project header is not included in the target, except in object code. The symbols are not visible to clients at all, only to you.
Project和Private的权限或者说是作用基本一致,都是私有化的一种方式,只是Project权限的头文件是不会放到编译产物中的,注意说的是头文件,而Private头文件会放到编译产物中,只是告诉编译器不要暴漏给外界。CocoaPods是通过Pods->Headers->Public/Private目录管理头文件的引用,来控制对某一文件的访问权限的。
推荐设置
podspec中在不指定private_header_files或project_header_files的时候source_files路径下的文件默认全都是public的,而public的头文件默认都会放到umbrella中,这样很容易导致umbrella中头文件爆炸,尤其是业务pod(比如我们的直播业务有2300多个头文件),特别影响编译速度。
解决办法:把那些暴露给swift的头文件放到public_header_files中,其他的头文件则默认变成project类型。或者是把全部头文件默认指定为private_header_files或project_header_files,然后把需要公开的放到public_header_files中,尽量减少umbrella中头文件的数量。
贴一下供参考的Swift podspec,请按需修改
Pod::Spec.new do |spec|
spec.name = "foo"
spec.version = "0.0.1"
spec.summary = "foo"
spec.description = <<-DESC
我是一只小柯基
DESC
spec.homepage = "https://foo/bar/abc"
spec.license = "MIT"
spec.platform = :ios, "12.0"
spec.source = {
:git => "https://foo/bar/abc.git",
:tag => "#{spec.version}"
}
spec.swift_versions = ['5.1']
publicHeaders = Dir["Source/Room/PublicHeaders/*.h"]
privateHeaders = Dir["Source/Room/**/*.{h}"] - publicHeaders
spec.source_files = 'Source/Room/**/*.{h,m,swift}'
spec.public_header_files = publicHeaders
# 下面这行可有可无,设置的话会放到private中,不设置则等价于 `spec.project_header_files = privateHeaders`,会放到project中
# spec.private_header_files = privateHeaders
spec.module_name = spec.name
spec.header_dir = "./"
spec.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
}
spec.dependency 'RxCocoa'
spec.dependency 'Cartography', '~> 4.0.0'
spec.dependency 'ZDFlexLayoutKit'
endCocoaPods 骚操作:
用踩坑中提到的RxCocoa做例子,为了编译成功,我们需要把它指定为动态库,而其他的保持不变,这种需求我们可以在 pre_install 阶段动态修改编译模式:把dynamic_framework数组中的repo编译为framework,其他未指定的默认还是静态库。
pre_install do |installer|
#以framework形式存在的pod
dynamic_frameworks = ['RxSwift', 'RxCocoa', 'RxRelay']
Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {}
installer.pod_targets.each do |pod|
if dynamic_frameworks.include?(pod.name)
def pod.build_type;
Pod::BuildType.dynamic_framework
end
end
end
endpre_install do |installer|
#以静态库形式存在的pod
static_library = ['Masonry', 'SDWebImage']
Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {}
installer.pod_targets.each do |pod|
if static_library.include?(pod.name)
def pod.build_type;
Pod::BuildType.static_library
end
end
end
end





