热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

iOSObjectiveC:+loadvs+initialize

在NSObject类中有两个非常特殊的类方法load和initialize,用于类的初始化。这两个看似非常简单的类方法在许多方面会让人感到困惑,比如&#

在 NSObject 类中有两个非常特殊的类方法 +load 和 +initialize ,用于类的初始化。这两个看似非常简单的类方法在许多方面会让人感到困惑,比如:

  1. 子类、父类、分类中的相应方法什么时候会被调用?
  2. 需不需要在子类的实现中显式地调用父类的实现?
  3. 每个方法到底会被调用多少次?

下面,我们将结合 runtime(我下载的是当前的最新版本 objc4-646.tar.gz) 的源码,一起来揭开它们的神秘面纱。

+load

+load 方法是当类或分类被添加到 Objective-C runtime 时被调用的,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。子类的 +load 方法会在它的所有父类的 +load 方法之后执行,而分类的 +load 方法会在它的主类的 +load 方法之后执行。但是不同的类之间的 +load 方法的调用顺序是不确定的。

打开 runtime 工程,我们接下来看看与 +load 方法相关的几个关键函数。首先是文件 objc-runtime-new.mm 中的 void prepare_load_methods(header_info *hi) 函数:

1
2
3
4
5
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

void prepare_load_methods(header_info *hi) { size_t count, i; rwlock_assert_writing(&runtimeLock); classref_t *classlist &#61; _getObjc2NonlazyClassList(hi, &count); for (i &#61; 0; i <count; i&#43;&#43;) { schedule_class_load(remapClass(classlist[i])); } category_t **categorylist &#61; _getObjc2NonlazyCategoryList(hi, &count); for (i &#61; 0; i <count; i&#43;&#43;) { category_t *cat &#61; categorylist[i]; Class cls &#61; remapClass(cat->cls); if (!cls) continue; // category for ignored weak-linked class realizeClass(cls); assert(cls->ISA()->isRealized()); add_category_to_loadable_list(cat); } }

顾名思义&#xff0c;这个函数的作用就是提前准备好满足 &#43;load 方法调用条件的类和分类&#xff0c;以供接下来的调用。其中&#xff0c;在处理类时&#xff0c;调用了同文件中的另外一个函数 static void schedule_class_load(Class cls) 来执行具体的操作。

1
2
3
4
5
6 7 8 9 10 11 12 13

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); }

其中&#xff0c;函数第 9 行代码对入参的父类进行了递归调用&#xff0c;以确保父类优先的顺序。void prepare_load_methods(header_info *hi) 函数执行完后&#xff0c;当前所有满足 &#43;load 方法调用条件的类和分类就被分别存放在全局变量 loadable_classes 和 loadable_categories 中了。

准备好类和分类后&#xff0c;接下来就是对它们的 &#43;load 方法进行调用了。打开文件 objc-loadmethod.m &#xff0c;找到其中的 void call_load_methods(void) 函数。

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

void call_load_methods(void) { static BOOL loading &#61; NO; BOOL more_categories; recursive_mutex_assert_locked(&loadMethodLock); // Re-entrant calls do nothing; the outermost call will finish the job. if (loading) return; loading &#61; YES; void *pool &#61; objc_autoreleasePoolPush(); do { // 1. Repeatedly call class &#43;loads until there aren&#39;t any more while (loadable_classes_used > 0) { call_class_loads(); } // 2. Call category &#43;loads ONCE more_categories &#61; call_category_loads(); // 3. Run more &#43;loads if there are classes OR more untried categories } while (loadable_classes_used > 0 || more_categories); objc_autoreleasePoolPop(pool); loading &#61; NO; }

同样的&#xff0c;这个函数的作用就是调用上一步准备好的类和分类中的 &#43;load 方法&#xff0c;并且确保类优先于分类的顺序。我们继续查看在这个函数中调用的另外两个关键函数 static void call_class_loads(void) 和 static BOOL call_category_loads(void) 。由于这两个函数的作用大同小异&#xff0c;下面就以篇幅较小的 static void call_class_loads(void) 函数为例进行探讨。

1
2
3
4
5
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

static void call_class_loads(void) { int i; // Detach current loadable list. struct loadable_class *classes &#61; loadable_classes; int used &#61; loadable_classes_used; loadable_classes &#61; nil; loadable_classes_allocated &#61; 0; loadable_classes_used &#61; 0; // Call all &#43;loads for the detached list. for (i &#61; 0; i <used; i&#43;&#43;) { Class cls &#61; classes[i].cls; load_method_t load_method &#61; (load_method_t)classes[i].method; if (!cls) continue; if (PrintLoading) { _objc_inform("LOAD: &#43;[%s load]\n", cls->nameForLogging()); } (*load_method)(cls, SEL_load); } // Destroy the detached list. if (classes) _free_internal(classes); }

这个函数的作用就是真正负责调用类的 &#43;load 方法了。它从全局变量 loadable_classes 中取出所有可供调用的类&#xff0c;并进行清零操作。

1
2
3

loadable_classes &#61; nil; loadable_classes_allocated &#61; 0; loadable_classes_used &#61; 0;

其中 loadable_classes 指向用于保存类信息的内存的首地址&#xff0c;loadable_classes_allocated 标识已分配的内存空间大小&#xff0c;loadable_classes_used 则标识已使用的内存空间大小。

然后&#xff0c;循环调用所有类的 &#43;load 方法。注意&#xff0c;这里是&#xff08;调用分类的 &#43;load 方法也是如此&#xff09;直接使用函数内存地址的方式 (*load_method)(cls, SEL_load); 对 &#43;load 方法进行调用的&#xff0c;而不是使用发送消息 objc_msgSend 的方式。

这样的调用方式就使得 &#43;load 方法拥有了一个非常有趣的特性&#xff0c;那就是子类、父类和分类中的 &#43;load 方法的实现是被区别对待的。也就是说如果子类没有实现 &#43;load 方法&#xff0c;那么当它被加载时 runtime 是不会去调用父类的 &#43;load 方法的。同理&#xff0c;当一个类和它的分类都实现了 &#43;load 方法时&#xff0c;两个方法都会被调用。因此&#xff0c;我们常常可以利用这个特性做一些“邪恶”的事情&#xff0c;比如说方法混淆&#xff08;Method Swizzling&#xff09;。

&#43;initialize

&#43;initialize 方法是在类或它的子类收到第一条消息之前被调用的&#xff0c;这里所指的消息包括实例方法和类方法的调用。也就是说 &#43;initialize 方法是以懒加载的方式被调用的&#xff0c;如果程序一直没有给某个类或它的子类发送消息&#xff0c;那么这个类的 &#43;initialize 方法是永远不会被调用的。那这样设计有什么好处呢&#xff1f;好处是显而易见的&#xff0c;那就是节省系统资源&#xff0c;避免浪费。

同样的&#xff0c;我们还是结合 runtime 的源码来加深对 &#43;initialize 方法的理解。打开文件 objc-runtime-new.mm &#xff0c;找到以下函数&#xff1a;

1
2
3
4
5
6 7 8 9 10 11 12 13 14 15 16 17 18 19

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { ... rwlock_unlock_write(&runtimeLock); } if (initialize && !cls->isInitialized()) { _class_initialize (_class_getNonMetaClass(cls, inst)); // If sel &#61;&#61; initialize, _class_initialize will send &#43;initialize and // then the messenger will send &#43;initialize again after this // procedure finishes. Of course, if this is not being called // from the messenger then it won&#39;t happen. 2778172 } // The lock is held to make method-lookup &#43; cache-fill atomic // with respect to method addition. Otherwise, a category could ... }

当我们给某个类发送消息时&#xff0c;runtime 会调用这个函数在类中查找相应方法的实现或进行消息转发。从第 8-14 的关键代码我们可以看出&#xff0c;当类没有初始化时 runtime 会调用 void _class_initialize(Class cls) 函数对该类进行初始化。

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

void _class_initialize(Class cls) { ... Class supercls; BOOL reallyInitialize &#61; NO; // Make sure super is done initializing BEFORE beginning to initialize cls. // See note about deadlock above. supercls &#61; cls->superclass; if (supercls && !supercls->isInitialized()) { _class_initialize(supercls); } // Try to atomically set CLS_INITIALIZING. monitor_enter(&classInitLock); if (!cls->isInitialized() && !cls->isInitializing()) { cls->setInitializing(); reallyInitialize &#61; YES; } monitor_exit(&classInitLock); if (reallyInitialize) { // We successfully set the CLS_INITIALIZING bit. Initialize the class. // Record that we&#39;re initializing this class so we can message it. _setThisThreadIsInitializingClass(cls); // Send the &#43;initialize message. // Note that &#43;initialize is sent to the superclass (again) if // this class doesn&#39;t implement &#43;initialize. 2157218 if (PrintInitializing) { _objc_inform("INITIALIZE: calling &#43;[%s initialize]", cls->nameForLogging()); } ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize); if (PrintInitializing) { _objc_inform("INITIALIZE: finished &#43;[%s initialize]", ... }

其中&#xff0c;第 7-12 行代码对入参的父类进行了递归调用&#xff0c;以确保父类优先于子类初始化。另外&#xff0c;最关键的是第 36 行代码&#xff08;暴露了 &#43;initialize 方法的本质&#xff09;&#xff0c;runtime 使用了发送消息 objc_msgSend 的方式对 &#43;initialize 方法进行调用。也就是说 &#43;initialize 方法的调用与普通方法的调用是一样的&#xff0c;走的都是发送消息的流程。换言之&#xff0c;如果子类没有实现 &#43;initialize 方法&#xff0c;那么继承自父类的实现会被调用&#xff1b;如果一个类的分类实现了 &#43;initialize 方法&#xff0c;那么就会对这个类中的实现造成覆盖。

因此&#xff0c;如果一个子类没有实现 &#43;initialize 方法&#xff0c;那么父类的实现是会被执行多次的。有时候&#xff0c;这可能是你想要的&#xff1b;但如果我们想确保自己的 &#43;initialize 方法只执行一次&#xff0c;避免多次执行可能带来的副作用时&#xff0c;我们可以使用下面的代码来实现&#xff1a;

1
2
3
4
5

&#43; (void)initialize { if (self &#61;&#61; [ClassName self]) { // ... do the initialization ... } }

总结

通过阅读 runtime 的源码&#xff0c;我们知道了 &#43;load 和 &#43;initialize 方法实现的细节&#xff0c;明白了它们的调用机制和各自的特点。下面我们绘制一张表格&#xff0c;以更加直观的方式来巩固我们对它们的理解&#xff1a;

 

 &#43;load&#43;initialize
调用时机被添加到 runtime 时收到第一条消息前&#xff0c;可能永远不调用
调用顺序父类->子类->分类父类->子类
调用次数1次多次
是否需要显式调用父类实现
是否沿用父类的实现
分类中的实现类和分类都执行覆盖类中的方法&#xff0c;只执行分类的实现

 

参考链接

  • https://www.mikeash.com/pyblog/friday-qa-2009-05-22-objective-c-class-loading-and-initialization.html
  • http://blog.iderzheng.com/objective-c-load-vs-initialize/
  • http://nshipster.com/method-swizzling/

转载于:https://www.cnblogs.com/LifeTechnologySupporter/p/6742861.html


推荐阅读
author-avatar
清宫佳伶330
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有