目录

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>

  1. 在MM主工程中创建个新的Swift文件(空文件即可),让Xcode自动生成一个bridge-header,目的是营造一个Swift环境(之前一直想不修改主工程而只在pod中营造,但是很遗憾,最后以失败告终);

  2. 由于混编pod中依赖的repo需要支持module,但是MM中的pod水平参差不齐,大部分都没有支持module,这就限制我们在业务pod中混编时编译失败。而让pod一下子都支持module是一个不太现实的要求,所以我们暂时采用了一种折中的方案;

  3. Swift单独放一个pod中去,让Swift尽量少的依赖其他repo,然后业务pod再依赖Swift repo来调用Swift代码;

踩坑记录

  • pod中不支持bridging-header,所以混编pod中要想引用OC类,pod需要支持module
  • 混编的Swift库需要打成framework形式才可以编译成功,比如RxCocoaPromiseKit
  • 限于苹果本身机制和现有二进制方案实现问题,不支持 :modular_headers => true,所以使用:modular_headers => true 时临时需要添加参数:use_source_code => true,切换为代码编译;
  • SwiftOC混编的pod所依赖的库需要改为动态库,比如ZDFlexLayout内部为Swift与OC混编的,依赖了Yoga,需要把Yoga编为动态库。 报错如下图

/images/swiftocmix/undefine_symbol.png
undefine_symbol

实际操作

  1. 跨模块引用时需要把要暴露给外部的类或者函数的访问权限设置为 public,并标记为 @objc,同时需要继承自NSObject

  2. pod 中引用其他pod都是通过 @import 语法

  3. Swift依赖的repo需要module化,

    有3种方式:

    i: 在podfile中让所有的repo开启modular: use_modular_headers!

    ii: 只给某几个repo开启modular,举个例子:pod 'SDWebImage', :modular_headers => true

    iii: 让repo自己开启module支持,需要在podspec中修改下设置:spec.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } , 这个设置不管你开不开启modular开关,都会自动创建module

  4. 如果podspec中不设置DEFINES_MODULE=true,默认是不会生成module的,哪怕你在podspec中设置了module_map也不行,除非你在podfile中手动开启modular_hear你自己的modulemap才会生效

  5. 如果你手动创建了modulemap就不要设置DEFINES_MODULE=true了,因为笔者发现开启DEFINES_MODULE后它还会自己再生成一份xxx-umbraller文件。

    推荐让CocoaPods帮我们创建modulemap,如非你特别懂modulemap,不建议自己手动创建。

  6. 只需要把Swift用到的OC类放到umbrella中(后面说控制方法)

同一混编pod内OC调用Swift

在头文件中引入 #import <module-name>-Swift.h",然后就可以调用Swift类了

/images/swiftocmix/oc_import_swift.png
oc_import_swift

同一混编pod内Swift调用OC

同一pod中,把oc类引用放入umbrella中(默认就有了),然后需要这个文件能被找到。

  1. 一种方式是修改此文件的membershippublic,目的是为了把它移到public header中去(静态库形式pod中的文件默认都是project的,动态库形式的pod才会区分publicprivateproject

    修改起来成本比较高,不推荐

    /images/swiftocmix/member_ship.png
    member_ship

    /images/swiftocmix/header.png
    header

  2. 第二种方式是把这个文件的路径包含搜索路径中,可以通过设置podspec中的 spec.header_dir参数

    header_dir 可以是任意名字, 笔者一般会设置为./,即当前文件夹


    这个选项也不是万能的,你会发现就算设置了这个选项,也会出现报错的问题。建议业务方的pod如无必要,把类都放到private_header_files 中,减少umbrella 中的头文件数量。

    如下设置

    /images/swiftocmix/header_dir_set.png
    header_dir_set

静态库中的import需要是全路径的,而动态库中的搜索路径会被flatten,所以动态库不会出现此问题

不过这里有点需要注意的是,设置 header_dir 后需要同时设置 module_name,否则 modulename 默认会取 header_dir 的值。。。

其实官方文档上都有提到,惭愧

/images/swiftocmix/module_name.png
module_name

解惑

为什么能够混编?

能够互相调用的类都需要继承NSObject

Swift中的类和Objective-C中的类底层元数据(class metadata)是共用的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 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编译为静态库支持的不好,这不是苹果的锅,而是三方库未进行及时的适配,KingfisherRxCocoa都有问题

这两个库笔者已经提了pr 来解决这个问题,现已合入主分支,从 RxCocoa 6.1.0Kingfisher 6.1.0 开始都已支持编译为静态库;

2. 为什么改为动态库就可以正常编译通过了?

静态库需要使用绝对路径引用,而动态库强制把头文件平铺了,所以动态库能引到,静态库引不到

可以自己验证一下,改成 #import "<module-name>/xxxx.h" 之后你再编译一下

3. 为什么设置 header_dir 编译就不报错了?

默认情况下使用的是普通的header ,设置header_dir之后,pod会以header_dir为名称创建一个文件夹,然后把所有public出来的头文件引用放里面,umbrella引用头文件的时候其实指向的都是这里;

见源码

/images/swiftocmix/header_dir_code.png
header_dir_code

4. pod中没有bridging-header为什么Swift还能引用Objective-C类?

podumbrella文件其实就相当于是主工程中的bridging-header

5. 为什么Xcode生成的hmap对我们的项目并没起到什么作用?

上面已经提到静态库的形式下我们的类文件的membershipprojecthmap生成的是#import "xx.h"形式的引用路径的cache,而我们通常引用库文件的方式为#import <A/B.h>,这就导致我们的引用根本就没办法命中hmap中的映射缓存(pcm),所以最终还是会走search_path的查找逻辑。而且由于Xcode中的USE_HEADERMAP设置默认是开启的,Xcode在编译期还会自动创建对我们用处不大的hmap,而这个过程间接拖慢了我们的编译速度。

6. publicprivateproject区别

  • 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.

ProjectPrivate的权限或者说是作用基本一致,都是私有化的一种方式,只是Project权限的头文件是不会放到编译产物中的,注意说的是头文件,而Private头文件会放到编译产物中,只是告诉编译器不要暴漏给外界。CocoaPods是通过Pods->Headers->Public/Private目录管理头文件的引用,来控制对某一文件的访问权限的。

推荐设置

podspec中在不指定private_header_filesproject_header_files的时候source_files路径下的文件默认全都是public的,而public的头文件默认都会放到umbrella中,这样很容易导致umbrella中头文件爆炸,尤其是业务pod(比如我们的直播业务有2300多个头文件),特别影响编译速度。

解决办法:把那些暴露给swift的头文件放到public_header_files中,其他的头文件则默认变成project类型。或者是把全部头文件默认指定为private_header_filesproject_header_files,然后把需要公开的放到public_header_files中,尽量减少umbrella中头文件的数量。

贴一下供参考的Swift podspec,请按需修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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'
end

CocoaPods 骚操作:

用踩坑中提到的RxCocoa做例子,为了编译成功,我们需要把它指定为动态库,而其他的保持不变,这种需求我们可以在 pre_install 阶段动态修改编译模式:把dynamic_framework数组中的repo编译为framework,其他未指定的默认还是静态库。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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
end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pre_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

参考