lly's Blog

用心记录点滴


  • 首页

  • 归档

OC类从应用启动到加载流程探究

发表于 2021-01-05   |  

要分析类的启动加载流程,我们可以从一个点入手,即load方法的调用,因为该方法会在应用启动时自动调用,我们根据这个特性查看一下调用堆栈,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
* frame #0: 0x0000000100bb9dec AppLaunch`+[ViewController load](self=ViewController, _cmd="load") at ViewController.m:17:5
frame #1: 0x00000001c0d3c25c libobjc.A.dylib`load_images + 944
frame #2: 0x0000000100fca21c dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 464
frame #3: 0x0000000100fdb5e8 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 512
frame #4: 0x0000000100fd9878 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
frame #5: 0x0000000100fd9940 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
frame #6: 0x0000000100fca6d8 dyld`dyld::initializeMainExecutable() + 216
frame #7: 0x0000000100fcf928 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 5216
frame #8: 0x0000000100fc9208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
frame #9: 0x0000000100fc9038 dyld`_dyld_start + 56

可以看到,程序是从_dyld_start这个函数开始运行的,老办法,我们到dyld的源码中去一探究竟。

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
#if __arm64__
.text
.align 2
.globl __dyld_start
__dyld_start:
mov x28, sp
and sp, x28, #~15 // force 16-byte alignment of stack
mov x0, #0
mov x1, #0
stp x1, x0, [sp, #-16]! // make aligned terminating frame
mov fp, sp // set up fp to point to terminating frame
sub sp, sp, #16 // make room for local variables
#if __LP64__
ldr x0, [x28] // get app's mh into x0
ldr x1, [x28, #8] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add x2, x28, #16 // get argv into x2
#else
ldr w0, [x28] // get app's mh into x0
ldr w1, [x28, #4] // get argc into x1 (kernel passes 32-bit int argc as 64-bits on stack to keep alignment)
add w2, w28, #8 // get argv into x2
#endif
adrp x3,___dso_handle@page
add x3,x3,___dso_handle@pageoff // get dyld's mh in to x4
mov x4,sp // x5 has &startGlue
// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)
bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm
mov x16,x0 // save entry point address in x16
#if __LP64__
ldr x1, [sp]
#else
ldr w1, [sp]
#endif
cmp x1, #0
b.ne Lnew
// LC_UNIXTHREAD way, clean up stack and jump to result
#if __LP64__
add sp, x28, #8 // restore unaligned stack pointer without app mh
#else
add sp, x28, #4 // restore unaligned stack pointer without app mh
#endif
#if __arm64e__
braaz x16 // jump to the program's entry point
#else
br x16 // jump to the program's entry point
#endif
// LC_MAIN case, set up stack for call to main()
Lnew: mov lr, x1 // simulate return address into _start in libdyld.dylib
#if __LP64__
ldr x0, [x28, #8] // main param1 = argc
add x1, x28, #16 // main param2 = argv
add x2, x1, x0, lsl #3
add x2, x2, #8 // main param3 = &env[0]
mov x3, x2
Lapple: ldr x4, [x3]
add x3, x3, #8
#else
ldr w0, [x28, #4] // main param1 = argc
add x1, x28, #8 // main param2 = argv
add x2, x1, x0, lsl #2
add x2, x2, #4 // main param3 = &env[0]
mov x3, x2
Lapple: ldr w4, [x3]
add x3, x3, #4
#endif
cmp x4, #0
b.ne Lapple // main param4 = apple
#if __arm64e__
braaz x16
#else
br x16
#endif
#endif // __arm64__

dyld的入口函数也是使用汇编来编写的,我们不需要逐行理解,找一下关键字,结合上面的堆栈信息和汇编注释,我们定位到dyldbootstrap::start这个关键方法,最终跟踪到以下调用链:

1
__dyld_start -> dyldbootstrap::start -> _main -> initializeMainExecutable -> runInitializers -> ImageLoader::runInitializers -> ImageLoader::processInitializers -> ImageLoader::recursiveInitialization -> notifySingle -> objc::load_images -> [ViewController load]

上面的流程有一个比较奇怪的调用出现在objc::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
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
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
if ( !image->inSharedCache() ) {
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,这个应该跟objc的初始化有关系,我们全局搜索一下,最后定位到下面的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//
// Note: only for use by objc runtime
// Register handlers to be called when objc images are mapped, unmapped, and initialized.
// Dyld will call back the "mapped" function with an array of images that contain an objc-image-info section.
// Those images that are dylibs will have the ref-counts automatically bumped, so objc will no longer need to
// call dlopen() on them to keep them from being unloaded. During the call to _dyld_objc_notify_register(),
// dyld will call the "mapped" function with already loaded objc images. During any later dlopen() call,
// dyld will also call the "mapped" function. Dyld will call the "init" function when dyld would be called
// initializers in that image. This is when objc calls any +load methods in that image.
//
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped)
{
dyld::registerObjCNotifiers(mapped, init, unmapped);
}

上面的注释写得已经比较清楚,这个函数专供 objc runtime。那我们就去runtime的源码中找找嘛。

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
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
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();
cache_init();
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}

果然在_objc_init初始化函数中发现了调用逻辑,分别注册了map_images和load_images两个回调函数,看到这里熟悉应用启动流程的同学应该想到了,这不就是可执行文件的mmap和load么。但是新的问题又来了,这个_objc_init又是在什么时候被调用的呢?我们直接在里面搞个断点看看调用堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
frame #0: 0x00000001c0d4ce0c libobjc.A.dylib`_objc_init
* frame #1: 0x00000001003b88f8 libdispatch.dylib`_os_object_init + 20
frame #2: 0x00000001003c7ea0 libdispatch.dylib`libdispatch_init + 292
frame #3: 0x00000001dadfd888 libSystem.B.dylib`libSystem_initializer + 200
frame #4: 0x0000000100120810 dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 424
frame #5: 0x0000000100120bd8 dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 52
frame #6: 0x000000010011b600 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 536
frame #7: 0x000000010011b56c dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 388
frame #8: 0x0000000100119878 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 184
frame #9: 0x0000000100119940 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 92
frame #10: 0x000000010010a688 dyld`dyld::initializeMainExecutable() + 136
frame #11: 0x000000010010f928 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 5216
frame #12: 0x0000000100109208 dyld`dyldbootstrap::start(dyld3::MachOLoaded const*, int, char const**, dyld3::MachOLoaded const*, unsigned long*) + 396
frame #13: 0x0000000100109038 dyld`_dyld_start + 56

前面我们已经看过的函数跳过,直接从ImageLoaderMachO::doInitialization开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
// 去掉我们不关心的信息 我眼里只有下面这句话
if ( ! dyld::gProcessInfo->libSystemInitialized ) {
// <rdar://problem/17973316> libSystem initializer must run first
const char* installPath = getInstallPath();
if ( (installPath == NULL) || (strcmp(installPath, libSystemPath(context)) != 0) )
dyld::throwf("initializer in image (%s) that does not link with libSystem.dylib\n", this->getPath());
}
}

看注释和异常文案,libSystem必须最先被初始化,这也解释了上面的堆栈情况。行吧,那我们再去libSystem的源码中看看有没有libSystem_initializer这个初始化方法。

果然是有的,去掉一些不关系的信息,大概如下:

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
// libsyscall_initializer() initializes all of libSystem.dylib
// <rdar://problem/4892197>
__attribute__((constructor))
static void
libSystem_initializer(int argc,
const char* argv[],
const char* envp[],
const char* apple[],
const struct ProgramVars* vars)
{
_libSystem_ktrace0(ARIADNE_LIFECYCLE_libsystem_init | DBG_FUNC_START);
__libkernel_init(&libkernel_funcs, envp, apple, vars);
_libSystem_ktrace_init_func(KERNEL);
__libplatform_init(NULL, envp, apple, vars);
_libSystem_ktrace_init_func(PLATFORM);
__pthread_init(&libpthread_funcs, envp, apple, vars);
_libSystem_ktrace_init_func(PTHREAD);
_libc_initializer(&libc_funcs, envp, apple, vars);
_libSystem_ktrace_init_func(LIBC);
// TODO: Move __malloc_init before __libc_init after breaking malloc's upward link to Libc
__malloc_init(apple);
_libSystem_ktrace_init_func(MALLOC);
#if TARGET_OS_OSX
/* <rdar://problem/9664631> */
__keymgr_initializer();
_libSystem_ktrace_init_func(KEYMGR);
#endif
// No ASan interceptors are invoked before this point. ASan is normally initialized via the malloc interceptor:
// _dyld_initializer() -> tlv_load_notification -> wrap_malloc -> ASanInitInternal
_dyld_initializer();
_libSystem_ktrace_init_func(DYLD);
libdispatch_init();
_libSystem_ktrace_init_func(LIBDISPATCH);
#if !TARGET_OS_DRIVERKIT
_libxpc_initializer();
_libSystem_ktrace_init_func(LIBXPC);
#if CURRENT_VARIANT_asan
setenv("DT_BYPASS_LEAKS_CHECK", "1", 1);
#endif
#endif // !TARGET_OS_DRIVERKIT
// must be initialized after dispatch
_libtrace_init();
_libSystem_ktrace_init_func(LIBTRACE);
#if !TARGET_OS_DRIVERKIT
#if defined(HAVE_SYSTEM_SECINIT)
_libsecinit_initializer();
_libSystem_ktrace_init_func(SECINIT);
#endif
#if defined(HAVE_SYSTEM_CONTAINERMANAGER)
_container_init(apple);
_libSystem_ktrace_init_func(CONTAINERMGR);
#endif
__libdarwin_init();
_libSystem_ktrace_init_func(DARWIN);
#endif // !TARGET_OS_DRIVERKIT
//...
}

可以看到,里面做了很多系统库的初始化工作,我们也看到了熟悉的身影

1
2
3
4
5
6
7
8
// 这里有点意思,因为libSystem的初始化流程就是从dyld进来的,但是进来后才对dyld做初始化。
_dyld_initializer();
_libSystem_ktrace_init_func(DYLD);
// 堆栈里面的
libdispatch_init();
_libSystem_ktrace_init_func(LIBDISPATCH);

看了一堆初始化方法,还是没有找到我们要的_objc_init呀,这货到底藏哪了。我们回到堆栈,发现在libdispatch的初始化后又调用了_os_object_init这个方法,猜测是不是跟这个方法有关系呢?没办法,我们还得去libdispatch的源码中看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void
_os_object_init(void)
{
_objc_init(); //我在这里哦~~~
Block_callbacks_RR callbacks = {
sizeof(Block_callbacks_RR),
(void (*)(const void *))&objc_retain,
(void (*)(const void *))&objc_release,
(void (*)(const void *))&_os_objc_destructInstance
};
_Block_use_RR2(&callbacks);
#if DISPATCH_COCOA_COMPAT
const char *v = getenv("OBJC_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("DISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
v = getenv("LIBDISPATCH_DEBUG_MISSING_POOLS");
if (v) _os_object_debug_missing_pools = _dispatch_parse_bool(v);
#endif
}

呵呵,妖怪哪里跑!!!

小结

现在我们终于把OC类从app启动到加载的整个流程跑通了,但是光知道流程显然还是不够啊,类是如何从二进制文件被创建成类对象的呢?我们之后再继续探索这一块的内容吧。

OC的消息转发流程底层探究

发表于 2021-01-04   |  

转发流程定位

接上一篇文章的最后,我们找了方法未实现时的默认函数,下面我们调一个未实现的方法,看看具体的调用栈.

1
2
2021-01-04 20:05:16.074234+0800 LLYObjc[11678:7221659] -[LLYModel fun0]: unrecognized selector sent to instance 0x1006f2150
2021-01-04 20:05:16.075692+0800 LLYObjc[11678:7221659] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[LLYModel fun0]: unrecognized selector sent to instance 0x1006f2150'

bt下看看堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
frame #0: 0x00007fff717f633a libsystem_kernel.dylib`__pthread_kill + 10
frame #1: 0x00000001004bc9bc libsystem_pthread.dylib`pthread_kill + 430
frame #2: 0x00007fff7177d808 libsystem_c.dylib`abort + 120
frame #3: 0x00007fff6e9e4458 libc++abi.dylib`abort_message + 231
frame #4: 0x00007fff6e9d58bf libc++abi.dylib`demangling_terminate_handler() + 262
* frame #5: 0x00000001002e99b3 libobjc.A.dylib`_objc_terminate() at objc-exception.mm:701:13
frame #6: 0x00007fff6e9e3887 libc++abi.dylib`std::__terminate(void (*)()) + 8
frame #7: 0x00007fff6e9e61a2 libc++abi.dylib`__cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*) + 27
frame #8: 0x00007fff6e9e6169 libc++abi.dylib`__cxa_throw + 113
frame #9: 0x00000001002e9158 libobjc.A.dylib`objc_exception_throw(obj="-[LLYModel fun0]: unrecognized selector sent to instance 0x1006f2150") at objc-exception.mm:591:5
frame #10: 0x00007fff37660936 CoreFoundation`-[NSObject(NSObject) doesNotRecognizeSelector:] + 132
frame #11: 0x00007fff37545ec0 CoreFoundation`___forwarding___ + 1427
frame #12: 0x00007fff37545898 CoreFoundation`__forwarding_prep_0___ + 120
frame #13: 0x0000000100003960 LLYObjc`main(argc=1, argv=0x00007ffeefbff4f8) at main.m:55:9 [opt]
frame #14: 0x00007fff716aecc9 libdyld.dylib`start + 1
frame #15: 0x00007fff716aecc9 libdyld.dylib`start + 1

在main后和doesNotRecognizeSelector方法前分别调用了 __forwarding_prep_0__ 和 __forwarding__ 这两个函数,我们猜测是OC的消息转发逻辑,那么如何去验证呢,老办法,我们先去源码中看看。

一无所获。

那我们再想想其他办法,我们分别点开这两个函数的堆栈,在头部发现了一些信息

1
2
3
CoreFoundation`__forwarding_prep_0___:
CoreFoundation`___forwarding___:

看来这两个函数的定义可能在CoreFoundation库中,那么我们自然就去开源库里面找找看,CoreFoundation的源码我们是找到了,打开发现还是没有上面的函数,看来并未全部开源,那么还是其他办法去探索么?

答案是有的,还可以通过反汇编工具来查看CF的动态库文件,这里推荐Hopper Disassembler.
在Hopper中,我们找到了如下伪代码:

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
int ____forwarding___(int arg0, int arg1) {
// 去掉了一些参数的赋值和传递逻辑和判断逻辑
loc_649bb:
var_148 = r13;
var_138 = r12;
var_158 = rsi;
rax = object_getClass(rbx);
r12 = rax;
r13 = class_getName(rax);
if (class_respondsToSelector(r12, @selector(forwardingTargetForSelector:)) == 0x0) goto loc_64a67;
loc_64a8a:
rax = class_respondsToSelector(r12, @selector(methodSignatureForSelector:));
r14 = var_138;
var_148 = r15;
if (rax == 0x0) goto loc_64dd7;
loc_64ad5:
r12 = rax;
rax = [rax _frameDescriptor];
r13 = rax;
if (((*(int16_t *)(*rax + 0x22) & 0xffff) >> 0x6 & 0x1) != rbx) {
rax = sel_getName(stack[-328]);
_CFLog(0x4, @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.", rax);
}
rax = object_getClass(r14);
rax = class_respondsToSelector(rax, @selector(_forwardStackInvocation:));
stack[-344] = r13;
if (rax == 0x0) goto loc_64c19;
loc_64c19:
if (class_respondsToSelector(object_getClass(r14), @selector(forwardInvocation:)) == 0x0) goto loc_64ec2;
loc_64e3c:
rax = sel_getName(var_140);
r14 = rax;
rax = sel_getUid(rax);
if (rax != var_140) {
_CFLog(0x4, @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", var_140, r14, rax, r9, stack[-360]);
}
if (class_respondsToSelector(object_getClass(var_138), @selector(doesNotRecognizeSelector:)) == 0x0) {
____forwarding___.cold.2(var_138);
}
(*_objc_msgSend)(var_138, @selector(doesNotRecognizeSelector:));
asm { ud2 };
rax = loc_64ec2(rdi, rsi);
return rax;
}

通过上面伪代码,我们提炼出了几个比较重要的函数,即我们常说的消息转发的流程(除了_forwardStackInvocation这个函数,这个函数我们下面在说)原来出处在CoreFoundation中,知道了出处,我们再来验证下他们的逻辑。

转发流程验证

resolveInstanceMethod && resolveClassMethod 动态方法决议

该部分的源码就在objc中,上一篇解释消息慢查询的时候只是一笔带过了,这里我们再来分析下:

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
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
//...
//上面是imp查找过程
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
//...
//下面的缓存过程
}
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass()) {
// cls是类对象
resolveInstanceMethod(inst, sel, cls);
}
else {
// cls是元类对象
resolveClassMethod(inst, sel, cls);
// 类方法列表中没有,再去实例方法中找,这里就有一个坑点,根元类的superclass指针指向根类,也就是最终会找到NSObject中去,所以理论上我们能在NSObject的分类中处理所有的类方法找不到问题。
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 未实现直接返回
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
// 如果该类resolveInstanceMethod方法已实现,帮我们调一次
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
// 继续走一遍查找逻辑
IMP imp = lookUpImpOrNil(inst, sel, cls);
// ...
// 非重要信息
}
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
// 未实现直接返回
if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
// 如果该类resolveClassMethod方法已实现,帮我们调一次
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 = lookUpImpOrNil(inst, sel, cls);
// ...
// 非重要信息
}

看了源码,我们知道了只要重写一下上面两个方法,系统就会调一次这两方法,相当于一个钩子,我们可以在钩子里面动态的给该类加上缺失的方法,正常逻辑ok,那如果我们只是重写但是不动态添加方法实现会怎么样呢?我们来验证下:

1
2
3
4
5
6
7
8
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%@ %s",NSStringFromSelector(sel),__func__);
return [super resolveInstanceMethod:sel];
}
2021-01-04 21:29:51.339930+0800 LLYObjc[12200:7268336] fun0 +[LLYModel resolveInstanceMethod:]
2021-01-04 21:29:51.340714+0800 LLYObjc[12200:7268336] fun0 +[LLYModel resolveInstanceMethod:]
2021-01-04 21:29:51.340894+0800 LLYObjc[12200:7268336] -[LLYModel fun0]: unrecognized selector sent to instance 0x101918dc0

奇怪的事情发生了,resolveInstanceMethod进来了两次,第一次进来比较好理解,但是第二次是从哪里进来的呢?是不是因为递归查找的原因?我们打个断点看看两个打印时的bt:

第一次

1
2
3
4
5
6
7
8
9
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
* frame #0: 0x0000000100313413 libobjc.A.dylib`resolveInstanceMethod(inst=0x000000010113abd0, sel="fun0", cls=LLYModel) at objc-runtime-new.mm:6005:30
frame #1: 0x00000001002fee83 libobjc.A.dylib`resolveMethod_locked(inst=0x000000010113abd0, sel="fun0", cls=LLYModel, behavior=1) at objc-runtime-new.mm:6043:9
frame #2: 0x00000001002fe7ac libobjc.A.dylib`lookUpImpOrForward(inst=0x000000010113abd0, sel="fun0", cls=LLYModel, behavior=1) at objc-runtime-new.mm:6192:16
frame #3: 0x00000001002d9899 libobjc.A.dylib`_objc_msgSend_uncached at objc-msg-x86_64.s:1101
frame #4: 0x0000000100003890 LLYObjc`main(argc=1, argv=0x00007ffeefbff4f8) at main.m:55:9 [opt]
frame #5: 0x00007fff716aecc9 libdyld.dylib`start + 1
frame #6: 0x00007fff716aecc9 libdyld.dylib`start + 1

第二次

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
* frame #0: 0x0000000100313413 libobjc.A.dylib`resolveInstanceMethod(inst=0x0000000000000000, sel="fun0", cls=LLYModel) at objc-runtime-new.mm:6005:30
frame #1: 0x00000001002fee83 libobjc.A.dylib`resolveMethod_locked(inst=0x0000000000000000, sel="fun0", cls=LLYModel, behavior=0) at objc-runtime-new.mm:6043:9
frame #2: 0x00000001002fe7ac libobjc.A.dylib`lookUpImpOrForward(inst=0x0000000000000000, sel="fun0", cls=LLYModel, behavior=0) at objc-runtime-new.mm:6192:16
frame #3: 0x00000001002d8379 libobjc.A.dylib`class_getInstanceMethod(cls=LLYModel, sel="fun0") at objc-runtime-new.mm:5922:5
frame #4: 0x00007fff3755d697 CoreFoundation`__methodDescriptionForSelector + 282
frame #5: 0x00007fff37579204 CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38
frame #6: 0x00007fff37545ac5 CoreFoundation`___forwarding___ + 408
frame #7: 0x00007fff37545898 CoreFoundation`__forwarding_prep_0___ + 120
frame #8: 0x0000000100003890 LLYObjc`main(argc=1, argv=0x00007ffeefbff4f8) at main.m:55:9 [opt]
frame #9: 0x00007fff716aecc9 libdyld.dylib`start + 1
frame #10: 0x00007fff716aecc9 libdyld.dylib`start + 1

可以看到,第一次堆栈就是正常的lookUpImpOrForward查找逻辑触发的,但是第二次的触发确是在消息转发中获取到函数签名后,那如何验证呢?很简单,我们去实现下methodSignatureForSelector这个方法看看:

1
2
3
4
5
6
7
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
2021-01-04 22:06:15.458988+0800 LLYObjc[12668:7297440] fun0 +[LLYModel resolveInstanceMethod:]
2021-01-04 22:06:15.459889+0800 LLYObjc[12668:7297440] _forwardStackInvocation: +[LLYModel resolveInstanceMethod:]
2021-01-04 22:06:15.460152+0800 LLYObjc[12668:7297440] -[LLYModel fun0]: unrecognized selector sent to instance 0x100738090

果然,第二次打印的函数变成了这个_forwardStackInvocation,这是哪来的呢?请看上面的CoreFoundation伪代码。大胆猜测就是如果没有实现自己的函数签名的话,系统还是用之前的函数签名(fun0)去调用一次lookUpImpOrForward,实现后换成CoreFoundation内部的(_forwardStackInvocation)函数签名去调用一次lookUpImpOrForward.

forwardingTargetForSelector 快速转发

快速转发需要做的操作比较简单,返回一个实现了该方法的类。

methodSignatureForSelector && forwardInvocation 慢速转发

慢速转发有下面两个有意思的点:

趣点一:方法签名并未指定格式,你可以返回任意正确的格式,比如我返回下面这种格式也ok:

1
2
3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"];
}

项目中应该没有那个函数带了这么多参数吧。

但是下面这个格式就不行:

1
2
3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@"];
}

因为缺少最基本的sel参数。

趣点二: 只需要重写forwardInvocation函数,即使是一个空函数,系统即认为当前被转发的方法已经被人处理,不会抛出异常。这里我们就可以做一些其他的事情了。

小结

通过以上探索,我们不仅掌握了OC消息转发的实现方式,还了解了转发逻辑内部的实现原理,也为我们在生产环境的灵活使用打下基础,比如我们可以通过消息转发机制实现方法调用的防护工作等比较重要的内容,更多的功能需要我们进一步的探索和实践。

OC方法调用慢查找流程底层逻辑探究

发表于 2021-01-04   |  

在OC方法调用快查找流程底层逻辑探究中,我们分析了方法调用快查找的逻辑,所谓的快查找也就是对方法缓存列表的查找,如果没有命中缓存,则会进入到慢查找的逻辑,即对类的方法列表的查找,下面我们就来探究下OC底层是如何进行慢查找的。

在快查找未命中的出口,有如下的代码逻辑:

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
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
STATIC_ENTRY __objc_msgLookup_uncached
UNWIND __objc_msgLookup_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
ret
END_ENTRY __objc_msgLookup_uncached

共同指向了MethodTableLookup这个方法,我们来看看这个方法的实现:

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
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// 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
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro

我们暂时忽略寄存器先关操作,主要看方法调用逻辑,可以看到,内部是调用了_lookUpImpOrForward 这个方法,看命名应该能猜到是跟imp的查找和转发逻辑相关,那我们找找这个函数的定义,发现在汇编代码中没有相关定义,我们去runtime原文件中看看,果然在objc_runtime-new.mm的6094行发现了该函数的定义:

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
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* Without LOOKUP_INITIALIZE: tries to avoid +initialize (but sometimes fails)
* Without LOOKUP_CACHE: skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use LOOKUP_INITIALIZE and LOOKUP_CACHE
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use LOOKUP_NIL.
**********************************************************************/
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache; // 这里imp有一个默认值 是一个坑点 具体下面解释。
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// Optimistic cache lookup // 再去缓存中查找一遍,防止多线程已调用
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
runtimeLock.lock(); // 线程安全哦
checkIsKnownClass(cls);
// 下面两个操作是对类的继承链,ro,rw等数据的递归实例化,比较重要,不过不在这个文章的讨论范围 先不进去细看。
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
// 类的initialize()方法就是在这里被调用
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
runtimeLock.assertLocked();
curClass = cls;
for (unsigned attempts = unreasonableClassCount();;) {
// 直接能从当前类的方法列表中查找到
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
// 如果已经查找完当前类的父类中了 给imp一个默认值然后跳出本次递归
// 还没有查找则将当前类赋值为父类 下面继续对父类进行查找
if (slowpath((curClass = curClass->superclass) == nil)) {
imp = forward_imp;
break;
}
// 是否还在继承链中查找
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// 从父类的缓存中查找 curclass已经在上面被赋值为父类了
// 这个方法也是汇编实现 复用的快查找的逻辑
imp = cache_getImp(curClass, sel);
// 父类缓存中没有找到 跳出本轮递归 下次又从父类的父类中开始查找
if (slowpath(imp == forward_imp)) {
break;
}
// 找到了 goto done
if (fastpath(imp)) {
goto done;
}
}
// 以上逻辑走完,说明imp还是没有找到,这个时候会做一个动态的方法决议,这部分属于方法转发逻辑,暂时不进去细看。
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
// imp找到了,则进行缓存,缓存逻辑参考之前的文章
log_and_fill_cache(cls, imp, sel, inst, curClass);
runtimeLock.unlock();
done_nolock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}

以上源码就是慢查找的整个流程,整体是一个递归查找继承连的过程,其中还有一些小的点可以在扩展下。

扩展一 对当前方法列表的查找策略

我们可以深入看看代码逻辑

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
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
// 方法列表是多级的
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
// 对已排序数据进行查找 你想到了什么???
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
return nil;
}
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
ASSERT(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
// 没有错 就是折半查找 不过apple的折半写的比较有逼格。
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
// 如果已找到 则找到同名方法的第一个 这里说明列表内可能有方法名相同的方法,什么情况会出现呢?比如分类同名方法。说明一点,上层的结论在底层都是都代码依据的。
if (keyValue == probeValue) {
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
扩展二 默认的imp

在查找函数的最前面,有下面这行初始化代码:

1
const IMP forward_imp = (IMP)_objc_msgForward_impcache;

这个默认的imp具体指向哪里,我们来全局查找一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
// Non-stret version
MI_GET_EXTERN(r12, __objc_forward_handler)
ldr r12, [r12]
bx r12
END_ENTRY __objc_msgForward

在汇编代码中找到 __objc_forward_handler这里就线索中断,这个猜测应该是一个回调函数,ok,那我们去源代码里面看看。

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;

是不是看到了我们非常熟悉的一个错误信息,没有错,这个默认的imp就是找不到方法实现的错误处理函数。此时,坑点也就出现了,如果你使用class_getMethodImplementation去查找一个未实现的方法时,不会返回空,而是返回这个函数的地址,这里需要注意一下。

小结

以上就是整个慢查找的流程和其中的一些相关的知识点的梳理,除了整体流程的分析,上面的两个扩展也是比较重要的。其中还有一些比较重要的内容没有做深入分析,如realizeClassMaybeSwiftAndLeaveLocked和initializeAndLeaveLocked,留到以后再做分析,毕竟这篇文章主要分析慢查找的流程。

从【nonpointer】探究OC类中isa指针

发表于 2021-01-04   |  

前言

在这篇文章从【class_getInstanceSize方法】探究iOS的内存分配策略中,我们分析了OC类的内存分配策略,但是具体的初始化过程并没有提及,这里我们来探究下OC在内存的初始化过程中都做了什么。

源码分析

我们还是先看一下整个OC类的创建流程,如下图:

其中标红的_class_createInstanceFromZone函数就是核心内容,我们直接来看看其内部实现逻辑:

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
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer(); //是否支持nonpointer 这个是关键
size_t size;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor); // 支持nonpointer的类初始化过程 见下面的分析
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls); // 不支持nonpointer的类初始化过程 见下面的分析
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags); // 默认添加一个cxx的析构函数
}
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor); // nonpointer直接写死为true
}
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer()); // 小对象类型直接返回 没有isa指针
if (!nonpointer) {
isa = isa_t((uintptr_t)cls); // 不支持nonpointer的对象直接将类对象赋值给isa
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA //判断平台
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3; // 前三位都不是存的类信息 所以先抛弃前三位再赋值
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}

从上面的源码中我们看到初始化的过程实际上就是对isa指针赋值的过程,对于不满足nonpointer的对象,isa指针直接等于类对象,而nonpointer对象的isa就比较复杂了,这里我们再来分析下nonpointer下的isa指针的数据结构,如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
// isa_t 也就是isa指针的结构类型是一个联合体,cls和bits共享64位的内存空间
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; //是否是nonponiter类型 \
uintptr_t has_assoc : 1; //是否有关联对象 \
uintptr_t has_cxx_dtor : 1; //是否添加了cxx的析构函数 \
uintptr_t shiftcls : 33;//类对象信息 /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; //魔数 \
uintptr_t weakly_referenced : 1; //是否有弱引用 \
uintptr_t deallocating : 1; //是否正在析构 \
uintptr_t has_sidetable_rc : 1; //引用计数表中是否存了该对象的引用计数 \
uintptr_t extra_rc : 19 //引用计数
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# 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 deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)

可视化结构

结合上面的源码和图,nonpointer的isa指针的结构应该就比较清楚了。这个结构我们之后在分析iOS的内存管理的时候应该还会在具体分析。这里暂时先只做了解。

小结

通过以上分析,OC类的创建过程就比较清晰了,首先是通过内存对齐原则计算需要分配的内存大小,然后对类的isa指针进行初始化。到这里就可以确定一个类的对象了,那么类中superclass,cache(第一次有消息发送行为时创建内存),bits等是在何时初始化的呢,这个留在以后探究。

OC方法调用快查找流程底层逻辑探究

发表于 2021-01-03   |  

As we know, OC上层的方法调用的底层逻辑都是通过objc_msgSend来实现的,那么如何验证呢?我们通过clang反编译看看就知道了:

上层的OC调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LLYModel *model = [LLYModel alloc];
[model func0];
[model func1];
NSLog(@"Hello, World!");
}
return 0;
}

使用clang命令:

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

反编译之后的底层实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LLYModel *model = ((LLYModel *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LLYModel"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)model, sel_registerName("func0"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)model, sel_registerName("func1"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_71_gd0d02_n7td1h8k9l7bx0jqh0000gp_T_main_f2985d_mi_0);
}
return 0;
}

可以看到,所有的方法调用全都被编译器转换为了objc_msgSend方法的调用,我们很自然的去runtime源码中找该函数的实现逻辑,结果在.mm文件中没有发现,以为线索就要断了的时候,在objc-msg-arm64.s文件内看到了下面几行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 //p0寄存器保存的应该是objc_msgSend()的第一个参数receive // nil check and tagged pointer check
// taggedpointer判断
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// 拿到receive的isa指针
ldr p13, [x0] // p13 = isa
// 拿到类对象
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend

这里应该就是objc_msgSend的入口了,原来是使用汇编代码来实现的,只能硬着头皮看下去了。

上面的逻辑最终调用了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
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
// #define CACHE (2 * __SIZEOF_POINTER__)
// 从isa指针首地址偏移16个字节,这一步操作是拿到类的cache数据,原理参考类结构的定义。
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// 参考cache_t中的结构 这一步是拿到缓存的buckts数组
and p10, p11, #0x0000ffffffffffff // p10 = buckets
// 计算_cmd(也就是sel)的索引
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
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
// #define PTRSHIFT 3 // 1<<PTRSHIFT == PTRSIZE
// 根据上面的索引值获取具体的bucket数据 每一个bucket是16个字节 所以这一要向左平移4位
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// 拿到sel和imp元组
ldp p17, p9, [x12] // {imp, sel} = *bucket
// 下面就是一个条件判断和分支语句了
// 找到的sel不是当前执行的方法
1: cmp p9, p1 // if (bucket->sel != _cmd)
// goto 2
b.ne 2f // scan more
// 否则直接命中
CacheHit $0 // call or return imp
// 未命中逻辑
2: // not hit: p12 = not-hit bucket
// 空判断
CheckMiss $0 // miss if bucket->sel == 0
// 当前是否已经寻址到buckets的第一个元素
cmp p12, p10 // wrap if bucket == buckets
// 是的话 goto 3
b.eq 3f
// 不是的话 向前移位继续查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
// 这是一个循环查找的过程
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// 已经寻址到buckets的第一个元素,直接干到最后一个元素,再重新开始向前查找。
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
// 这里的逻辑和上面类似
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
// 如果再次寻址到第一个元素
cmp p12, p10 // wrap if bucket == buckets
// goto 3 未命中 跳出循环
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
.endmacro
```
通过以上汇编代码分析,可以发现方法缓存的查找是一个从后往前遍历buckets的过程,如果当前遍历到第一个元素,会回到队尾继续遍历,知道找到我们需要的元素,或者在再一次查找到第一个元素后,会退出循环,然后我们看看缓存命中和未命中分别做了啥:
##### 缓存命中

.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don’t ptrauth a nil imp
AuthAndResignAsIMP x0, x12, 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, x12, x1, x16 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro

1
2
##### 缓存未命中

.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b objc_msgSend_uncached
.elseif $0 == LOOKUP
b
objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

```

都只是根据类型不同调用了对应的方法,这里已经不再是快查询的逻辑,我们暂时不看。

小结

这里先点个题,所谓的快查找就是对cache的查找,与之对应的慢查找就是对method_list的查找了,method_list的查找可能涉及到不同文件的遍历过程,所以会比cache慢很多。通过查看汇编实现,我的主要感受就是对类结构的熟悉是非常重要的,因为所有的查找逻辑都是基于类的结构来的。我们在实际编码过程中,也应该做到庖丁解牛,而不是盲人摸象。

OC方法缓存策略底层探究

发表于 2021-01-03   |  

源码结构分析

我们先来看一下OC类的最新结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_class : objc_object {
// 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
// 函数部分...
}
struct objc_object {
private:
isa_t isa;
public:
// 函数部分...
}

之前的文章中我们分析了bits这个属性,里面存放的就是类的子结构,比如方法,属性,协议等。今天我们再来探究下上面的cache内部的结构和底层原理。

实操

相信大家对cache都不会陌生,即OC中的方法缓存,在objc_msgSend的流程中,最先查找的就是这个列表,那OC是如何维护这个列表的呢,内部的存储结构又是如何?今天我们就来一探究竟。

之前我们使用lldb调试了bits内部的存储结构,但是这个方式比较繁琐,今天我们换一种简单点的方式来Debug,我们首先镜像一个objc_class结构体,然后对镜像的结构体进行分析。

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
struct lly_bucket_t {
SEL _sel;
IMP _imp;
};
struct lly_cache_t {
struct lly_bucket_t * _buckets;
uint32_t _mask;
uint16_t _flags;
uint16_t _occupied;
};
struct lly_class_data_bits_t {
Class objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
};
struct lly_objc_class {
Class ISA;
Class superclass;
struct lly_cache_t cache;
struct lly_class_data_bits_t bits;
};
void printCache(Class model) {
struct lly_objc_class * llyClass = (__bridge struct lly_objc_class *)(model);
struct lly_cache_t cache = llyClass->cache;
NSLog(@"_occupied = %d,_mask = %d",cache._occupied,cache._mask);
for (uint32_t i = 0; i < cache._mask; i++) {
struct lly_bucket_t bucket = cache._buckets[i];
NSLog(@"method : sel = %@, imp = %p",NSStringFromSelector(bucket._sel),bucket._imp);
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LLYModel *objc2 = [LLYModel alloc];
Class llyClass = [LLYModel class];
printCache(llyClass);
}
}

首先我们不调用任何实例方法,查看打印结果

1
2021-01-03 15:59:51.995108+0800 LLYObjc[1738:6603344] _occupied = 0,_mask = 0

可以看到,初始状态都是0,里面的for也没有进 说明当前缓存列表为空。

然后我们分次调用方法并打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LLYModel *objc2 = [LLYModel alloc];
Class llyClass = [LLYModel class];
printCache(llyClass);
[objc2 fun0];
[objc2 fun1];
printCache(llyClass);
[objc2 fun2];
[objc2 fun3];
printCache(llyClass);
}
return 0;
}

查看打印结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2021-01-03 16:05:09.335088+0800 LLYModel[1804:6607181] _occupied = 0,_mask = 0
2021-01-03 16:05:09.335668+0800 LLYModel[1804:6607181] ---------------------------------------------------------------------------
2021-01-03 16:05:09.335785+0800 LLYModel[1804:6607181] -[LLYModel fun0]
2021-01-03 16:05:09.335875+0800 LLYModel[1804:6607181] -[LLYModel fun1]
2021-01-03 16:05:09.335959+0800 LLYModel[1804:6607181] _occupied = 2,_mask = 3
2021-01-03 16:05:09.336174+0800 LLYModel[1804:6607181] method : sel = fun1, imp = 0xbf90
2021-01-03 16:05:09.336245+0800 LLYModel[1804:6607181] method : sel = (null), imp = 0x0
2021-01-03 16:05:09.336344+0800 LLYModel[1804:6607181] method : sel = fun0, imp = 0xbfc0
2021-01-03 16:05:09.336397+0800 LLYModel[1804:6607181] ---------------------------------------------------------------------------
2021-01-03 16:05:09.336447+0800 LLYModel[1804:6607181] -[LLYModel fun2]
2021-01-03 16:05:09.336497+0800 LLYModel[1804:6607181] -[LLYModel fun3]
2021-01-03 16:05:09.336542+0800 LLYModel[1804:6607181] _occupied = 2,_mask = 7
2021-01-03 16:05:09.336618+0800 LLYModel[1804:6607181] method : sel = (null), imp = 0x0
2021-01-03 16:05:09.336737+0800 LLYModel[1804:6607181] method : sel = fun3, imp = 0xbf30
2021-01-03 16:05:09.336818+0800 LLYModel[1804:6607181] method : sel = (null), imp = 0x0
2021-01-03 16:05:09.344380+0800 LLYModel[1804:6607181] method : sel = (null), imp = 0x0
2021-01-03 16:05:09.344554+0800 LLYModel[1804:6607181] method : sel = fun2, imp = 0xbf60
2021-01-03 16:05:09.344657+0800 LLYModel[1804:6607181] method : sel = (null), imp = 0x0
2021-01-03 16:05:09.344746+0800 LLYModel[1804:6607181] method : sel = (null), imp = 0x0
2021-01-03 16:05:09.344833+0800 LLYModel[1804:6607181] ---------------------------------------------------------------------------

分析打印结果,我们存在几个疑惑的地方:

  • _occupied 和 _mask 分别是什么,为什么调用方法会改变它们的值?
  • 方法的调用顺序和缓存列表的顺序为什么不一致?
  • 当我们调用后面的方法后,前面缓存的方法为什么会丢失?

带着上面的问题,我们去源码中看看能不能找到满意的答案。

源码逻辑分析

通过对occupied关键字的搜索,我们最终定位到下面这个函数:

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
ALWAYS_INLINE
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
// 去掉我们不关系的旁枝末节
// Use the cache as-is if it is less than 3/4 full
// 每次新插入缓存时occupied + 1,这里我们大概就能猜到它的含义了,就是保存当前已缓存方法的数量。
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// 如果当前缓存列表为空 去创建一个
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
// INIT_CACHE_SIZE_LOG2 = 2,
// INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),
// 看这里,初始的缓存列表的容量是 1 << 2 = 4。
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) {
// Cache is less than 3/4 full. Use it as-is.
// (当前缓存列表用到的容量 + 1 ) <= 总容量的四分之三时 说明还够用 啥也不用干
}
else {
// 要扩容了,新的容量大小为当前容量的2倍哦 当然有最大值的限制哈。
// 具体扩容逻辑下看面的扩容函数注释
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 = 当前容量 - 1
mask_t m = capacity - 1;
// 缓存的索引通过这个hash计算
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 because the
// minimum size is 4 and we resized at 3/4 full.
do {
// 如果当前索引没有数据,直接插入
if (fastpath(b[i].sel() == 0)) {
// _occupied++;
incrementOccupied();
b[i].set<Atomic, Encoded>(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));
cache_t::bad_cache(receiver, (SEL)sel, cls);
}
/// 初始化和扩容都走这个函数
ALWAYS_INLINE
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
// 分配内存
bucket_t *newBuckets = allocateBuckets(newCapacity);
// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
// 存储新创建缓存列表 _occupied置0
setBucketsAndMask(newBuckets, newCapacity - 1);
// 要扩容了 free调旧值
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
}
}
// 当前索引
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
// 下一个索引
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}

这又是一个内联函数,意味这你在调用堆栈里面是看不到的。但是我们通过源码还是可以Debug进来的。
通过以上源码的分析过程,应该能够回答我们上面的疑问了。

  • _occupied表示当前已缓存的的方法数,_mask标识当前缓存列表最大数-1,有新的方法调用_occupied就会更新,缓存列表扩容时_mask会更新。
  • 存储到缓存列表中的方法并不一定是连续的,和具体的hash算法有关。
  • 当缓存列表扩容后,之前缓存过的方法都会被清除,所以会丢失。

小结

当前的探索因为工程比较小,可能还不能看出方法cache的好处,在实际的工程中,方法的调用是大量且频繁的,这时就能体现出方法缓存的实际意义。之前的学习中对方法缓存丢失的逻辑不太了解,经过这次的探索,对整个方法缓存的策略有了更深刻的理解。

OC方法isKindOfClass趣探

发表于 2021-01-03   |  

起

我们先来看一下下面的一个面试题:

1
2
3
4
5
6
7
8
9
10
11
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; //
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; //
BOOL re3 = [(id)[LLYModel class] isKindOfClass:[LLYModel class]]; //
BOOL re4 = [(id)[LLYModel class] isMemberOfClass:[LLYModel 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)[LLYModel alloc] isKindOfClass:[LLYModel class]]; //
BOOL re8 = [(id)[LLYModel alloc] isMemberOfClass:[LLYModel 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
9
re1 :1
re2 :0
re3 :0
re4 :0
re5 :1
re6 :1
re7 :1
re8 :1

承

我们直接看源码进行分析:

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
//直接判断当前类的元类是否和后面的类相等。
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
//直接判断是不是同一个类对象
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
//使用当前类对象的元类的继承链和后面的类对象进行比较,只要后面的类对象在当前元类的继承链中,既判断相等。
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
//使用当前实例的类对象的继承链和后面的类对象进行比较,只要后面的类在当前类的继承链中,既判断是相等。
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}

总结一下上面的判断逻辑大概是这样:

isKindOfClass会使用继承链比较,实例方法使用类对象的继承链,类方法使用元类对象的继承链。
isMemberOfClass直接比较,实例方法使用类对象,类方法使用元类对象。

看到这里,不得不再次祭出下面这张图了:

我们来瞧瞧元类对象的继承链,根元类的superclass指针指向的是根类,这也就是

1
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; //

这里为什么返回1的原因。

转

有了以上结论,我们再来Debug一下runtime的代码,看一下这几个函数内部的运行逻辑是否和我们上面的结论一致。

isMemberOfClass lldb分析

先来看看类方法:

1
2
3
4
5
6
7
8
9
10
11
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; //
(lldb) x/4gx self //类对象
0x10034c140: 0x000000010034c0f0 0x0000000000000000
0x10034c150: 0x0000000100888a40 0x0001801000000003
(lldb) x/4gx cls //类对象
0x10034c140: 0x000000010034c0f0 0x0000000000000000
0x10034c150: 0x0000000100888a40 0x0001801000000003
(lldb) x/4gx 0x000000010034c0f0(ISA) //元类对象也是根源类
0x10034c0f0: 0x000000010034c0f0 0x000000010034c140
0x10034c100: 0x0000000100739650 0x0004e03100000007

这里就很清晰,类对象只有一个,因为他们的首地址和内部变量都相同 ,根元类对象也只有一个,但是和类对象首地址不同,ISA指针相同,这又证明了上图的内容,即根源类的ISA指向自己。根元类和类对象不相等,返回0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BOOL re4 = [(id)[LLYModel class] isMemberOfClass:[LLYModel class]]; //
(lldb) x/4gx self //类对象
0x100008408: 0x00000001000083e0 0x000000010034c140
0x100008418: 0x0000000100346430 0x0000802400000000
(lldb) x/4gx cls //类对象
0x100008408: 0x00000001000083e0 0x000000010034c140
0x100008418: 0x0000000100346430 0x0000802400000000
(lldb) x/4gx 0x00000001000083e0 //元类对象
0x1000083e0: 0x000000010034c0f0 0x000000010034c0f0
0x1000083f0: 0x00000001007396d0 0x0002e03500000003
(lldb) x/4gx 0x000000010034c0f0 //根源类对象
0x10034c0f0: 0x000000010034c0f0 0x000000010034c140
0x10034c100: 0x0000000100739650 0x0004e03100000007
(lldb)

因为是自定义的类,所以会有一个元类对象,元类对象isa指向上面的根元类,元类和类对象不相等,返回0,没有问题。

然后我们来看看实例方法,实例对象的内存分析和类对象有一点区别,请注意!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; //
(lldb) x/4gx self //实例对象
0x100629c30: 0x001d80010034c141 0x0000000000000000
0x100629c40: 0x00007fff7ba35c58 0x00000000e642baab
(lldb) x/4gx cls //类对象
0x10034c140: 0x000000010034c0f0 0x0000000000000000
0x10034c150: 0x0000000100629c80 0x0001801000000007
(lldb) p/x 0x001d80010034c141 & 0x00007ffffffffff8ULL //这里是重要,实例对象的isa指针是nonpointer_isa 如果想要拿到类对象信息,需要带个面具。!!!
(unsigned long long) $40 = 0x000000010034c140
(lldb) x/4gx 0x000000010034c140 //类对象
0x10034c140: 0x000000010034c0f0 0x0000000000000000
0x10034c150: 0x0000000100629c80 0x0001801000000007

在分析前,我们先回到源码部分

1
2
3
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}

这里的

1
[self class]

怎么理解呢?

实际上分两种情况:

  • 当self是实例对象时,class是实例方法,返回isa指针,也就是类对象
  • 当self是类或者元类对象时,class是类方法,返回自己。

所以上面我需要拿到self的isa指向的内存,但是self的isa不能直接打印内存,因为nonpointer_isa对isa进行了优化,存储很多其他的信息,我们需要先使用面具过滤掉其他不需要的信息,才能拿到isa中存储的类对象地址。从lldb打印情况可以看到,self实例对象的isa指向的就是类对象,所以这里返回1.

re8的分析过程和re6相似,就不再重复劳动了。

特殊的isKindOfClass

为什么要加上【特殊】这个前缀呢,原因很简单,isKindOfClass里面的断点根本没有进去,可见runtime内部对这个方法的调用可能走的就不是我们上面的实现,这种情况我们之前在alloc的分析过程中也遇到过,所以不要慌。经过在源码中一番搜索,我们最终找到下面这个函数:

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__
if (slowpath(!obj)) return NO;
Class cls = obj->getIsa();
if (fastpath(!cls->hasCustomCore())) {
for (Class tcls = cls; tcls; tcls = tcls->superclass) {
if (tcls == otherClass) return YES;
}
return NO;
}
#endif
return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

内部的判断逻辑和我们上面看到的源码一样,所有我们可以在这个函数中进行debug了,不过这个debug过程已经不是重点了,重点是为什么会走到这个函数里面,上面的函数又是什么时候调用,因为有了之前的经验,我们直接去llvm中看能否寻找的一些答案。

果不其然,我们在llvm的源码中发现了这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// This is the table of ObjC "accelerated dispatch" functions. They are a set
// of objc methods that are "seldom overridden" and so the compiler replaces the
// objc_msgSend with a call to one of the dispatch functions. That will check
// whether the method has been overridden, and directly call the Foundation
// implementation if not.
// This table is supposed to be complete. If ones get added in the future, we
// will have to add them to the table.
const char *AppleObjCTrampolineHandler::g_opt_dispatch_names[] = {
"objc_alloc",
"objc_autorelease",
"objc_release",
"objc_retain",
"objc_alloc_init",
"objc_allocWithZone",
"objc_opt_class",
"objc_opt_isKindOfClass",
"objc_opt_new",
"objc_opt_respondsToSelector",
"objc_opt_self",
};

在注释中,我们发现了【加速派发】,【不常重写】等字样,这里我们大胆猜测就是llvm在底层为了优化方法的调用速度,对下面列表中的方法做了优化,会优先进到这些优化方法中去,如果我们重写isKindOfClass等方法,可能才会进入之前我们看到的函数中去。

ok 来尝试一下我们的猜测,很简单,我们只需要重写一下:

1
2
3
4
5
6
7
+ (BOOL)isKindOfClass:(Class)aClass {
return [super isKindOfClass:aClass];
}
- (BOOL)isKindOfClass:(Class)aClass {
return [super isKindOfClass:aClass];
}

果然,在我们重写之后,之前的断点就生效了,完美!!!

合

源码的探索过程还是比较有趣的,不过如果你找不到正确的方法的话,可能跟到一个方法后就很难再深入进入,导致直接放弃,打击学习的积极性,所以还是要多看多学,不断积累才行啊。

借助lldb探究OC类结构的底层实现

发表于 2021-01-02   |  

类结构源码

我们先来看一下runtime中类的结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct objc_object {
private:
isa_t isa;
//下面的函数部分去掉 因为不影响类结构
//...
}
struct objc_class : objc_object {
// Class ISA; // 8字节
Class superclass; // 8字节
cache_t cache; // 16字节 // formerly cache pointer and vtable
class_data_bits_t bits; //这里存的就是类的主要结构 // class_rw_t * plus custom rr/alloc flags
// 具体结构体数据
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
//下面的函数部分去掉 因为不影响类结构
//...
}

从上面的结构体定义可以看出,每一个类的第一个属性都会是一个isa指针,然后是super和cache,之后的bits里面就是类的结构,我们需要使用llbd命令Debug bits内部的内存分配情况,在开始探究之前,我们可以先熟悉下bits中包含的主要数据结构,如下:

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
struct class_rw_t {
// 去掉了一些不关心的函数 只留下比较关心的下面这几个数据
// 只读部分
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>()->ro;
}
return v.get<const class_ro_t *>();
}
void set_ro(const class_ro_t *ro) {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
v.get<class_rw_ext_t *>()->ro = ro;
} else {
set_ro_or_rwe(ro);
}
}
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 *>()->methods;
} else {
return method_array_t{v.get<const class_ro_t *>()->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 *>()->properties;
} else {
return property_array_t{v.get<const class_ro_t *>()->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 *>()->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
}
}
}

可以看到,我们熟悉的函数列表,属性列表和协议列表都在其中,下面我们就通过内存堆栈的Debug来证明下上面的类结构。

知识储备

在开始探索前,我们先来看一下这张图:

这里有一个知识点我们需要了解,一个类(不考虑继承)在内存中会存在3种与它有关联的对象,分别是实例对象,类对象和元类对象,这3种对象通过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
@interface LLYModel : NSObject
@property (nonatomic, strong) NSString *name;
- (void)sayHey;
+ (void)sayBey;
@end
@implementation LLYModel {
int _age;
}
- (void)sayHey{}
+ (void)sayBey{}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LLYModel *objc2 = [LLYModel alloc];
objc2.name = @"lly";
NSLog(@"Hello, World! %@",objc2.name);
}
return 0;
}
实例对象

在主函数中我们简单创建一个上面的model实例,然后就可以断点调试了,首先我们查看实例对象的内存地址:

1
2
3
(lldb) x/4gx objc2
0x1006a8a20: 0x001d8001000083ed 0x0000000000000000
0x1006a8a30: 0x0000000100004018 0x0000000000000000

根据对源码的分析,我们知道bits数据存储在首地址偏移32个字节的地方,so我们这样访问bits的地址

1
2
(lldb) p (class_data_bits_t *)0x1006a8a40
(class_data_bits_t *) $94 = 0x00000001006a8a40

拿到bits的地址后,然后访问改结构体的data方法拿到class_rw_t数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(lldb) p $94->data()
(class_rw_t *) $97 = 0x00007fff97ca45c0
(lldb) p *$97
(class_rw_t) $98 = {
flags = 2546615872
witness = 32767
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 140735591042296
}
}
firstSubclass = 0x0000000100346430
nextSiblingClass = 0x0000800000000000
}

然后我们就可以访问该结构内部的相关方法获取数据了,比如方法列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(lldb) p $98.methods()
(const method_array_t) $99 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000102004ee0
arrayAndFlag = 4328541920
}
}
}
(lldb) p $99.list
(method_list_t *const) $100 = 0x0000000102004ee0
(lldb) p *$100
(method_list_t) $101 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 2148007936
count = 0
first = {
name = "\x10"
types = 0x00007fff97ca45c0 "@Fʗ
imp = 0x00007fff8ee94d20 ((void *)0x00007fff8ee959a0: __NSStackBlock)
}
}
}

通过查看内存情况,发现方法列表内部的count为0,说明方法并未存放在实例对象中。

然后看看属性列表

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) p $98.properties()
(const property_array_t) $102 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000001000000000
arrayAndFlag = 68719476736
}
}
}
(lldb) p $102.list
(property_list_t *const) $103 = 0x0000001000000000
(lldb) p *$103
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory

属性列表访问失败,说明也是没有数据的。

类对象

然后我们再来看看类对象中的内存情况,这里我们先拿到类对象的首地址:

1
2
3
(lldb) x/4gx LLYModel.class
0x1000083e8: 0x00000001000083c0 0x000000010034c140
0x1000083f8: 0x00000001006ace60 0x0004802400000007

上面也提到过,类对象是唯一的,所以可以这样访问,其他步骤都差不多就不在重复粘贴,这里只放最后的结果

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
(method_list_t) $117 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 4
first = {
name = "sayHey"
types = 0x0000000100003f9a "v16@0:8"
imp = 0x0000000100003da0 (KCObjc`-[LLYModel sayHey])
}
}
}
(lldb) p $117.get(0)
(method_t) $118 = {
name = "sayHey"
types = 0x0000000100003f9a "v16@0:8"
imp = 0x0000000100003da0 (KCObjc`-[LLYModel sayHey])
}
(lldb) p $117.get(1)
(method_t) $119 = {
name = ".cxx_destruct"
types = 0x0000000100003f9a "v16@0:8"
imp = 0x0000000100003e00 (KCObjc`-[LLYModel .cxx_destruct])
}
(lldb) p $117.get(2)
(method_t) $120 = {
name = "name"
types = 0x0000000100003f87 "@16@0:8"
imp = 0x0000000100003db0 (KCObjc`-[LLYModel name])
}
(lldb) p $117.get(3)
(method_t) $121 = {
name = "setName:"
types = 0x0000000100003f8f "v24@0:8@16"
imp = 0x0000000100003dd0 (KCObjc`-[LLYModel setName:])
}

除了我们定义的实例方法外,还看到了属性的get和set方法,还有一个编译器默认添加的析构方法。接着是属性列表

1
2
3
4
5
6
7
(property_list_t) $124 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 1
first = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
}
}

只有一个我们定义的name属性。

元类对象

最后我们来看看元类的内存分配情况,首先我们拿到元类的首地址:

1
2
3
(lldb) x/4gx objc_getMetaClass(object_getClassName(objc2))
0x1000083c0: 0x000000010034c0f0 0x000000010034c0f0
0x1000083d0: 0x000000010200d880 0x0004e03500000007

方法列表:

1
2
3
4
5
6
7
8
9
10
11
(method_list_t) $135 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 1
first = {
name = "sayBey"
types = 0x0000000100003f9a "v16@0:8"
imp = 0x0000000100003d90 (KCObjc`+[LLYModel sayBey])
}
}
}

元类中存放了我们上面定义的一个类方法

属性列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) p $132.properties()
(const property_array_t) $136 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
(lldb) p $136.list
(property_list_t *const) $137 = 0x0000000000000000
(lldb) p *$137
error: Couldn't apply expression side effects : Couldn't dematerialize a result variable: couldn't read its memory

元类中没有存放属性。

小结

通过以上的探索过程,证明了我们之前的结论其中类对象存储实例方法和属性,元类对象存储类方法。,通过源码+lldb相结合的方式,可以更好的证明我们已知的一些结论。也给我们探索更广阔的空间提供了可能。

从【class_getInstanceSize方法】探究iOS的内存分配策略

发表于 2021-01-01   |  

先抛问题

各位看官请看下面两种case的输出分别是多少:

case1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface LLYModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@end
LLYModel *objc = [LLYModel alloc];
objc.name = @"lly";
objc.nickName = @"123";
objc.age = 18;
objc.height = 180;
NSLog(@"%@ - %lu - %lu - %lu",objc,sizeof(objc),class_getInstanceSize([LLYModel class]),malloc_size((__bridge const void *)(objc)));
case2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface LLYModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, assign) long weight;
@end
LLYModel *objc = [LLYModel alloc];
objc.name = @"lly";
objc.nickName = @"123";
objc.age = 18;
objc.height = 180;
objc.weight = 180;
NSLog(@"%@ - %lu - %lu - %lu",objc,sizeof(objc),class_getInstanceSize([LLYModel class]),malloc_size((__bridge const void *)(objc)));

可以先分析一下打印情况,我这里直接上结果:

1
2
case1:<LLYModel: 0x1006cf490> - 8 - 40 - 48
case2:<LLYModel: 0x1006cf490> - 8 - 48 - 48

这就有点意思了,class_getInstanceSize获取到的大小并不完全和对象的真实大小完全一致,通过对相关源码的探究,最终发现了原因,这里先上结论:

class_getInstanceSize获取的大小是根据8字节对齐计算,而内存的实际分配策略是根据16字节对齐计算

源码分析

我们首先来看看runtime中class_getInstanceSize的实现,通过调用栈的追踪,最终定位到下面几个比较重要的函数:

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
// Class's ivar size rounded up to a pointer-size boundary.
// 这里返回类的实例变量的大小 因为只有实例变量是存放在类对象中的。
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
// May be unaligned depending on class's ivars.
// 未对齐的实例变量的大小
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
// 64位下8字节对齐 32位下4字节对齐
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}

通过以上几个函数的调用,就可以验证上面的结论class_getInstanceSize是以8字节对齐的,
在上面的函数中,我们还发现了一个很熟悉的身影,就是下面这个结构:

1
class_ro_t *ro()

我们知道,这里面的数据都是只读数据,是在程序编译期就决定了的,我们常用的实例变量就存放在这个结构体中,这也是为什么类在初始化后不能再动态添加实例变量的原因。

然后我们再来看看类的创建过程,还是从源码入手,不过内存创建的函数调用栈比较复杂,我画个简单的流程图来表示下先:

在runtime库中,这个调用堆栈跟到最后两个标黄部分就进不去了,不过我们还是通过一些细节找到了这两个函数的出处,并最终定位到了负责内存分配过程中size计算的函数,该部分代码位于libsystem_malloc库中,具体代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
//最小16字节
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
//先加上16个字节 然后抹掉零头
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, ..., 256} */
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
#define NANO_QUANTA_MASK (NANO_REGIME_QUANTA_SIZE - 1)
#define NANO_SIZE_CLASSES (NANO_MAX_SIZE/NANO_REGIME_QUANTA_SIZE)

通过以上源码也证明了上面的结论内存的实际分配策略是根据16字节对齐计算

补充部分

因为iOS中的类本质上都是结构体,所以他们的内存大小应该也需要满足c语言中结构体对齐的原则,这里简单介绍下c语言中如何进行字节对齐:

内部对齐

即结构体内部变量的起始地址需要是变量自身大小的倍数。具体对齐逻辑参考下图:

外部对齐

即结构体的整体大小需要和当前运行环境的字对齐。

这里还有一个知识点需要提及,为了更高效的利用内存,xcode编译器会对实例变量进行重排序,这个在我上面的LLYModel中并未体现,感兴趣的同学可以自行实验

小结

以上就是这次对iOS类分配内存大小探索的全部内容,首先是发现问题,然后通过源码的分析找到问题产生的原因,中间的探索过程一度中断,不过最后还是通过一些小的线索定位到问题的所在,在源码分析过程中,我们应该做到抓住关键问题,忽略干扰条件,多一些细心和耐心。

架构设计的一些思考

发表于 2020-07-04   |  

从宏观上看,计算机系统整体上都是一种分层的架构设计,从网络的五层协议到分布式系统;从操作系统到应用软件。架构设计要解决的核心问题就是如何分层,层与层之间以及同层之前如何去交互的问题。在我们着手去做分层架构和通信设计之前,我们可能还需要一些设计理论和原则的支撑,历史一次次证明,如果只是全凭实践经验,我们是无法成功设计出一款好的产品的。(比如飞机的发明,飞行器古代就已经出现,但是现代意义上的飞机直到空气动力学理论出现后才由莱特兄弟设计制造出来,这样的例子在科技史上还有很多)这里我总结了三个部分:模块设计原则,设计模式的应用和重构原则。

模块设计原则

模块设计原则就是做架构的理论基础,只有熟练掌握以下这些原则,你在着手软件架构时才能得心应手,信手拈来,设计出更合理的架构,就好比有了九阳神功的张无忌再去修炼乾坤大挪移。

单一职能原则SRP

单一职能原则强调一个模块或者一个类只应该有一种行为,也只能对一种行为负责,比如负责UI展示的类,就不要在内部去处理业务逻辑,负责业务逻辑的类,则内部必要去修改UI布局等。这个原则在MVVM的模式中被广泛采用。

开闭原则OCP

开闭原则指类或者模块应该易于扩展,而难以修改。要遵守该原则,我们在设计模块时就要尽量设计“害羞”的代码,隐藏自己的大部分行为,只将必要的接口暴露给外部,并且尽量只暴露只读的接口,不要暴露能够修改内部属性的接口,这是难以修改部分,那易于扩展部分,我们应该预留适当的钩子接口,该钩子可以扩展类的行为,将该行为的实现交于外部去处理。

里斯替换原则LSP

里斯替换也是描述的接口扩展问题,但是和开闭描述的场景又不太一样,里斯替换更像工厂,一个接口的具体行为不依赖该类,而依赖其扩展类,该接口的行为可以很容易被其扩展类替换。

接口隔离原则ISP

接口隔离很重要,但是经常被忽视,只有正在懂的人才会在模块封装的过程中采用这个原则,大部分人则无视该原则的好处,只是为了代码写起来更方便。我们是封装系统库和三方库时,往往会使用该原则去对系统方法做一层隔离,但是大部分也仅仅只是做到了这一层,选择忽略更上层的情况。其实对更上层的业务实体来说,每一个实体都应该再加一层的隔离,这样做的利大于弊,模块的职能划分的越清晰,维护和扩展的成本就会越低。

依赖反转原则DIP

该原则主要解决模块间比较常见的相互依赖的问题,相互依赖的坏处这里不再多描述,依赖反转将双向或者多向的依赖关系梳理为单向的依赖关系,依赖反转的工具常常使用抽象接口去实现。

组件聚合原则

组件聚合原则主要介绍在做组件封装时类的归属问题,哪些类应该放入一个组件,哪些又不应该放入一个组件内,该原则主要包括以下三部分:

  • 复用/发布等同原则

    该原则指组件中的类和模块应该是可以共同发布,也能被其他组件共同复用的,即该组件中的类应该具有紧密的关系和共同的主题,而不是毫不相干的内容。

  • 共同闭包原则

    一个组件中的各类应该是会因为同一个行为而被一起修改的,如果有一些独善其身的类,那说明该类可能并不适合该组件,应该将其剥离该组件。

  • 共同复用原则

    一个组件中的类应该是可以被外部共同复用的,而不应该存在只需要复用一部分的情况,一个组件应该是不可再拆分的。

组件解耦原则

该原则主要介绍组件内部或者组件之间的关系。

  • 无依赖环原则,这个上面已经介绍过(DIP).
  • 稳定依赖原则:

    这里先介绍什么是模块的稳定性,一个模块被其他模块依赖称为入口依赖,该模块依赖其他模块称为出口依赖,入口依赖越多,该模块就越稳定。

    组件之间的依赖关系应该是从不稳定指向稳定方向的。这里已一个分层组件举例的话,越上层的组件,应该是越不稳定,因为它依赖了太多其他组件,越底层的组件,应该越稳定,因为它几乎只被其他组件依赖,分层组件的依赖关系应该是自顶向下的单向依赖。

  • 稳定抽象原则:

    抽象这个概念应该都知道,这里已接口举例,一个接口就是一个抽象方法。稳定抽象原则是指一个组件的稳定性应该和它的抽象性保持一致。

    这里又要解释一下抽象性的概念,组件中的抽象类和抽象方法 / 组件中的实现类和实现方法 = 组件的抽象性。其实很好理解,抽象类和抽象方法越多,表示该组件越抽象。

    既然稳定性和抽象性要保持一致,还是按上面的分层组件举例,就可以解释为:越是上层的组件应该越具体,越是底层的组件应该越抽象。

设计模式的应用

设计模式在代码中角色很奇妙,有的人可能根据自身经验采用了许多设计模式而不自知,其实设计模式本身也是从实践和经验中总结出来的一套代码设计的真理,了解并应用这些设计模式,在模块和架构设计过程中是必不可少的。

创建型

创建型设计模式这里重点介绍抽象工厂和工厂模式,工厂模式比较单一,一次只能生产一种对象,抽象工厂在工厂的基础上做了扩展,可同时生产多个对象。

考虑下面这种情况:

这个活动目前有5种类型,我们在展示UI时可以选择创建5种cell去分别适配,如果以后还有类型的扩展再新建cell,但是这种方案会造成子类爆炸,也贡献了大量的重复代码,更好的方案是新建一个类型的抽象工厂,抽象工厂提供变化的UI元素的生成接口(设计模式的核心就是对变化的概念进行抽象),具体生成逻辑放到工厂实体类进行。这样就只需要一个cell和一个抽象工厂实体即可完全展示,后续扩展也更方便。整体类结构大概长这样:

其他的创建型设计模式比如原型描述的是对象的复用(clone),单例则是对象的共享,生成器一般用在比较复杂的对象创建上,比如这个对象由很多子对象组成,如一个订单等。

结构型

结构型设计模式主要描述如何组合类和对象的行为已获得更大的结构

适配器模式主要描述如何对类和对象的行为进行扩展,方式主要是依赖和继承。iOS中还可以通过协议和消息转发扩展类。

桥接有点像抽象工厂,基类只提供行为接口,具体的行为逻辑放到实体类进行。这种设计方便了对行为进行替换和扩展。

组合是同一个类型的集合,方便统一处理一些行为,iOS中的subviews集合就是一个典型的组合模式的应用。

装饰其实也是在给类添加属性或者方法,iOS中的分类就是装饰模式的应用。

外观是将一系列相关联的方法进行封装,然后提供一个统一的入口。编译器的封装采用了外观模式,将整个编译链进行封装,隐藏内部过程,只提供一个统一的api供外部调用.

享元模式有点类似上面的原型,只是享元复用的对象可能颗粒度更细,而且只是内部数据,内部数据是不变的,外部数据是可变的。

代理模式比较好理解,类的某个行为自己不去实现,而是交给另一个代理类去实现,代理模式可以用来解耦模块间的相互依赖。

行为型

行为型设计模式具体描述算法和对象之间职能的分配方式

责任链模式描述组合对象对同一方法的调用情况,强调每个对象都有机会去处理改方法,具体实现逻辑可以参考iOS响应链传递机制中hitTest方法的处理。

命令模式将用户的行为进行封装,每一个行为都抽象为一个命令,使用命令队列进行维护,该模式主要应用于编辑软件中redo和undo操作的支持。

解释器模式对特定的语法进行解释,这些语法往往比较复杂且多变,如果直接使用比较麻烦,比如正则表达式的匹配,就需要使用专门的解释器。

迭代器模式主要来用进行集合的访问,在无需暴露集合内部具体结构的情况下。不同的遍历策略对应不同的迭代器类,即多态迭代。编程语言一般都有自己的集合迭代器。

中介者模式为各类之间的交互提供一个环境,避免各类因为相互调用而产生双向依赖。将与其他类的通信转变为和中介者的通信。

备忘录模式是一种数据持久化的应用,当我们需要保存对象信息时,通过将对象持久化本地,下次需要使用时直接从本地获取。

观察者模式提供了一种方式,保证依赖同一属性的多个类的一致性。通过注册对这个属性的观察回调,在这个属性改变时,可以很方便的通知这些依赖类。观察者模式是一对多的通信方式。iOS中的通知和KVO都属于这种模式。

状态模式用来解决对象在不同的时间节点时有不同行为的场景。我们可以将不同的时间节点抽象为不同的状态,通过对状态的观察改变对象的行为。tcp的建连和断开的过程就是应用的状态模式。

策略模式和状态模式相似,不同的是对象在各时间节点需要调用不同的算法,这里我们将变化的概念抽象为策略,每一种算法对应一种策略。

模板方法将基类的逻辑复用,而将具体的数据等属性延迟到子类实现。这样子类在继承父类的逻辑的同时还能拥有自己的行为。模板方法是基类预留的钩子,能够钩住子类的特定行为。

访问者模式提供了一种访问类簇对象的解决方案。可以将访问行为抽象为访问者,每一种具体行为对应具体的一个访问者类,该访问者为类簇中每一个类添加一个该行为的访问方法。如果以后有新的访问行为,在不修改类簇结构的情况下,即可通过扩展访问者来实现。

重构原则

类的重构

函数的重构

12…8
lly

lly

耿直的一Boy

76 日志
© 2021 lly
由 Hexo 强力驱动
主题 - NexT.Mist