0%

本文我们主要探索应用程序的加载流程,也就是main方法之前,链接器都做了什么。了解这些对我们项目的启动优化有很大帮助。

编译过程和库

编译的过程

我们知道库是一种可执行文件,从源代码到可执行文件工经历了下面几个步骤:

  • 源文件:主要就是我们写的代码,.h、.m、.cpp等文件。
  • 预编译:主要处理哪些源代码文件中以#开始的预编译指令,比如#include#define、删除所有的注释///* */、添加行号和文件名标识、保留所有的#pragma编译期指令、产生.i文件。
  • 编译:将预处理完的文件进行词法分析语义分析及优化后输出汇编代码文件即.s文件。
  • 汇编:将汇编代码转变成机器可以执行的指令即.o文件。
  • 链接:对.o文件中引用其他库的地方进行引用,生成最后的可执行文件

动态库和静态库

我们项目中经常会使用到动态库静态库,它们的区别是:

  • 静态库:在链接阶段会将汇编生成的目标文件和引用的库一起链接打包到可执行文件中。即静态库在链接阶段就被载入了。
    • 优点:编译完成之后的目标程序没有外部依赖,可以直接运行。
    • 缺点:静态库可以会有多份,会导致目标程序体积增加,对内存、性能、速度消耗较大。
  • 动态库:程序编译并不会链接到目标代码中,在程序可执行文件里面会保留对动态库的引用,在程序运行时才被载入,苹果大部分官方的库都是动态库
    • 优点:
      1. 可以减少App包体积大小:因为不需要拷贝到目标程序中,所以不会影响目标程序的体积.
      2. 共享内存,节约资源:同一份库可以被多个程序使用。
      3. 可以更新动态库,而目标程序不需要重新编译:这是因为动态库运行时才载入,可以随时对库进行替换,而·不需要重新编译代码。
    • 缺点:由于是运行时载入会带来一部分性能损失,使用动态库使得程序依赖于外部环境,如果环境缺少了动态库程序就无法运行。

dyld是什么

dyld动态链接器,目前最新的版本是dyld3,我们首先看一下dyld的版本演变。在dyld之前,NeXT使用的是静态二进制数据。

dyld版本演变

  • dyld1.0(1996-2004)
    • 包含在NeXTStep 3.3
    • 历史早于标准化POSIX dlopen()的调用
    • macOS 10之前编写第三方包装器用来以支持标准Unix软件,但是这些包装器并不能完美的支持相同的语义,在边界情况不能正常工作。
    • 在大多数使用C++动态库的系统之前编写的。
    • mac OS 10.0增加了预绑定。使用预绑定技术为系统中所有的dylib和我们的程序找到固定地址,动态加载器将会加载这些地址的所有内容。
  • dyld2.0(2004-2007)
    • 包含在macOS Tiger
    • 相比1.0版本是完全重写(Complete rewrite)的。
    • 支持了C++初始化语义,扩展了mach-o格式。
    • 有完整的本地(native)dlopendlsym的实现。
    • 2.0版本设计的目标是提高速度,仅进行有限的及安全性检查。
    • 提高了安全性。
    • 减少预编译的工作量(时长)
  • dyld2.x(2007-2017)
    • 增加了更多的基础架构和平台,比如x86x86_64armarm64iOStvOSwatchOS
    • 增强了安全性。增加代码签名ASLR(地址空间配置随机加载),增加了mach-o头文件中的项目边界检查功能它可以避免恶意二进制数据的加入。
    • 增强了性能:用共享缓存代替了预绑定共享缓存是一个包含大部分系统dylib的单文件(Single file),可以节省大量内存,它实际是预链接库。
  • dyld3(2017-至今)
    • 完全改变动态链接器的概念
    • 默认适用于大部分Apple OS系统应用。
    • 完全替代了dyld2.x
    • 提高了性能,尽量提高启动速度和运行速度。
    • 提高安全性:将大多数dyld移出进程,允许部分dyld驻留在进程之中,驻留部分尽可能小,从而减少受攻击的面积。
    • 可测试性和可靠性

dyld 2dyld 3加载过程的区别

  • dyld 2的加载过程

    1. Parse mach-o headers:分析mach-o文件,通过分析mach-o文件弄清楚需要那些库,这些库可能需要其他库,所以会进行递归分析,直到获得所有dylib的完整图。一般普通iOS程序需要3-600个dylib,数据庞大,需要进行大量的处理。
    2. Map mach-o files:映射所有mach-o文件,将它们放入地址空间
    3. Perform symbol lookups:执行符号查找,例如程序使用了printf函数,将会查找printf是否在库系统中,然后找到它的地址,将它复制到你的程序中的函数指针。
    4. Bind and rebase:绑定和基址重置,复制3步的指针,由于使用随机地址,所有指针必须使用基址。
    5. Run initializers:运行初始化器,接下来准备执行main函数。

    流程图如下图,其中红色表示影响性能和安全性的步骤:

dyld2process

  • dyld3加载过程

    dyld3包括三个部分:

    • An out of process MachO parser/compiler:进程外mach-o分析器和编译器。

      1. Resolves all search paths, @rpaths, environment variables:解析所有搜索路径、rpaths、环境变量。
      2. Parses the mach-o binaries:分析mach-o二进制数据
      3. Perform symbol lookups:执行符号查找,例如程序使用了printf函数,将会查找printf是否在库系统中,然后找到它的地址,将它复制到你的程序中的函数指针。
      4. Creates a launch closure with results:创建收尾处理
    • An in-process engine that runs launch:进程内引擎执行启动收尾处理,进驻在内存中。

      1. Validates launch closure:检查启动收尾处理是否正确。
      2. Maps in all dylibs:映射到所有的dylib
      3. Applies fixups:应用修正
      4. Run initializers:运行初始化器,接下来准备执行main函数。
    • A launch closure caching service:启动收尾缓存服务。大部分程序启动会使用缓存但始终不需要调用进程外mach-o分析器和编译器,启动收尾比mach-o更简单,启动收尾文件是内存映射文件,不需要用复杂的方法进行分析从而提高速度。

      流程图(来源WWDC ppt)如下:

      dyld3

注:本小节内容来源于WWDC2017 App Startup Time: Past, Present, and Future感兴趣的童鞋可以查看视频。

dyld加载流程分析

通过上一小节我们其实对dyld的加载有一个初步的了解了,本小节主要通过看源码来探索一下加载流程。本小节需要的源码有:

  • dyld
  • libobjc
  • libSystem
  • libdispatch

源码可直接去苹果Source Browser下载

dyldstart探索

  • 通过main函数

    因为我们程序的入口是main函数,dyld是在main之前执行的,我们很容易想到在main函数打一个断点,然后查看调用堆栈信息来查看dyld的具体调用方法:

    1
    2
    3
    4
    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    * frame #0: 0x000000010000339b DyldTest`main(argc=3, argv=0x00007ffeefbff500) at main.m:13:5
    frame #1: 0x00007fff6e7d3cc9 libdyld.dylib`start + 1

    通过这个方式我们看到了start,但是通过打符号断点并没有找到start方法,所以这种方式无效。

  • 通过load方法

    根据我们的经验,我们知道load方法是在main函数之前执行的,我们通过load方法能不能找到dyld的入口呢,心动不如行动,我们试一下,在ViewController类加入load方法,打上断点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    * frame #0: 0x00000001000032d7 DyldTest`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:19:5
    frame #1: 0x00007fff6d61e560 libobjc.A.dylib`load_images + 1529
    frame #2: 0x000000010001626c dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 418
    frame #3: 0x0000000100029fe9 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 475
    frame #4: 0x00000001000280b4 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 188
    frame #5: 0x0000000100028154 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 82
    frame #6: 0x00000001000166a8 dyld`dyld::initializeMainExecutable() + 199
    frame #7: 0x000000010001bbba dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 6667
    frame #8: 0x0000000100015227 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 453
    frame #9: 0x0000000100015025 dyld`_dyld_start + 37

    通过这个堆栈我们看到了_dyld_start就是dyld开始的函数,我们依次探讨堆栈里的方法。

_dyld_start

我们首先在dyld源码中搜索_dyld_start,发现是一段汇编代码

dyld_start汇编

通过注释我们可以看到,调用的是dyldbootstrapstart函数。

dyldbootstrap::start

我们在源码中搜索dyldbootstrap找到命名空间,继续查找start函数

1
2
3
4
5
6
7
8
9
10
11
//  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
// In dyld we have to do this manually.
//
uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue)
{
///省略代码
// now that we are done bootstrapping dyld, call dyld's main
uintptr_t appsSlide = appsMachHeader->getSlide();
return dyld::_main((macho_header*)appsMachHeader, appsSlide, argc, argv, envp, apple, startGlue);
}

这个函数关键是最后一行,调用了dyld::_main

dyld::_main

这个方法很长(900+行),可以从返回值倒退看这个方法都做了什么。方法太长我们省略大部分代码(因为返回值和mainExecutable相关,所以截取的代码基本都和mainExecutable相关):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
///省略代码
// Grab the cdHash of the main executable from the environment
// 创建主程序cdHash的空间
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
unsigned bufferLenUsed;
if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
mainExecutableCDHash = mainExecutableCDHashBuffer;
}
/////配置信息,获取主程序的mach-o header、silder(ASLR的偏移值)
getHostInfo(mainExecutableMH, mainExecutableSlide);
///通过silder+ASLR可以找到信息
uintptr_t result = 0;
sMainExecutableMachHeader = mainExecutableMH;
sMainExecutableSlide = mainExecutableSlide;

///省略代码
//设置上下文,将这里所有的变量放到了gLinkContext中了,保存起来
setContext(mainExecutableMH, argc, argv, envp, apple);
//配置进程是否受限,envp是环境变量
configureProcessRestrictions(mainExecutableMH, envp);
configureProcessRestrictions(mainExecutableMH, envp);
///检测是否强制dyld3
// Check if we should force dyld3. Note we have to do this outside of the regular env parsing due to AMFI
if ( dyld3::internalInstall() ) {
if (const char* useClosures = _simple_getenv(envp, "DYLD_USE_CLOSURES")) {
if ( strcmp(useClosures, "0") == 0 ) {
sClosureMode = ClosureMode::Off;
} else if ( strcmp(useClosures, "1") == 0 ) {
#if !__i386__ // don't support dyld3 for 32-bit macOS
sClosureMode = ClosureMode::On;
sClosureKind = ClosureKind::full;
#endif
} else if ( strcmp(useClosures, "2") == 0 ) {
sClosureMode = ClosureMode::On;
sClosureKind = ClosureKind::minimal;
} else {
dyld::warn("unknown option to DYLD_USE_CLOSURES. Valid options are: 0 and 1\n");
}
}
}
#if TARGET_OS_OSX
///受限制的进程,环境变量可能会变化,需要重新设置
if ( !gLinkContext.allowEnvVarsPrint && !gLinkContext.allowEnvVarsPath && !gLinkContext.allowEnvVarsSharedCache ) {
pruneEnvironmentVariables(envp, &apple);
// set again because envp and apple may have changed or moved
setContext(mainExecutableMH, argc, argv, envp, apple);
}
else
#endif
{
///检查环境变量
checkEnvironmentVariables(envp);
///default value for DYLD_FALLBACK_FRAMEWORK_PATH, if not set in environment
///如果没有环境变量 设置默认值
defaultUninitializedFallbackPaths(envp);
}
// load shared cache 加载共享缓存
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide)
#if !TARGET_OS_SIMULATOR
///是否有启动闭包 dyld3有闭包
if ( sClosureMode == ClosureMode::Off ) {
if ( gLinkContext.verboseWarnings )
dyld::log("dyld: not using closures\n");
} else {
///dyld3有闭包
///设置加载启动模式
sLaunchModeUsed = DYLD_LAUNCH_MODE_USING_CLOSURE;
///配置闭包
const dyld3::closure::LaunchClosure* mainClosure = nullptr;
dyld3::closure::LoadedFileInfo mainFileInfo;
mainFileInfo.fileContent = mainExecutableMH;
mainFileInfo.path = sExecPath;
// check for closure in cache first
// 判断缓存中是否已经有闭包
if ( sSharedCacheLoadInfo.loadAddress != nullptr ) {
mainClosure = sSharedCacheLoadInfo.loadAddress->findClosure(sExecPath);
if ( gLinkContext.verboseWarnings && (mainClosure != nullptr) )
dyld::log("dyld: found closure %p (size=%lu) in dyld shared cache\n", mainClosure, mainClosure->size());
if ( mainClosure != nullptr )
sLaunchModeUsed |= DYLD_LAUNCH_MODE_CLOSURE_FROM_OS;
}
///如果闭包已失效
if ( (mainClosure != nullptr) && !closureValid(mainClosure, mainFileInfo, mainExecutableCDHash, true, envp) ) {
mainClosure = nullptr;
sLaunchModeUsed &= ~DYLD_LAUNCH_MODE_CLOSURE_FROM_OS;
}
///没有闭包创建一个
if ( (mainClosure == nullptr) && allowClosureRebuilds ) {
// if forcing closures, and no closure in cache, or it is invalid, check for cached closure
if ( !sForceInvalidSharedCacheClosureFormat )
mainClosure = findCachedLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, bootToken);
if ( mainClosure == nullptr ) {
// if no cached closure found, build new one
mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, bootToken);
if ( mainClosure != nullptr )
sLaunchModeUsed |= DYLD_LAUNCH_MODE_BUILT_CLOSURE_AT_LAUNCH;
}
}
// try using launch closure 使用启动闭包
if ( mainClosure != nullptr ) {
CRSetCrashLogMessage("dyld3: launch started");
if ( mainClosure->topImage()->fixupsNotEncoded() )
sLaunchModeUsed |= DYLD_LAUNCH_MODE_MINIMAL_CLOSURE;
bool closureOutOfDate;
bool recoverable;
///启动闭包
bool launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
mainExecutableSlide, argc, argv, envp, apple, diag, &result, startGlue, &closureOutOfDate, &recoverable);
/// 如果启动失败
if ( !launched && closureOutOfDate && allowClosureRebuilds ) {
// closure is out of date, build new one
mainClosure = buildLaunchClosure(mainExecutableCDHash, mainFileInfo, envp, bootToken);
///重新启动
launched = launchWithClosure(mainClosure, sSharedCacheLoadInfo.loadAddress, (dyld3::MachOLoaded*)mainExecutableMH,
mainExecutableSlide, argc, argv, envp, apple, diag, &result, startGlue, &closureOutOfDate, &recoverable);
}
///启动成功 返回main函数
if ( launched ) {
gLinkContext.startedInitializingMainExecutable = true;
if (sSkipMain)
result = (uintptr_t)&fake_main;
return result;
}
///不是dyld3的省略代码
}
#endif
///省略代码
// load any inserted libraries插入动态库
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}
///弱引用绑定主程序
sMainExecutable->weakBind(gLinkContext);
// run all initializers
// 运行所有initializers
initializeMainExecutable();
// notify any montoring proccesses that this process is about to enter main()
/// 通知可以进入main函数了
notifyMonitoringDyldMain();
}

大体流程就是:

  • 配置环境变量
  • 检查共享缓存是否开启,以及共享缓存是否映射到共享区域
  • 主程序初始化即instantiateFromLoadedImage
  • 插入动态库
  • link主程序
  • link动态库
  • 弱符号绑定
  • 执行初始化方法
  • 主程序入口

dyld::initializeMainExecutable

主要是循环遍历执行runInitializers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;

// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
///遍历 执行
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}

// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);

// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}

ImageLoader::runInitializers

核心代码是调用processInitializers方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void ImageLoader::runInitializers(const LinkContext& context, InitializerTimingList& timingInfo)
{
uint64_t t1 = mach_absolute_time();
mach_port_t thisThread = mach_thread_self();
ImageLoader::UninitedUpwards up;
up.count = 1;
up.imagesAndPaths[0] = { this, this->getPath() };
///调用
processInitializers(context, thisThread, timingInfo, up);
context.notifyBatch(dyld_image_state_initialized, false);
mach_port_deallocate(mach_task_self(), thisThread);
uint64_t t2 = mach_absolute_time();
fgTotalInitTime += (t2 - t1);
}

ImageLoader::processInitializers

对镜像列表调用recursiveInitialization函数进行递归实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread,
InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images)
{
uint32_t maxImageCount = context.imageCount()+2;
ImageLoader::UninitedUpwards upsBuffer[maxImageCount];
ImageLoader::UninitedUpwards& ups = upsBuffer[0];
ups.count = 0;
// Calling recursive init on all images in images list, building a new list of
// uninitialized upward dependencies.
// 递归实例化
for (uintptr_t i=0; i < images.count; ++i) {
images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups);
}
// If any upward dependencies remain, init them.
if ( ups.count > 0 )
processInitializers(context, thisThread, timingInfo, ups);
}

ImageLoader::recursiveInitialization

主要是加载完镜像后通知出去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void ImageLoader::recursiveInitialization(const LinkContext& context, mach_port_t this_thread, const char* pathToInitialize,
InitializerTimingList& timingInfo, UninitedUpwards& uninitUps)
{
///递归锁
recursive_lock lock_info(this_thread);
recursiveSpinLock(lock_info);
if ( fState < dyld_image_state_dependents_initialized-1 ) {
uint8_t oldState = fState;
// break cycles 结束递归
fState = dyld_image_state_dependents_initialized-1;
try {
// initialize lower level libraries first
for(unsigned int i=0; i < libraryCount(); ++i) {
ImageLoader* dependentImage = libImage(i);
if ( dependentImage != NULL ) {
// don't try to initialize stuff "above" me yet
if ( libIsUpward(i) ) {
uninitUps.imagesAndPaths[uninitUps.count] = { dependentImage, libPath(i) };
uninitUps.count++;
}
else if ( dependentImage->fDepth >= fDepth ) {
dependentImage->recursiveInitialization(context, this_thread, libPath(i), timingInfo, uninitUps);
}
}
}
// record termination order
if ( this->needsTermination() )
context.terminationRecorder(this);

// let objc know we are about to initialize this image
// 让objc知道我们要初始化此镜像
uint64_t t1 = mach_absolute_time();
fState = dyld_image_state_dependents_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_dependents_initialized, this, &timingInfo);

// initialize this image
/// 初始化镜像
bool hasInitializers = this->doInitialization(context);

// let anyone know we finished initializing this image
/// 让任何人都知道我们完成了这个镜像的初始化
fState = dyld_image_state_initialized;
oldState = fState;
context.notifySingle(dyld_image_state_initialized, this, NULL);

if ( hasInitializers ) {
uint64_t t2 = mach_absolute_time();
timingInfo.addTime(this->getShortName(), t2-t1);
}
}
catch (const char* msg) {
// this image is not initialized
fState = oldState;
recursiveSpinUnLock();
throw;
}
}
recursiveSpinUnLock();
}

dyld::notifySingle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
static void notifySingle(dyld_image_states state, const ImageLoader* image, ImageLoader::InitializerTimingList* timingInfo)
{
//dyld::log("notifySingle(state=%d, image=%s)\n", state, image->getPath());
std::vector<dyld_image_state_change_handler>* handlers = stateToHandlers(state, sSingleHandlers);
if ( handlers != NULL ) {
dyld_image_info info;
info.imageLoadAddress = image->machHeader();
info.imageFilePath = image->getRealPath();
info.imageFileModDate = image->lastModified();
for (std::vector<dyld_image_state_change_handler>::iterator it = handlers->begin(); it != handlers->end(); ++it) {
const char* result = (*it)(state, 1, &info);
if ( (result != NULL) && (state == dyld_image_state_mapped) ) {
//fprintf(stderr, " image rejected by handler=%p\n", *it);
// make copy of thrown string so that later catch clauses can free it
const char* str = strdup(result);
throw str;
}
}
}
if ( state == dyld_image_state_mapped ) {
// <rdar://problem/7008875> Save load addr + UUID for images from outside the shared cache
// <rdar://problem/50432671> Include UUIDs for shared cache dylibs in all image info when using private mapped shared caches
if (!image->inSharedCache()
|| (gLinkContext.sharedRegionMode == ImageLoader::kUsePrivateSharedRegion)) {
dyld_uuid_info info;
if ( image->getUUID(info.imageUUID) ) {
info.imageLoadAddress = image->machHeader();
addNonSharedCacheImageUUID(info);
}
}
}
if ( (state == dyld_image_state_dependents_initialized) && (sNotifyObjCInit != NULL) && image->notifyObjC() ) {
uint64_t t0 = mach_absolute_time();
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
///重点操作
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
uint64_t t1 = mach_absolute_time();
uint64_t t2 = mach_absolute_time();
uint64_t timeInObjC = t1-t0;
uint64_t emptyTime = (t2-t1)*100;
if ( (timeInObjC > emptyTime) && (timingInfo != NULL) ) {
timingInfo->addTime(image->getShortName(), timeInObjC);
}
}
// mach message csdlc about dynamically unloaded images
if ( image->addFuncNotified() && (state == dyld_image_state_terminated) ) {
notifyKernel(*image, false);
const struct mach_header* loadAddress[] = { image->machHeader() };
const char* loadPath[] = { image->getPath() };
notifyMonitoringDyld(true, 1, loadAddress, loadPath);
}
}

其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());,我们全局搜索sNotifyObjCInit并没有实现,但是有赋值操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
// record functions to call
sNotifyObjCMapped = mapped;
///赋值操作
sNotifyObjCInit = init;
sNotifyObjCUnmapped = unmapped;

// call 'mapped' function with all images mapped so far
try {
notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
}
catch (const char* msg) {
// ignore request to abort during registration
}

// <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
ImageLoader* image = *it;
if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
}
}
}

registerObjCNotifiers是在_dyld_objc_notify_register调用,而_dyld_objc_notify_register函数是在libobjc源码_objc_init代用的,所以sNotifyObjCInit赋值的就是objc中的load_images,而load_images会调用所有的+load方法。所以综上所述,notifySingle是一个回调函数,所以我们继续看load_images方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;

// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}

load_images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}

// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;

recursive_mutex_locker_t lock(loadMethodLock);

// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}

// Call +load methods (without runtimeLock - re-entrant)
// 调用了load方法
call_load_methods();
}

方法比较简单,主要调用了call_load_methods方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;

loadMethodLock.assertLocked();

// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}

// 2. Call category +loads ONCE
more_categories = call_category_loads();

// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}

call_load_methods方法的核心就是循环调用load方法。

###总结

load的源码链为:_dyld_start –> dyldbootstrap::start –> dyld::_main –> dyld::initializeMainExecutable –> ImageLoader::runInitializers –> ImageLoader::processInitializers –> ImageLoader::recursiveInitialization –> dyld::notifySingle(是一个回调处理) –> sNotifyObjCInit –> load_images(libobjc.A.dylib)

上一篇中我们探索了消息发送找不到方法的一个流程动态方法决议,本篇探索动态方法决议也找不到imp的下一步,也就是消息转发

和前面的流程不同的是,消息转发的源码并不在libobjc库里,而是在CFFoundtion框架里,CFFoundtion框已经开源的代码里没有找到消息转发的内容,所以我们必须找其它方式探索这个流程。

通过instrumentObjcMessageSends打印消息日志方式

instrumentObjcMessageSends的由来

通过方法调用这条链路lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,我们在logMessageSend方法源码下方找到instrumentObjcMessageSends的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)
return;
// If enabling, flush all method caches so we get some traces
if (enable)
_objc_flush_caches(Nil);
// Sync our log file
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
//赋值是否需要打印message log
objcMsgLogEnabled = enable;
}

log文件存储的路径我们从logMessageSend方法里可以看到,为/tmp/msgSends目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
///日志文件打印路径
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
// Make the log entry
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));

objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();

// Tell caller to not cache the method
return false;
}

instrumentObjcMessageSends的使用

  1. main.m文件里,通过extern声明instrumentObjcMessageSends方法。
  2. 在调用方法前打开日志,调用方法后关闭日志。代码如下:
1
2
3
4
5
6
7
8
9
10
11
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
@autoreleasepool {
instrumentObjcMessageSends(YES);
JSPerson *person = [JSPerson alloc];
[person saySomething];
instrumentObjcMessageSends(NO);
}
return 0;
}
  1. 运行代码,查看日志文件

1625403519834

通过日志文件可以清楚的看到都执行了哪些方法:

  • resolveInstanceMethod方法,即动态方法决议
  • forwardingTargetForSelector方法,即快速消息转发
  • methodSignatureForSelector方法,即慢速消息转发
  • resolveInstanceMethod方法。即第二次动态方法决议
  • 最后执行doesNotRecognizeSelector,抛出异常。

这种方式我们就可以清楚的看到方法的调用流程,下面我们换一种方式验证一下。

反编译方式探索

Hopper是一款帮助我们静态分析可执行文件的工具。有了工具后我们还缺少两个东西才能继续探索:

  1. 反编译之后,搜索代码的关键字

    我们执行运行下面的代码,注意:sayNB方法是没有实现的

    1
    2
    3
    4
    5
    6
    7
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    JSPerson *person = [JSPerson alloc];
    [person sayNB];
    }
    return 0;
    }

    运行之后会直接崩溃,我们使用bt命令查看调用栈信息

    1625404761976

    可以看到这里方法调用的起点是CoreFoundation_forwarding_prep_0__ + 120`,所以我们搜索反编译代码的关键字就是forwarding_prep_0

  2. 怎么获取我们需要反编译的可执行文件

    通过1步,我们知道__forwarding_prep_0___CoreFoundation框架中,我们通过lldbimage list命令,找到CoreFoundation可执行文件的位置。

    1625405481311

    通过上图中目录我们找到CoreFoundation文件。

  3. 前面两个必要条件明确之后,我们打开Hooper软件,选择Try The Demo(主要因为软件太贵,土豪请直接购买正版),然后将上一步的可执行文件拖入Hooper进行反编译,选择x86(64 bit)

    1625405841994

    1625406148399

    Hooper软件我们用到的工具栏的示意图如下图:

    Hopperc菜单示意图

  4. 搜索框位置搜索__forwarding_prep_0___,选择伪代码视图,发现和我们打印的堆栈信息一致,调用了____forwarding___方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    int ___forwarding_prep_0___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
    var_20 = rax;
    var_30 = zero_extend_64(xmm7);
    var_40 = zero_extend_64(xmm6);
    var_50 = zero_extend_64(xmm5);
    var_60 = zero_extend_64(xmm4);
    var_70 = zero_extend_64(xmm3);
    var_80 = zero_extend_64(xmm2);
    var_90 = zero_extend_64(xmm1);
    var_A0 = zero_extend_64(xmm0);
    var_A8 = arg5;
    var_B0 = arg4;
    var_B8 = arg3;
    var_C0 = arg2;
    var_C8 = arg1;
    ///和我们打印的调用栈一致
    rax = ____forwarding___(&var_D0, 0x0);
    if (rax != 0x0) {
    rax = *rax;
    }
    else {
    rax = objc_msgSend(var_D0, var_C8);
    }
    return rax;
    }
  5. 继续看____forwarding___方法的伪代码,首先是判断是否实现快速转发方法forwardingTargetForSelector,如果没有跳转到loc_64a67慢速转发流程

    1625407414541

  6. goto loc_64a67查看慢速转发

    1625407939155

  7. 如果没有实现则跳转,直接报错。

通过使用反编译的方式我们也验证了消息转发的流程。

消息转发实例

在前面的例子中,我们补充一下消息转发的部分.

快速消息转发

  • 我们先定义一个JSProxy类,它实现了sayNB方法
1
2
3
4
5
6
7
8
9
10
11
12
@interface JSPerson : NSObject

- (void)sayNB;

@end
@implementation JSProxy

- (void)sayNB{
NSLog(@"%@ - %s",self , __func__);
}

@end
  • JSPerson类中添加forwardingTargetForSelector方法
1
2
3
4
5
6
7
#import "JSProxy.h"
@implementation JSPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"forwardingTargetForSelector :%@-%@",self,NSStringFromSelector(aSelector));
return [JSProxy alloc];
}
@end
  • 运行代码程序可以正常运行,打印log如下:
1
2
3
4
2021-07-04 22:24:49.089260+0800 ResolveMethodTest[7682:442095] resolveInstanceMethod :JSPerson-sayNB
2021-07-04 22:24:49.090863+0800 ResolveMethodTest[7682:442095] resolveInstanceMethod :JSPerson-encodeWithOSLogCoder:options:maxLength:
2021-07-04 22:24:49.091431+0800 ResolveMethodTest[7682:442095] forwardingTargetForSelector :<JSPerson: 0x600000010160>-sayNB
2021-07-04 22:24:49.091571+0800 ResolveMethodTest[7682:442095] <JSProxy: 0x6000000080d0> - -[JSProxy sayNB]

慢速消息转发

我们在上面例子基础上,在JSPerson类里实现methodSignatureForSelectorforwardInvocation,forwardingTargetForSelector方法返回nil标示不进行快速消息转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation JSPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"forwardingTargetForSelector :%@-%@",self,NSStringFromSelector(aSelector));
return nil;
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"methodSignatureForSelector :%@-%@",self,NSStringFromSelector(aSelector));
if (aSelector == @selector(sayNB)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%@ - %s",self , __func__);
}
@end

我们运行代码,发现运行正常,打印结果如下:

1
2
3
4
5
6
7
8
2021-07-04 22:34:32.163137+0800 ResolveMethodTest[7780:447311] resolveInstanceMethod :JSPerson-sayNB
2021-07-04 22:34:32.163803+0800 ResolveMethodTest[7780:447311] resolveInstanceMethod :JSPerson-encodeWithOSLogCoder:options:maxLength:
2021-07-04 22:34:32.164673+0800 ResolveMethodTest[7780:447311] forwardingTargetForSelector :<JSPerson: 0x600000008070>-sayNB
2021-07-04 22:34:32.165289+0800 ResolveMethodTest[7780:447311] resolveInstanceMethod :JSPerson-encodeWithOSLogCoder:options:maxLength:
2021-07-04 22:34:32.165436+0800 ResolveMethodTest[7780:447311] methodSignatureForSelector :<JSPerson: 0x600000008070>-sayNB
2021-07-04 22:34:32.165551+0800 ResolveMethodTest[7780:447311] resolveInstanceMethod :JSPerson-_forwardStackInvocation:
2021-07-04 22:34:32.165634+0800 ResolveMethodTest[7780:447311] resolveInstanceMethod :JSPerson-encodeWithOSLogCoder:options:maxLength:
2021-07-04 22:34:32.165986+0800 ResolveMethodTest[7780:447311] <JSPerson: 0x600000008070> - -[JSPerson forwardInvocation:]

这里可能有人对添加forwardInvocation方法有因为,可以查看苹果官方文档查看原因

总结

到这里我们就就把整个objc_msgSend的流程探索完了。

  1. 汇编代码快速查找缓存
  2. loopUpImpForward慢速递归查找类以及父类(包括缓存)的方法列表
  3. 动态方法解析处理消息
  4. 快速消息转发流程
  5. 慢速消息转发流程

前面两篇文章我们探索了消息查找的流程,包括快速查找和慢速查找,本文开始探索消息查找未找到时候的处理流程。

unrecognized selector

unrecognized selector是我们开发中很熟悉的一种错误,就是找不到方法的时候报错的提示,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//  JSPerson.h
@interface JSPerson : NSObject

- (void)sayNB;

@end
// JSPerson.m
@implementation JSPerson

@end
// main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
JSPerson *person = [JSPerson alloc];
[person sayNB];
}
return 0;
}

我们定义一个JSPerson类,声明一个sayNB方法,但是并没有实现,因为oc是一种动态语言,可以在运行时添加方法实现,所以没有实现编译期并不会报错。在main函数里实例化一个JSPerson对象,调用sayNB方法,这个时候程序就会崩溃,报错:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[JSPerson sayNB]: unrecognized selector sent to instance 0x101844590'

这个错误是怎么来的呢,我们从上一节最后方法找不到的地方开始看,当没有找到方法实现时,lookUpImpOrForward方法返回值是imp = forward_imp;_objc_msgForward_impcacheobjc源码工程里全局搜索这个关键字,在objc-msg-arm64.s文件的745行找到实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward

END_ENTRY __objc_msgForward_impcache

ENTRY __objc_msgForward

adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
///返回x17
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward

这段代码比较简单,调用了__objc_forward_handler方法,继续搜索__objc_forward_handler方法,并没有找到方法实现,根据前面的经验,去掉_继续搜索objc_forward_handler,在objc_runtime.mm文件中找到了实现:

1
2
3
4
5
6
7
8
9
10
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

这段代码就显而易见了,找不到方法的时候会执行unrecognized selector报错。

动态方法决议

了解了unrecognized selector后我们回到lookUpImpOrForward的流程,当递归完查找所有父类流程没有找到方法实现会继续执行方法后面的代码:

1
2
3
4
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}

behaviorlookUpImpOrForward方法的最后一个参数,通过找到方法的调用behavior=LOOKUP_INITIALIZE | LOOKUP_RESOLVER=3LOOKUP_RESOLVER=2

1
2
3
4
5
6
7
	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
enum {
LOOKUP_INITIALIZE = 1,
LOOKUP_RESOLVER = 2,
LOOKUP_NIL = 4,
LOOKUP_NOCACHE = 8,
};

这个if代码块其实是巧妙的使用了单例思想只会执行一次:

  • 初始behavior & LOOKUP_RESOLVER = 3 & 2 = 2,条件为true执行
  • behavior ^= LOOKUP_RESOLVER = 3 ^ 2 = 1
  • 如果后面再次进行if判断,behavior & LOOKUP_RESOLVER = 1 & 2 = 0,条件为false就不会执行代码块。

前面if代码块会执行方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
//判断是否是元类
if (! cls->isMetaClass()) {
//实例方法 解析
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
//类方法 解析
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
// 如果前面处理了 重新进行查找
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

实例方法流程

我们先看实例方法,实例方法会调用resolveInstanceMethod方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
//resolveInstanceMethod 类方法
SEL resolve_sel = @selector(resolveInstanceMethod:);

if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
// 因为系统有默认实现这里不会执行到
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);//resolved 表示方法返回值
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
// 继续查找 一次
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

if (resolved && PrintResolving) {
if (imp) {
//找到了imp
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// resolved = true 但是不没有对应的处理方法
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

可以看到,其实是在类中添加一个resolveInstanceMethod:进行处理,我们在前面例子的基础上给JSPerson类添加一个resolveInstanceMethod:方法处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@implementation JSPerson

- (void)saySomethingDefalut{
NSLog(@"%@ - %s",self , __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(sayNB)) {
IMP sayDefalutImp = class_getMethodImplementation(self, @selector(saySomethingDefalut));
Method method = class_getInstanceMethod(self, @selector(saySomethingDefalut));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, sayDefalutImp, type);
}
return [super resolveInstanceMethod:sel];
}
@end

重新运行main方法,发现程序可以正常运行了,控制台打印结果

1
2
3
resolveInstanceMethod :JSPerson-sayNB
resolveInstanceMethod :JSPerson-encodeWithOSLogCoder:options:maxLength:
<JSPerson: 0x10194fab0> - -[JSPerson saySomethingDefalut]

通过打印结果我们看到确实进入了resolveInstanceMethod,而且最终执行了saySomethingDefalut方法,实例方法的动态方法解析正常流程就是这样了。

还有一个异常情况就是,如果我们不添加方法处理,只是打印执行NSLog方法:

1
2
3
4
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}

这个时候程序崩溃是毋庸置疑的,因为这样写相当于没有处理,但是这时候控制台的打印结果是:

1
2
3
resolveInstanceMethod :JSPerson-sayNB
resolveInstanceMethod :JSPerson-sayNB
-[JSPerson sayNB]: unrecognized selector sent to instance 0x10193a810

resolveInstanceMethod执行了两次,这是为什么呢,我们在resolveInstanceMethod方法添加断点,使用bt命令查看调用栈:

  • 第一次调用,就是我们刚刚探索的来源是libobjc框架的_objc_msgSend_uncached方法。调用信息如下图

    1625382251804

  • 第二次调用,消息的来源是CoreFoundation___forwarding___方法,下一篇我们会探索,调用信息如下图

    1625382659258

类方法流程

类方法的流程就是在resolveClassMethod实现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
//入参是元类 返回是当前类
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//向当前类发送消息
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

这个流程和实例方法很相似,在类中添加一个resolveClassMethod:的类方法处理,同样前面的例子我们验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@interface JSPerson : NSObject

+ (void)sayHowAreYou;

@end

@implementation JSPerson

+ (void)sayAndYou{
NSLog(@"%@ - %s",self , __func__);
}

+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"resolveClassMethod :%@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(sayHowAreYou)) {
IMP sayAndYouImp = class_getMethodImplementation(objc_getMetaClass("JSPerson"), @selector(sayAndYou));
Method method = class_getInstanceMethod(objc_getMetaClass("JSPerson"), @selector(sayAndYou));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("JSPerson"), sel, sayAndYouImp, type);
}
return [super resolveClassMethod:sel];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[JSPerson sayHowAreYou];
}
return 0;
}

编译运行发现程序正常运行了,打印结果为

1
2
resolveClassMethod :JSPerson-sayHowAreYou
JSPerson - +[JSPerson sayAndYou]
1
2
3
4
5
6
7
8
9
else{
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
//类方法 解析
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}

细心地我们现在会发现在上面这段代码也就是类方法解析这个流程还有一个resolveInstanceMethod(inst, sel, cls);解析,这个原因是类方法在元类里是以实例方法的形式存在的,所以元类里resolveInstanceMethod对方法做了处理也会正常运行。

AOP思想处理动态方法决议

通过上面的动态方法解析流程,和前面我们探索的isa走位图和继承链,我们可以使用AOP的思想,全局处理类的动态方法解析。具体实现就是在NSObject的分类中添加resolveInstanceMethod:类方法,处理方法的动态解析,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@interface NSObject (JS)

@end
@implementation NSObject (LG)

- (void)saySomethingDefalut{
NSLog(@"%@ - %s",self , __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(sayNB)) {
IMP sayDefalutImp = class_getMethodImplementation(self, @selector(saySomethingDefalut));
Method method = class_getInstanceMethod(self, @selector(saySomethingDefalut));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, sayDefalutImp, type);
}
return NO;
}
@end

@interface JSPerson : NSObject

- (void)sayNB;

@end

@implementation JSPerson

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
JSPerson *person = [JSPerson alloc];
[person sayNB];
}
return 0;
}

运行项目发现正常运行,打印结果为:

1
2
resolveInstanceMethod :JSPerson-sayNB
<JSPerson: 0x100593ed0> - -[NSObject(LG) saySomethingDefalut]

总结

本节我们主要探讨了动态方法解析流程,分实例方法和类方法两种情况

  • 实例方法是在类中实现resolveInstanceMethod:的类方法,处理方法得动态绑定到异常处理的方法。
  • 类方法分两部分查找
    1. 搜索当前类的resolveClassMethod:方法。
    2. 搜索类的元类的resolveInstanceMethod:方法
  • 我们可以利用AOP思想在根类的分类中实现resolveInstanceMethod:从而完成全局方法的动态解析。

如果没有进行动态方法解析,接下来就会进入消息转发的流程,我们下一节探索。

上一篇文章中我们探索了消息的快速查找(缓存),本文我们探索慢速查找。

汇编探索回到C++探索

上一篇我们已经知道当快速查找找不到的时候会执行到__objc_msgSend_uncached方法,我们先看一下这个方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
	STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
// imp
//方法表查找
MethodTableLookup
//调用返回 x17
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached

.macro MethodTableLookup
///
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
//方法查找
bl _lookUpImpOrForward

// IMP in x0 --
//将查找到的方法传给 x17
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro

因为x17是反回值,mov x17, x0是将查找结果传给x17,必然前面的代码是查找过程,或者通过名字我们也很容易猜到_lookUpImpOrForward就是具体查找的方法。我们在objc源码中全局搜索_lookUpImpOrForward,发现并没有搜索到定义方法的代码。因为汇编的方法相比c++会有前缀_,所以我们尝试去掉下划线看有没有对应c++的方法,在objc-runtime-new.mm文件的6400行,找到了方法的定义。

lookUpImpOrForward

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
///省略代码
//判断类是否已加载
checkIsKnownClass(cls);
///加载类 本文不探讨
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookup the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().
///循环查找方法实现
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
///共享缓存查找
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
// 二分法 查找当前类的方法列表
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
///找到方法 结束循环跳转到done:
imp = meth->imp(false);
goto done;
}
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
/// 找到根类也没找到方法实现 进行下一步操作forward_imp 下一篇探索
imp = forward_imp;
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
// 查找父类缓存
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
if (fastpath(imp)) {
//找到done
// Found the method in a superclass. Cache it in this class.
goto done;
}
//父类缓存没找到继续for循环 现在curClass实际是superclass
}
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
//查找成功 添加到缓存
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}

看代码和注释,我们可以清楚的看到方法实现的递归查找流程

  1. 查找共享缓存是否有实现
  2. 查找当前类是否有方法,如果有查找有没有分类方法有实现,如果分类有实现就返回分类的实现即imp,如果分类没有实现返回查找到的实现imp
  3. 如果当前类没有实现方法,继续查找父类的缓存是否有实现
  4. 如果父类缓存有实现,则到done:插入查找的类(注意不是父类)的缓存
  5. 如果父类没有缓存实现,就继续for循环查找父类的方法列表
  6. 最后如果找到根类还是没找到,就执行imp = forward_imp;也就是动态方法解析,动态方法解析我们下一篇探索。

二分法查找

getMethodNoSuper_nolock一步步跟进查找,我们可以看到查找遍历方法列表的方法是findMethodInSortedMethodList,它巧妙的使用了二分查找的方式遍历方法列表,源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);

auto first = list->begin();
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)getName(probe);
//当前sel==要查找的sel
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
// 可能有同名方法 分类里的实现优先
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}

if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}

二分查找可能不是很容易发现,我们举个例子验证一下:

  • 1、假设count初始值是9,要查找的sel在6号位,count != 0执行for循环
  • 2、probe = base(0) + 4(9>>1=4) = 4
  • 3、keyValue == probeValue标示找到sel退出
  • 4、keyValue > probeValue == true,此时base = 4+1 = 5 ,count=count--=8
  • 5、count = count >>1 = 4
  • 6、第二次进入for循环,probe = base(5) + 2(4>>1=2) = 7
  • 7、keyValue > probeValue == false,count = count >>1 = 2
  • 8、第三次进入for循环,probe = base(5) + 1(1>>1=2) = 6
  • 9、此时keyValue == probeValue找到了方法返回,循环了3次找到实现。

我们这个例子是sel实际的位置大于中间值的情况,小于中间值的情况也类似,就不举例验证了。

总结

本节我们探索了消息的慢速查找流程,具体流程上面已列出,大体流程就是递归查找类以及其父类的sel,如果找到就添加到缓存后返回imp,如果找到根类也没有找到就是走动态方法解析过程,我们下一篇继续探索。

上一篇探究了catch_t的结构和insert流程,insert的调用方法还不清楚,我们本篇就从insert的调用开始探索。 在探索之前我们首先看一下runtime的概念。

runtime

runtime翻译过来就是运行时,与其对应的一个词是编译时,它们的区别是:

  • 编译时 顾名思义就是正在编译的时候 . 那啥叫编译呢?就是编译器帮你把源代码翻译成机器能识别的代码,(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言)

    编译时就是简单的作一些翻译工作,词法分析,语法分析之类的过程。如果发现错误编译器就告诉你,这时的错误就叫编译时错误。这个过程中做的类型检查也就叫编译时类型检查或静态类型检查。(所谓静态就是没有真把代码放内存中运行起来,而只是把代码当作文本来扫描)。

  • 运行时:代码跑起来了被装载到内存中了(代码保存在磁盘上没装入内存之前是个死代码,只有跑到内 存中才变成活的)。运行时类型检查与编译时类型检查(静态类型检查)不一样,不是简单的扫描代码而是在内存中做些操作以及判断。比如:

    1
    2
    NSObjcet *obj = [[NSObjcet alloc] init];
    [obj performSelector:@selector(saySomething)];

insert的调用

前面文章我们知道在调用一个方法的时候会添加缓存,所以我们实例化一个JSPerson实例,调用saySomething方法,查看调用栈的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//  JSPerson.h
@interface JSPerson : NSObject
- (void)saySomething;
@end
// JSPerson.m
#import "JSPerson.h"
@implementation JSPerson
- (void)saySomething{
NSLog(@"%s",__func__);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
JSPerson *person = [JSPerson alloc];
[person saySomething];
}
return 0;
}

查看编译器的调用栈:

WeChat9862144aabb8df7479fd49bc79b7f67f

可以清楚的看到,insert方法之前的调用栈从main执行到_objc_msgSend_uncached最后执行到insert。即前面没有找到缓存会执行到这里。查找缓存的流程是什么样的呢?我们其实都知道OC里调用方法的本质是消息发送,通过clang命令将main.m转换成main.cpp文件也能验证这一点:

clang -rewrite-objc main.m

1
2
3
4
5
6
7
8
9
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

JSPerson *person = ((JSPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("JSPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("saySomething"));

}
return 0;
}

可以看到我们的代码其实就是向person对象发送了一个sel_registerName("saySomething")的消息,在到达_objc_msgSend_uncached之前做了缓存的查找。所以我们从objc_msgSend开始探索缓存查找的过程。

快速查找方法(缓存)

objc源码中搜索objc_msgSend,在objc-msg-arm64.s文件中找到了实现END_ENTRY _objc_msgSend

ENTRY _objc_msgSend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
	ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// 判断 p0是否为空,p0是第一个参数 - 消息接收者receiver
cmp p0, #0 // nil check and tagged pointer check
// 支持taggedpointer(小对象类型)的流程
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//p0 等于 0 时,直接返回 空
b.eq LReturnZero
#endif
//根据对象拿出isa ,即从x0寄存器指向的地址 取出 isa,存入 p13寄存器
//取isa的原因是,不论是对象方法还是类方法,都需要在类或元类的缓存或者方法列表中查找。
ldr p13, [x0] // p13 = isa
//在64位架构下通过 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone://找到isa之后执行
// calls imp or objc_msgSend_uncached
//如果有isa,走到CacheLookup 即查找方法缓存,也就是所谓的sel imp快速查找流程 否则走__objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS //
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend

GetClassFromIsa_p16获取isa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
// __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__) 优化的isa
#if SUPPORT_INDEXED_ISA
// Indexed isa
// 将 isa 存入p16寄存器
mov p16, \src // optimistically set dst = src
//判断是否是 not nonapointer
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// 将_objc_indexed_classes所在的页的基址 读入x10寄存器 isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
//x10 = x10 + _objc_indexed_classes(page中的偏移量) --x10基址 根据 偏移量 进行 内存偏移
add x10, x10, _objc_indexed_classes@PAGEOFF
//从p16的第ISA_INDEX_SHIFT位开始,提取 ISA_INDEX_BITS 位 到 p16寄存器,剩余的高位用0补充
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
//
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
//64位
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else
// 64-bit packed isa
//p16 = class = isa & ISA_MASK(位运算 & 即获取isa中的shiftcls信息)
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa 32位系统
mov p16, \src
#endif
.endmacro
.macro ExtractISA
//p16 = class = isa & ISA_MASK(位运算 & 即获取isa中的shiftcls信息)
and $0, $1, #ISA_MASK
.endmacro

CacheLookup 缓存查找汇编源码

缓存查找执行的是CacheLookup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//
// Restart protocol:
//
// As soon as we're past the LLookupStart\Function label we may have
// loaded an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd\Function,
// then our PC will be reset to LLookupRecover\Function which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//

mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
//64位真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//将x16 平移 CACHE大小 16位 CACHE = (2 * __SIZEOF_POINTER__)2个指针大小
//也就是isa 平移 0x10 找到cache p11 = cache
ldr p11, [x16, #CACHE] // p11 = mask|buckets
//64位真机
#if CONFIG_USE_PREOPT_CACHES
//arm64e A12(iPhone X)及之后机型
#if __has_feature(ptrauth_calls)
//判断p11(cache)是否存在 不存在跳转LLookupPreopt 存在继续执行
tbnz p11, #0, LLookupPreopt\Function
//p10 = cahce&mask = buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function
#endif
// p12 = p1^(p1>>7) p1 = _cmd
eor p12, p1, p1, LSR #7
// p12 = p12&(p11>>48) 哈希编码index
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
// 根据index找到对应的地址 p13 = bukects[index] PTRSHIFT=3
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

// do {
// 从p13取出imp和sel p17=imp p9=sel bucket--
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//判断p9是否等于传入的cmd
cmp p9, p1 // if (sel != _cmd) {
//不相等,跳转到 3f
b.ne 3f // scan more
// } else {
//缓存命中
2: CacheHit \Mode // hit: call or return imp
// }
//p9为空
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
//p13>p10(首地址) 循环查找buckets
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b

// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits on LP64)
// p12 = _cmd & mask
//
// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
// So stop when we circle back to the first probed bucket
// rather than when hitting the first bucket again.
//
// Note that we might probe the initial bucket twice
// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
//64位真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket

// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
cmp p9, p1 // if (sel == _cmd)
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b

LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic

宏的定义参考定义:

1
2
3
4
5
6
7
8
9
10
11
#if defined(__arm64__) && __LP64__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#endif
#elif defined(__arm64__) && !__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif

CashHit

找到缓存就是执行Cashit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
cmp x16, x15
cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
ret // return imp via x17
.else
.abort oops
.endif
.endmacro

.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
// $0 = imp ^ isa = 哈希编码index
eor $0, $0, $3
// 执行imp
br $0
.endmacro

###总结

通过代码的注释可以看出_objc_msgSend查找缓存的步骤是

  • 判断消息接收者(receiver)是否为空
  • 根据对象首地址找到isa
  • 对象的isa指向找到类classp16
  • class内存平移16找到cache
  • cache&mask -> buckets
  • (_cmd ^ (_cmd >> 7)) & mask得到index
  • 通过buckets[index]找到indexbucket
  • 根据bucket取到impsel
  • 比较sel是否是传入的_cmd
  • 如果是 执行Cashit 执行imp
  • 如果不是 循环查找
  • 找不到的话执行_objc_msgSend_uncached流程

下一篇我们继续探索找不到的情况也就是_objc_msgSend_uncached的流程。

前面我们探索了Classbits属性,这篇我们主要探究一下类的第三个属性cache,了解一下苹果对缓存的处理。我们首先看一下cache的内存结构。

cache_t的结构

先看一下cache_t的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 4
#if __LP64__
uint16_t _flags; // 2
#endif
uint16_t _occupied; // 2
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
};
///省略代码
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
///省略代码
unsigned capacity() const;
struct bucket_t *buckets() const;
Class cls() const;
void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
///省略代码
void insert(SEL sel, IMP imp, id receiver);
void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
void destroy();
void eraseNolock(const char *func);

static void init();
static void collectNolock(bool collectALot);
static size_t bytesForCapacity(uint32_t cap);
}

对定义有一个大题了解之后,我们像探索bits一样的方式,用lldb探索cache,定义一个类JSPerson:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//  JSPerson.h
@interface JSPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic, strong) NSString *hobby;
- (void)saySomething;
- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
@end
// JSPerson.m
#import "JSPerson.h"
@implementation JSPerson
- (void)saySomething{
NSLog(@"%s",__func__);
}
- (void)say1{
NSLog(@"%s",__func__);
}
- (void)say2{
NSLog(@"%s",__func__);
}
- (void)say3{
NSLog(@"%s",__func__);
}
- (void)say4{
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {

JSPerson *p = [JSPerson alloc];
NSLog(@"%@",p);//断点

}
return 0;
}

lldb调试查看缓存

main()方法初始化JSPerson对象,使用lldb调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
(lldb) p/x JSPerson.class
(Class) $1 = 0x0000000100008660 JSPerson//类首地址
(lldb) p/x 0x0000000100008670 //便宜16位找到cache
(long) $2 = 0x0000000100008670
(lldb) p/x (cache_t *)$2
(cache_t *) $3 = 0x0000000100008670
(lldb) p *$3
(cache_t) $4 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4298437472
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 0
}
}
_flags = 32808
_occupied = 0 //没有调用过方法 所以为0
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0000802800000000
}
}
}
}
(lldb) p $4.buckets()
(bucket_t *) $5 = 0x000000010034f360
(lldb) p *$5
(bucket_t) $6 = {
_sel = {
std::__1::atomic<objc_selector *> = (null) {
Value = (null)//没有调用过方法 为null 即没有缓存
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 0
}
}
}
(lldb) p [p say1] //手动执行一个方法,重复上面的过程
2021-06-27 14:03:32.208774+0800 KCObjcBuild[2660:112775] -[JSPerson say1]
(lldb) p *$3
(cache_t) $7 = {
_bucketsAndMaybeMask = {
std::__1::atomic<unsigned long> = {
Value = 4321236704
}
}
= {
= {
_maybeMask = {
std::__1::atomic<unsigned int> = {
Value = 7//这里为什么是7后面再分析
}
}
_flags = 32808
_occupied = 1//有一个缓存
}
_originalPreoptCache = {
std::__1::atomic<preopt_cache_t *> = {
Value = 0x0001802800000007
}
}
}
}
(lldb) p $7.buckets()
(bucket_t *) $8 = 0x000000010190d6e0
(lldb) p *$8
(bucket_t) $9 = {
_sel = {
std::__1::atomic<objc_selector *> = "" {
Value = ""
}
}
_imp = {
std::__1::atomic<unsigned long> = {
Value = 48864
}
}
}
(lldb) p $9.sel()
(SEL) $11 = "say1"
(lldb) p $9.imp(nil,JSPerson.class)
(IMP) $12 = 0x0000000100003880 (KCObjcBuild`-[JSPerson say1])

可以看到,我们调用方法之后,cache里会增加一条缓存。调用buckets()方法可以查看缓存的内容,buckets()方法返回值是bucket_t,我们再看一下bucket_t的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
// 省略代码
public:
static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
inline SEL sel() const { return _sel.load(memory_order_relaxed); }

#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
#define MAYBE_UNUSED_ISA
#else
#define MAYBE_UNUSED_ISA __attribute__((unused))
#endif
inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
imp ^= (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
#else
#error Unknown method cache IMP encoding.
#endif
return (IMP)imp;
}

inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
uintptr_t imp = _imp.load(memory_order_relaxed);
if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
SEL sel = _sel.load(memory_order_relaxed);
return (IMP)
ptrauth_auth_and_resign((const void *)imp,
ptrauth_key_process_dependent_code,
modifierForSEL(base, sel, cls),
ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
}
template <Atomicity, IMPEncoding>
void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};

bucket_t通过sel()方法获取方法的SEL,通过imp()方法获取方法的实现IMP

脱离源码环境查看缓存

前面我们用lldb的方法查看了cache的缓存,如果源码环境不可调试的话,lldb这种方式就不可用了,我们如果要查看缓存,可以参考一份源码的数据结构,然后打印查看结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
struct js_bucket_t {
SEL _sel;
IMP _imp;
};
struct js_cache_t {
struct js_bucket_t *_bukets; // 8
mask_t _maybeMask; // 4
uint16_t _flags; // 2
uint16_t _occupied; // 2
};
struct js_class_data_bits_t {
uintptr_t bits;
};
// cache class
struct js_objc_class {
Class isa;
Class superclass;
struct js_cache_t cache; // formerly cache pointer and vtable
struct js_class_data_bits_t bits;
};

main()方法中调用方法,打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(int argc, const char * argv[]) {
@autoreleasepool {
JSPerson *p = [JSPerson alloc];
Class pClass = p.class; // objc_clas
[p say1];
[p say2];
[pClass sayHappy];
struct js_objc_class *js_class = (__bridge struct js_objc_class *)(pClass);
NSLog(@"%hu - %u",js_class->cache._occupied,js_class->cache._maybeMask);//打印occupied和maybeMask
for (mask_t i = 0; i<js_class->cache._maybeMask; i++) {//打印缓存方法
struct js_bucket_t bucket = js_class->cache._bukets[i];
NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
}
NSLog(@"Hello, World!");
}
return 0;
}

先执行say1()say2()方法,打印结果:

1
2
3
4
5
6
7
LGPerson say : -[JSPerson say1]
LGPerson say : -[JSPerson say2]
LGPerson say : +[JSPerson sayHappy]
2 - 3 //occupied = 2, _maybeMask=3
say1 - 0xb840f
say2 - 0xb810f
(null) - 0x0f

可以看到现在缓存是2个,_maybeMask=3,下面我们多调用一个方法:

1
2
3
[p say1];
[p say2];
[p say3];

打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
LGPerson say : -[JSPerson say1]
LGPerson say : -[JSPerson say2]
LGPerson say : -[JSPerson say3]
LGPerson say : +[JSPerson sayHappy]
1 - 7//occupied = 1, _maybeMask=7
(null) - 0x0f
(null) - 0x0f
say3 - 0xb9e8f
(null) - 0x0f
(null) - 0x0f
(null) - 0x0f
(null) - 0x0f

我们看到现在occupied = 1,_maybeMask=7,缓存里只有一个say3方法,这是为什么,我们就需要看一下源码。本小节提供一个思路可以脱离源码分析cache的内容,后面我们也可以用类似的思想分析其他问题。

cache扩容

上一小节我们探究过程中发现,调用say3方法之后,缓存中只有一个say3方法了,缓存的容量(_maybeMask)也增加了,其实就是扩容的原因,在什么情况下会触发扩容呢,我们看一下源码,在看catch_t数据结构中我们发现一个insert方法,缓存的存储必然会执行这个方法,我们直接看insert方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
//省略代码
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1; // 1+1
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;//初始大小为4
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {//是否需要扩容
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else {// 4*2 = 8
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}

bucket_t *b = buckets();
mask_t m = capacity - 1; // maybeMask = 4-1=3
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));

bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
// Historical fill ratio of 75% (since the new objc runtime was introduced).
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}

可以看出:

  • 初始大小INIT_CACHE_SIZE=4,初始maybeMask=3
  • 触发扩容的条件是(fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)))也就是插入新元素后的容量是否大于等于当前容量的3/4,如果是就扩容。
  • 扩容的大小是capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;也就是之前容量的2倍,就是上述例子里say3执行后的容量=8
  • 扩容后不会移动之前缓存的数据,而是直接将新的数据缓存,也就是say3执行扩容后,缓存里只有一个say3方法,之前的方法不会移动到新的缓存区。

以上是缓存扩容的流程,为什么扩容的时候之前缓存的内容不会拷贝过来,个人理解是拷贝过来性能较差,如果数据量大影响性能。

总结来看insert方法的流程如下:

cache_tInsert流程

总结

  • cache主要是用来存储类的方法缓存的变量,缓存的初始大小为4
  • 如果insert新方法时容量达到缓存大小的3/4就会触发扩容,扩容后的大小为之前的两倍。
  • 扩容并不会将之前缓存的数据平移到新的空间而是直接释放之前的缓存内存。
  • 缓存的存储方式是哈希表,哈希的key是通过cache_hash方法生成。
  • 如果缓存已存在就直接返回。
  • 如果缓存不存在执行set方法存储缓存后结束方法。

上一篇文章中我们探索了类的结构,留下了一个疑问就是class_ro_tclass_rw_t的区别,我们从这个区别开始。

class_ro_tclass_rw_t的区别

class_ro_t

class_ro_t存储了当前类在编译期就已经确定的属性方法和遵循的协议,里面没有category的方法。运行时添加的方法存储在运行时生成的class_rw_t中。

ro标示的是read only,是无法进行修改的,我们看一下它的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList;//方法列表的获取方法
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;//成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;//属性列表

// This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
_objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
_objc_swiftMetadataInitializer swiftMetadataInitializer() const {
if (flags & RO_HAS_SWIFT_INITIALIZER) {
return _swiftMetadataInitializer_NEVER_USE[0];
} else {
return nil;
}
}
const char *getName() const {
return name.load(std::memory_order_acquire);
}
static const uint16_t methodListPointerDiscriminator = 0xC310;
#if 0 // FIXME: enable this when we get a non-empty definition of __ptrauth_objc_method_list_pointer from ptrauth.h.
static_assert(std::is_same<
void * __ptrauth_objc_method_list_pointer *,
void * __ptrauth(ptrauth_key_method_list_pointer, 1, methodListPointerDiscriminator) *>::value,
"Method list pointer signing discriminator must match ptrauth.h");
#endif
method_list_t *baseMethods() const {
#if __has_feature(ptrauth_calls)
method_list_t *ptr = ptrauth_strip((method_list_t *)baseMethodList, ptrauth_key_method_list_pointer);
if (ptr == nullptr)
return nullptr;
// Don't auth if the class_ro and the method list are both in the shared cache.
// This is secure since they'll be read-only, and this allows the shared cache
// to cut down on the number of signed pointers it has.
bool roInSharedCache = objc::inSharedCache((uintptr_t)this);
bool listInSharedCache = objc::inSharedCache((uintptr_t)ptr);
if (roInSharedCache && listInSharedCache)
return ptr;

// Auth all other small lists.
if (ptr->isSmallList())
ptr = ptrauth_auth_data((method_list_t *)baseMethodList,
ptrauth_key_method_list_pointer,
ptrauth_blend_discriminator(&baseMethodList,
methodListPointerDiscriminator));
return ptr;
#else
return (method_list_t *)baseMethodList;
#endif
}
///省略代码
};

class_rw_t

类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t中,它是可读可写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
///省略代码
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}

const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
};

class_rw_t生成在运行时,在编译期间,class_ro_t结构体就已经确定,objc_class中的bitsdata部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtimerealizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。

WWDC2020中类结构优化

二进制类在磁盘中的表现是:

截屏2021-06-21 上午9.49.58

类对象本身,包含最常访问的信息:指向元类,超类和方法缓存的指针,在类结构之中有指向包含更多数据的结构体class_ro_t的指针,包含了类的名称,方法,协议,实例变量等等编译期确定的信息。其中 ro 表示 read only 的意思。

当类被Runtime加载之后,类的结构会发生一些变化在了解这些变化之前,我们需要知道2个概念:

Clean Memory:加载后不会发生更改的内存块,class_ro_t属于Clean Memory,因为它是只读的。
Dirty Memory:运行时会进行更改的内存块,类一旦被加载,就会变成Dirty Memory,例如,我们可以在 Runtime 给类动态的添加方法。

Dirty MemoryClean Memory要昂贵的多,因为它需要更多的内存信息,并且只要进程正在运行,就必须保留它。对于我们来说,越多的Clean Memory显然是更好的,因为它可以节约更多的内存。我们可以通过分离出永不更改的数据部分,将大多数类数据保留为Clean Memory,应该怎么做呢?

我们先看一下,类加载后的结构

截屏2021-06-21 上午9.50.15

在类加载到 Runtime 中后会被分配用于读取/写入数据的结构体class_rw_t

事实证明,class_rw_t会占用比class_ro_t占用更多的内存,在 iPhone 中,我们在系统测量了大约 30MB 的这些class_rw_t结构。应该如何优化这些内存呢?通过测量实际设备上的使用情况,我们发现大约 10% 的类实际会存在动态的更改行为,如动态添加方法,使用 Category 方法等。因此,我们能可以把这部分动态的部分提取出来,我们称之为class_rw_ext_t,所以,结构会变成这个样子。

截屏2021-06-21 上午9.50.41

经过拆分,可以把 90% 的类优化为Clean Memory,在系统层面,取得效果是节省了大约 14MB 的内存,使内存可用于更有效的用途。

更多内容可以查看苹果方法视频

小结

class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容。

Type Encodings

在前面我们执行clangmain.m文件转换成main.cpp文件的时候,我们发现有一些编码,例如

{(struct objc_selector *)"setNickName:", "v24@0:8@16", (void *)_I_JSPerson_setNickName_},中的v24@0:8@16,这其实就是运行时中的encode编码,它的编码表如下:

截屏2021-06-20 20.05.54

苹果官方链接地址

v24@0:8@16的含义也就是:

  • v:void
  • 24:占用的内存
  • @: 对象类型参数self
  • 0:上面参数从0位置开始
  • :: SEL
  • 8:SEL8位置开始
  • @:对象类型,实际传入的第一个参数
  • 16:从16位置开始

setter方法底层

我们首先定义一个JSPerson类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface JSPerson : NSObject
{
NSString *hobby; //
int a;
NSObject *objc; //
}

@property (nonatomic, copy) NSString *nickName;
@property (atomic, copy) NSString *acnickName;
@property (nonatomic) NSString *nnickName;
@property (atomic) NSString *anickName;

@property (nonatomic, strong) NSString *name;
@property (atomic, strong) NSString *aname;

@end

@implementation JSPerson


@end

通过clang命令将其转换成转换成c++代码:

clang -rewrite-objc main.m -o main.cpp

我们在main.cpp全局搜索JSPerson,定位到类的方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// @implementation JSPerson
static NSString * _I_JSPerson_nickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_JSPerson_setNickName_(JSPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _nickName), (id)nickName, 0, 1); }

extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);

static NSString * _I_JSPerson_acnickName(JSPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), 1); }
static void _I_JSPerson_setAcnickName_(JSPerson * self, SEL _cmd, NSString *acnickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), (id)acnickName, 1, 1); }

static NSString * _I_JSPerson_nnickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)); }
static void _I_JSPerson_setNnickName_(JSPerson * self, SEL _cmd, NSString *nnickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)) = nnickName; }

static NSString * _I_JSPerson_anickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)); }
static void _I_JSPerson_setAnickName_(JSPerson * self, SEL _cmd, NSString *anickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)) = anickName; }

static NSString * _I_JSPerson_name(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_name)); }
static void _I_JSPerson_setName_(JSPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_name)) = name; }

static NSString * _I_JSPerson_aname(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_aname)); }
static void _I_JSPerson_setAname_(JSPerson * self, SEL _cmd, NSString *aname) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_aname)) = aname; }
// @end

可以看到,各个属性的getset方法如上,可以发现,不同属性的set方法执行的方法不一定相同,比如:

  • nameset方法:static void _I_JSPerson_setName_(JSPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_name)) = name; }
  • nickNameset方法:static void _I_JSPerson_setNickName_(JSPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _nickName), (id)nickName, 0, 1); }

nameset方法内存平移nickNameset方法调用的是objc_setProperty,这两种方式的是怎么确定的呢,什么情况下属性的set方法调用objc_setProperty方法呢?探究这个问题我们就需要使用LLVM了,我们从github上下载LLVM的源码,下载地址

LLVM源码里我们全局搜索objc_setProperty,查找调用这个方法的地方,发现了getSetPropertyFn方法,它的返回值是CGM.CreateRuntimeFunction(FTy, "objc_setProperty");

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
llvm::FunctionCallee getSetPropertyFn() {
CodeGen::CodeGenTypes &Types = CGM.getTypes();
ASTContext &Ctx = CGM.getContext();
// void objc_setProperty (id, SEL, ptrdiff_t, id, bool, bool)
CanQualType IdType = Ctx.getCanonicalParamType(Ctx.getObjCIdType());
CanQualType SelType = Ctx.getCanonicalParamType(Ctx.getObjCSelType());
CanQualType Params[] = {
IdType,
SelType,
Ctx.getPointerDiffType()->getCanonicalTypeUnqualified(),
IdType,
Ctx.BoolTy,
Ctx.BoolTy};
llvm::FunctionType *FTy =
Types.GetFunctionType(
Types.arrangeBuiltinFunctionDeclaration(Ctx.VoidTy, Params));
return CGM.CreateRuntimeFunction(FTy, "objc_setProperty");
}

接下来搜索关键字getSetPropertyFn(),同样是查找调用的地方:

1
2
3
llvm::FunctionCallee GetPropertySetFunction() override {
return ObjCTypes.getSetPropertyFn();
}

这里只是一个中间调用的方法,我们继续搜索关键字GetPropertySetFunction(),找调用的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void
CodeGenFunction::generateObjCSetterBody(const ObjCImplementationDecl *classImpl,
const ObjCPropertyImplDecl *propImpl,
llvm::Constant *AtomicHelperFn) {
//省略代码
switch (strategy.getKind()) {
case PropertyImplStrategy::Native: {
// We don't need to do anything for a zero-size struct.
if (strategy.getIvarSize().isZero())
return;

Address argAddr = GetAddrOfLocalVar(*setterMethod->param_begin());

LValue ivarLValue =
EmitLValueForIvar(TypeOfSelfObject(), LoadObjCSelf(), ivar, /*quals*/ 0);
Address ivarAddr = ivarLValue.getAddress(*this);

// Currently, all atomic accesses have to be through integer
// types, so there's no point in trying to pick a prettier type.
llvm::Type *bitcastType =
llvm::Type::getIntNTy(getLLVMContext(),
getContext().toBits(strategy.getIvarSize()));

// Cast both arguments to the chosen operation type.
argAddr = Builder.CreateElementBitCast(argAddr, bitcastType);
ivarAddr = Builder.CreateElementBitCast(ivarAddr, bitcastType);

// This bitcast load is likely to cause some nasty IR.
llvm::Value *load = Builder.CreateLoad(argAddr);

// Perform an atomic store. There are no memory ordering requirements.
llvm::StoreInst *store = Builder.CreateStore(load, ivarAddr);
store->setAtomic(llvm::AtomicOrdering::Unordered);
return;
}

case PropertyImplStrategy::GetSetProperty:
case PropertyImplStrategy::SetPropertyAndExpressionGet: {

llvm::FunctionCallee setOptimizedPropertyFn = nullptr;
llvm::FunctionCallee setPropertyFn = nullptr;
if (UseOptimizedSetter(CGM)) {
// 10.8 and iOS 6.0 code and GC is off
setOptimizedPropertyFn =
CGM.getObjCRuntime().GetOptimizedPropertySetFunction(
strategy.isAtomic(), strategy.isCopy());
if (!setOptimizedPropertyFn) {
CGM.ErrorUnsupported(propImpl, "Obj-C optimized setter - NYI");
return;
}
}
else {
setPropertyFn = CGM.getObjCRuntime().GetPropertySetFunction();
if (!setPropertyFn) {
CGM.ErrorUnsupported(propImpl, "Obj-C setter requiring atomic copy");
return;
}
}
}

这里是个switch语句,调用的条件取决于strategy.getKind(),我们接下来就搜索PropertyImplStrategy找一下类型是什么时候设置的,在PropertyImplStrategy的定义中我们找到了答案:copy修饰的属性会有Kind = GetSetProperty,也就是set方法会调用objc_setProperty

1
2
3
4
5
6
// If we have a copy property, we always have to use getProperty/setProperty.
// TODO: we could actually use setProperty and an expression for non-atomics.
if (IsCopy) {
Kind = GetSetProperty;
return;
}

我们写代码验证一下:

1
2
3
4
@property (nonatomic, copy) NSString *nickName;
@property (atomic, copy) NSString *acnickName;
@property (nonatomic) NSString *nnickName;
@property (atomic) NSString *anickName;

我们定义四个属性,然后用clang将其转换成c++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static NSString * _I_JSPerson_nickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_JSPerson_setNickName_(JSPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _nickName), (id)nickName, 0, 1); }//copy

extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);

static NSString * _I_JSPerson_acnickName(JSPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), 1); }
static void _I_JSPerson_setAcnickName_(JSPerson * self, SEL _cmd, NSString *acnickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), (id)acnickName, 1, 1); }//copy

static NSString * _I_JSPerson_nnickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)); }
static void _I_JSPerson_setNnickName_(JSPerson * self, SEL _cmd, NSString *nnickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)) = nnickName; }//strong

static NSString * _I_JSPerson_anickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)); }
static void _I_JSPerson_setAnickName_(JSPerson * self, SEL _cmd, NSString *anickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)) = anickName; }//strong

小结

  • set方法在底层并不是定义了很多set方法调用,而是采用内存平移或调用objc_setProperty方法。
  • 使用copy修饰的属性的set方法调用的是objc_setProperty方法。
  • 没有copy修饰的属性的set方法是内存平移

isKindOfClassvsisMemberOfClass

我们平时开发经常会使用isKindOfClassisMemberOfClass方法来判断对象的类型,从一个例子开始。首先定义两个继承关系的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface JSPerson : NSObject{
NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@end
@implementation JSPerson
@end
@interface JSTeacher : JSPerson
@property (nonatomic, copy) NSString *hobby;
- (void)teacherSay;
@end
@implementation JSTeacher
- (void)teacherSay{
NSLog(@"%s",__func__);
}
@end

我们定义一个方法,用来打印方法的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
void jsKindofDemo(void){
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; //
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; //
BOOL re3 = [(id)[JSPerson class] isKindOfClass:[JSPerson class]]; //
BOOL re4 = [(id)[JSPerson class] isMemberOfClass:[JSPerson class]]; //
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; //
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; //
BOOL re7 = [(id)[JSPerson alloc] isKindOfClass:[JSPerson class]]; //
BOOL re8 = [(id)[JSPerson alloc] isMemberOfClass:[JSPerson class]]; //
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
}

打印结果如下:

1
2
3
4
5
6
7
8
re1 :1
re2 :0
re3 :0
re4 :0
re5 :1
re6 :1
re7 :1
re8 :1

re5-re8符合我们平时使用的思想,对象判断是否是类,re1-re4是什么情况呢,我们看isKindOfClass底层源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__//我们只看这里 objc2
if (slowpath(!obj)) return NO;
Class cls = obj->getIsa();//获取对象(类对象)的类(元类)
if (fastpath(!cls->hasCustomCore())) {
for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {//遍历父类
if (tcls == otherClass) return YES;
}
return NO;
}
#endif
return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

通过源码和上一篇中isa的走位和继承链,我们可以看出

  • 实例对象调用:1、获取对象的类。2、依次查找对象的类和父类,如果和传入的类相等返回YES,遍历结束未找到返回NO

    即查找顺序:对象的类->父类->根类(NSObject)->nil

  • 类对象:1、获取到类的元类。2、依次查找元类及元类的父类,如果和传入的类相等返回YES,遍历结束未找到返回NO

    即查找顺序:元类->元类父类->根元类->根类(NSObject)->nil

通过上面的结论,我们看上面的例子:

  • re1:传入是NSObject的类对象,首先找它的元类即根元类,根元类的父类是NSObject=传入的第二个参数,所以re1=1
  • re3:传入是JSPerson的类对象,首先找它的元类,依次找元类的父类到根元类,最后到根类,没有类=[JSPerson class],所以re3=0
  • re5:传入是NSObject的实例对象,找它的类就是NSObject=[NSObject class],所以re5=1
  • re7:传入是JSPerson的实例对象,找它的类就是JSPerson=[JSPerson class],所以re7=1

我们继续看isMemberOfClass的源码

1
2
3
4
5
6
7
8
//类方法
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
//实例方法
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}

可以看出:

  • 实例对象:判断实例对象的类和传入的类是否相等,即是否相等
  • 类对象:判断类的元类是否和传入的类相等,即看元类

通过上面结论我们继续看例子:

  • re2:传入是NSObject的类对象,首先找它的元类即根元类,根元类不是NSObject类,所以re2=0
  • re4:传入是JSPerson的类对象,首先找它的元类,元类!=[JSPerson class],所以re4=0
  • re6:传入是NSObject的实例对象,找它的类就是NSObject=[NSObject class],所以re6=1
  • re8:传入是JSPerson的实例对象,找它的类就是JSPerson=[JSPerson class],所以re8=1

本文我们主要研究了类的rorw的区别,属性的set方法,以及isKindOfClassvsisMemberOfClass的源码,类的探究就到这里了,有遗漏的话后面会补充。

我们在上一篇文章里探索了对象的本质,我们知道对象在底层是objc_object结构体,objc_object第一个成员是isa,今天我们从isa开始探索。

isa的指向

我们首先还是先定义一个继承NSObject的类JSPerson,在main方法里实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//  JSPerson.h
@interface JSPerson : NSObject

@end
// JSPerson.m
@implementation JSPerson

@end
//main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 0x00007ffffffffff8
JSPerson *p = [JSPerson alloc];
NSLog(@"%@",p);//断点
}

我们在NSLog行打断点,使用lldb打印对象的地址:

1
2
3
4
5
6
7
(lldb) x/4gx p
0x10045e6e0: 0x001d8001000083a9 0x0000000000000000
0x10045e6f0: 0x6c6f6f54534e5b2d 0x7370616e53726162
(lldb) p/x 0x001d8001000083a9 & 0x00007ffffffffff8 //isa&掩码 得到isa指向内容的地址
(long) $1 = 0x00000001000083a8
(lldb) po 0x00000001000083a8 //打印isa指向地址
JSPerson

通过上面lldb命令,我们发现isa指向的内容是JSPerson类,即对象的isa指向的是类。上一节我们探索了类在底层其实是objc_class它继承自objc_object,那意味着类应该也有isa指针,类的isa指针指向哪里呢?带着这个疑问我们继续探索:

1
2
3
4
5
6
7
(lldb) x/4gx 0x00000001000083a8 //类对象地址
0x1000083a8: 0x0000000100008380 0x00007fff8e92c118
0x1000083b8: 0x000000010055b1a0 0x0004801000000007
(lldb) p/x 0x0000000100008380 & 0x00007ffffffffff8 //isa&掩码 得到isa指向内容的地址
(long) $6 = 0x0000000100008380
(lldb) po 0x0000000100008380 //打印isa指向地址
JSPerson

我们发现类对象isa指向的地址打印的结果也是JSPerson,而且这个地址和对象isa指向的地址不是同一个。为什么一个类会有两个内存地址不同的类对象呢,难道类对象和实例对象一样也是可以创建多个吗?我们写一段代码验证一下是不是这样:

1
2
3
4
5
6
7
void jsTestClassNum(void){
Class class1 = [JSPerson class];
Class class2 = [JSPerson alloc].class;
Class class3 = object_getClass([JSPerson alloc]);
Class class4 = [JSPerson alloc].class;
NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}

我们定义一个函数,里面打印四种方式获取类对象的地址:

1
2
3
4
0x1000083a8-
0x1000083a8-
0x1000083a8-
0x1000083a8

这四种方式打印的结果都是同一个,说明类对象并没有多个,而且这个类对象地址和实例对象的isa指向的地址一致。类对象的isa指向的是一个新的东西即元类。下面我们用MachOView打开编译好的二进制文件来验证一下元类是否真的存在:

macho_metaclass

我们在符号表中搜索class关键字,发现了_OBJC_METACLASS_$_JSPerson,说明编译器确实在编译期生成了元类对象。

现在我们知道编译期会帮我们创建元类对象,那元类对象是不是也有isa指针?我们使用lldb继续探索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(lldb) x/4gx JSPerson.class
0x1000083a8: 0x0000000100008380 0x00007fff8e92c118
0x1000083b8: 0x00007fff671dc140 0x0000801000000000
(lldb) p/x 0x0000000100008380 & 0x00007ffffffffff8
(long) $1 = 0x0000000100008380
(lldb) po 0x0000000100008380 //拿到元类地址
JSPerson

(lldb) x/4gx 0x0000000100008380
0x100008380: 0x00007fff8e92c0f0 0x00007fff8e92c0f0
0x100008390: 0x00000001006058e0 0x0001e03100000007
(lldb) p/x 0x00007fff8e92c0f0 & 0x00007ffffffffff8
(long) $3 = 0x00007fff8e92c0f0
(lldb) po 0x00007fff8e92c0f0 //元类isa指向地址
NSObject

(lldb) p/x NSObject.class
(Class) $5 = 0x00007fff8e92c118 NSObject //与 元类isa指向地址不同
(lldb) x/4gx 0x00007fff8e92c0f0
0x7fff8e92c0f0: 0x00007fff8e92c0f0 0x00007fff8e92c118
0x7fff8e92c100: 0x0000000100605960 0x0005e03100000007
(lldb) p/x 0x00007fff8e92c0f0 & 0x00007ffffffffff8
(long) $6 = 0x00007fff8e92c0f0
(lldb) po 0x00007fff8e92c0f0//根元类isa指向自己
NSObject

通过上面的探索,发现元类的isa指向的是NSObject的元类也就是根元类,根元类的isa指向的是根元类自己。到这里isa的走位就比较清晰了,也就是官方文档里的一个经典的图:

isa流程图

图中除了isa的走位,还有superClass的走位,我们用代码验证打印一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//  JSStudent.h
@interface JSStudent : JSPerson

@end
// JSStudent.m
@implementation JSStudent

@end
void JSTestNSObject(void){
// NSObject实例对象
NSObject *object1 = [NSObject alloc];
// NSObject类
Class class = object_getClass(object1);
// NSObject元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);

// JSPerson元类
Class pMetaClass = object_getClass(JSPerson.class);
Class psuperClass = class_getSuperclass(pMetaClass);
NSLog(@"%@ - %p",psuperClass,psuperClass);

// JSStudent -> JSPerson -> NSObject
// 元类也有一条继承链
Class tMetaClass = object_getClass(JSStudent.class);
Class tsuperClass = class_getSuperclass(tMetaClass);
NSLog(@"%@ - %p",tsuperClass,tsuperClass);

// NSObject 根类特殊情况
Class nsuperClass = class_getSuperclass(NSObject.class);
NSLog(@"%@ - %p",nsuperClass,nsuperClass);
// 根元类 -> NSObject
Class rnsuperClass = class_getSuperclass(metaClass);
NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
}

打印结果:

1
2
3
4
5
6
7
8
9
0x1006055e0 实例对象
0x7fff8e92c118
0x7fff8e92c0f0 元类
0x7fff8e92c0f0 根元类
0x7fff8e92c0f0 根根元类
NSObject - 0x7fff8e92c0f0
JSPerson - 0x100008418
(null) - 0x0 //NSObject没有父类
NSObject - 0x7fff8e92c118

打印结果显而易见,至此isa的走位图和继承链我们探究完了,总结起来就是官方的那张经典的走位图。

内存平移

在探究类的结构之前,我们先介绍一个概念内存平移。我们定义一个数组array,定义一个指针pArray指向数组array,代码如下:

1
2
3
4
5
6
7
int array[4] = {1,2,3,4};
int *pArray = array;
NSLog(@"%p - %p - %p - %p",&array,&array[0],&array[1],&array[2]);
NSLog(@"%p - %p - %p",pArray,pArray+1,pArray+2);
//以下为打印结果:
0x7ffeefbff440 - 0x7ffeefbff440 - 0x7ffeefbff444 - 0x7ffeefbff448
0x7ffeefbff440 - 0x7ffeefbff444 - 0x7ffeefbff448

我们看到arrayarray[0]的地址相同,这个好理解,因为数组指向的就是第一个元素的地址。同时我们看到pArraypArray+1pArray+2分别指向了array[0]array[1]array[2],这就是内存平移的作用。我们可以利用内存平移原理取到数组中任意位置的元素:

1
2
3
4
5
6
7
8
9
for (int i = 0; i<4; i++) {
int value = *(pArray+i);
NSLog(@"%d",value);
}
//打印结果
1
2
3
4

内存平移了解之后,我们继续探索类。

类的结构内存

我们打开objc搜索objc_class,找到类的结构

1
2
3
4
5
6
7
8
9
10
11
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
///省略代码
}

我们知道结构体占用的内存大小的影响因素是成员变量(方法是存在方法区),所以这里我们省略下面方法的相关代码。看类的结构体有四个成员变量isasuperclasscachebitsisa我们之前探究过了,superclass指向的是父类也很清楚,cache我们后面单独分析,我们先看bits成员。根据上一小节的内存平移我们知道,bits的内存地址是类的地址加上前三个成员内存大小而得到,isasuperclass都是指向类的指针类型各占用8个字节很好理解,这里关键是cache占用多少字节,我们看一下cache_t的源码结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;//4
#if __LP64__
uint16_t _flags;//2
#endif
uint16_t _occupied;//2
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
///省略 静态变量和方法代码
}
typedef unsigned long uintptr_t;
typedef uint32_t mask_t; //4字节

cache_t的内容很多,一看很容易懵逼,到时我们发现有规律,就是352行后面的代码是静态变量和方法,静态变量实际是存储在静态区,方法是存储在方法区,它们都不会占用结构体的内存,所以cache_t的内存大小就取决于_bucketsAndMaybeMask和一个联合体。

_bucketsAndMaybeMask的大小就是uintptr_t的大小8。联合体我们探究过,它的内存是其最大成员的大小,联合体包括一个结构体和_originalPreoptCache,结构体的大小是8,_originalPreoptCache大小取决于preopt_cache_t:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct preopt_cache_t {
int32_t fallback_class_offset;//4字节
union {
struct {
uint16_t shift : 5;
uint16_t mask : 11;
};
uint16_t hash_params;
};//1字节
uint16_t occupied : 14;//1字节
uint16_t has_inlines : 1;//1字节
uint16_t bit_one : 1;//1字节
preopt_cache_entry_t entries[];

inline int capacity() const {
return mask + 1;
}
};
typedef unsigned short uint16_t;//占一个字节

可以看出_originalPreoptCache的大小也是8。所以我们得到cache的内存大小是16。所以bits成员的内存地址就是类的地址平移32(16进制就是0x20)字节。

bits

有了前面的基础,我们开始看bits成员里的内容,我们首先看一下class_data_bits_t的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
struct class_data_bits_t {
friend objc_class;

// Values are the FAST_ flags above.
uintptr_t bits;
private:
bool getBit(uintptr_t bit) const
{
return bits & bit;
}

// Atomically set the bits in `set` and clear the bits in `clear`.
// set and clear must not overlap.
void setAndClearBits(uintptr_t set, uintptr_t clear)
{
ASSERT((set & clear) == 0);
uintptr_t newBits, oldBits = LoadExclusive(&bits);
do {
newBits = (oldBits | set) & ~clear;
} while (slowpath(!StoreReleaseExclusive(&bits, &oldBits, newBits)));
}

void setBits(uintptr_t set) {
__c11_atomic_fetch_or((_Atomic(uintptr_t) *)&bits, set, __ATOMIC_RELAXED);
}

void clearBits(uintptr_t clear) {
__c11_atomic_fetch_and((_Atomic(uintptr_t) *)&bits, ~clear, __ATOMIC_RELAXED);
}

public:

class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
ASSERT(!data() || (newData->flags & (RW_REALIZING | RW_FUTURE)));
// Set during realization or construction only. No locking needed.
// Use a store-release fence because there may be concurrent
// readers of data and data's contents.
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}

// Get the class's ro data, even in the presence of concurrent realization.
// fixme this isn't really safe without a compiler barrier at least
// and probably a memory barrier when realizeClass changes the data field
const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}
///省略代码
};

class_data_bits_t有两个对外公开的返回值的方法data()safe_ro()。我们先看data()它的返回值是class_rw_t我们看一下它的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
///省略代码
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}

const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
};

class_rw_t的定义中最下面我们看到三个方法methods()properties()protocols(),貌似类的方法属性、协议、方法是存储在这里的,我们验证一下,我们在JSPerson类中加属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//  JSPerson.h
@interface JSPerson : NSObject
{
NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *hobby;

- (void)sayNB;
+ (void)saySomething;

@end
// JSPerson.m
#import "JSPerson.h"

@implementation JSPerson
- (void)sayNB{

}
+ (void)saySomething{

}

@end
//main
JSPerson *p1 = [[JSPerson alloc] init];
NSLog(@"%@",p1);

属性

我们在main方法中打断点,使用lldb调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
(lldb) p/x JSPerson.class
(Class) $0 = 0x0000000100008530 JSPerson
(lldb) p/x 0x0000000100008530+0x20
(long) $1 = 0x0000000100008550 //bits地址
(lldb) p (class_data_bits_t *)0x0000000100008550
(class_data_bits_t *) $2 = 0x0000000100008550
(lldb) p $2->data() //bits.data()
(class_rw_t *) $3 = 0x000000010102db50
(lldb) p *$3
(class_rw_t) $4 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000224
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
(lldb) p $3.properties()//取属性列表
(const property_array_t) $5 = {
list_array_tt<property_t, property_list_t, RawPtr> = {
= {
list = {
ptr = 0x00000001000081d0
}
arrayAndFlag = 4295000528
}
}
}
Fix-it applied, fixed expression was:
$3->properties()
(lldb) p $5.list
(const RawPtr<property_list_t>) $6 = {
ptr = 0x00000001000081d0
}
(lldb) p $6.ptr
(property_list_t *const) $7 = 0x00000001000081d0
(lldb) p *$7
(property_list_t) $8 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 2)
}//可以看到count=2,只有两个属性
(lldb) p $8.get(0)
(property_t) $9 = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
(lldb) p $8.get(1)
(property_t) $10 = (name = "hobby", attributes = "T@\"NSString\",C,N,V_hobby")

通过上面的调试和注释我们发现bits.data()properties()方法里存储了对象的属性,但是没有成员变量nickName

成员变量

那成员变量存储在哪里呢,class_rw_t结构体中没有成员变量ivar关键字的属性或方法,我们回到class_data_bits_t继续查找,发现有一个safe_ro()方法,我们看一下safe_ro()方法返回值结构体class_ro_t的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
///省略代码
};

我们发现有一个ivars,这是不是就是实例变量存储的位置呢,我们继续用lldb探索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
(lldb) p/x JSPerson.class
(Class) $0 = 0x0000000100008530 JSPerson
(lldb) p/x 0x0000000100008530+0x20
(long) $1 = 0x0000000100008550
(lldb) p (class_data_bits_t *)0x0000000100008550
(class_data_bits_t *) $2 = 0x0000000100008550
(lldb) p $2.safe_ro()
(const class_ro_t *) $3 = 0x00000001000080a0
Fix-it applied, fixed expression was:
$2->safe_ro()
(lldb) p $3->ivars//获取ivars
(const ivar_list_t *const) $4 = 0x0000000100008168
(lldb) p *$4
(const ivar_list_t) $5 = {
entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 3)
}
(lldb) p $5.get(0)
(ivar_t) $6 = {
offset = 0x00000001000084d8
name = 0x0000000100003f18 "nickName"
type = 0x0000000100003f79 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $5.get(1)
(ivar_t) $7 = {
offset = 0x00000001000084e0
name = 0x0000000100003f21 "_name"
type = 0x0000000100003f79 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $5.get(2)
(ivar_t) $8 = {
offset = 0x00000001000084e8
name = 0x0000000100003f27 "_hobby"
type = 0x0000000100003f79 "@\"NSString\""
alignment_raw = 3
size = 8
}

果然成员变量在safe_ro()ivars成员里,可以看到编译器给属性自动生成了带_的成员变量。其实我们还看到class_ro_t结构体中还有baseMethodListbaseProtocolsbaseProperties,这些我们后边再探索。

实例方法

我们用继续看方法列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
(lldb) p/x JSPerson.class
(Class) $0 = 0x0000000100008530 JSPerson
(lldb) p/x 0x0000000100008530+0x20
(long) $1 = 0x0000000100008550
(lldb) p (class_data_bits_t *)$1
(class_data_bits_t *) $2 = 0x0000000100008550
(lldb) p $2.data()
(class_rw_t *) $3 = 0x000000010092c330
Fix-it applied, fixed expression was:
$2->data()
(lldb) p *$3
(class_rw_t) $4 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000224
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
(lldb) p $4.methods()
(const method_array_t) $5 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x00000001000080e8
}
arrayAndFlag = 4295000296
}
}
}
(lldb) p $5.list
(const method_list_t_authed_ptr<method_list_t>) $6 = {
ptr = 0x00000001000080e8
}
(lldb) p $6.ptr
(method_list_t *const) $7 = 0x00000001000080e8
(lldb) p *$7
(method_list_t) $8 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 5)
}//共有5个方法
(lldb) p $8.get(0)
(method_t) $9 = {}//不能像属性一样调用get(0)获取,可以查看method_t结构体
(lldb) p $8.get(0).big()
(method_t::big) $10 = {
name = "sayNB"
types = 0x0000000100003f71 "v16@0:8"
imp = 0x0000000100003b50 (KCObjcBuild`-[JSPerson sayNB])
}
(lldb) p $8.get(1).big()
(method_t::big) $11 = {
name = "hobby"
types = 0x0000000100003f85 "@16@0:8"
imp = 0x0000000100003bc0 (KCObjcBuild`-[JSPerson hobby])
}
(lldb) p $8.get(2).big()
(method_t::big) $12 = {
name = "setHobby:"
types = 0x0000000100003f8d "v24@0:8@16"
imp = 0x0000000100003bf0 (KCObjcBuild`-[JSPerson setHobby:])
}
(lldb) p $8.get(3).big()
(method_t::big) $13 = {
name = "name"
types = 0x0000000100003f85 "@16@0:8"
imp = 0x0000000100003b60 (KCObjcBuild`-[JSPerson name])
}
(lldb) p $8.get(4).big()
(method_t::big) $14 = {
name = "setName:"
types = 0x0000000100003f8d "v24@0:8@16"
imp = 0x0000000100003b90 (KCObjcBuild`-[JSPerson setName:])
}

我们分析可以发现方法列表里有两个属性的getset方法,还有一个是我们定义的实例方法sayNB,但是没有类方法saySomething,类方法存储在哪里呢。

类方法

我们很容易联想到元类,那我们就用相同的方式查看元类的方法列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
(lldb) x/4gx JSPerson.class
0x100008530: 0x0000000100008508 0x0000000100357140
0x100008540: 0x000000010076a5b0 0x0001802800000003
(lldb) po 0x0000000100008508 //找到元类地址
JSPerson
(lldb) p/x 0x0000000100008508+0x20
(long) $2 = 0x0000000100008528
(lldb) p (class_data_bits_t *)0x0000000100008528
(class_data_bits_t *) $3 = 0x0000000100008528
(lldb) p $3.data()
(class_rw_t *) $4 = 0x000000010076a550
Fix-it applied, fixed expression was:
$3->data()
(lldb) p *$4
(class_rw_t) $5 = {
flags = 2684878849
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4312049361
}
}
firstSubclass = nil
nextSiblingClass = 0x00007fff861ddcd8
}
(lldb) p $5.methods()
(const method_array_t) $6 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100008080
}
arrayAndFlag = 4295000192
}
}
}
(lldb) p $6.list
(const method_list_t_authed_ptr<method_list_t>) $7 = {
ptr = 0x0000000100008080
}
(lldb) p $7.ptr
(method_list_t *const) $8 = 0x0000000100008080
(lldb) p *$8
(method_list_t) $9 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $9.get(0).big()
(method_t::big) $10 = {
name = "saySomething"
types = 0x0000000100003f71 "v16@0:8"
imp = 0x0000000100003b40 (KCObjcBuild`+[JSPerson saySomething])
}

所以类的类方法是存储在元类的方法列表里的。

总结

本节我们主要探索了类的结构,isa指针的走位,以及类中属性、方法、成员变量的存储。

  • 类的本质是对象。
  • 实例方法存放在类中
  • 类方法存放在元类中

类在class_rw_t存储属性、方法、协议等信息,在class_ro_t里存储了成员变量、baseMethodListbaseProtocolsbaseProperties等信息,那class_ro_tclass_rw_t的区别是什么呢,我们下一篇文章继续探索。

前面我们探索过NSObjectalloc方法及内存对齐的规则,我们本文主要探索OC对象的本质。

查找对象的定义

提到对象的本质,我们最先想到的就是看源码,那怎么定位源码中对象的定义的内容呢。感觉无从下手,那我们就先定义一个对象初始化一下,利用clang命令,将OC代码转换成c++代码找一下线索。我们先定义一个类JSPerson,然后初始化对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface JSPerson : NSObject

@property (nonatomic, strong) NSString *firstName;

@property (nonatomic, strong) NSString *lastName;
@end

@implementation JSPerson
@end

#import "JSPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
JSPerson *person = [[JSPerson alloc] init];
person.firstName = @"Jason";
person.lastName = @"Test";
NSLog(@"%@",person.firstName);
NSLog(@"%@",person.firstName);
}
return 0;
}

cdmain.m文件的目录,我们使用clang命令将main.m文件转换成c++代码:
clang -rewrite-objc main.m -o main.cpp在目录里生成了main.cpp文件。我们在main.cpp文件中搜索JSPerson,搜查查找到了下面这段代码:

1
2
3
4
5
6
7
typedef struct objc_object JSPerson;
///省略代码
struct JSPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_firstName;
NSString *_lastName;
};

这个结构体JSPerson_IMPL里包含了我们定义的两个属性_firstName_lastName,说明对象的本质是objc-object类型的结构体,我们发现`

1
2
3
4
5
6
7
typedef struct objc_object JSPerson;
///省略代码
struct JSPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_firstName;
NSString *_lastName;
};

这个结构体JSPerson_IMPL里包含了我们定义的两个属性_firstName_lastName,说明对象的本质是objc-object类型的结构体,我们发现`

1
2
3
4
5
6
7
typedef struct objc_object JSPerson;
///省略代码
struct JSPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_firstName;
NSString *_lastName;
};

这个结构体JSPerson_IMPL里包含了我们定义的两个属性_firstName_lastName,说明对象的本质是objc-object类型的结构体,我们发现JSPerson_IMPL第一个属性是NSObject_IVARS是什么呢,它并不是我们定义的属性,我们搜索它的类型NSObject_IMPL发现:

1
2
3
struct NSObject_IMPL {
Class isa;
};

显而易见NSObject_IVARS就是isa指针。我们接下来开始探索isa,在探索isa指针之前,我们先看一个概念位域

位域

我们先定义两个结构体JSCar1JSCar2,我们在main方法里打印一下两个结构体的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct JSCar1 {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
struct JSCar2 {
BOOL front: 1;
BOOL back : 1;
BOOL left : 1;
BOOL right: 1;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
struct JSCar1 car1;
struct JSCar2 car2;
NSLog(@"%ld-%ld",sizeof(car1),sizeof(car2));
}

发现打印的结果是

1
4-1

为什么结构体的属性个数和类型都相同,而结构体占用的内存的大小不一样呢,这里就是位域的作用,在结构体属性后面加上:1表示这个属性只占用一位(注意这里不是字节,一个字节是8位),所以car2的四个属性只占用了4位,但是内存分配最少是一个字节(8位),所以car2的内存大小是1个字节。示例图如下:

位域

可以看出使用位域我们可以节省内存(4B->1B)。

联合体

除了在结构体里使用位域,我们使用联合体也能达到节省作用:

1
2
3
4
5
6
7
8
9
10
union JSCar3 {
BOOL front;
BOOL back;
BOOL left;
BOOL right;
};
struct JSCar1 car1;
struct JSCar2 car2;
union JSCar3 car3;
NSLog(@"%ld-%ld-%ld",sizeof(car1),sizeof(car2),sizeof(car3));

我们定义一个联合体JSCar3,然后打印其大小:

1
4-1-1

可以看到car3联合体的内存大小也是1,我们很容易猜想car2car3的内存大小相同,那内存存储结构是相同的吗?带着这个疑问我们继续探索,在图中箭头位置使用lldb打印相应结构体:

截图

下面是打印的结果:

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) p car2
(JSCar2) $0 = (front = NO, back = NO, left = NO, right = NO)
(lldb) p car2
(JSCar2) $1 = (front = 255, back = NO, left = NO, right = NO)
(lldb) p car2
(JSCar2) $2 = (front = 255, back = 255, left = NO, right = NO)
(lldb) p car3
(JSCar3) $3 = (front = NO, back = NO, left = NO, right = NO)
(lldb) p car3
(JSCar3) $4 = (front = YES, back = YES, left = YES, right = YES)
(lldb) p car3
(JSCar3) $5 = (front = YES, back = YES, left = YES, right = YES)

从打印结果我们发现,car2一个属性的赋值并不会影响其他属性,而car3一个属性的值变化了,其他属性的值也会变化,说明car2car3的内存存储结构是不一样的,结构体各成员变量是共存的,联合体各成员变量是互斥的,一般联合体位域配合使用。

联合体的内存规则如下

  • 联合体中可以定义多个成员,联合体的大小由最大的成员大小决定。
  • 联合体的成员公用一个内存,一次只能使用一个成员。
  • 对某一个成员赋值,会覆盖其他成员的值。

isa指针

有了上面知识的铺垫,我们继续研究isa指针。我们打开objc的源码,找到isa指针的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
union isa_t {
//构造方法
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};

通过源码可以看到isa是一个联合体,它有一个位域成员ISA_BITFIELD,我们继续看ISA_BITFIELD的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t weakly_referenced : 1; \
uintptr_t shiftcls_and_sig : 52; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# else
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
//省略代码
# elif __x86_64__
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8

这些位代表的含义分别是:

  • nonpointer:表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等
  • has_assoc:关联对象标志位,0没有,1存在
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
  • weakly_referenced:志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
  • deallocating:标志对象是否正在释放内存
  • has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位
  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc

可以看到x86平台下,isa指针的第4-48存储的是类的地址,下面我们通过位运算验证一下我们的理解是否正确,我们在main方法初始化JSPerson的实例:

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
@autoreleasepool {
JSPerson *p = [JSPerson alloc];//打断点
NSLog(@"%@",p);
}
return 0;
}

我们在注释位置打断点,使用lldb命令,打印p的地址:

1
2
3
4
5
6
7
8
9
10
11
(lldb) x/4gx p
0x1005186b0: 0x001d800100008309 0x0000000000000000
0x1005186c0: 0x63756f54534e5b2d 0x746e6f4372614268
(lldb) p/x 0x001d800100008309 >> 3 //isa指针地址 右移3位
(long) $2 = 0x0003b00020001061
(lldb) p/x 0x0003b00020001061 << 20//左移 20位(因为要补上一步右移的三位)
(long) $3 = 0x0002000106100000
(lldb) p/x 0x0002000106100000 >> 17//右移 17位
(long) $4 = 0x0000000100008308 // 通过位运算 shiftcls的值
(lldb) p/x JSPerson.class
(Class) $5 = 0x0000000100008308 JSPerson // 直接打印 JSPerson类的地址

通过上面的位运算,我们可以确定shiftcls存储的就是JSPerson类的地址。

在我们实际开发中我们定义的对象的isa基本都是nonpointer,好处不言而喻可以增加内存的利用率,减少内存浪费。对象的本质就先探索到这里,后续文章我们继续探索。

在上一篇alloc方法的探索中,我们提到了内存对齐的概念,这篇文章我们就主要介绍一下内存对齐

结构体的字节对齐

一个例子

我们首先定义几个结构体,初始化,看一下每个结构体的内存大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct JSPerson1 {
double a;
char b;
int c;
short d;
}struct1;
struct JSPerson2 {
double a;
int b;
char c;
short d;
}struct2;
struct JSPerson3 {
double a;
int b;
char c;
short d;
int e;
struct JSPerson1 str;
int f;
}struct3;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"struct1的大小==%lu",sizeof(struct1));
NSLog(@"struct2的大小==%lu",sizeof(struct2));
NSLog(@"struct3的大小==%lu",sizeof(struct3));
}
return 0;
}

可以看到打印结果为:

1
2
3
struct1的大小==24
struct2的大小==16
struct3的大小==56

JSPerson1JSPerson2的属性数量相同,为什么打印的大小结果不一样呢,这里就引入了字节对齐的概念,我们先看一下OC中各类型占用的内存大小:

类型占用空间

结构体对齐原则

  • 1、数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。
  • 2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
  • 3、收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员(基本数据类型)的整数倍.不足的要补⻬。

根据上面的表格和规则我们分析一下例子中三个结构体占用的内存大小(64位系统)

  • 首先看struct1

    1
    2
    3
    4
    5
    6
    struct JSPerson1 {
    double a;
    char b;
    int c;
    short d;
    }struct1;

    根据规则1,a变量存储的空间是[0,7],b变量存储的位置是[8],c因为本身是int4个字节大小,所以不能从9的位置开始存储,要从4整数倍开始存储所以c存储的空间是[12,15],dshort类型大小为2162的整数倍,所以d的存储位置是[16,17]。根据规则3,结构体的总大小需要是内部最大成员的整数倍,所以struct1的大小为24.

  • 再看struct2

    1
    2
    3
    4
    5
    6
    struct JSPerson2 {
    double a;
    int b;
    char c;
    short d;
    }struct2;

    和上面一样a变量存储的空间是[0,7]b变量存储的位置是[8,11]c变量的存储空间是[12],d变量的存储空间是[14,15]

    根据规则3struct2的大小为16

  • 最后看struct3

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct JSPerson3 {
    double a;
    int b;
    char c;
    short d;
    int e;
    struct JSPerson1 str;
    int f;
    }struct3;

    前四个变量和struct2类似,a变量存储的空间是[0,7]b变量存储的位置是[8,11]c变量的存储空间是[12],d变量的存储空间是[14,15],e变量存储空间是[16,19],结构体成员比较特殊我们根据规则2,str成员的起始地址要是它内部最大成员的大小的整数倍也就是8,所以str存储的空间是[24,47],f所占的内存空间是[48,51],注意这个地方有个点:结构体的总大小是其最大基本成员变量的大小的整数倍,所以struct3的内存大小是56(8的整数倍)。

对象的字节对齐

看完了结构体的内存对齐,我们看一下对象的,其实我们很容易可以想到,对象其实也是一个结构体,它和结构体的字节对齐规则也是一样的。我们来验证一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@interface Person1 : NSObject{
int _age;
int _weight;
NSString *_name;
}
@end
@implementation Person1

@end

@interface Person2 : NSObject{
int _age;
NSString *_name;
int _weight;
}
@end

@implementation Person2

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
Person1 *obj1 = [[Person1 alloc] init];
NSLog(@"Person1实际占用: class_getInstanceSize = %zd", class_getInstanceSize([Person1 class]));
NSLog(@"Person1系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj1)));
Person2 *obj2 = [[Person2 alloc] init];
NSLog(@"Person2实际占用: class_getInstanceSize = %zd", class_getInstanceSize([Person2 class]));
NSLog(@"Person2系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj2)));
}
return 0;
}

我们定义两个类Person1Person2,分别打印他们实际占用内存和系统分配内存大小:

1
2
3
4
Person1实际占用: class_getInstanceSize = 24
Person1系统分配:malloc_size = 32
Person2实际占用: class_getInstanceSize = 32
Person2系统分配:malloc_size = 32

这里有个注意的点就是不要漏掉isa指针,对象结构体中第一个成员变量就是isa,所以我们分析的方法和结构体一样,Person1isa存储地址是[0,7],_age的存储空间是[8,11],_weight的存储空间是[12,15]_name的存储空间是[16,23],所以Person1实际空间是24,同样的方法可得Person2的实际空间是32

这里我们可以看到,成员变量的顺序是会影响到对象实际占用的内存大小,类的属性最后其实也是会生成对应的成员变量,那么属性的定义顺序是不是也会影响对象的实际占用内存大小呢,我们带着这个问题继续探索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@interface Person1 : NSObject

@property (nonatomic,assign)int age;
@property (nonatomic,assign)int weight;
@property (nonatomic,strong)NSString *name;

@end

@implementation Person1

@end

@interface Person2 : NSObject

@property (nonatomic,assign)int age;
@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)int weight;

@end

@implementation Person2

@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person1 *obj1 = [[Person1 alloc] init];
NSLog(@"Person1实际占用: class_getInstanceSize = %zd", class_getInstanceSize([Person1 class]));
NSLog(@"Person1系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj1)));
Person2 *obj2 = [[Person2 alloc] init];
NSLog(@"Person2实际占用: class_getInstanceSize = %zd", class_getInstanceSize([Person2 class]));
NSLog(@"Person2系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj2)));
}
return 0;
}

我们把成员变量换成属性继续打印上面的内容:

1
2
3
4
Person1实际占用: class_getInstanceSize = 24
Person1系统分配:malloc_size = 32
Person2实际占用: class_getInstanceSize = 24
Person2系统分配:malloc_size = 32

发现这次打印Person1Person2实际占用的内存大小是相同的,这是为什么呢?我们将main.m文件转换成c++代码看一下:

1
xcrun -sdk iphonesimulator clang -rewrite-objc main.m

我们打开main.cpp文件,搜索Person1Person2:

1
2
3
4
5
6
7
8
9
10
11
12
struct Person1_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _weight;
NSString * _Nonnull _name;
};
struct Person2_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _weight;
NSString *_name;
};

看到这里我们明白了,原来编译器帮我们做了一层优化,将属性按照最优的顺序转换成成员变量。所以我们定义属性的顺序并不会影响到对象实际占用的内存大小。

内存对齐的内容就探索到这里,后面会继续探索ios 底层