iOS开发中的头文件引用

头文件引用一般都会随着项目规模变大而可读性变差。当大部分精力花费在业务上往往容易忽视头文件的使用和规范。整理下常见的头文件疑问和知识点,希望能给正在寻找这方面答案的人一些帮助。

一、基本头文件引用方式

说到OC中的头文件引用,不得不说三个基本的引用方式(#include、#import、@class) 和 预编译头文件(.pch)。

#include:

  • 是编译指令,也是C/C++中的头文件引用方式,#include是简单的复制粘贴,将目标.h文件中的内容一字不落地拷贝到当前文件中,并替换掉这句#inclued。(但GCC现在为C/C++做了特殊处理使得#import可以被编译)

#import:

  • 是OC中的头文件引用方式,本质功能是和#include是一样的。但是多了一个功能:避免头文件重复引用带来的编译错误。(例如:B、C同时通过#include引用A;D又同时引用B、C;相当于D通过B、C重复引用了A)

如果想深究,import的实现是通过#ifndef一个标志进行判断,然后在引入后#define这个标志,来避免重复引用的。

想象一下它长这样:

  #ifndef MyFile_h

  #define MyFile_h

  // Some code

  #endif

@class:

  • 仅仅是声明一个类名,并不会包含类的完整声明。
  • 能解决循环包含的问题:当两个类文件有循环依赖关系 ( A 引用 B , B 引用 A ) 时,需要用 @class

.pch预编译头文件:

  • 一般情况下,在OC中使用#import足矣。不过这样的拷贝和复制对编译速度和代码可读性真的好吗?

import的实质还是拷贝粘贴,这样就带来两个问题:

  • 当引用关系很复杂 或者 一个头文件被非常多的实现文件引用时,编译时引用所占的代码量就会大幅上升(因为被引用的头文件在各个地方都被copy了一遍);
  • 公用头文件遍布代码之中将导致头文件很长。

为了解决这些问题,C系语言引入了预编译头文件(PreCompiled Header),将公用的头文件放入预编译头文件中预先进行编译,然后在真正编译工程时再将预先编译好的产物加入到所有待编译的Source中去,来加快编译速度。比如iOS开发中Supporting Files组内的.pch文件就是一个预编译头文件,默认情况下,它引用了UIKit和Foundation两个头文件--这是在iOS开发中基本每个实现文件都会用到的东西。

当然预编译头文件也不应该乱用误用,否则将导致工程中随处可用本不该能访问的代码。编译器无法给出准确的问题报错,增加了编译出错的可能性。

预编译头文件的使用标准:
// include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
// 包含系统基础库文件 或者 项目中经常使用到但不经常修改的特定头文件

二、OC中的Modules和它的头文件引用

  • 关于Modules的由来,可以追溯到2012年的 LLVM Developers' Meeting。简单总结下Modules的出现是可以保留预编译头文件的一个优势和解决几个问题:
    保留优势:预编译处理,提高编译速度。
    解决问题:引用滥用;难以定位编译问题;难以维护;

Modules相当于将框架进行了封装,然后加入在实际编译之时加入了一个用来存放已编译添加过的Modules列表。如果在编译的文件中引用到某个Modules的话,将首先在这个列表内查找,找到的话说明已经被加载过则直接使用已有的,如果没有找到,则把引用的头文件编译后加入到这个表中。这样被引用到的Modules只会被编译一次,但是在开发时又不会被意外使用到,从而同时解决了编译时间和引用泛滥两方面的问题。

我们看看.moudulemap的定义:

  framework module LDPMChart {
       umbrella header "LDPMChart-umbrella.h"
       export *
       module * { export * }
  }

这个Module定义了首要头文件(LDPMChart-umbrella.h),需要导出的子modules(所有),以及需要link的框架名称(LDPMChart)。需要指出的是,现在Module还不支持第三方的框架,所以只有SDK内置的框架能够从这个特性中受益。另外,在C++的源代码中,Modules也是被禁用的。

@import:

  • 说了这么多,module 到底怎么用呢?
    答:Apple在LLVM5.0(也就是Xcode5带的最新的编译器前端中)引入了一个新的编译符号@import,使用@符号将告诉编译器去使用Modules的引用形式,从而获取好处。

    @import LDPMChart; 
    

大部分情况下等价于
#import <LDPMChart/LDPMChart.h>。
但后者需要增加一条使用限制:在Build Settings中将Enable Modules(C and Objective-C)打开。
这样就可以保持旧的写法,避免把项目中的所有引用都进行手动修改。不过,后者的引用方式在语法检查时并不严格,在缺少modulemap头文件暴露支持时,可以编译通过但会有missing submodule的warning。如果方便还是建议使用新的引用方式@import 规范化引用。

  • @import 小特性
    • 使用@import之后,你不用在project settings那里添加framework,系统会自动帮你加载上了

三、小问题附录

#import "" vs #import <>

  • <>: 引用系统文件,它用于对系统自带的头文件的引用,编译器会在系统文件目录下去查找该文件。
  • "": 用户自定义的文件用双引号引用,编译器首先会在用户目录下查找,然后到安装目录中查。
  • 上面两种方式的引用实质是搜索路径的不同。但无论哪种方式,编译器会将相对路径与引用内容组合成头文件的绝对路径:搜索路径+相对路径

Header Search Path

  • (User) Header Search Path

    • Header Search Path指的是头文件的搜索路径。
    • User Header Search Paths指的是用户自定义的头文件的搜索路径
  • Always Search User Paths

    • 如果设置了Always Search User Paths为YES,编译器会优先搜索User Header Search Paths配置的路径,在这种情况下#include <string.h>,User Header Search Paths搜索目录下面的文件会覆盖系统的头文件。

推荐阅读更多精彩内容