引子
在我们之前探索dyld
流程时,我们发现其实dyld
和objc_init()
之间是存在联系的。
iOS底层探索 -- dyld 流程分析
其中在我们追踪到_dyld_objc_notify_register
方法时,我们能发现其调用的入口是 _objc_init()
, 那么顺着这个线索,我们来看一看这个_objc_init()
方法。
objc_init()
首先,我们继续使用我们之前的objc源码工程
我们来看一下objc_init()
源码
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();
//关于线程key的绑定,例如线程数据的析构函数
tls_init();
//运行C++静态构造函数。
//在dyld调用静态构造函数之前,libc调用了objc_init(),
//所以我们必须自己动手。
static_init();
//runtime运行时环境初始化
runtime_init();
//初始化libobjc的异常处理系统。
//由map_images()调用。
exception_init();
//缓存条件的初始化
cache_init();
//初始化回调机制。通常情况下,这里不会做什么
//所有的初始化都是惰性的,但是对于某些进程,我们急切地加载
_imp_implementationWithBlock_init();
//注册要在映射、取消映射和初始化objc映像时调用的处理程序。
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
其中我们能看到中间进行了很多初始化的工作
-
environ_init()
读取环境变量 -
tls_init()
关于线程key的绑定,例如线程数据的析构函数 -
static_init()
运行C++静态构造函数。在dyld
调用静态构造函数之前,libc
调用了objc_init()
所以我们必须自己动手。 -
exception_init()
初始化libobjc
的异常处理系统。由map_images()
调用。 -
cache_init()
缓存条件的初始化。 -
_imp_implementationWithBlock_init()
初始化回调机制。通常情况下,这里不会做什么。所有的初始化都是惰性的,但是对于某些进程,我们急切地加载 -
_dyld_objc_notify_register()
注册要在映射、取消映射和初始化objc映像时调用的处理程序。
environ_init()
void environ_init(void)
{
....
// Print OBJC_HELP and OBJC_PRINT_OPTIONS output.
if (PrintHelp || PrintOptions) {
if (PrintHelp) {
_objc_inform("Objective-C runtime debugging. Set variable=YES to enable.");
_objc_inform("OBJC_HELP: describe available environment variables");
if (PrintOptions) {
_objc_inform("OBJC_HELP is set");
}
_objc_inform("OBJC_PRINT_OPTIONS: list which options are set");
}
if (PrintOptions) {
_objc_inform("OBJC_PRINT_OPTIONS is set");
}
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[i];
if (PrintHelp) _objc_inform("%s: %s", opt->env, opt->help);
if (PrintOptions && *opt->var) _objc_inform("%s is set", opt->env);
}
}
}
我们发现其中的for()
循环可以在满足条件下打印环境标量
我们想了解到底有哪些环境变量,那我们直接拿出其中的for()
循环,并去掉所有条件
for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
const option_t *opt = &Settings[I];
_objc_inform("%s: %s", opt->env, opt->help);
_objc_inform("%s is set", opt->env);
}
直接运行,得到打印结果
objc[3473]: OBJC_PRINT_IMAGES: log image and library names as they are loaded
objc[3473]: OBJC_PRINT_IMAGES is set
objc[3473]: OBJC_PRINT_IMAGE_TIMES: measure duration of image loading steps
objc[3473]: OBJC_PRINT_IMAGE_TIMES is set
objc[3473]: OBJC_PRINT_LOAD_METHODS: log calls to class and category +load methods
objc[3473]: OBJC_PRINT_LOAD_METHODS is set
objc[3473]: OBJC_PRINT_INITIALIZE_METHODS: log calls to class +initialize methods
objc[3473]: OBJC_PRINT_INITIALIZE_METHODS is set
objc[3473]: OBJC_PRINT_RESOLVED_METHODS: log methods created by +resolveClassMethod: and +resolveInstanceMethod:
objc[3473]: OBJC_PRINT_RESOLVED_METHODS is set
objc[3473]: OBJC_PRINT_CLASS_SETUP: log progress of class and category setup
objc[3473]: OBJC_PRINT_CLASS_SETUP is set
objc[3473]: OBJC_PRINT_PROTOCOL_SETUP: log progress of protocol setup
objc[3473]: OBJC_PRINT_PROTOCOL_SETUP is set
objc[3473]: OBJC_PRINT_IVAR_SETUP: log processing of non-fragile ivars
objc[3473]: OBJC_PRINT_IVAR_SETUP is set
objc[3473]: OBJC_PRINT_VTABLE_SETUP: log processing of class vtables
objc[3473]: OBJC_PRINT_VTABLE_SETUP is set
objc[3473]: OBJC_PRINT_VTABLE_IMAGES: print vtable images showing overridden methods
objc[3473]: OBJC_PRINT_VTABLE_IMAGES is set
objc[3473]: OBJC_PRINT_CACHE_SETUP: log processing of method caches
objc[3473]: OBJC_PRINT_CACHE_SETUP is set
objc[3473]: OBJC_PRINT_FUTURE_CLASSES: log use of future classes for toll-free bridging
objc[3473]: OBJC_PRINT_FUTURE_CLASSES is set
objc[3473]: OBJC_PRINT_PREOPTIMIZATION: log preoptimization courtesy of dyld shared cache
objc[3473]: OBJC_PRINT_PREOPTIMIZATION is set
objc[3473]: OBJC_PRINT_CXX_CTORS: log calls to C++ ctors and dtors for instance variables
objc[3473]: OBJC_PRINT_CXX_CTORS is set
objc[3473]: OBJC_PRINT_EXCEPTIONS: log exception handling
objc[3473]: OBJC_PRINT_EXCEPTIONS is set
objc[3473]: OBJC_PRINT_EXCEPTION_THROW: log backtrace of every objc_exception_throw()
objc[3473]: OBJC_PRINT_EXCEPTION_THROW is set
objc[3473]: OBJC_PRINT_ALT_HANDLERS: log processing of exception alt handlers
objc[3473]: OBJC_PRINT_ALT_HANDLERS is set
objc[3473]: OBJC_PRINT_REPLACED_METHODS: log methods replaced by category implementations
objc[3473]: OBJC_PRINT_REPLACED_METHODS is set
objc[3473]: OBJC_PRINT_DEPRECATION_WARNINGS: warn about calls to deprecated runtime functions
objc[3473]: OBJC_PRINT_DEPRECATION_WARNINGS is set
objc[3473]: OBJC_PRINT_POOL_HIGHWATER: log high-water marks for autorelease pools
objc[3473]: OBJC_PRINT_POOL_HIGHWATER is set
objc[3473]: OBJC_PRINT_CUSTOM_CORE: log classes with custom core methods
objc[3473]: OBJC_PRINT_CUSTOM_CORE is set
objc[3473]: OBJC_PRINT_CUSTOM_RR: log classes with custom retain/release methods
objc[3473]: OBJC_PRINT_CUSTOM_RR is set
objc[3473]: OBJC_PRINT_CUSTOM_AWZ: log classes with custom allocWithZone methods
objc[3473]: OBJC_PRINT_CUSTOM_AWZ is set
objc[3473]: OBJC_PRINT_RAW_ISA: log classes that require raw pointer isa fields
objc[3473]: OBJC_PRINT_RAW_ISA is set
objc[3473]: OBJC_DEBUG_UNLOAD: warn about poorly-behaving bundles when unloaded
objc[3473]: OBJC_DEBUG_UNLOAD is set
objc[3473]: OBJC_DEBUG_FRAGILE_SUPERCLASSES: warn about subclasses that may have been broken by subsequent changes to superclasses
objc[3473]: OBJC_DEBUG_FRAGILE_SUPERCLASSES is set
objc[3473]: OBJC_DEBUG_NIL_SYNC: warn about @synchronized(nil), which does no synchronization
objc[3473]: OBJC_DEBUG_NIL_SYNC is set
objc[3473]: OBJC_DEBUG_NONFRAGILE_IVARS: capriciously rearrange non-fragile ivars
objc[3473]: OBJC_DEBUG_NONFRAGILE_IVARS is set
objc[3473]: OBJC_DEBUG_ALT_HANDLERS: record more info about bad alt handler use
objc[3473]: OBJC_DEBUG_ALT_HANDLERS is set
objc[3473]: OBJC_DEBUG_MISSING_POOLS: warn about autorelease with no pool in place, which may be a leak
objc[3473]: OBJC_DEBUG_MISSING_POOLS is set
objc[3473]: OBJC_DEBUG_POOL_ALLOCATION: halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools
objc[3473]: OBJC_DEBUG_POOL_ALLOCATION is set
objc[3473]: OBJC_DEBUG_DUPLICATE_CLASSES: halt when multiple classes with the same name are present
objc[3473]: OBJC_DEBUG_DUPLICATE_CLASSES is set
objc[3473]: OBJC_DEBUG_DONT_CRASH: halt the process by exiting instead of crashing
objc[3473]: OBJC_DEBUG_DONT_CRASH is set
objc[3473]: OBJC_DISABLE_VTABLES: disable vtable dispatch
objc[3473]: OBJC_DISABLE_VTABLES is set
objc[3473]: OBJC_DISABLE_PREOPTIMIZATION: disable preoptimization courtesy of dyld shared cache
objc[3473]: OBJC_DISABLE_PREOPTIMIZATION is set
objc[3473]: OBJC_DISABLE_TAGGED_POINTERS: disable tagged pointer optimization of NSNumber et al.
objc[3473]: OBJC_DISABLE_TAGGED_POINTERS is set
objc[3473]: OBJC_DISABLE_TAG_OBFUSCATION: disable obfuscation of tagged pointers
objc[3473]: OBJC_DISABLE_TAG_OBFUSCATION is set
objc[3473]: OBJC_DISABLE_NONPOINTER_ISA: disable non-pointer isa fields
objc[3473]: OBJC_DISABLE_NONPOINTER_ISA is set
objc[3473]: OBJC_DISABLE_INITIALIZE_FORK_SAFETY: disable safety checks for +initialize after fork
objc[3473]: OBJC_DISABLE_INITIALIZE_FORK_SAFETY is set
环境变量列表说明:
变量名 | 介绍 | 备注 |
---|---|---|
OBJC_PRINT_OPTIONS | list which options are set | 输出OBJC已设置的选项 |
OBJC_PRINT_IMAGES | log image and library names as they are loaded | 输出已load的image信息 |
OBJC_PRINT_LOAD_METHODS | log calls to class and category +load methods | 打印 Class 及 Category 的 + (void)load 方法的调用信息 |
OBJC_PRINT_INITIALIZE_METHODS | log calls to class +initialize methods | 打印 Class 的 + (void)initialize 的调用信息 |
OBJC_PRINT_RESOLVED_METHODS | log methods created by +resolveClassMethod and +resolveInstanceMethod: | 打印通过 +resolveClassMethod: 或 +resolveInstanceMethod: 生成的类方法 |
OBJC_PRINT_CLASS_SETUP | log progress of class and category setup | 打印 Class 及 Category 的设置过程 |
OBJC_PRINT_PROTOCOL_SETUP | log progress of protocol setup | 打印 Protocol 的设置过程 |
OBJC_PRINT_IVAR_SETUP | log processing of non-fragile ivars | 打印 Ivar 的设置过程 |
OBJC_PRINT_VTABLE_SETUP | log processing of class vtables | 打印 vtable 的设置过程 |
OBJC_PRINT_VTABLE_IMAGES | print vtable images showing overridden methods | 打印 vtable 被覆盖的方法 |
OBJC_PRINT_CACHE_SETUP | log processing of method caches | 打印方法缓存的设置过程 |
OBJC_PRINT_FUTURE_CLASSES | log use of future classes for toll-free bridging | 打印从 CFType 无缝转换到 NSObject 将要使用的类(如 CFArrayRef 到 NSArray * ) |
OBJC_PRINT_GC | log some GC operations | 打印一些垃圾回收操作 |
OBJC_PRINT_PREOPTIMIZATION | log preoptimization courtesy of dyld shared cache | 打印 dyld 共享缓存优化前的问候语 |
OBJC_PRINT_CXX_CTORS | log calls to C++ ctors and dtors for instance variables | 打印类实例中的 C++ 对象的构造与析构调用 |
OBJC_PRINT_EXCEPTIONS | log exception handling | 打印异常处理 |
OBJC_PRINT_EXCEPTION_THROW | log backtrace of every objc_exception_throw() | 打印所有异常抛出时的 Backtrace |
OBJC_PRINT_ALT_HANDLERS | log processing of exception alt handlers | 打印 alt 操作异常处理 |
OBJC_PRINT_REPLACED_METHODS | log methods replaced by category implementations | 打印被 Category 替换的方法 |
OBJC_PRINT_DEPRECATION_WARNINGS | warn about calls to deprecated runtime functions | 打印所有过时的方法调用 |
OBJC_PRINT_POOL_HIGHWATER | log high-water marks for autorelease pools | 打印 autoreleasepool 高水位警告 |
OBJC_PRINT_CUSTOM_RR | log classes with un-optimized custom retain/release methods | 打印含有未优化的自定义 retain/release 方法的类 |
OBJC_PRINT_CUSTOM_AWZ | log classes with un-optimized custom allocWithZone methods | 打印含有未优化的自定义 allocWithZone 方法的类 |
OBJC_PRINT_RAW_ISA | log classes that require raw pointer isa fields | 打印需要访问原始 isa 指针的类 |
OBJC_DEBUG_UNLOAD | warn about poorly-behaving bundles when unloaded | 卸载有不良行为的 Bundle 时打印警告 |
OBJC_DEBUG_FRAGILE_SUPERCLASSES | warn about subclasses that may have been broken by subsequent changes to superclasses | 当子类可能被对父类的修改破坏时打印警告 |
OBJC_DEBUG_FINALIZERS | warn about classes that implement -dealloc but not -finalize | 警告实现了 -dealloc 却没有实现 -finalize 的类 |
OBJC_DEBUG_NIL_SYNC | warn about @synchronized(nil), which does no synchronization | 警告 @synchronized(nil) 调用,这种情况不会加锁 |
OBJC_DEBUG_NONFRAGILE_IVARS | capriciously rearrange non-fragile ivars | 打印突发地重新布置 non-fragile ivars 的行为 |
OBJC_DEBUG_ALT_HANDLERS | record more info about bad alt handler use | 记录更多的 alt 操作错误信息 |
OBJC_DEBUG_MISSING_POOLS | warn about autorelease with no pool in place, which may be a leak | 警告没有 pool 的情况下使用 autorelease,可能内存泄漏 |
OBJC_DEBUG_DUPLICATE_CLASSES | halt when multiple classes with the same name are present | 当出现类重名时停机 |
OBJC_USE_INTERNAL_ZONE | allocate runtime data in a dedicated malloc zone | 在一个专用的 malloc 区分配运行时数据 |
OBJC_DISABLE_GC | force GC OFF, even if the executable wants it on | 强行关闭自动垃圾回收,即使可执行文件需要垃圾回收 |
OBJC_DISABLE_VTABLES | disable vtable dispatch | 关闭 vtable 分发 |
OBJC_DISABLE_PREOPTIMIZATION | disable preoptimization courtesy of dyld shared cache | 关闭 dyld 共享缓存优化前的问候语 |
OBJC_DISABLE_TAGGED_POINTERS | disable tagged pointer optimization of NSNumber et al. | optimization of NSNumber et al. 关闭 NSNumber 等的 tagged pointer 优化 |
OBJC_DISABLE_NONPOINTER_ISA | disable non-pointer isa fields | 关闭 non-pointer isa 字段的访问 |
这些环境变量,我们可以通过Project
-> Scheme
-> EditScheme
-> Run
-> Arguments
-> Environment Variables
去配置
通过OBJC_DISABLE_NONPOINTER_ISA
和 OBJC_PRINT_LOAD_METHODS
应用举例一下
OBJC_DISABLE_NONPOINTER_ISA 关闭 non-pointer isa 字段的访问
测试代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
FQPerson *person = [FQPerson alloc];
[person sayHelloWorld];
}
return 0;
}
我们在初始化person
后加入一个断点 使用lldb
调试
首先我们去除 OBJC_DISABLE_NONPOINTER_ISA
(lldb) x/4gx person
0x100752f50: 0x001d800100008735 0x0000000000000000
0x100752f60: 0x0000000000000000 0x0000000000000000
(lldb) p/t 0x001d800100008735
(long) $1 = 0b0000000000011101100000000000000100000000000000001000011100110101
然后我们加上OBJC_DISABLE_NONPOINTER_ISA
设置为YES
(lldb) x/4gx person
0x100a4a300: 0x0000000100008730 0x0000000000000000
0x100a4a310: 0x0000000000000000 0x0000000000000000
(lldb) p/t 0x0000000100008730
(long) $1 = 0b0000000000000000000000000000000100000000000000001000011100110000
在之前,我们研究isa
的信息时
iOS底层原理探索 -- isa的本质
我们知道
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# 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 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)
isa
的第一位就是代表了nonpointer
信息,两次测试最后一位数值不同,正好符合我们的测试的结果
OBJC_PRINT_LOAD_METHODS 打印 Class 及 Category 的 + (void)load 方法的调用信息
同样,我们添加OBJC_PRINT_LOAD_METHODS
并设置值为YES
运行代码 可以得到打印结果
objc[9171]: LOAD: class 'OS_xpc_connection' scheduled for +load
...
objc[9171]: LOAD: category 'NSError(FPAdditions)' scheduled for +load
objc[9171]: LOAD: +[NSError(FPAdditions) load]
objc[9171]: LOAD: class '_DKEventQuery' scheduled for +load
objc[9171]: LOAD: +[_DKEventQuery load]
我们再在我们自己的类中也添加一个+ load
方法 运行
objc[9266]: LOAD: class 'OS_xpc_connection' scheduled for +load
...
objc[9266]: LOAD: class '_DKEventQuery' scheduled for +load
objc[9266]: LOAD: +[_DKEventQuery load]
objc[9266]: LOAD: class 'FQPerson' scheduled for +load
objc[9266]: LOAD: +[FQPerson load]
此时,我们可以看到多了我们刚刚添加的我们自己类中的load
方法
environ_init()小结
所以我们可以通过environ_init()
的配置,做一下指定处理或者消息检索,在我们开发中可以方便我们去进行优化
配置项通过终端命令同样可以查看
$ export OBJC_HELP=1
tls_init()
这里关于线程key的绑定,例如线程数据的析构函数
void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}
static_init()
运行C++
静态构造函数。在dyld
调用静态构造函数之前,libc
调用了objc_init()
,所以我们必须自己动手。
static void static_init()
{
size_t count;
auto inits = getLibobjcInitializers(&_mh_dylib_header, &count);
for (size_t i = 0; i < count; i++) {
inits[i]();
}
}
runtime_init()
runtime运行时环境初始化
void runtime_init(void)
{
objc::unattachedCategories.init(32);
objc::allocatedClasses.init();
}
exception_init()
初始化libobjc的异常处理系统。由map_images()调用。
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
cache_init()
缓存条件的初始化
void cache_init()
{
#if HAVE_TASK_RESTARTABLE_RANGES
mach_msg_type_number_t count = 0;
kern_return_t kr;
while (objc_restartableRanges[count].location) {
count++;
}
kr = task_restartable_ranges_register(mach_task_self(),
objc_restartableRanges, count);
if (kr == KERN_SUCCESS) return;
_objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
kr, mach_error_string(kr));
#endif // HAVE_TASK_RESTARTABLE_RANGES
}
_imp_implementationWithBlock_init()
初始化回调机制。通常情况下,这里不会做什么。所有的初始化都是惰性的,但是对于某些进程,我们急切地加载
void
_imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
if (__progname &&
(strcmp(__progname, "QtWebEngineProcess") == 0 ||
strcmp(__progname, "Steam Helper") == 0)) {
Trampolines.Initialize();
}
#endif
}
_dyld_objc_notify_register(&map_images, load_images, unmap_image)
这是我们需要研究的另一个比较关键的方法
//注意:只供objc运行时使用
//注册要在映射、取消映射和初始化objc映像时调用的处理程序。
//Dyld将用一个包含objc图像信息部分的图像数组来调用“mapped”函数。
//那些是dylibs的图像的ref计数将自动被缓冲,因此objc将不再需要
//对它们调用dlopen()以防止它们被卸载。在调用_dyld_objc_notify_register()期间,
//dyld将使用已经加载的objc图像调用“mapped”函数。在以后的任何dlopen()调用中,
//dyld还将调用“mapped”函数。Dyld将在调用Dyld时调用“init”函数
//图像中的初始化器。这时objc调用该映像中的任何+load方法。
void _dyld_objc_notify_register(_dyld_objc_notify_mapped mapped,
_dyld_objc_notify_init init,
_dyld_objc_notify_unmapped unmapped);
而在其中的三个参数 mapped
init
unmapped
分别对应的是三个函数
typedef void (*_dyld_objc_notify_mapped)(unsigned count, const char* const paths[], const struct mach_header* const mh[]);
typedef void (*_dyld_objc_notify_init)(const char* path, const struct mach_header* mh);
typedef void (*_dyld_objc_notify_unmapped)(const char* path, const struct mach_header* mh);
那么问题来了,什么时候调用了map_images
和load_images
,也就是
_dyld_objc_notify_mapped
和 _dyld_objc_notify_init
方法
在我们之前研究dyld
的流程时,其中我们讲到过一个方法 notifySingle
方法
其中sNotifyObjCInit
我们点击跳转一下就能发现
就是我们之前提到的load_images
方法
notifySingle
的调用时机我们之前已经提到过,现在不在赘述。
但通过上面sNotifyObjCInit
我们不由联想到map_images
也就是_dyld_objc_notify_mapped
我们也可以去搜索sNotifyObjCMapped
去了解其调用的时机。
通过全局搜索sNotifyObjCMapped
,我们可以发现其被调用的地方在notifyBatchPartial
中
而notifyBatchPartial
则在registerObjCNotifiers
中被调用。
最终入口回到了objc
中调用dyld
的入口_dyld_objc_notify_register
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
和dyld
的关联关系大致就在于此。
借用一张图