Alibaba.com Swift混编踩坑实践——工程篇

4,513 阅读5分钟

前言

很多iOS工程都是基于Object-C开发,再逐步向Swift演进,演进过程中不可避免要进行Swift混编。Swift模块需要支持LLVM Module规范,混编工程会遇到各种Module编译错误。这对于不熟悉的同学来说简直是灾难,严重影响开发效率。本文介绍了大型工程Swift混编的工程问题和解决思路,希望对大家有所帮助。

常见错误1:Could not build module xxx

当一个OC模块引用了Swift模块就产生了混编,混编的OC模块依赖链中的所有模块都必须满足LLVM Module Standard。如果AModule依赖了BModule,BModule不符合Module规范,构建会报 “could not build module BModule”的错误。

解决方案1:通过一下设置“Framework Module允许导入非modular的头文件” 临时跳过问题

Approach1:模块的Podspec中设置

CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES

Approach2: 在Podfile里设置

post_install do|installer|
installer.pods_project.build_configuration_list.build_configurations.eachdo|configuration|
configuration.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES']='YES'
end
end

解决方案2:将BModule改造为符合LLVM Module标准(推荐)

常见错误2:No such module 'XXXModule'

原因1:A模块的依赖树里不存在B模块

如果“Pod A” 需要import “BModule”,“AModule”的Podspec描述文件需要声明对于BModule的依赖,否则就会报“No such module 'XXXModule'”错误。Cocoapod会将依赖关系转化成XCode的FRAMEWORK_SEARCH_PATHS或LIBRARY_SEARCH_PATHS,因此只需要检测xcconfig是否存在对应的配置,就能判断Podspec描述是否缺失依赖。

step 1: 打开A模块的xcconfig文件

Pods/Target Support Files/AModule/AModule.xcconfig

Step 2: 查看xcconfig里的frameowork和library配置,如果不存在是模块B,说明A模块的依赖树里没有B,需要再A模块的podspec文件中添加模块B的依赖。

LIBRARY_SEARCH_PATHS "PODS_CONFIGURATION_BUILD_DIR/B"FRAMEWORK_SEARCH_PATHS="{PODS\_CONFIGURATION\_BUILD\_DIR}/B" FRAMEWORK\_SEARCH\_PATHS ="{PODS_ROOT}/B"

原因2:BModule的Framework缺少modulemap文件

符合LLVM Module Standard的Framework里都会有一个.modulemap文件,如果缺少.modulemap文件说明Framework工程没有配置Define Module,同样会报上述的错误。

Step1:检查Pods目录里BModule的Framework是否包含正确的.modulemap文件

../Pods/Masonry/Masonry.framework

Step2: 如果当前版本有问题而历史版本正常,可以查看CocoaPods的Cache目录的各个版本,对比历史版本的Framework

../Library/Caches/CocoaPods/Pods/Release/Masonry/1.1.0-framework-DG-1-cbeee/Masonry.framework

下面是Masonry Framework解包的目录,目录中包含一个module.modulemap文件

framework module Masonry {
umbrella header"Masonry.h"
export *
module * { export * }
}

原因3:umbrella header中引用头文件不符合规范

umbrella header 会暴露给外部引用的public header。LLVM Module规定所有public header都需要使用“<A/A.h> ”的方式应用头文件。iOS工程中通常有下面几种引用方式,只有第一种是符合LLVM Module 规范的。

#import <A/A.h> right
#import "A/A.h" wrong
#import "A.h" wrong
#import <A.h> wrong

例如下面Masonry的umbrella header就不符合规范

解决方案是将头文件import方式从“#import "A.h" ”改为“#import <Masonry/xxx.h> ”

设置以下选项后,不标准的头文件引用会报错,方便检测和修复

Quoted Include In Framework Header Yess(Error)

原因4:swift compiler incompatible

No such Module 'XModule'
Failed to build module 'AModule'; this SDK is not supported by the compiler (the SDK is built with 'Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)', while this compiler is 'Apple Swift version 5.5 (swiftlang-1300.0.19.104 clang-1300.0.18.4)'). Please select a toolchain which matches the SDK.

本地调试时可能会遇到,比如AModule是静态库,AModule依赖了XModule, XModule是源码库。具体原因是AModule使用Xcode 12.4构建(Swift version 5.3.1),本地当前正在使用了Xcode12.5(Swift version 5.3.1 )调试。解决方案1是将本地使用的XCode版本先降到低于12.4.1 以下的版本。解决方案2是将AModule用新版本的XCode重新构建发布新版本。

常见错误3:Include of non-modular header inside framework module :

如果Framework Module ’AModule‘中加载了非AModule的头文件,比如AModule.h import了一个OC头文件X.h,而X.h头文件又Include某个C/C++ 的头文件

AModule.h:9:9: error: include of non-modular header inside framework module 'BModule.AModule'

解决方案:设置“Framework Module允许导入非modular的头文件”临时跳过问题

CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES

常见错误4:Definition of 'AMoudle' must be imported from module 'BMoudle.xxxheader' before it is required

如果AMoudle和XModule的公共头文件同时引用了BModule的某个公共头文件,就会出现这个错误。

AMoudle_A.h 文件:
import <BModule/BModule_B.h>
XModule_X.m 文件:
import <BModule/BModule_B.h>

解决方案1:增加import <AModule/AMoudleXXX.h>

XModule_X.m 文件:
import <AModule/AMoudle_A.h>
import <BModule/BModule_B.h>

解决方案2:使用project引入模式调试pod

使用Development pod进行源码调试pod会遇到上述错误,而如果使用project引入模式调试pod可以隔离各个Project,就不会遇到这个错误。project引入模式类似swift package manager的edit模式。

常见错误5:Missing required module 'CModule'

AModule是一个Swift模块,AModule依赖了BModule。BModule自身符合LLVM Module Standard,但 BModule依赖了CModule,CModule是一个OC源码模块,不符合LLVM Module Standard。此时编译AModule的framework工程会报“Missing required module 'CModule'”的错误。

解决方案1,在AModule的Podfile描述中加上'use_frameworks!',重新Pod update。

use_frameworks! tells CocoaPods that you want to use Frameworks instead of Static Libraries. Since Swift does not support Static Libraries you have to use frameworks.

target 'AModule' do
  use_frameworks!
  # 强制使用静态链接 use_frameworks! :linkage => :static 
end

解决方案2:

Xcode 9 beta 4, and CocoaPods 1.5.0以上,支持了Swift使用静态库,不需要再使用“use_frameworks!”将Static Libraries转为Frameworks。

因为CModule是OC源码模块,还需要增加“:modular_headers => true ”,使CModule可以使用@import导入

pod 'CModule', :modular_headers => true 

The release notes for Xcode 9 beta 4 note

The new build system supports static library targets which contain Swift code. Debugging applications which use Swift static libraries may require having a complete set of build artifacts (in their original location) available. (33297067)

CocoaPods 1.5.0 was released in early April 2018

With CocoaPods 1.5.0, developers are no longer restricted into specifying use_frameworks! in their Podfile in order to install pods that use Swift. Interop with Objective-C should just work. However, if your Swift pod depends on an Objective-C, pod you will need to enable "modular headers" (see below) for that Objective-C pod.

自定义modulemap

OC Framework通过Cocoapod自定义modulemap

AModule是OC模块,可以在Podspec进行如下配置自定义modulemap

spec.preserve_path = 'Modules/module.modulemap'
spec.module_map = 'Modules/module.modulemap'
spec.xcconfig = { 'HEADER_SEARCH_PATHS' => '(SDKROOT)/usr/include/libxml2(SDKROOT)/usr/include/libxml2 (PODS_ROOT)/AModule/Modules/module' }
spec.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '$(PODS_ROOT)/AModule/Modules/module' }

Swift framework不允许自定义modulemap

[!] Using Swift static libraries with custom module maps is currently not supported. Please build `XXModule` as a framework or remove the custom module map.

总结

本文重点介绍了OC和Swift混编常见编译错误,如果你们的项目正从Object-C工程逐步向Swift演化,可以将本文作为参考。

参考

从预编译的角度理解Swift与Objective-C及混编机制