When used well, precompiled headers can save you precious compilation time. But when used poorly, precompiled headers can hide problems in your source code that you may not notice until you try to reuse parts of it for another project.
This post is part of the Code Smells in Objective-C series.
Precompiled headers were invented for one purpose: to make compiling faster. Rather than parsing the same header files over and over, these files get parsed once, ahead of time. Speed is important! The faster you compile, the faster you can complete the feedback loop to see if recent changes were successful.
In Xcode, you do this by including the header files you want in a “prefix header,” and enabling “Precompile Prefix Header” so they get precompiled. But the idea behind a prefix header is different from precompiling. A prefix header is implicitly included at the start of every source file. For example, if your prefix header is Prefix.pch, it’s like each source file sneaks
at the top of the file, before anything else. This can be handy for project-wide #defines. (Just remember that in general, #defines are a code smell.)
It’s also handy for precompiled headers. The fact that every source file includes these precompiled headers is an artifact of being in the prefix header.
And this is where things start to go wrong…
Precompiled headers don’t exist to save you typing!
Apple’s iOS project templates start you off with Prefix.pch including Foundation and UIKit. From a compilation speed point of view, this makes a lot of sense. The problem is that people noticed and said, “Those files are already implicitly included. So I don’t need to include them again.” Upon discovering this side-effect, some programmers start dumping more headers into Prefix.pch. Because hey, then you don’t have to #import it ever again.
The purpose shifted from “make this project compile as fast as possible” to “save myself some typing.” A Stack Overflow question reflects this, asking, “Why have both?” Even the Wikipedia entry for prefix header reflects this incorrect conclusion: “As a result, it is unnecessary to explicitly include any of the above files.” This misunderstanding is widespread.
And it’s flat-out wrong.
Four problems of over-relying on precompiled headers
The problem is that to successfully compile a file, it’s no longer enough to have the paired header (.h) and the implementation (.m). You also need Prefix.pch — not because they’re precompiled, but because they’re implicitly included.
“So?” you ask. “What does prevent you from doing?” Basically, you end up creating incomplete source files. There are at least four ways this can cause problems:
1. Source files can’t be copied to different projects
Say you’ve added <QuartzCore/QuartzCore.h> to your prefix header. A particular source file uses QuartzCore. Try copying that source to a different project.
Chances are good that it won’t compile, because the other project has a different set of precompiled headers. You’ve managed to create a nonportable source file!
2. Dependencies are hidden
One of the benefits of any system of importing other files is that it reveals the file’s dependencies. You can scan the beginning of a .h or a .m file and see what other files it uses. This gives you a quick sense of its scope.
Not so if your imports are implicitly bound up in the prefix header.
3. Dependencies are buried
A large project may have a large number of precompiled headers. Say you’re looking at a source file, and trying to find its dependencies. You’re clever enough to realize that earlier programmers relied on precompiled headers to save typing, omitting many #imports. So you look at the prefix file as well.
But if Prefix.pch has more than a handful of #imports, which ones does your source file need? All of them? None of them? Some of them? Which ones?
4. Dependencies get out of hand
Even if you make all #imports explicit, it’s easy to create an explosion of file dependencies. Keeping the dependency tree tamed is hard enough.
But if no effort has gone into a) making all #imports explicit, and b) taming them, these dependencies can silently grow out of hand. Dependency rot can spread unnoticed, for years — until it’s too late. Suddenly you’re working on a new project and have no clean way of reusing earlier code, without bringing it all in as a massive, wasteful glob of cruft.
Find and fix the missing #imports
Because of the way Xcode married the prefix header with precompiled headers, omitting #import statements is a common Objective-C code smell. But it’s an unusual one, because the smell itself can go unnoticed for a long time. (Silent but deadly!)
To fix the problems, you have to find the problems. And to find the problems, you have to temporarily remove the smell-stopper:
- Edit your prefix file. Temporarily comment out all #imports and #includes. (Select them all and Command-/ to comment them all at once. Repeat this to uncomment them.)
- Try building your project. You’ll see the problems right away.
The larger the project, the longer it’ll take to do this first pass of fixes. If you get tired, set it aside and resume the cleanup later. But I urge you to get your project clean. Making dependencies explicit is an important first step in reducing them.
Question: Have you tried this cleanup? How bad was it? Leave a comment below.
Did you find this useful? Subscribe today to get regular posts on clean iOS code.