Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Time:2022-11-21

written in front

This article covers a wide range of topics and is long in length. It will take a certain amount of time and energy to finish reading. If you have a clear reading purpose, you can refer to the following suggestions to complete the reading:

  • If you already know the theoretical knowledge of precompilation, you can start reading directly from the chapter [So it is like this], which will give you a more intuitive understanding of precompilation.
  • If you are interested in the working mechanism of Search Path, you can read directly from the chapter [About the first question], which will give you a deeper and more comprehensive understanding of their operating mechanism,
  • If you are confused about the Header settings in Xcode Phases, you can start reading directly from the chapter [Uncover the true face of Public, Private, Project], which will let you understand why Private is not a real private header file
  • If you want to know how to improve compilation speed through hmap technology, you can start reading from the chapter of “Strategy for Optimizing Search Path Based on hmap”, which will provide you with a new way to speed up compilation.
  • If you want to know how to build Swift products through VFS technology, you can start reading from the [About the second question] chapter, which will let you understand how to use another method to improve the efficiency of building Swift products.
  • If you want to understand how Swift and Objective-C look for method declarations, you can read the chapter [Swift is coming], which will allow you to understand the core ideas and solutions of mixing in principle.

overview

With the development of Swift, there have been some articles in the domestic technical community about how to realize the mixed compilation of Swift and Objective-C. Turn on an option in the Setting, add a field in the podspec, but few articles analyze the working mechanism behind these operations, and most of the core concepts are also mentioned in one stroke.

It is precisely because of this situation that many developers are at a loss when faced with behaviors that do not meet expectations, or when encountering various strange error reports, and this is also caused by a lack of understanding of its working principles.

The author is responsible for CI/CD related work on the Meituan platform, which also includes the content of mixing Objective-C and Swift. In order to allow more developers to further understand the working mechanism of mixing, I wrote this technology article.

Without further ado, let’s get started!

Precompiled Knowledge Guide

#importMechanisms and Disadvantages

When we use some system components, we usually write code in the following form:

#import <UIKit/UIKit.h>

#importActually#includeMinor innovations in grammar, they are still very close in nature.#includeWhat to do is actually a simple copy and paste, the target.hCopy the contents of the file to the current file word by word, and replace this sentence#include,and#importin essence doing and#includeIt’s the same, except that it has an additional ability to avoid repeated references to header files.

In order to better understand the following content, we need to expand on how it works here?

From the most intuitive point of view:

suppose inMyApp.mfile, we#importupiAd.hfile, after the compiler parses this file, it looks for iAd contains content (ADInterstitialAd.hADBannerView.h), and the child content contained within those contents (UIKit.hUIController.hUIView.hUIResponder.h), and recursively go down, finally, you will find#import <iAd/iAd.h>This code becomes a header file dependency on different SDKs.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

If you think it sounds a bit laborious, or vaguely understand, we can give a more detailed example here, but please remember that for the C language preprocessor,#importIt is a special kind of copy and paste.

Combined with the content mentioned above, add in AppDelegateiAd.h

#import <iAd/iAd.h>
@implementation AppDelegate
//...
@end

The compiler will then start looking foriAd/iAd.hWhich file is it and what content does it contain, assuming its content is as follows:

/* iAd/iAd.h */
#import <iAd/ADBannerView.h>
#import <iAd/ADBannerView_Deprecated.h>
#import <iAd/ADInterstitialAd.h>

After finding the above, the compiler copies and pastes this into AppDelegate:

#import <iAd/ADBannerView.h>
#import <iAd/ADBannerView_Deprecated.h>
#import <iAd/ADInterstitialAd.h>

@implementation AppDelegate
//...
@end

Now, the compiler finds that there are 3 in the file#importstatement, then you need to continue to look for these files and their corresponding content, assumingADBannerView.hThe content is as follows:

/* iAd/ADBannerView.h */
@interface ADBannerView : UIView
@property (nonatomic, readonly) ADAdType adType;

- (id)initWithAdType:(ADAdType)type

/* ... */
@end

Then the compiler will continue to copy and paste its content into AppDelegate, and finally it will look like this:

@interface ADBannerView : UIView
@property (nonatomic, readonly) ADAdType adType;

- (id)initWithAdType:(ADAdType)type

/* ... */
@end
#import <iAd/ADBannerView_Deprecated.h>
#import <iAd/ADInterstitialAd.h>

@implementation AppDelegate
//...
@end

This operation will continue until all the#importpointed to is replaced, which also means.mDocumentation can end up being extremely verbose.

Although this mechanism seems feasible, it has two obvious problems: robustness and scalability.

robustness

First of all, this compilation model will lead to poor robustness of the code!

Here we continue to use the previous example and define in AppDelegatereadonlyfor0x01, and the declaration of this definition is in#importstatement, so what happens at this time?

The compiler will also do the copy and paste operations just now, but the scary thing is that you will find those in the attribute declarationreadonlyalso became0x01, which triggers a compiler error!

@interface ADBannerView : UIView
@property (nonatomic, 0x01) ADAdType adType;

- (id)initWithAdType:(ADAdType)type

/* ... */
@end

@implementation AppDelegate
//...
@end

Faced with this kind of error, you might say it’s the developer’s fault.

Indeed, usually we will bring a fixed prefix when declaring macros to distinguish them. But there are always some surprises in life, isn’t it?

Assuming that someone does not follow this rule, you may get different results under different import orders. It is quite troublesome to troubleshoot this kind of error. However, this is not the most troublesome thing, because there is still the existence of dynamic macros, which makes me feel uncomfortable.

Therefore, this solution to circumvent the problem by adhering to the agreement cannot fundamentally solve the problem, which also reflects from the side that the robustness of the compilation model is relatively poor.

Expansibility

After talking about the issue of robustness, let’s look at the issue of scalability.

Apple has done an analysis of their Mail App, the picture below shows all the.mFile sorting, the horizontal axis is the file number sorting, and the vertical axis is the file size.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

It can be seen that the size distribution range of these files composed of business codes is very wide. The smallest may be a few kb, and the largest may be 200+ kb. But in general, 90% of the code may be below the order of 50kb. or even less.

If we add a pair to a core file of the project (a core file refers to a file that other files may need to depend on)iAd.hWhat do references to documents mean to other documents?

The core file here refers to the file that other files may need to depend on

This means that other files will also putiAd.hOf course, the good news is that the iAd SDK itself is only about 25KB in size.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

But you have to know that iAd also depends on components like UIKit, which is a big guy of 400KB+

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

So, how to say?

All code in the Mail App needs to cover the content of the nearly 425KB header file, even if your code is only one lineHello World

If you think this is already frustrating, there is even more news for you, because UIKit is really too small compared to the Cocoa series spree on macOS, which is 29 times the size of UIKit. …..

So if you put this data into the above chart, you will find that the proportion of real business code on the File Size axis is really too insignificant.

So this is one of the problems caused by poor scalability!

Obviously, it is impossible for us to introduce code in this way. Suppose you have M source files and each file will import N header files. According to the explanation just now, the time to compile them will be M * N, which is very scary. of!

Remarks: The iAd component mentioned in the article is 25KB, the UIKit component is about 400KB, and the Cocoa component of macOS is 29 times that of UIKit. The data is published in WWDC 2013 Session 404 Advances in Objective-C. With the continuous development of functions Iteration, from the current point of view, these data may be too small. The Foundation component is mentioned in WWDC 2018 Session 415 Behind the Scenes of the Xcode Build Process. It contains more than 800 header files and the size has exceeded 9MB.

PCH (PreCompiled Header) is a double-edged sword

In order to optimize the problems mentioned above, a compromise technical solution was born, which is PreCompiled Header.

We can often see that the header files of certain components appear frequently, such as UIKit, and this is easily reminiscent of an optimization point. Can we use some means to avoid repeatedly compiling the same content?

And that’s what PCH brings to the precompilation pipeline!

Its general principle is that when we compile any.mBefore saving the file, the compiler will pre-compile the content in the PCH, and convert it into a binary intermediate format and cache it for subsequent use. when start compiling.mWhen editing a file, if you need the compiled content in PCH, you can read it directly without compiling again.

Although this technology has certain advantages, there are still many problems in practical application.

First of all, its maintenance has a certain cost. For most components with heavy historical burdens, it is very troublesome to sort out the reference relationship in the project, and it is even more troublesome to sort out reasonable PCH content on this basis. , and as the version continues to iterate, which header files need to be moved out of the PCH and which header files need to be moved into the PCH will become more and more troublesome.

Secondly, PCH will cause the problem of namespace pollution, because the header files introduced by PCH will appear everywhere in your code, and this may be redundant operations, such as iAd should appear in some advertising-related code , it does not need to appear in help-related code (that is, logic that has nothing to do with advertising), but when you put it in PCH, it means that all places in the component will introduce iAd code, including the help page, This may not be the result we want!

If you want a deeper understanding of the dark side of PCH, it is recommended to read4 Ways Precompiled Headers Cripple Your Code, which has been quite comprehensive and thorough.

So PCH is not a perfect solution, it can improve compilation speed in some scenarios, but it also has flaws!

The arrival of the Clang Module!

In order to solve the problems mentioned above, Clang proposed the concept of Module, its introduction can be found inClang official websitefound on

In simple terms, you can understand it as a description of components, including a description of the interface (API) and implementation (dylib/a). At the same time, the products of Modules are compiled independently, and different Modules will not be affected.

When actually compiling, the compiler will create a new space to store the compiled Module products. If a Module is referenced in the compiled file, the system will first check whether there is a corresponding intermediate product in this list. If it can be found, it means that the file has been compiled, and the intermediate product will be used directly. If there is no If found, compile the referenced header file, and add the product to the corresponding space for reuse.

Under this compilation model, the referenced Modules will only be compiled once and will not affect each other during operation, which fundamentally solves the problems of robustness and scalability.

The use of Module is not troublesome. You only need to write this way to refer to the iAd component.

@import iAd;

At the usage level, this would be equivalent to the previous#import <iAd/iAd.h>statement, but will use the Clang Module feature to load the entire iAd component. If you only want to import specific files (such asADBannerView.h), the original way of writing is#import <iAd/ADBannerView.h.h>, which can now be written as:

@import iAd.ADBannerView;

This way of writing will import the API of the iAd component into our application, and this way of writing is more in line with semantics (semanitc import).

Although this import method is not much different from the previous writing method, they are still quite different in essence. Module will not “copy and paste” the content in the header file, nor will it let@importThe exposed API is tampered with by the developer’s local context, such as the aforementioned#define readonly 0x01

At this point, if you feel that the previous description of Clang Module is still too abstract, we can further explore its working principle, and this will introduce a new concept – modulemap.

In any case, Module is just an abstract description of components, and modulemap is the concrete presentation of this description. It provides a structured description of all files in the framework. The following is the modulemap file of UIKit.

framework module UIKit {
  umbrella header "UIKit.h"
  module * {export *}
  link framework "UIKit"
}

This Module defines the Umbrella Header file (UIKit.h) of the component, the sub-Module (all) that needs to be exported, and the framework name (UIKit) that needs Link. It is through this file that the compiler understands the logical structure of the Module and How the header file structure is associated.

Some people may wonder why I have never seen it@importHow about writing?

This is because Xcode’s compiler is able to convert a formatted#importStatements are automatically converted into Module-recognized@importstatement, thus avoiding manual modification by developers.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

The only thing that needs to be done by the developer is to enable the relevant compilation options.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

For the above compilation options, developers need to pay attention to:

Apple Clang - Language - ModulesinsideEnable ModuleThe option refers to whether to use the form of Module when referencing the system library.

andPackaginginnerDefines ModuleIt refers to whether the component written by the developer adopts the form of Module.

Having said all that, I think you should be right#importpch@importWith a certain concept. Of course, if we go deeper, we may still have the following questions:

  • For components that do not have the Clang Module feature enabled, what mechanism does Clang use to find header files? Is there any difference in the process of finding system header files and non-system header files?
  • For components that have enabled the Clang Module feature, how does Clang decide to compile the Module of the current component? In addition, what are the details of the construction, and how to find these Modules? Also, is there any difference between finding a system module and a non-system module?

In order to answer these questions, we might as well practice it first to see what the above theoretical knowledge looks like in reality.

it turned out to be like this

In the previous chapters, we focused on the introduction of the principles, and in this chapter, we will take a look at what these precompilation links actually look like.

#importlook like

Suppose our source code style is as follows:

#import "SQViewController.h"
#import <SQPod/ClassA.h>

@interface SQViewController ()
@end

@implementation SQViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    ClassA *a = [ClassA new];
    NSLog(@"%@", a);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}
@end

To see what the code looks like precompiled, we can do it inNavigate to Related Itemsfound in the buttonPreprocessoptions

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Now that we know how to view the precompiled appearance, let’s take a look at the code in use#import, PCH and@importAfter that, what will it look like?

Here we assume that the imported header file, that is, the content in ClassA is as follows:

@interface ClassA : NSObject
@property (nonatomic, strong) NSString *name;
- (void)sayHello;
@end

Through preprocess, you can see that the code is roughly as follows. Here, for the convenience of display, the useless code is deleted. Remember to set the Packaging in Build Setting hereDefine ModuleSet to NO, since its default value is YES, which causes us to enable the Clang Module feature.

@import UIKit;
@interface SQViewController : UIViewController
@end

@interface ClassA : NSObject
@property (nonatomic, strong) NSString *name;
- (void)sayHello;
@end

@interface SQViewController ()
@end

@implementation SQViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    ClassA *a = [ClassA new];
    NSLog(@"%@", a);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}
@end

Looking at it this way,#importThe function is really a Copy & Write.

PCHtrue face

For the components created by CocoaPods by default, the related functions of PCH are generally turned off. For example, the SQPod component created by the author, itsPrecompile Prefix HeaderThe function default value is NO.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

To see the effect of precompilation, we willPrecompile Prefix HeaderChange the value to YES and compile the entire project. By looking at the Build Log, we can find that compared to the state of NO, a step has been added during the compilation process, namelyPrecompile SQPod-Prefix.pchA step of.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

By looking at the command’s-oparameter, we can know that its product is namedSQPod-Prefix.pch.gchdocument.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

This file is the product of PCH precompilation, and when compiling the real code, it will pass-includeparameter to bring it in.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

See also Clang Module

After enabling Define Module, the system will automatically create the corresponding modulemap file for us, which can be found in the Build Log.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Its contents are as follows:

framework module SQPod {
  umbrella header "SQPod-umbrella.h"

  export *
  module * { export * }
}

Of course, if the modulemap automatically generated by the system cannot meet your requirements, we can also use the file created by ourselves. At this time, we only need to fill in the file path in the Module Map File option of Build Setting. The corresponding clang command parameter is-fmodule-map-file

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Finally, let’s take a look at the product form after Module compilation.

Here we build a Module named SQPod and provide it to the project named Example. By viewing-fmodule-cache-pathparameter, we can find the cache path of the Module.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

After entering the corresponding path, we can see the following files:

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

where the suffix ispcmThe file is the built binary intermediate product.

Now, we not only know the basic theoretical knowledge of precompilation, but also have a hands-on look at the products of the precompilation link in a real environment. Now we are going to start answering the two questions mentioned earlier!

Break the casserole and ask the end

about the first question

For components that do not have the Clang Module feature enabled, what mechanism does Clang use to find header files? Is there any difference in the process of finding system header files and non-system header files?

In the early Clang compilation process, the header file search mechanism was still based on the Header Seach Path, which is also a working mechanism that most people are familiar with, so we won’t go into details, just a brief review.

Header Search Path is an important parameter provided by the build system to the compiler. Its function is to provide the compiler with information to find the path of the corresponding header file when compiling the code. By consulting the Build System information of Xcode, we can know the relevant There are three settings for Header Search Path, System Header Search Path, and User Header Search Path.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

The difference between them is also very simple, System Header Search Path is the setting for the system header file, usually refers to<>Files imported by way, uUser Header Search Path is set for non-system header files, usually refers to""Files imported by way, and Header Search Path does not have any restrictions, it is universally applicable to header file references in any way.

It sounds complicated, but there are four ways to introduce it:

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

We can understand this problem in two dimensions, one is the symbolic form of introduction, and the other is the content form of introduction.

  • Imported symbolic form:usually, the introduction of double quotes (“A.h”or"A/A.h") is used to find the local header file, you need to specify a relative path, and the introduction method of angle brackets (<A.h>or<A/A.h>) is a global reference, and its path is provided by the compiler, such as the library of the reference system, but with the addition of Header Search Path, this distinction has been diluted.
  • Imported Content Forms: ForX/X.handX.hThese two imported content forms, the former means that in the corresponding Search Path, find the directory A and search under the A directoryA.h, while the latter means to search under Search PathA.hFiles, not necessarily limited to the A directory, whether to search recursively depends on whether the directory option is enabledrecursivemodel

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

In many projects, especially those based on CocoaPods, we no longer distinguish between System Header Search Path and User Header Search Path, but add all header file paths to Header Search Path, which leads us to quote When using a certain header file, it is no longer limited to the conventions mentioned above, and even in some cases, the four methods mentioned above can be used to introduce a specified header file.

Header Maps

With the iteration and development of the project, the original header file indexing mechanism still encountered some challenges. To this end, Clang officials also proposed their own solutions.

In order to understand this thing, we first need to enable the Use Header Map option in the Build Setting.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Then get the compilation command of the corresponding file in the corresponding component in the Build Log, and add at the end-vParameters to view the secrets of its operation:

$ clang <list of arguments> -c SQViewController.m -o SQViewcontroller.o -v

In the output of the console, we will find an interesting piece of content:

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Through the above figure, we can see that the compiler will display the order and corresponding path of looking for header files, and in these paths, we see some strange things, that is, the suffix named.hmapdocument.

So what exactly is hmap?

When we turn on the Use Header Map option in Build Setting, a mapping table of header file name and header file path will be automatically generated, and this mapping table is hmap file, but it is a binary format file, also called It is Header Map. In short, its core function is to enable the compiler to find the location of the corresponding header file.

To understand it better, we can use the gadget written by milendhmapto check its contents.

After executing the relevant command (iehmap print), we can find that the information structure stored in these hmaps is roughly as follows:

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

It should be noted that the key value of the mapping table is not a simple file name and absolute path, and its content will vary depending on the usage scenario. For example, the header file reference is in"..."form, or<...>form, or the configuration of the Header in the Build Phase.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

At this point, I think you should understand that once the Use Header Map option is turned on, Xcode will first go to the hmap mapping table to find the path of the header file. Only if it cannot find it, it will go to the path provided in Header Search Path traverse search.

Of course, this technology is not a new thing, in Facebook’sbuckSomething similar is available in the tool, but the file type becomesHeaderMap.javalook.

Find the header files of the system library

The above process allows us to understand how the compiler finds the corresponding header files under the Header Map technology, and how is the file for the system library indexed? E.g#import <Foundation/Foundation.h>

Recall the output of the console in the previous section, its form is roughly as follows:

#include "..." search starts here:
XXX-generated-files.hmap (headermap)
XXX-project-headers.hmap (headermap)

#include <...> search starts here:
XXX-own-target-headers.hmap (headermap)
XXX-all-target-headers.hmap (headermap) 
Header Search Path 
DerivedSources
Build/Products/Debug (framework directory)
$(SDKROOT)/usr/include 
$(SDKROOT)/System/Library/Frameworks(framework directory)

We will find that most of these paths are used to find non-system library files, that is, the header files introduced by developers themselves, and there are only the following two paths related to system libraries:

#include <...> search starts here:
$(SDKROOT)/usr/include 
$(SDKROOT)/System/Library/Frameworks.(framework directory)

when we look upFoundation/Foundation.hWhen creating this file, we will first determine whether there is a Framework called Foundation.

$SDKROOT/System/Library/Frameworks/Foundation.framework

Next, we will enter the Headers folder of the Framework to find the corresponding header files.

$SDKROOT/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h

If no corresponding file is found, the indexing process is interrupted here and the search ends.

The above is the header file search logic of the system library.

Framework Search Path

So far, we have explained how to rely on Header Search Path, hmap and other technologies to find the working mechanism of header files, and also introduced the working mechanism of finding system library (System Framework) header files.

So is this the search mechanism for all header files? The answer is no. In fact, we still have a header file search mechanism, which is based on the file structure of Framework.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

For the developer’s own Framework, there may be “private” header files, such as using in podspecprivate_header_filesThe description files, these files will be placed in the PrivateHeaders directory in the Framework file structure when they are built.

Therefore, for a Framework with a PrivateHeaders directory, after checking the Headers directory, Clang will go to the PrivateHeaders directory to find whether there is a matching header file. If there are no such two directories, the search will end.

$SDKROOT/System/Library/Frameworks/Foundation.framework/PrivateHeaders/SecretClass.h

But it is precisely because of this working mechanism that a particularly interesting problem arises, that is, when we use the Framework method to introduce a component with a “Private” header file, we can always import this header in the following way document!

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

How, isn’t it amazing, why is this header file described as “Private” not private?

The reason is still due to the working mechanism of Clang, so why did Clang design this seemingly strange working mechanism?

Uncover the true face of Public, Private, Project

In fact, you can also see that in my writing in the previous paragraph, I marked all the words Private with double quotation marks, which is actually implying that we misinterpreted the meaning of Private.

So what exactly does “Private” mean?

In Apple’s officialXcode Help – What are build phases?In the document, we can see the following explanation:

Associates public, private, or project header files with the target. Public and private headers define API intended for use by other clients, and are copied into a product for installation. For example, public and private headers in a framework target are copied into Headers and PrivateHeaders subfolders within a product. Project headers define API used and built by a target, but not copied into a product. This phase can be used once per target.

In general, we can know that Public and Private mentioned in Build Phases – Headers refer to header files that can be used by the outside world, and are placed in the Headers and PrivateHeaders directories of the final product, while the header files in Project It is not used externally, nor will it be placed in the final product.

If you continue to read some materials, such asStackOverflow – Xcode: Copy Headers: Public vs. Private vs. Project?andStackOverflow – Understanding Xcode’s Copy Headers phase, you will find that in the Project Editor chapter of the early Xcode Help, there is a paragraph called Setting the Role of a Header File, which details the differences between the three types.

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.

At this point, we should have a thorough understanding of the differences between Public, Private, and Project. In short, Public is still Public in the usual sense, Private means In Progress, and Project is the private meaning in the usual sense.

Then there is also in the Syntax of Podspec in CocoaPodspublic_header_filesandprivate_header_filesTwo fields, do their real meanings conflict with the concepts in Xcode?

Here we read carefullyExplanation of official documents,especiallyprivate_header_filesfield.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

We can see that,private_header_filesThe meaning here is that it is relative to Public. The original meaning of these header files is not intended to be exposed to users, and no relevant documents will be generated, but they will appear in the final product when building , only header files that are neither marked as Public nor Private will be considered as real private header files and will not appear in the final product.

In fact, it seems that CocoaPods’ understanding of Public and Private is consistent with the description in Xcode. The Private in the two places is not the Private we usually understand. Its original intention should be that the developer is ready to open to the outside world, but not yet fully Ready. The header file is more like the meaning of In Progress.

So, if you really don’t want to expose some header files, please don’t use Private in Headers or in podspecprivate_header_filesup.

So far, I think you should have a thorough understanding of Search Path’s search mechanism and the somewhat strange Public, Private, and Project settings!

Strategy of Optimizing Search Path Based on hmap

In the section on finding header files for system libraries, we passed-vParameters see the search order for looking for header files:

#include "..." search starts here:
XXX-generated-files.hmap (headermap)
XXX-project-headers.hmap (headermap)

#include <...> search starts here:
XXX-own-target-headers.hmap (headermap)
XXX-all-target-headers.hmap (headermap) 
Header Search Path 
DerivedSources
Build/Products/Debug (framework directory)
$(SDKROOT)/usr/include 
$(SDKROOT)/System/Library/Frameworks(framework directory)

Suppose, if we do not enable hmap, all searches will rely on Header Search Path or Framework Search Path, then there will be three kinds of problems:

  • The first question, in some huge projects, assuming that there are 400+ dependent components, then the number of index paths at this time will reach 800+ (one Public path, one Private path), and the search operation can be regarded as An IO operation, and we know that an IO operation is usually a time-consuming operation, then such a large number of time-consuming operations will inevitably lead to an increase in compilation time.
  • The second problem is that during the packaging process, if the Header Search Path is too long, it will trigger an error that the command line is too long, which will cause the command to fail to execute.
  • The third problem is that when importing the header files of the system library, Clang will traverse the aforementioned directories before entering the path to search the system library, that is,$(SDKROOT)/System/Library/Frameworks(framework directory), that is, the more the previous Header Search paths, the longer it will take, which is quite uneconomical.

So if we turn on hmap, can we solve all the problems?

In fact, it is not possible, and in the case of managing projects based on CocoaPods, it will bring new problems. The following is a full-source engineering project based on CocoaPods. Its overall structure is as follows:

First, Host and Pod are our two projects, and the product type of Target under Pods is Static Library.

Secondly, there will be a Target with the same name under the Host, and there will be n+1 Targets under the Pods directory, where n depends on the number of components you depend on, and 1 is a Target named Pods-XXX, and finally, Pods-XXX The product of this Target will be depended on by the Target in the Host.

The whole structure looks like this:

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

At this point, we put all the files in PodA in the Project type of Header.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Under the framework-based search mechanism, we cannot import ClassB in any way, because it is neither in the Headers directory nor in the PrivateHeader directory.

But if we enable Use Header Map, since both PodA and PodB are under the Pods Project, which satisfies the Header Project definition, the hmap file automatically generated by Xcode will bring this path, so we can also use PodB in PodB#import "ClassB.h"way to introduce.

And this kind of behavior, I think it should be the result that most people don’t want, so once the Use Header Map is turned on, combined with the CocoaPods management project mode, we are very likely to have some misuse of private header files. The essence of this problem is caused by the conflict between Xcode and CocoaPods in terms of engineering and header files.

In addition, CocoaPods still has some confusing issues in dealing with header files. Its logic in creating header file products is roughly as follows:

  • In the case where the build product is Framework

    • According to the podspecpublic_header_filesThe content of the field, set the corresponding header file as Public type, and put it in Headers.
    • According to the podspecprivate_header_filesThe content of the field, set the corresponding file as the Private type, and put it in the PrivateHeader.
    • Set the remaining undescribed header files as Project type and not put them into the final product.
    • If Public and Private are not marked in the podspec, all files will be set as Public and placed in the Header.
  • In the case where the build product is a Static Library

    • No matter what is set in the podspecpublic_header_filesandprivate_header_files, the corresponding header files will be set as Project type.
    • existPods/Headers/Publicwill save all items that are declared aspublic_header_filesheader file.
    • existPods/Headers/PrivateAll header files are saved in thepublic_header_filesorprivate_header_filesDescribed, or those not described, this directory is the complete set of all header files of the current component.
    • If Public and Private are not marked in the podspec,Pods/Headers/PublicandPods/Headers/Privateand will include all header files.

It is precisely because of this mechanism that another interesting problem arises.

In the case of Static Library, once we enable Use Header Map, combined with the fact that all header files in the component are of type Project, this hmap will only contain#import "A.h"A key-value reference for , that is, only#import "A.h"Only in this way can the strategy of hmap be hit, otherwise, the related path will be searched through Header Search Path.

And we also know that when referring to other components, usually use#import <A/A.h>way to introduce. As for why this method is used, on the one hand, this way of writing will clarify the origin of the header file and avoid problems. On the other hand, this method allows us to switch at will between whether to enable the Clang Module. Of course, there is another point that Apple In WWDC, it has been suggested more than once that developers use this method to introduce header files.

Following the topic above, so in the case of Static Library and with#import <A/A.h>When this standard method introduces header files, turning on Use Header Map will not improve the compilation speed, and this is also caused by the conflict of ideas between Xcode and CocoaPods on projects and header files.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Looking at it this way, although hmap has various advantages, it seems out of place in the world of CocoaPods and cannot give full play to its own advantages.

So is there really no solution?

Of course, there is a way to solve the problem, we can make a hmap file based on CocoaPods rules by ourselves.

To give a simple example, build the content of the index table by traversing the contents of the PODS directory, with the help ofhmapThe tool generates a header map file, then deletes the path generated by Cocoapods in the Header Search Path, and only adds a path pointing to the hmap file generated by ourselves, and finally closes the Ues Header Map function of Xcode, which is the function of Xcode to automatically generate hmap, so In this way, we have implemented a simple Header Map function based on CocoaPods.

At the same time, on this basis, we can also use this function to implement many control methods, such as:

  • Fundamentally eliminate the possibility of private files being exposed.
  • Unified header file reference form

At present, we have developed a set of cocoapods plug-ins based on the above principles. Its name is cocoapods-hmap-prebuilt, which was jointly developed by the author and colleagues.

Having said all that, let’s see how it works in real projects!

After the test of full source code compilation, we can see that this technology has obvious benefits in speed-up. Taking Meituan and Dianping App as examples, the full link duration can be increased by more than 45%, and the Xcode packaging time can be increased by 50%.

Regarding the second question

For components that have enabled the Clang Module feature, how does Clang decide to compile the Module of the current component? In addition, what are the details of the construction, and how to find these Modules? Also, is there any difference between finding a system module and a non-system module?

First of all, let’s clarify a question, how does Clang decide to compile the Module of the current component?

by#import <Foundation/NSString.h>For example, when we encounter this header file:

First, it will go to the Headers directory of the Framework to find whether the corresponding header file exists, and then it will go to the Modules directory to find the modulemap file.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

At this point, Clang will check the content in the modulemap to see if NSString is part of the Foundation module.

// Module Map - Foundation.framework/Modules/module.modulemap
framework module Foundation [extern_c] [system] {
    umbrella header "Foundation.h"
    export *
    module * {
        export *
    }

    explicit module NSDebug {
        header "NSDebug.h"
        export *
    }
}

Obviously, through the Umbrella Header here, we canFoundation.hfound inNSString.hof.

// Foundation.h
…
#import <Foundation/NSStream.h>
#import <Foundation/NSString.h>
#import <Foundation/NSTextCheckingResult.h>
…

At this point, Clang will determineNSString.hIt is a part of the Foundation Module and compiles accordingly, which means#import <Foundation/NSString.h>It will change from the previous textual import to module import.

Module build details

The above content solves whether to build a Module, and in this piece we will elaborate on the process of building a Module!

Before the construction starts, Clang will create a completely independent space to build the Module, which will contain all the files involved in the Module, and will not bring in any other file information, which is also the robustness of the Module one of the key factors.

However, this does not mean that we cannot affect the uniqueness of the Module. What can really affect its uniqueness is the parameters of its construction, that is, the content behind the Clang command. This point will continue to be expanded later. Here we first So far.

When we build Foundation, we will find that Foundation itself depends on some components, which means that we also need to build the Module of the dependent components.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

But it is obvious that we will find that these dependent components also have their own dependencies, and there are very likely to be repeated references in these dependencies.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

At this point, the Module reuse mechanism shows its advantages. We can reuse the previously built Module without having to create or reference it again and again, such as the Drawin component. The location to save these cache files is mentioned in the previous chapter. preservation ofpcmtype file.

We mentioned earlier that the parameters of the Clang command will really affect the uniqueness of the Module, so what is the specific principle?

Clang will hash the corresponding compilation parameters once, and use the obtained Hash value as the name of the Module cache folder. It should be noted here that different parameters and values ​​will lead to different folders, so you want to use the Module cache as much as possible , it must be ensured that the parameters do not change.

$ clang -fmodules —DENABLE_FEATURE=1 …
## The generated directory is as follows
98XN8P5QH5OQ/
  CoreFoundation-2A5I5R2968COJ.pcm
  Security-1A229VWPAK67R.pcm
  Foundation-1RDF848B47PF4.pcm
  
$ clang -fmodules —DENABLE_FEATURE=2 …
## The generated directory is as follows
1GYDULU5XJRF/
  CoreFoundation-2A5I5R2968COJ.pcm
  Security-1A229VWPAK67R.pcm
  Foundation-1RDF848B47PF4.pcm

Here we have a general understanding of the module construction mechanism of system components, which is also the openingEnable Modules(C and Objective-C)core working principle.

Mysterious Virtual File System (VFS)

For system components, we can find the/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk/System/Library/FrameworksFind it in the directory, and its directory structure is probably like this:

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

That is to say, for system components, the whole process of building a Module is based on such a complete file structure, that is, look for the modulemap in the Modules directory of the Framework, and load the header files in the Headers directory.
How does Clang build Modules for components created by users?

Usually, our development directory looks like the following. It does not have a Modules directory, a Headers directory, or a modulemap file. It seems that it is very different from the Framework’s file structure.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

In this case, Clang cannot build Module according to the aforementioned mechanism, because in this file structure, there are no Modules and Headers directories at all.

In order to solve this problem, Clang has proposed a new solution called Virtual File System (VFS).

To put it simply, through this technology, Clang can virtualize a Framework file structure on the existing file structure, and then let Clang comply with the above-mentioned construction guidelines and successfully complete the compilation of the Module. At the same time, VFS will also record the real location of the file. , so that when a problem occurs, the real information of the file is exposed to the user.

In order to further understand VFS, we still find some details from the Build Log!

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

In the above compilation parameters, we can find a-ivfsoverlayParameters, check the Help description, you can know that its function is to pass a VFS description file to the compiler and overwrite the real file structure information.

-ivfsoverlay <value>    Overlay the virtual filesystem described by file over the real file system

Following this clue, let’s take a look at the file pointed to by this parameter. It is a file in yaml format. After some trimming of the content, its core content is as follows:

{
  "case-sensitive": "false",
  "version": 0,
  "roots": [
    {
      "name": "XXX/Debug-iphonesimulator/PodA/PodA.framework/Headers",
      "type": "directory",
      "contents": [
        { "name": "ClassA.h", "type": "file",
          "external-contents": "XXX/PodA/PodA/Classes/ClassA.h"
        },
        ......
        { "name": "PodA-umbrella.h", "type": "file",
          "external-contents": "XXX/Target Support Files/PodA/PodA-umbrella.h"
        }
      ]
    },
    {
      "contents": [
        "name": "XXX/Products/Debug-iphonesimulator/PodA/PodA.framework/Modules",
        "type": "directory"
        { "name": "module.modulemap", "type": "file",
          "external-contents": "XXX/Debug-iphonesimulator/PodA.build/module.modulemap"
        }
      ]
    }
  ]
}

Combined with the aforementioned content, it is not difficult to see that it is describing such a file structure:

Borrow a real folder to simulate the Headers folder in the Framework, in this Headers folder there is a folder namedPodA-umbrella.handClassA.hand other files, but these virtual files are related toexternal-contentsThe real file pointed to is associated, and the Modules folder and themodule.modulemapdocument.

Through this form, a virtual Framework directory structure was born! At this point, Clang can finally create Modules for users according to the previous construction mechanism!

Swift is here

Swift without header files

In the previous chapters, we talked a lot about the precompilation knowledge of the C language system. Under this system, the compilation of files is separated. When we want to refer to the content in other files, we must import the corresponding header files.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

For the Swift language, it does not have the concept of header files. For developers, this does save the repetitive work of writing header files, but it also means that the compiler will perform additional operations to find Interface definition and requires continuous attention to interface changes!

In order to better explain how Swift and Objective-C find each other’s method declarations, we introduce an example here, which consists of three parts:

  • The first part is the code of a ViewController, which contains a View, where both PetViewController and PetView are Swift codes.
  • The second part is an App proxy, which is Objective-C code.
  • The third part is a single test code to test the ViewController in the first part, which is Swift code.
import UIKit
class PetViewController: UIViewController {
  var view = PetView(name: "Fido", frame: frame)
  …
}
#import "PetWall-Swift.h"
@implementation AppDelegate
…
@end
@testable import PetWall
class TestPetViewController: XCTestCase {
}

Their relationship is roughly as follows:

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

In order for these codes to compile successfully, the compiler will face the following four scenarios:

The first is to find the declaration, which includes finding the method declaration (PetView) in the current Target, as well as the declaration from the Objective-C component (UIViewController or PetKit).

Then generate the interface, which includes the interface used by Objective-C, and also includes the Swift interface used by other Target (Unit Test).

Step 1 – How to find the Swift method declaration inside Target

compilingPetViewController.swift, the compiler needs to know the type of the initialization constructor of PetView to check whether the call is correct.

At this point, the compiler loads thePetView.swiftfile and parse its contents, the purpose of this is to ensure that the initialization constructor really exists, and to get the relevant type information, so thatPetViewController.swiftauthenticating.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

The compiler does not check the interior of the initialization constructor, but it still performs some additional operations. What does this mean?

Different from the Clang compiler, when Swiftc compiles, it will analyze other Swift files in the same Target once to check whether the interface part associated with the compiled file meets expectations.

At the same time, we also know that the compilation of each file is independent, and the compilation of different files can be carried out in parallel, so this means that every time a file is compiled, the remaining files in the current Target need to be used as interfaces and recompiled once . Equal to any file, in the whole compilation process, only 1 time is used as production.oThe input of the product will be repeatedly parsed as an interface file at other times.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

However, after Xcode 10, Apple optimized this compilation process.

While ensuring parallelism as much as possible, the files are grouped and compiled, which avoids repeated parsing of files within the Group, and only files between different Groups will have repeated parsing.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

And the logic of this grouping operation is some additional operations just mentioned.

At this point, we should understand how Target finds Swift method declarations internally.

Step 2 – How to find method declarations in Objective-C components

Going back to the first piece of code, we can see that PetViewController is inherited from UIViewController, which also means that our code will interact with Objective-C code, because most system libraries, such as UIKit, etc., still use Objective-C prepared.

On this issue, Swift adopts a different solution from other languages!

Generally speaking, when two different languages ​​are mixed, an interface mapping table needs to be provided, such as when JavaScript and TypeScript are mixed.d.tsfile so that TypeScript knows what the JavaScript method looks like in the TS world.

However, Swift does not need to provide such an interface mapping table, freeing developers from declaring how each Objective-C API looks like in the Swift world, so how does it do it?

Quite simply, the Swift compiler includes most of Clang’s functions in its own code, which allows us to directly reference Objective-C code in the form of Module.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Since Objective-C is introduced in the form of Module, the file structure of Framework is the best choice. At this time, the way the compiler finds method declarations will have the following three scenarios:

  • For most Targets, when importing an Objective-C type Framework, the compiler will look for the method declaration through the Header information in the modulemap.
  • For a Framework with both Objective-C and Swift code, the compiler will find the method declaration from the Umbrella Header of the current Framework to solve its own compilation problem, because usually the modulemap will use the Umbrella Header as Its own Header value.
  • For App or Unit Test type Target, developers can import the required Objective-C header file by creating a Briding Header for the Target, and then find the required method declaration.

However, we should know that the Swift compiler does not expose the Objective-C API to Swift in the process of obtaining the Objective-C code, but will make some “Swift-like” changes. For example, the following Objective-C API is will be converted to a more compact form.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

This conversion process is not a sophisticated technology, it is just hard-coded on the compiler, if you are interested, you can find the corresponding code in Swift’s open source library –PartsOfSpeech.def

Of course, the compiler also gives developers the right to define the “API appearance” by themselves. If you are interested in this piece, you may wish to read another article of mine –WWDC20 10680 – Refine Objective-C frameworks for Swift, which contains many tips for reshaping Objective-C APIs.

But I still want to mention here, if you are confused about the generated interface, you can view the Swift interface generated by the compiler for Objective-C in the following way.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Step 3 – How the Swift code inside the Target provides an interface to Objective-C

We mentioned earlier how Swift code refers to Objective-C API, so how does Objective-C refer to Swift API?

From the perspective of use, we all know that the Swift compiler will automatically generate a header file for us so that Objective-C can import the corresponding code, just like the one introduced in the second codePetWall-Swift.hFile, this kind of header file is usually automatically generated by the compiler, and the composition of the name isComponent Name - Swiftform.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

But how exactly did it come about?

In Swift, if a class inherits the NSObject class and the API is@objcKeyword annotation means that it will be exposed to Objective-C code.

However, for targets of App and Unit Test types, the automatically generated Header will contain APIs with access levels of Public and internal, which enables Objective-C code in the same Target to access internal APIs in Swift, which is also The default access level for all Swift code.

But for the Framework type Target, the header file automatically generated by Swift will only contain the Public type API, because this header file will be used as a build product, so the internal type API will not be included in this file.

Note that this mechanism will cause in the Framework type Target, if Swift wants to expose some APIs to the internal Objective-C code, it means that these APIs must also be exposed to the outside world, that is, the access level must be set to Public .

So what does the API automatically generated by the compiler look like, and what are its characteristics?

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

The above is an intercepted piece of automatically generated header file code. The left side is the original Swift code, and the right side is the automatically generated Objective-C code. We can see that in the Objective-C class, there is a class namedSWIFT_CLASSmacro that associates Swift with two classes in Objective-C.

If you pay a little attention, you will find that the current component name (PetWall) is also bound to the associated piece of garbled code. The purpose of this is to avoid conflicts between the classes with the same name of the two components at runtime.

Of course, you can also send@objc(Name)The keyword passes an identifier by which to control its name in Objective-C. If it does so, it is up to the developer to ensure that the converted class name does not conflict with other class names.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

This is roughly the mechanism of how Swift exposes interfaces like Objective-C. If you want to know more about the origin of this file, you need to look at the fourth step.

Step 4 – How the Swift Target generates an interface for use by external Swift

Swift adopts the concept of Clang module and makes a series of improvements in combination with its own language features.

In Swift, Module is the distribution unit of method declaration. If you want to reference the corresponding method, you must import the corresponding Module. We also mentioned before that the Swift compiler contains most of Clang, so it is also compatible with Clang Module of.

So we can introduce Objective-C Modules, such as XCTest, or Modules generated by Swift Target, such as PetWall.

import XCTest
@testable import PetWall
class TestPetViewController: XCTestCase {
  func testInitialPet() {
    let controller = PetViewController()
    XCTAssertEqual(controller.view.name, "Fido")
  }
}

After introducing the Swift Module, the compiler will deserialize a suffix named.swiftmodulefile, and learn about the relevant interface information through the contents of this file.

For example, take the following figure as an example. In this unit test, the compiler will load the Module of PetWall and find the method declaration of PetViewController in it, thus ensuring that its creation behavior is in line with expectations.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

This looks very similar to how Target looks for internal Swift method declarations in the first step, except that the step of parsing Swift files is replaced by parsing Swiftmodule files.

However, it should be noted that this Swfitmodule file is not a text file, it is a binary format content, usually we can find it in the Modules folder of the build product.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

In the process of Target compilation, the Swiftmodule file for the entire Target is not generated at once, each Swift file will generate a Swiftmodule file, the compiler will summarize these files, and finally generate a complete one representing the entire Target Swiftmodule is also based on this file, and the compiler constructs an Objective-C header file for external use, which is the header file mentioned in the third step.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

However, with the development of Swift, the working mechanism of this part has also undergone some changes.

The Swiftmodule file we mentioned earlier is a binary format file, and this file format will contain some internal data structures of the compiler. The Swiftmodule files generated by different compilers are incompatible with each other, which leads to different Xcode builds. The products produced cannot be used universally. If you are interested in the details of this aspect, you can read the two official blogs in the Swift community:Evolving Swift On Apple Platforms After ABI StabilityandABI Stability and More, will not be discussed here.

In order to solve this problem, Apple provides a new compilation parameter Build Libraries for Distribution in the Build Setting of Xcode 11. Just like the name of this compilation parameter, when we turn it on, the built product will no longer be affected by the compiler The impact of the version, how does it do this?

To resolve this compiler version dependency, Xcode provides a new artifact, the Swiftinterface file, on the build artifact.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

The content in this file is very similar to Swiftmodule, which is the API information in the current Module, but Swiftinterface is recorded in text form, not the binary form of Swiftmodule.

This makes the Swiftinterface behave the same as the source code, and subsequent versions of the Swift compiler can also import the Swiftinterface file created by the previous compiler and use it like the source code.

To understand it further, let’s take a look at what Swiftinterface actually looks like, here is a.swiftfiles and.swiftinterfaceComparison chart of files.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

In the Swiftinterface file, there are the following points to note

  • The file will contain some meta information such as the file format version, compiler information, and the subset of command lines required by the Swift compiler to import it as a module.
  • The file will only contain Public interfaces, not Private interfaces such as currentLocation.
  • The file will only contain method declarations, not method implementations, such as Spaceship’s init, fly, and other methods.
  • The file will contain all implicitly declared methods, such as Spaceship’s deinit method, Speed’s Hashable protocol.

In general, the Swiftinterface file will remain stable in each version of the compiler. The main reason is that the interface file will contain all the information at the interface level, and the compiler does not need to make any inferences or assumptions.

Ok, so far we should understand how Swift Target generates an interface for external Swift.

What do these four steps mean?

This Module is not that Module

Through the above example, I think everyone should be able to clearly feel that Swift Module and Clang Module are not exactly the same thing, although they have many similarities.

Clang Module is a technology for the C language family, organized through modulemap files.hThe interface information in the file, the intermediate product is the pcm file in binary format.

Swift Module is a technology for the Swift language, organized through Swiftinterface files.swiftThe interface information in the file, the Swiftmodule file in binary format of the intermediate product.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

So after clarifying these concepts and relationships, we will know which files and parameters are not necessary when we build the product of Swift components.

For example, if your Swift component does not want to expose its own API to external Objective-C code, you can set the Install Objective-C Compatiblity Header parameter in Swift Compiler – General in Build Setting to NO, and the compilation parameter isSWIFT_INSTALL_OBJC_HEADER, this time does not generate<ProductModuleName>-Swift.hType of file, which means that the external component cannot refer to the API of the Swift code in the component in the way of Objective-C.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

And when there is no Objective-C code in your component at all, you can set the Defines Module parameter in Packaging in Build Setting to NO, and its compilation parameter isDEFINES_MODULE, will not generate<ProductModuleName>.modulemaptype of file.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Three “routines” mixed with Swift and Objective-C

Based on the example just now, we should understand how Swift finds other APIs at compile time, and how it exposes its own APIs, and this knowledge is the basic knowledge in solving the mixing process. In order to deepen the impact, we can use It is drawn as 3 flowcharts.

When Swift and Objective-C files are in an App or Unit Test type Target at the same time, the API search mechanism for different types of files is as follows:

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

When Swift and Objective-C files are in different Targets, such as different Frameworks, the API search mechanism for different types of files is as follows:

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

When Swift and Objective-C files are in the same Target at the same time, for example, in the same Framework, the API search mechanism for different types of files is as follows:

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

For the third flowchart, the following supplementary instructions are required:

  • Since Swiftc, the Swift compiler, contains most of the Clang functions, including the Clang Module, the Swift compiler can easily find the corresponding Objective-C code through the existing modulemap file in the component.
  • Compared with the second process, the modulemap in the third process is inside the component, and in the second process, if you want to reference the Objective-C code in other components, you need to import the modulemap file in other components. Can.
  • Therefore, based on this consideration, the modulemap is not marked in process 3.

New ways of building Swift artifacts

In the previous chapters, we mentioned the way Swift finds Objective-C. It is mentioned that, except for the App or Unit Test type Target, in other cases, the Objective-C API is found through the Framework’s Module Map. So what if we don’t want to use the Framework form?

At present, this cannot be directly implemented in Xcode, the reason is very simple, there is no Search Path configuration parameter of modulemap in the Search Path option in Build Setting.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

Why do we need the Search Path of modulemap?

Based on what we learned earlier, Swiftc contains most of the logic of Clang. In terms of precompilation, Swiftc only contains the Clang Module mode and no other modes. Therefore, if Objective-C wants to expose its own API, it must pass modulemap to Finish.

For the standard folder structure of Framework, the relative path of the modulemap file is fixed, and it is in the Modules directory, so Xcode directly built-in related logic based on this standard structure, without needing to expose these configurations again come out.

From the perspective of a component developer, he only needs to care about whether the content of the modulemap meets expectations and whether the path conforms to the specification.

From the perspective of a component user, he only needs to correctly introduce the corresponding Framework to use the corresponding API.

This method of only needing to configure the Framework avoids the configuration of the Header Search Path and the Static Library Path. It can be said to be a very friendly way. If the configuration of the modulemap is opened up, it will be superfluous.

So if we put aside Xcode and Framework restrictions, is there any other way to build Swift products?

The answer is yes, which requires the help of the VFS technology mentioned above!

Suppose our file structure looks like this:

├── LaunchPoint.swift
├── README.md
├── build
├── repo
│   └── MyObjcPod
│       └── UsefulClass.h
└── tmp
    ├── module.modulemap
    └── vfs-overlay.yaml

inLaunchPoint.swiftquotedUsefulClass.hA public API in , and a dependency is generated.

in addition,vfs-overlay.yamlThe file remaps the existing file directory structure with the following contents:

{
  'version': 0,
  'roots': [
    { 'name': '/MyObjcPod', 'type': 'directory',
      'contents': [
        { 'name': 'module.modulemap', 'type': 'file',
          'external-contents': 'tmp/module.modulemap'
        },
        { 'name': 'UsefulClass.h', 'type': 'file',
          'external-contents': 'repo/MyObjcPod/UsefulClass.h'
        }
      ]
    }
  ]
}

So far, we can get LaunchPoint’s Swiftmodule, Swiftinterface and other files through the following commands. For specific examples, please refer to my link on Github –manually-expose-objective-c-API-to-swift-example

swiftc -c LaunchPoint.swift -emit-module -emit-module-path build/LaunchPoint.swiftmodule -module-name index -whole-module-optimization -parse-as-library -o build/LaunchPoint.o -Xcc -ivfsoverlay -Xcc tmp/vfs-overlay.yaml -I /MyObjcPod

So what does this mean?

This means that only the appropriate.hfiles and.modulemapThe file can complete the construction of the Swift binary product without relying on the Framework entity. At the same time, for the CI system, when building the product, it can avoid downloading useless binary products (.afile), which will improve compilation efficiency to some extent.

If you don’t quite understand the meaning of the above, we can expand on it.

For example, for the PodA component, it itself depends on the PodB component. When using the original construction method, we need to pull the complete Framework product of the PodB component, which will include the Headers directory, the necessary content in the Modules directory, and of course A binary file (PodB), but in the process of actually compiling the PodA component, we don’t need the binary file in the B component, which makes it redundant to pull the complete Framework file.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation

With the help of VFS technology, we can avoid pulling redundant binary files and further improve the compilation efficiency of the CI system.

Summarize

Thank you for your patience in reading. So far, the whole article is finally over. Through this article, I think you should:

  • Understand the three pre-compiled working mechanisms of Objective-C, among which Clang Module has achieved the introduction of semantics in the true sense, improving the robustness and scalability of compilation.
  • The hmap technology is used in various technical details of Xcode’s Search Path. By loading the mapping table, a large number of repeated IO operations can be avoided, which can improve compilation efficiency.
  • When processing the Framework’s header file index, the Headers directory is always searched before the PrivateHeader directory.
  • Understand that in the Xcode Phases build system, Public represents public header files, Private represents files that do not need to be perceived by users but physically exists, and Project represents files that should not be perceived by users and do not physically exist.
  • Without using the Framework and ending with#import <A/A.h>Using hmap on CocoaPods does not improve compilation speed when importing header files in this standard way.
  • passcocoapods-hmap-builtThe plug-in can save more than 45% of the full link time of large projects, and save more than 50% of the time of Xcode packaging.
  • The construction mechanism of Clang Module ensures that it is not affected by the context (independent compilation space), high reuse efficiency (dependency resolution), and uniqueness (parameter hashing).
  • System components realize the basic conditions for building a Module through the existing Framework file structure, while non-system components virtualize a similar Framework file structure through VFS, and then meet the compilation conditions.
  • You can superficially put the Clang Module in.h/m.moduelmap.pchThe concept corresponds to the Swift Module in.swift.swiftinterface.swiftmodulethe concept of
  • Understand three universal methods of mixing Swift and Objective-C

    • Within the same Target (App or Unit type), based on<PorductModuleName>-Swift.hand<PorductModuleName>-Bridging-Swift.h
    • Within the same Target, based on<PorductModuleName>-Swift.hand Clang’s own capabilities.
    • Within different Targets, based on<PorductModuleName>-Swift.handmodule.modulemap
  • Using the VFS mechanism to build, you can avoid downloading useless binary products during the process of building Swift products, and further improve compilation efficiency.

reference documents

About the Author

  • Siqi, pseudonymSketchK, an iOS engineer at Meituan Dianping, is currently in charge of CI/CD work on the mobile terminal and matters related to Swift technology in the platform.
  • Xu Tao, an iOS engineer at Meituan, is currently in charge of improving the efficiency of iOS-side development.
  • Shuang Ye joined Meituan in 2015 and has worked on Hybrid container, iOS basic components, iOS development tool chain and client continuous integration portal system.

|If you want to read more technical articles, please pay attention to the official WeChat public account of Meituan Technology Team (meituantech).

|Reply keywords such as [2020 goods], [2019 goods], [2018 goods], [2017 goods], [algorithm] and other keywords in the menu bar of the official account, and you can view the collection of technical articles of the Meituan technical team over the years.

Understand Swift and Objective-C and the mixing mechanism from the perspective of precompilation