通过Xcode 10链接libstdc++来深入分析tbd文件

相信玩iOS开发的同学对tbd这个格式的文件已经不再陌生了。最近Xcode 10升级的时候,你会发现很多原先用libstdc++的库在新的Xcode已经没有链接通过。而临时的解决方案也比较简单,网上也很多这样的文章,简而言之就是从Xcode 9中拷贝对应的libstdc++.tbd文件给新的Xcode 10来使用。

Ok,解决方案是有了,我们需要更深入的理解下:为什么拷贝tbd文件,就能够成功解决链接问题?

tbd格式解析

tbd全称是text-based stub libraries,本质上就是一个YAML描述的文本文件。

他的作用是用于记录动态库的一些信息,包括导出的符号、动态库的架构信息、动态库的依赖信息。

为什么需要包含这些信息呢?

  • 动态库的架构信息是了确保运行的程序在运行的平台加载正确的库。比如你不能在运行ARM指令集的iOS设备上加载x86格式的库。

后续我们会举一个手动修改tbdinstall-name字段的小例子来让运行在模拟器的时候加载ARM64架构的动态库

  • 导出的符号。写过程序的人都知道,我们肯定会依赖别人提供的一些函数方法。一般业界都会把这些函数或者方法封装成库的形势。

那库就分为静态库和动态库两种。相信网上关于这两者的讨论和阐述已经很多了,再次不再赘述。唯一需要提及的一点是,动态库是在程序运行(启动依赖或者按需加载)时候加载进程序的地址空间的,那么我们在静态期的时候,是如何得知动态库提供了哪些能力呢?而这就是tbd格式提供的导出符号表的加载,它会指导链接器在链接过程中,将需要决议的符号先做个标记,标记是来自哪个动态库。

这里举个小例子吧。
在程序构建的过程中,比如我们开发一个iOS应用,毋庸置疑的会用到UIKit这个动态库。而为了使我们的程序能够构建成功,这里分为了两个步骤:

  • 通过引入头文件,import <UIKit/UIKit.h>,我们知道了UIKit里面的函数、变量声明。有声明,就能通过编译器的检查。

  • 我们在代码里面使用了UIKit的函数,其本质是一种符号,因此需要链接器来决议这个符号来自哪?要是所有地方都找到,就会报类似undefined symbol之类的错误(想必大家已经很熟悉了)。

为什么要改造成tbd格式

tbd格式实际上是从Xcode 7时代引入的。

用于取代在真机开发过程中直接使用传统的dylib

用于取代在真机开发过程中直接使用传统的dylib

用于取代在真机开发过程中直接使用传统的dylib

我们都知道一个库在没有strip诸如调试信息、非导出符号的情况下是非常大的。但是由于在开发过程中,调试等过程是必不可少的,我们来对比下传统直接包含dylib的时候大小,我们以CoreImage.framework来举例:

  • 首先看下模拟器上的传统架构大小:

  • 再看下对应的真机上的伪framework(包含tbd)的大小

差距很明显了吧,对于真机来说,由于动态库都是在设备上,在Xcode上使用基于tbd格式的伪framework可以大大减少Xcode的大小。

题外话:网上有人说模拟器上还是使用dylib,的确没错。但是模拟器现在也桥了一层tbd格式,真正的dylib是在这个路径下:iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents

此外,虽然从Xcode 7时代到现在,一直都是tbd的说法,但是它也是经历了一些演变了,目前已经发展了v3格式的版本。

为什么拷贝tbd文件能解决Xcode 10上的问题

网上很多人都研究过dyld的代码,与之对应还有一种ld,就是平时我们在构建程序过程中,链接过程中出错的根因:

既然我们通过拷贝tbd的方式能解决链接不过的问题,那我们就要知道ld是如何运用tbd文件的。

既然报错事library not found,我们扒一下linker的源码即可:

Options::FileInfo Options::findLibrary(const char* rootName, bool dylibsOnly) const
{
    FileInfo result;
    const int rootNameLen = strlen(rootName);
    // if rootName ends in .o there is no .a vs .dylib choice
    if ( (rootNameLen > 3) && (strcmp(&rootName[rootNameLen-2], ".o") == 0) ) {
        for (std::vector<const char*>::const_iterator it = fLibrarySearchPaths.begin();
             it != fLibrarySearchPaths.end();
             it++) {
            const char* dir = *it;
            if ( checkForFile("%s/%s", dir, rootName, result) )
                return result;
        }
    }
    else {
        bool lookForDylibs = false;
        switch ( fOutputKind ) {
            case Options::kDynamicExecutable:
            case Options::kDynamicLibrary:
            case Options::kDynamicBundle:
            case Options::kObjectFile:  // <rdar://problem/15914513> 
                lookForDylibs = true;
                break;
            case Options::kStaticExecutable:
            case Options::kDyld:
            case Options::kPreload:
            case Options::kKextBundle:
                lookForDylibs = false;
                break;
        }
        switch ( fLibrarySearchMode ) {
        case kSearchAllDirsForDylibsThenAllDirsForArchives:
                // first look in all directories for just for dylibs
                if ( lookForDylibs ) {
                    for (std::vector<const char*>::const_iterator it = fLibrarySearchPaths.begin();
                         it != fLibrarySearchPaths.end();
                         it++) {
                        const char* dir = *it;
                        auto path = std::string(dir) + "/lib" + rootName + ".dylib";
                        if ( findFile(path, {".tbd"}, result) )
                            return result;
                    }
                    for (std::vector<const char*>::const_iterator it = fLibrarySearchPaths.begin();
                         it != fLibrarySearchPaths.end();
                         it++) {
                        const char* dir = *it;
                        if ( checkForFile("%s/lib%s.so", dir, rootName, result) )
                            return result;
                    }
                }
                // next look in all directories for just for archives
                if ( !dylibsOnly ) {
                    for (std::vector<const char*>::const_iterator it = fLibrarySearchPaths.begin();
                         it != fLibrarySearchPaths.end();
                         it++) {
                        const char* dir = *it;
                        if ( checkForFile("%s/lib%s.a", dir, rootName, result) )
                            return result;
                    }
                }
                break;

            case kSearchDylibAndArchiveInEachDir:
                // look in each directory for just for a dylib then for an archive
                for (std::vector<const char*>::const_iterator it = fLibrarySearchPaths.begin();
                     it != fLibrarySearchPaths.end();
                     it++) {
                    const char* dir = *it;
                    auto path = std::string(dir) + "/lib" + rootName + ".dylib";
                    if ( lookForDylibs && findFile(path, {".tbd"}, result) )
                        return result;
                    if ( lookForDylibs && checkForFile("%s/lib%s.so", dir, rootName, result) )
                        return result;
                    if ( !dylibsOnly && checkForFile("%s/lib%s.a", dir, rootName, result) )
                        return result;
                }
                break;
        }
    }
    throwf("library not found for -l%s", rootName);
}
  • throwf("library not found for -l%s", rootName); 这里我们就找到了错误发生的原因,我们再往上溯源,找到linker处理编译单元的入口:

  • InputFiles::addOtherLinkerOptions里面存在如下代码:

    CStringSet newLibraries = std::move(state.unprocessedLinkerOptionLibraries);
        state.unprocessedLinkerOptionLibraries.clear();
        for (const char* libName : newLibraries) {
            if ( state.linkerOptionLibraries.count(libName) )
                continue;
            try {
                Options::FileInfo info = _options.findLibrary(libName);
                if ( ! this->libraryAlreadyLoaded(info.path) ) {
                    _linkerOptionOrdinal = _linkerOptionOrdinal.nextLinkerOptionOrdinal();
                    info.ordinal = _linkerOptionOrdinal;
                     //<rdar://problem/17787306> -force_load_swift_libs
                    info.options.fForceLoad = _options.forceLoadSwiftLibs() && (strncmp(libName, "swift", 5) == 0);
                    ld::File* reader = this->makeFile(info, true);
                    ld::dylib::File* dylibReader = dynamic_cast<ld::dylib::File*>(reader);
                    ld::archive::File* archiveReader = dynamic_cast<ld::archive::File*>(reader);
                    if ( dylibReader != NULL ) {
                        dylibReader->forEachAtom(handler);
                        dylibReader->setImplicitlyLinked();
                        dylibReader->setSpeculativelyLoaded();
                        this->addDylib(dylibReader, info);
                    }
                    else if ( archiveReader != NULL ) {
                        _searchLibraries.push_back(LibraryInfo(archiveReader));
                        _options.addDependency(Options::depArchive, archiveReader->path());
                        //<rdar://problem/17787306> -force_load_swift_libs
                        if (info.options.fForceLoad) {
                            archiveReader->forEachAtom(handler);
                        }
                    }
                    else {
                        throwf("linker option dylib at %s is not a dylib", info.path);
                     }
                 }
             }
            catch (const char* msg) {
                // <rdar://problem/40829444> only warn about missing auto-linked library if some missing symbol error happens later
                state.missingLinkerOptionLibraries.insert(libName);
            }
            state.linkerOptionLibraries.insert(libName);
        }
    

而上述这些需要查询的library是从哪里来的呢?

  • 我们以xcconfig举例来看:

    OTHER_LDFLAGS = $(inherited) -ObjC -l"stdc++"
    

在链接过程中就需要处理这样的stdc++ Library,而查询的方式就是在特定目录结构中搜索是否有对应的库文件或者tbd文件。

后记

使用tbd当然不止减少Xcode体积大小这一个好处,嘿嘿,你们自己摸索下吧~

而且,基于这种思路,能玩出许多类似文体两开花,中美合拍美猴王的玩法,加油吧。