load方法的调用时机 我们都知道,每个类都有两个初始化方法,其中一个就是load
方法,对于每一个Class
和Category
来说,必定会调用此方法,而且仅调用一次。当包含Class
和Category
的程序被库载入系统时,就会执行此方法,并且此过程通常是在程序启动的时候执行。
不同的是,现在iOS
系统中已经加入了动态加载特性,这是从macOS
应用程序中迁移而来的特性,等应用程序启动好之后再去加载程序库。如果Class
和其Category
中都重写了load
方法,则先调用Class
中的。那么为什么会先调用Class
的load
方法呢?通过这篇文章想必你会有个答案。
因为Objective-C
的runtime
只能在macOS
下才能编译,所以,文章中的所有代码都是在macOS
下运行了,这里推荐大家直接使用RetVal 封装好的debug
版最新源码进行断点调试,来追踪一下load
方法的全部处理过程,以便于了解这个函数以及Objective-C
强大的动态性。
创建一个Class
文件GGObject
和两个分类GGObject+GG
、NSString+GG
,然后分别在这三个文件中添加load
方法。运行程序,会看到load
方法的调用时机是在入口函数主程序之前。
然后在GGObject
中load
方法下增加断点,查看其调用栈并跟踪函数执行时候的上层代码:
调用栈显示栈情况如下:
0 +[GGObject load]
1 call_class_loads()
2 call_load_methods()
3 load_images(const char *, const mach_header *)
4 dyld::notifySingle(dyld_image_states, ImageLoader const *, ImageLoader::InitializerTimingList*)
11 _dyld_start
追其源头,从_dyld_start
开始研究。dyld(The Dynamic Link Editor) 是苹果的动态链接库,系统内核做好程序启动的初始准备后,将其他事务交给dyld
负责。这里不再细究。
在研究load_images
方法之前,先来研究一下什么是images
,images
表示的是二进制文件编译后的符号、代码等。所以load_images
的工作是传入处理过后的二进制文件并让runtime
进行处理,并且每一个文件对应一个抽象实例来负责加载,这里的实例是ImageLoader
,从调用栈的方法4可以清楚的看到参数类型:
dyld::notifySingle(dyld_image_states, ImageLoader const *, ImageLoader::InitializerTimingList*)
ImageLoader
处理二进制文件的时机是在main
入口函数以前,它在加载文件时主要做两个工作:
在程序运行时它先将动态链接的image
递归加载
再从可执行文件image
递归加载所有符号
我们可以通过断点来打印出所有加载的image
。在刚才断点的调用栈中,选中3 load_images(const char*, const mach_header *)
,并添加断点:
这样可以将当前的image
全部显示,我们列出来image
的path
和slice
信息:
(const char *) $0 = 0x000000010004d0b8 "/Users/guiyongdong/Library/Developer/Xcode/DerivedData/objc-gursabanmdkytddcknzhdonlrrvk/Build/Products/Debug/libobjc.A.dylib"
(const mach_header *) $1 = 0x00000001000ad000
(const char *) $2 = 0x00007fffd60caec8 "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
(const mach_header *) $3 = 0x00007fffd60ca000
(const char *) $4 = 0x00007fffead2d9d0 "/usr/lib/libnetwork.dylib"
(const mach_header *) $5 = 0x00007fffead2d000
(const char *) $6 = 0x00007fffd52bbc50 "/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork"
(const mach_header *) $7 = 0x00007fffd52bb000
(const char *) $8 = 0x00007fffda1a5610 "/System/Library/Frameworks/NetFS.framework/Versions/A/NetFS"
(const mach_header *) $9 = 0x00007fffda1a5000
(const char *) $10 = 0x00007fffe4ef0a20 "/System/Library/PrivateFrameworks/LanguageModeling.framework/Versions/A/LanguageModeling"
(const mach_header *) $11 = 0x00007fffe4ef0000
(const char *) $12 = 0x00007fffd5d42b10 "/System/Library/Frameworks/CoreData.framework/Versions/A/CoreData"
(const mach_header *) $13 = 0x00007fffd5d42000
(const char *) $14 = 0x00007fffeaa53ac0 "/usr/lib/libmecabra.dylib"
(const mach_header *) $15 = 0x00007fffeaa53000
...
这里会传入很多的动态链接库.dylib
以及官方静态框架.framework
的image,而path
就是其对应的二进制文件的地址。在<mach-o/dyld.h>
动态库头文件中,也为我们提供了查询所有动态库image
的方法,如下:
#import <Foundation/Foundation.h>
#import <mach-o/dyld.h>
#import <stdio.h>
void listImages () {
uint32_t i;
uint32_t ic = _ dyld_image_count();
printf ("image 的个数 %d \n" ,ic);
for (i = 0 ; i < ic; i++) {
printf ("%d: %p\t%s\t(slide: %ld)\n" ,
i,
_ dyld_get_image_header(i),
_ dyld_get_image_name(i),
_ dyld_get_image_vmaddr_slide(i));
}
}
int main (int argc, const char * argv[]) {
listImages();
@autoreleasepool {
NSLog(@"Application start" );
}
return 0 ;
}
我们可以通过系统库提供的接口方法,来深入学习官方的动态库情况:
load_images 此时,系统已经将所有的image
加载进内存,然后交由load_images
函数来解析。我们来分析一下load_images
函数:
extern bool hasLoadMethods (const headerType *mhdr) ;
extern void prepare_load_methods (const headerType *mhdr) ;
void
load_images (const char *path __u nused, const struct mach_header *mh)
{
if (!hasLoadMethods((const headerType *)mh)) return ;
recursive_mutex_locker_t lock(loadMethodLock);
{
rwlock_writer_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
call_load_methods();
}
接下来我们一步一步分析。首先调用的是hasLoadMethods
函数。其中为了查询load
函数列表,会分别查询该函数在内存数据段上指定section
区域是否有所记录。
bool hasLoadMethods (const headerType *mhdr)
{
size_t count;
if (_ getObjc2NonlazyClassList(mhdr, &count) && count > 0 ) return true ;
if (_ getObjc2NonlazyCategoryList(mhdr, &count) && count > 0 ) return true ;
return false ;
}
在objc-file.mm
文件中存在以下定义:
#define GETSECT(name, type, sectname) \
type *name(const headerType *mhdr, size_t *outCount) { \
return getDataSection<type> (mhdr, sectname, nil, outCount); \
} \
type *name(const header_info *hi, size_t *outCount) { \
return getDataSection<type> (hi->mhdr(), sectname, nil, outCount); \
}
GETSECT(_ getObjc2NonlazyClassList, classref_t , "__objc_nlclslist" );
GETSECT(_ getObjc2NonlazyCategoryList, category_t *, "__objc_nlcatlist" );
在Apple
的官方文档中,我们可以在__DATA
段中查询到__objc_classlist
的用途,主要是用在访问Objective-C
的类列表,而__objc_nlcatlist
用于访问Objective-C
的分类列表。这一块对类信息的解析是由dyld
处理时期完成的,也就是我们上文提到的map_images
方法的解析工作。而且从侧面可以看出,Objective-C
的强大动态性,与dyld
前期处理密不可分。
通过这一步,会将image
中的类列表和分类列表的个数快速的查询出来,只要满足其中一个条件就能继续进行,否则image
中连类列表和分类列表都没有,就一定不会有load
方法。
可递归锁 接下来需要定义锁,然后加锁。在load_image
方法所在的objc-runtime-new.mm
中,全局loadMethodLock
是一个recursive_mutex_t
类型的变量。这个是苹果通过C
实现的一个互斥递归锁Class
,来解决多次上锁而不会发生死锁的问题。之所以用递归锁,是因为接下来会递归类的父类直到NSObject
。
recursive_mutex_t
其作用与NSRecursiveLock
相同,但不是由NSLock
再封装,而是通过C
为runtime
的使用场景而写的一个Class
。更多关于线程锁的知识,可以看看我这篇iOS多线程之各种锁的简单介绍
准备 load 运行的从属Class void prepare_load_methods (const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertWriting();
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 ;
realizeClass(cls);
assert(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
prepare_load_methods
作用是为load方法做准备,从代码中可以看出Class
的load
方法是优先于Category
。其中在收集Class
的load
方法中,因为需要对Class
关系树的根节点逐层遍历运行,在schedule_class_load
方法中使用深层递归的方式递归到根节点,优先进行收集。
static void schedule_class_load (Class cls)
{
if (!cls) return ;
assert(cls->isRealized());
if (cls->data()->flags & RW_LOADED) return ;
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
在schedule_class_load
中,Class
的读取方式是cls
指针方式,其中有很多内存符号位用来记录状态。isRealized()
查看的就是RW_REALIZED
位,改位记录的是当前Class
是否初始化一个类的指标。而之后查看的RW_LOADED
是记录当前类的load
方法是否已经被检测。
void add_class_to_loadable_list (Class cls)
{
IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod();
if (!method) return ;
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++;
}
在存储静态表的方法中,方法对象会以指针的方式作为传递参数,然后用名为loadable_classes
的静态类型数组对即将运行的load
方法进行存储,以及方法所属的Class
。其下标索引loadable_classes_used
为(从0开始)的全局量,并在每次录入方法后自加操作实现索引的偏移。
赛选过Class
以后,接下来会继续赛选Category
。通过_getObjc2NonlazyCategoryList
获取到image
中所有的Category
后,遍历执行add_category_to_loadable_list
方法,将有load
方法的Category
添加到全局loadable_categories
静态类型的数组中。add_category_to_loadable_list
方法的实现原理与add_class_to_loadable_list
几乎一样。这里不再细说。
由此可以看出,在prepare_load_methods
方法中,runtime
对Class
和Category
进行了筛选工作,并且将即将执行的load
方法以指针的形式组织成一个线性表结构,为之后执行操作打下基础。
通过函数指针让load方法跑起来 通过加载镜像(image)、缓存类和分类列表后,开始执行call_load_methods
方法。
void call_load_methods (void )
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
if (loading) return ;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
while (loadable_classes_used > 0 ) {
call_class_loads();
}
more_categories = call_category_loads();
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
其实call_load_methods
由以上代码可知,仅是运行load
方法的入口,其中最重要的方法call_class_loads
和call_category_loads
会分别从loadable_classes
和loadable_categories
列表中找出对应的Class
和Category
,并分别使用selector(load)
的实现并加载。
static void call_class_loads (void )
{
int i;
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0 ;
loadable_classes_used = 0 ;
for (i = 0 ; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t )classes[i].method;
if (!cls) continue ;
if (PrintLoading) {
_ objc_inform("LOAD: +[%s load]\n" , cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}
if (classes) free (classes);
}
(*load_method)(cls, SEL_load)
通过这一句就可以调用load
方法。这是一个函数指针。其中load_method_t
的定义如下:
typedef void (*load_method_t ) (id, SEL) ;
可以看到,我们将Class
和SEL
传递过去,至此完成load
方法的动态调用。call_category_loads
和call_class_loads
的调用机制类似,只是后续会继续做很多内存操作,有兴趣的可以看看。
至此完成了load
方法的动态调用。
总结 你过去可能会听说,对于load
方法的调用顺序有两条规则:
父类先于子类调用
类先于分类调用
通过我们的整体分析,你会发现这种现象是很有原因的。在schedule_class_load
递归方法中,会保证父类先于子类加入到loadable_classes
数组红,从而确保类的调用顺序的正确性。
而在call_load_methods
方法中:
do {
while (loadable_classes_used > 0 ) {
call_class_loads();
}
more_categories = call_category_loads();
} while (loadable_classes_used > 0 || more_categories);
会一次性将所有类的load
方法调用完毕,之后才会调用分类的load
放法。至此,整个load
调用流程图如下:
load
可以说是我们日常开发中接触到调用时间最靠前的方法,这就成为了我们玩黑魔法的绝佳时机。
但是由于load
方法的运行时间过早,所以这里可能不是一个理想的环境,因为某些类可能需要在在其它类之前加载,但是这是我们无法保证的。不过在这个时间点,所有的framework
都已经加载到了运行时中,所以调用framework
中的方法都是安全的。
扩展initialize 说到load
方法就不得不提initialize
方法,我们都知道load
会在程序启动的时候加载,而initialize
方法会在类或者类的子类收到第一条消息之前被调用。现在,我们已经非常清楚load
方法的调用原理,至于initialize
呢?我们现在继续分析。
紧接着我们刚才的例子,新建类GGSuperObject
,并实现initialize
方法,让GGObject
继承GGSuperObject
,接着实现GGObject
和GGObject (GG)
的initialize
方法,在main
中,我们创建GGObject
的实例,运行程序如下:
运行结果很符合我们的预期,父类会优先调用,分类会覆盖本类的initialize
,下面我们通过代码来看具体的实现原理。当我们向某个类发送消息时,runtime
会调用lookUpImpOrForward
这个函数。
IMP lookUpImpOrForward (Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
...
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_ class_initialize (_ class_getNonMetaClass(cls, inst));
runtimeLock.read();
}
...
return imp;
}
从中可以看到当类没有初始化时,会调用_class_initialize
对类进行初始化,_class_getNonMetaClass
这里主要是对类进行一些转换,我们这里不用过多考虑。
void _ class_initialize(Class cls)
{
assert(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
_ class_initialize(supercls);
}
...
if (reallyInitialize) {
...
@try {
callInitialize(cls);
...
}
...
}
...
}
在这里,显示对入参的父类进行递归调用,以确保父类优先于子类初始化,还有一个关键的地方,我们来看callInitialize
发送消息的具体实现:
void callInitialize (Class cls)
{
((void (*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm ("" );
}
有没有很熟悉,runtime
使用了发送消息objc_msgSend
的方式对initialize
方法进行调用,这样,initialize
方法的调用就是与普通方法的调用是一致的,都是走的发送消息的流程,那么我们再回到lookUpImpOrForward
方法中:
IMP lookUpImpOrForward (Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
if (!cls->isRealized()) {
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_ class_initialize (_ class_getNonMetaClass(cls, inst));
runtimeLock.read();
}
retry:
runtimeLock.assertReading();
imp = cache_getImp(cls, sel);
if (imp) goto done;
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0 ) {
_ objc_fatal("Memory corruption in class list." );
}
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_ objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break ;
}
}
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_ class_resolveMethod(cls, sel, inst);
runtimeLock.read();
triedResolver = YES;
goto retry;
}
imp = (IMP)_ objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
至此,整个调用流程我们已经很清晰了,其实initialize
走的就是完整的一个消息发送流程。
当我们第一次调用某个类的方法时,首先会递归遍历此类的父类,给父类发送initialize
消息。接着又回调消息发送机制上,先查类的缓存,之后查类的方法列表,然后沿着继承链查父类的缓存,之后查父类的方法,如果都没有查到IMP,则走消息转发流程。至此,我们也明白为何子类会覆盖父类的方法,其实都是runtime
的作用。
可能你还有个疑惑,为什么分类的initialize
方法会覆盖本来的initialize
方法呢?通过下面一段代码你会发现端倪:
Method* methodList_f = class_copyMethodList(object_getClass([GGObject class ]),&count_f);
for (int i=0 ;i<count_f;i++) {
Method temp_f = methodList_f[i];
const char * name_s =sel_getName(method_getName(temp_f));
NSLog (@"方法名:%@" ,[NSString stringWithUTF8String:name_s]);
}
free(methodList_f);
你会发现打印了两个initialize
,其实这是因为类先于分类加载,在加载分类的时候,会将分类的方法放在类的方法的前面,所以类的方法列表中有两个initialize
方法,并不是分类中的方法覆盖了本类中的方法,只是runtime
在遍历方法列表的时候,只要找到一个就会返回,runtime
不知道后面还有一个initialize
方法。想必你现在知道类和分类的调用关系了吧。
好了,至此load
方法和initialize
方法咱们已经说完。
参考资料 你真的了解 load 方法么?