Skip to content

Latest commit

 

History

History
235 lines (157 loc) · 9.91 KB

CocoaPods组件中Swift与Objective-C混编.md

File metadata and controls

235 lines (157 loc) · 9.91 KB

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

image.png

注意事项:

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

  2. 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伞文件。

    笔者推荐让pod自己创建modulemap,一是省事,二是大部人对modulemap这东西都不了解,你敢保证你创建的modulemap没毛病吗?

CocoaPods 骚操作:

用踩坑中提到的RxCocoa做例子,为了编译成功,我们需要把它打成动态库,而其他的保持不变,这种需求我们可以在 pre_install 阶段动态修改编译模式;

pre_install do |installer|    
    $dynamic_framework = ['RxSwift', 'RxCocoa', 'RxRelay'] #以framework形式存在的pod
    Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {}
    installer.pod_targets.each do |pod|
      if $dynamic_framework.include?(pod.name)
        def pod.build_type;
          Pod::BuildType.dynamic_framework
        end
      end
    end
end

上面操作是把dynamic_framework数组中的repo编译为framework,其他未指定的默认还是静态库

同一混编pod内OC调用Swift

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

image.png

同一混编pod内Swift调用OC

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

  1. 一种方式是修改此文件的membershippublic,目的是为了把它移到public header中去(默认是project的)

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

image.png
image.png

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

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


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

    如下设置

image.png

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

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

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

image.png

为什么能够混编?

能够互相调用的类都需要集成NSObject Swift中的类和OC中的类底层元数据(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);
    }
};

后续

需要平台制定规范,推动pod库的改造,以达到无缝混编

福利

贴一下我的Swift podspec,没什么特别的,仅仅指定了一下Swift兼容的版本而已,仅供参考,按需修改

Pod::Spec.new do |spec|
  spec.name         = "KLiaoMarrySwift"
  spec.version      = "0.0.1"
  spec.summary      = "坑"
  spec.description  = <<-DESC
    真特么坑
                   DESC
  spec.homepage     = "https://foo/bar/abc"
  spec.license      = "MIT"
  spec.platform     = :ios, "10.0"
  spec.source       = { 
    :git => "https://foo/bar/abc.git", 
    :tag => "#{spec.version}" 
  }
  spec.swift_versions = ['5.1', '5.2', '5.3']
  spec.source_files = "Source/**/*.{h,m,swift}"
  spec.module_name = spec.name
  spec.header_dir = "./"

  spec.pod_target_xcconfig = {
    'DEFINES_MODULE' => 'YES',
    #'SWIFT_OBJC_BRIDGING_HEADER' => "$(PODS_ROOT)/#{spec.name}/KLiaoMarrySwift-Bridging-Header.h"
  }

  spec.dependency 'RxSwift'
  spec.dependency 'RxCocoa'
  spec.dependency 'Cartography', '~> 4.0.0'
  spec.dependency 'ZDFlexLayoutKit'

end

解惑:

1. Xcode9 & Cocopoads 1.5 之后,不是已经支持把Swift编译为静态库了吗,为什么会报错呢?

第三方库对于把混编pod编译为静态库支持的不好,这不是苹果的锅,而是三方库的锅。像 `Kingfisher`、`RxCocoa`都有问题,这两个库我已经提了`pr` 来解决这个问题,现已合入主分支,从 `RxCocoa 6.1.0`、`Kingfisher 6.1.0` 开始都已支持编译为静态库;

RxCocoa #2281 Kingfisher #1608

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

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

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

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

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

见源码

image.png

参考: