iOS 底层探索系列数组
- iOS 底层探索 - alloc & init
- iOS 底层探索 - calloc 和 isa
- iOS 底层探索 - 类
- iOS 底层探索 - cache_t
- iOS 底层探索 - 方法
- iOS 底层探索 - 消息查找
- iOS 底层探索 - 消息转发
- iOS 底层探索 - 应用加载
- iOS 底层探索 - 类的加载
- iOS 底层探索 - 分类的加载
- iOS 底层探索 - 类拓展和关联对象
- iOS 底层探索 - KVC
- iOS 底层探索 - KVO
iOS 查漏补缺系列缓存
上一章咱们对应用的加载有了初步的认识,咱们知道了markdown
exec()
会咱们的应用映射到新的地址空间dyld
进行加载、连接、初始化主程序和主程序所依赖的各类动态库initializeMainExecutable
方法中通过一系列初始化调用 notifySingle
函数,该函数会执行一个 load_images
的回调doModinitFuntions
函数内部会调用 __attribute__((constructor))
的 c
函数dyld
返回主程序的入口函数,开始进入主程序的 main
函数在 main
函数执行执行,其实 dyld
还会在流程中初始化 libSystem
,而 libSystem
又会去初始化 libDispatch
,在 libDispatch
初始化方法里面又会有一步 _os_object_init
,在 _os_object_init
内部就会调起 _objc_init
。而对于 _objc_init
咱们还须要继续探索,由于这里面会进行类的加载等一系列重要的工做。数据结构
_objc_init
首先来到 libObjc
源码的 _objc_init
方法处,你能够直接添加一个符号断点 _objc_init
或者全局搜索关键字来到这里:app
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(); lock_init(); exception_init(); _dyld_objc_notify_register(&map_images, load_images, unmap_image); } 复制代码
咱们接着进行分析:函数
接着来到 environ_init
方法内部:工具
咱们能够看到,这里主要是读取影响 Runtime
的一些环境变量,若是须要,还能够打印环境变量帮助提示。oop
咱们能够在终端上测试一下,直接输入 export OBJC_HELP=1
:post
能够看到不一样的环境变量对应的内容都被打印出来了。学习
接着来到 tls_init
方法内部:
void tls_init(void) { #if SUPPORT_DIRECT_THREAD_KEYS _objc_pthread_key = TLS_DIRECT_KEY; pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific); #else _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific); #endif } 复制代码
这里执行的是关于线程 key
的绑定,好比每线程数据的析构函数。
接着来到 static_init
方法内部:
/*********************************************************************** * static_init * Run C++ static constructor functions. * libc calls _objc_init() before dyld would call our static constructors, * so we have to do it ourselves. **********************************************************************/ static void static_init() { size_t count; auto inits = getLibobjcInitializers(&_mh_dylib_header, &count); for (size_t i = 0; i < count; i++) { inits[i](); } } 复制代码
这里会运行 C++
的静态构造函数,在 dyld
调用咱们的静态构造函数以前,libc
会调用 _objc_init
,因此这里咱们必须本身来作,而且这里只会初始化系统内置的 C++
静态构造函数,咱们本身代码里面写的并不会在这里初始化。
接着来到 lock_init
方法内部:
void lock_init(void) { } 复制代码
咱们能够看到,这是一个空的实现。也就是说 objc
的锁是彻底采用的 C++
那一套的锁逻辑。
接着来到 exception_init
方法内部:
/*********************************************************************** * exception_init * Initialize libobjc's exception handling system. * Called by map_images(). **********************************************************************/ void exception_init(void) { old_terminate = std::set_terminate(&_objc_terminate); } 复制代码
这里是初始化 libobjc
的异常处理系统,咱们程序触发的异常都会来到:
咱们能够看到 _objc_terminate
是未处理异常的回调函数,其内部逻辑以下:
OC
抛出的异常OC
抛出的异常,调用 uncaught_handeler
回调函数指针OC
抛出的异常,则继续 C++
终止操做接下来使咱们今天探索的重点了: _dyld_objc_notify_register
,咱们先看下它的定义:
注意:仅供
objc
运行时使用 当objc
镜像被映射(mapped)、**卸载(unmapped)和初始化(initialized)**的时候,注册的回调函数就会被调用。 这个方法是dlyd
中声明的,一旦调用该方法,调用结果会做为该函数的参数回传回来。好比,当全部的images
以及section
为objc-image-info
被加载以后会回调mapped
方法。load
方法也将在这个方法中被调用。
_dyld_objc_notify_register
方法的三个参数 map_images
、 load_images
、 unmap_image
其实都是函数指针:
这三个函数指针是在 dyld
中回调的,咱们打开 dyld
的源码便可一探究竟,咱们直接搜索 _dyld_objc_notify_register
:
接着来到 dyld
的 registerObjCNotifiers
方法内部:
经过上面两张截图的内容说明在 registerObjCNotifiers
内部, libObjc
传过来的这三个函数指针被 dyld
保存在了本地静态变量中。换句话来讲,最终函数指针是否能被调用,取决于这三个静态变量:
sNotifyObjCMapped
sNotifyObjCInit
sNotifyObjCUnmapped
咱们注意到 registerObjCNotifiers
的 try-catch
语句中的 try
分支注释以下:
call 'mapped' function with all images mapped so far 调用
mapped
函数来映射全部的镜像
那么也就是说 notifyBatchPartial
里面会进行真正的函数指针的调用,咱们进入这个方法内部:
咱们能够看到,在 notifyBatchPartial
方法内部,这里的注释:
tell objc about new images 告诉
objc
镜像已经映射完成了
而图中箭头所指的地方正是 sNotifyObjCMapped
函数指针真正调用的地方。
弄清楚了三个函数指针是怎么调用的还不够,接下来咱们要深刻各个函数的内部看里面究竟作了什么样的事情。
首先是 map_images
,咱们来到它的实现:
/*********************************************************************** * map_images * Process the given images which are being mapped in by dyld. * Calls ABI-agnostic code after taking ABI-specific locks. * * Locking: write-locks runtimeLock **********************************************************************/ void map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) { mutex_locker_t lock(runtimeLock); return map_images_nolock(count, paths, mhdrs); }C 复制代码
Process the given images which are being mapped in by dyld. Calls ABI-agnostic code after taking ABI-specific locks.
处理由
dyld
映射的给定镜像 取得特定于ABI
的锁后,调用与ABI
无关的代码。
这里会继续往下走到 map_images_nolock
map_images_nolock
内部代码十分冗长,咱们通过分析以后,前面的工做基本上都是进行镜像文件信息的提取与统计,因此能够定位到最后的 _read_images
:
这里进入
_read_images
的条件是hCount
大于 0,hCount
表示的是Mach-O
中header
的数量
OK,咱们的主角登场了, _read_images
和 lookupImpOrForward
能够说是咱们学习 Runtime
和 iOS
底层里面很是重要的两个概念了, lookUpImpOrForward
已经探索过了,剩下的 _read_images
咱们也不能落下。
Perform initial processing of the headers in the linked list beginning with headerList. 从
headerList
开始,对已经连接了的Mach-O
镜像表中的头部进行初始化处理
咱们能够看到,整个 _read_images
有接近 400 行代码。咱们不妨折叠一下里面的分支代码,而后总览一下:
经过折叠代码,以及日志打印提示信息,咱们大体能够将 _read_images
分为下面几个流程:
doneOnce 流程
**
咱们从第一个分支 doneOnce
开始,这个名词顾名思义,只会执行一次:
SUPPORT_NONPOINTER_ISA
判断当前是否支持开启内存优化的 isa
SUPPORT_INDEXED_ISA
判断当前是不是将类存储在 isa
做为类表索引
Mach-O
的头部,而且判断若是是 Swift 3.0
以前的代码,就须要禁用对 isa
的内存优化TARGET_OS_OSX
判断是不是 macOS
执行环境macOS
的系统版本,若是小于 10.11
则说明 app
太陈旧了,须要禁用掉 non-pointer isa
Mach-O
的头部,判断若是有 __DATA__,__objc_rawisa
段的存在,则禁用掉 non-pointer isa
,由于不少新的 app
加载老的扩展的时候会须要这样的判断操做。预先优化过的类不会加入到
gdb_objc_realized_classes
这个哈希表中来,gdb_objc_realized_classes
哈希表的装载因子为 0.75,这是一个通过验证的效率很高的扩容临界值。
gdb_objc_realized_classes
表中来咱们查看这个表的定义:
// This is a misnomer: gdb_objc_realized_classes is actually a list of // named classes not in the dyld shared cache, whether realized or not.
这是一个误称:gdb_objc_realized_classes 表实际上存储的是不在
dyld
共享缓存里面的命名类,不管这些类是否实现
除了 gdb_objc_realized_classes
表以外,还有一张表 allocatedClasses
:
objc_allocateClassPair
开辟以后的类和元类存储的表(也就是说须要 alloc
)其实 gdb_objc_realized_classes
对 allocatedClasses
是一种包含的关系,一张是类的总表,一张是已经开辟了内存的类表,
Discover classes 流程
Discover classes. Fix up unresolved future classes. Mark bundle classes. 发现类。修正未解析的
future
类,标记bundle
类。
_getObjc2ClassList
来获取到全部的类,咱们能够经过 MachOView
来验证:Mach-O
的 header
部分,而后经过 mustReadClasses
来判断哪些条件能够跳过读取类这一步骤header
是不是 Bundle
header
是否开启了 预优化_getObjc2ClassList
取出的全部的类
readClass
来读取类信息readClass
结果不为空,则须要从新为类开辟内存Fix up remapped classes 流程
修复 重映射类 类表和非懒加载类表没有被重映射 (也就是 _objc_classlist) 因为消息转发,类引用和父类引用会被重映射 (也就是 _objc_classrefs)
**
noClassesRemapped
方法判断是否有类引用(_objc_classrefs)须要进行重映射
EACH_HEADER
_getObjc2ClassRefs
和 _getObjc2SuperRefs
取出当前遍历到的 Mach-O
的类引用和父类引用,而后调用 remapClassRef
进行重映射Fix up @selector references 流程
修正
SEL
引用
selLock
锁EACH_HEADER
Mach-O
_getObjc2SelectorRefs
拿到全部的 SEL
引用SEL
引用调用 sel_registerNameNoLock
进行注册也就是说这一流程最主要的目的就是注册 SEL
,咱们注册真正发生的地方: __sel_registerName
,这个函数若是你们常常玩 Runtime
确定不会陌生:
咱们简单分析一下 __sel_registerName
方法的流程:
sel
为空,则返回一个空的 SEL
builtins
中搜索,看是否已经注册过,若是找到,直接返回结果namedSelectors
哈希表中查询,找到了就返回结果namedSelectors
未初始化,则建立一下这个哈希表sel_alloc
来建立一下 SEL
,而后把新建立的 SEL
插入哈希表中进行缓存的填充Fix up old objc_msgSend_fixup call sites 流程
修正旧的
objc_msgSend_fixup
调用
**
这个流程的执行前提是 FIXUP
被开启。
EACH_HEADER
_getObjc2MessageRefs
方法来获取当前遍历到的 Mach-O
镜像的全部消息引用fixupMessageRef
进行修正Discover protocols 流程
发现协议,并修正协议引用
**
Fix up @protocol references 流程
对全部的协议作重映射
**
Realize non-lazy classes 流程
初始化非懒加载类(
**+load**
方法和静态实例)
Realize newly-resolved future classes 流程
初始化新解析出来的
future
类
**
Discover categories 流程
处理全部的分类,包括类和元类
**
到这里, _read_images
的流程就分析完毕,咱们能够新建一个文件来去掉一些干扰的信息,只保留核心的逻辑,这样从宏观的角度来分析更直观:
Q & A 环节 Q:
dyld
主要逻辑是加载库,也就是镜像文件,可是加载完是怎么读取的呢? A:_read_images
是真正读取的地方Q:
SEL
方法编号什么时候加载? A:_read_images
咱们探索了 _read_images
方法的流程,接下来让咱们把目光放到本文的主题 - 类的加载
既然是类的加载,那么咱们在前面所探索的类的结构中出现的内容都会一一重现。
因此咱们不妨直接进行断点调试,让咱们略过其它干扰信息,聚焦于类的加载。
doneOnce
流程中会建立两个哈希表,并无涉及到类的加载,因此咱们跳过
咱们在下图所示的位置处打上断点:
classList
中取出的
cls
只是一个内存地址,咱们尝试经过
LLDB
打印
cls
的
clas_rw_t
:
能够看到 cls
的属性、方法、协议以及类名都为空,说明这里类并无被真正加载完成,咱们接着聚焦到 read_class
函数上面,咱们进入其内部实现,咱们大体浏览以后会定位到以下图所示的代码:
看起来类的信息在这里完成了加载,那么为了验证咱们的猜测,直接断点调试一下但发现断点根本走不进来,缘由在于这里的判断语句
if (Class newCls = popFutureNamedClass(mangledName)) 复制代码
判断当前传入的类的类名是否有 future
类的实现,可是咱们刚才已经打印了,类名是空的,因此确定不会执行这里。咱们接着往下走:
cls
插入到 gdb_objc_realized_classes
表cls
插入到 allocatedClasses
表分析完 read_class
,咱们回到 _read_images
方法
咱们能够看到 read_class
返回的 newCls
会进行一个判断,判断与传入 read_class
以前的 cls
是否相等,而在 read_class
内部只有一个地方对类的内容进行了改动,可是咱们刚才测试了是进不去的,因此这个 if
里面的内容咱们能够略过,也就是说 resolvedFutureClasses
的内容咱们均可以暂时略过。
总结一下 readClass
:
data()
类设置 ro/rw
经过分析 read_class
,咱们能够得知,类已经被注册到两个哈希表中去了,那么如今一切时机都已经成熟了。可是咱们仍是要略过像 Fix up remapped classes
、 Fix up @selector references
、 fix up old objc_msgSend_fixup call sites
、 Discover protocols. Fix up protocol refs
、 Fix up @protocol references
,由于咱们的重点是类的加载,咱们最终来到了 Realize non-lazy classes (for +load methods and static instances)
,略去无关信息以后,咱们能够看到咱们的
主角 realizeClassWithoutSwift
闪亮登场了:
从方法的名称以及方法注释咱们能够知道, realizeClassWithoutSwift
是进行类的第一次初始化操做,包括分配读写数据也就是咱们常说的 rw
,可是并不会进行任何的 Swift
端初始化。咱们直接聚焦下面的代码:
calloc
开辟内存空间,返回一个新的 rw
cls
取出来的 ro
赋值给这个 rw
rw
设置到 cls
身上那么是否是说在这里 rw
就有值了呢,咱们 LLDB
打印大法走起:
能够清楚地看到,此时 rw
仍是为空,说明这里只是对 rw
进行了初始化,可是方法、属性、协议这些都没有被添加上。
咱们接着往下走:
这里能够看到父类和元类都会递归调用 realizeClassWithoutSwift
来初始化各自的 rw
。为何在类的加载操做里面要去加载类和元类呢?回忆一下类的结构,答案很简单,要保证 superclass
和 isa
的完整性,也就是保证类的完整性,
上面的截图就是最好的证实,初始化完毕的父类和元类被赋值到了类的 superclass
和 isa
上面。
接着往下走能够看到,不光要把父类关联到类上面,还要让父类知道子类的存在。
最后一行代码是 methodizeClass(cls)
,注释显示的是 attach categories
,附加分类到类?咱们进入其内部实现一探究竟。
在探索 methodizeClass
前,咱们先总结一下 realizeClassWithoutSwift
:
class
的 data()
ro/rw
赋值对类的方法列表、协议列表和属性列表进行修正 附加
category
到类上面来
咱们直接往下面走:
// Install methods and properties that the class implements itself. method_list_t *list = ro->baseMethods(); if (list) { prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls)); rw->methods.attachLists(&list, 1); } 复制代码
ro
中取出方法列表附加到 rw
上property_list_t *proplist = ro->baseProperties; if (proplist) { rw->properties.attachLists(&proplist, 1); } 复制代码
ro
中取出属性列表附加到 rw
上protocol_list_t *protolist = ro->baseProtocols; if (protolist) { rw->protocols.attachLists(&protolist, 1); } 复制代码
ro
中取出协议列表附加到 rw
上category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/); attachCategories(cls, cats, false /*don't flush caches*/); 复制代码
cls
中取出未附加的分类进行附加操做咱们能够看到,这里有一个操做叫 attachLists
,为何方法、属性、协议都能调用这个方法呢?
咱们能够看到,方法、属性、协议的数据结构都是一个二维数组,咱们深刻 attachLists
方法内部实现:
void attachLists(List* const * addedLists, uint32_t addedCount) { if (addedCount == 0) return; if (hasArray()) { // many lists -> many lists uint32_t oldCount = array()->count;//10 uint32_t newCount = oldCount + addedCount;//4 setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); array()->count = newCount;// 10+4 memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0])); memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } else if (!list && addedCount == 1) { // 0 lists -> 1 list list = addedLists[0]; } else { // 1 list -> many lists List* oldList = list; uint32_t oldCount = oldList ? 1 : 0; uint32_t newCount = oldCount + addedCount; setArray((array_t *)malloc(array_t::byteSize(newCount))); array()->count = newCount; if (oldList) array()->lists[addedCount] = oldList; memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } } 复制代码
attachLists
的 list_array_tt
二维数组有多个一维数组
realloc
对容器进行从新分配,大小为原来的大小加上新增的大小memmove
把原来的数据移动到容器的末尾attachLists
的 list_array_tt
二维数组为空且新增大小数目为 1,则直接取 addedList
的第一个 list
返回attachLists
的 list_array_tt
二维数组只有一个一维数组
咱们接着探索 _dyld_objc_notify_register
的第二个参数 load_images
,这个函数指针是在何时调用的呢,一样的,咱们接着在 dyld
源码中搜索对应的函数指针 sNotifyObjCInit
:
能够看到,在 notifySingle
方法内部, sNotifyObjCInit
函数指针被调用了。根据咱们上一篇文章探索 dyld
底层能够知道, _load_images
应该是对于每个加载进来的 Mach-O
镜像都会递归调用一次。
咱们来到 libObjc
源码中 load_images
的定义处:
处理由
dyld
映射的给定镜像中的+load
方法
load
方法,若是没有,直接返回load
方法,具体实现经过 prepare_load_methods
load
方法,具体实现经过 call_load_methods
从这个方法名称,咱们猜想这里应该作的是 load
方法的一些预处理工做,让咱们来到源码进行分析:
void prepare_load_methods(const headerType *mhdr) { size_t count, i; runtimeLock.assertLocked(); classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count); for (i = 0; i < count; i++) { schedule_class_load(remapClass(classlist[i])); } category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count); for (i = 0; i < count; i++) { category_t *cat = categorylist[i]; Class cls = remapClass(cat->cls); if (!cls) continue; // category for ignored weak-linked class if (cls->isSwiftStable()) { _objc_fatal("Swift class extensions and categories on Swift " "classes are not allowed to have +load methods"); } realizeClassWithoutSwift(cls); assert(cls->ISA()->isRealized()); add_category_to_loadable_list(cat); } } /*********************************************************************** * prepare_load_methods * Schedule +load for classes in this image, any un-+load-ed * superclasses in other images, and any categories in this image. **********************************************************************/ // Recursively schedule +load for cls and any un-+load-ed superclasses. // cls must already be connected. static void schedule_class_load(Class cls) { if (!cls) return; assert(cls->isRealized()); // _read_images should realize if (cls->data()->flags & RW_LOADED) return; // Ensure superclass-first ordering schedule_class_load(cls->superclass); add_class_to_loadable_list(cls); cls->setInfo(RW_LOADED); } /*********************************************************************** * add_class_to_loadable_list * Class cls has just become connected. Schedule it for +load if * it implements a +load method. **********************************************************************/ void add_class_to_loadable_list(Class cls) { IMP method; loadMethodLock.assertLocked(); method = cls->getLoadMethod(); if (!method) return; // Don't bother if cls has no +load method if (PrintLoading) { _objc_inform("LOAD: class '%s' scheduled for +load", cls->nameForLogging()); } if (loadable_classes_used == loadable_classes_allocated) { loadable_classes_allocated = loadable_classes_allocated*2 + 16; loadable_classes = (struct loadable_class *) realloc(loadable_classes, loadable_classes_allocated * sizeof(struct loadable_class)); } loadable_classes[loadable_classes_used].cls = cls; loadable_classes[loadable_classes_used].method = method; loadable_classes_used++; } 复制代码
_getObjc2NonlazyClassList
获取全部已经加载进去的类列表schedule_class_load
遍历这些类
load
方法,确保父类的 load
方法顺序排在子类的前面add_class_to_loadable_list
, 把类的 load
方法存在 loadable_classes
里面schedule_class_load
以后,经过 _getObjc2NonlazyCategoryList
取出全部分类数据realizeClassWithoutSwift
来防止类没有初始化,若是已经初始化了则不影响add_category_to_loadable_list
,加载分类中的 load
方法到 loadable_categories
里面
经过名称咱们能够知道 call_load_methods
应该就是 load
方法被调用的地方了。咱们直接看源码:
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
方法是父类优先调用的 而在父类的+load
以后才会调用分类的+load
方法
objc_autoreleasePoolPush
压栈一个自动释放池do-while
循环开始
+load
方法直到找不到为止+load
方法objc_autoreleasePoolPop
出栈一个自动释放池至此, _objc_init
和 _dyld_objc_notify_register
咱们就分析完了,咱们对类的加载有了更细致的认知。 iOS
底层有时候探索起来确实很枯燥,可是若是能找到高效的方法以及明确本身的所探索的方向,会让本身从宏观上从新审视这门技术。是的,技术只是工具,咱们不能被技术所绑架,咱们要作到有的放矢的去探索,这样才能事半功倍。