相信玩iOS
开发的同学对tbd
这个格式的文件已经不再陌生了。最近Xcode 10
升级的时候,你会发现很多原先用libstdc++
的库在新的Xcode
已经没有链接通过。而临时的解决方案也比较简单,网上也很多这样的文章,简而言之就是从Xcode 9
中拷贝对应的libstdc++.tbd
文件给新的Xcode 10
来使用。
Ok,解决方案是有了,我们需要更深入的理解下:为什么拷贝tbd文件,就能够成功解决链接问题?
tbd格式解析
tbd
全称是text-based stub libraries
,本质上就是一个YAML描述的文本文件。
他的作用是用于记录动态库的一些信息,包括导出的符号、动态库的架构信息、动态库的依赖信息。
为什么需要包含这些信息呢?
- 动态库的架构信息是了确保运行的程序在运行的平台加载正确的库。比如你不能在运行
ARM
指令集的iOS
设备上加载x86
格式的库。
后续我们会举一个手动修改
tbd
中install-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
体积大小这一个好处,嘿嘿,你们自己摸索下吧~
而且,基于这种思路,能玩出许多类似文体两开花,中美合拍美猴王的玩法,加油吧。