懒惰的 initialize 方法

因为 ObjC 的 runtime 只能在 Mac OS 下才能编译,所以文章中的代码都是在 Mac OS,也就是 x86_64 架构下运行的,对于在
arm64 中运行的代码会特别说明。

写在前面

这篇文章可能是对 Objective-C 源代码解析系列文章中最短的一篇了,在 Objective-C 中,我们总是会同时想到 loadinitialize 这两个类方法。而这两个方法也经常在一起比较:

在上一篇介绍 load 方法的文章中,已经对 load 方法的调用时机、调用顺序进行了详细地分析,所以对于 load 方法,这里就不在赘述了。

这篇文章会假设你知道:假设你是 iOS 开发者。

本文会主要介绍:

  1. initialize 方法的调用为什么是惰性的
  2. 这货能干啥

initialize 的调用栈

在分析其调用栈之前,首先来解释一下,什么是惰性的。

这是 main.m 文件中的代码:

#import <Foundation/Foundation.h>

@interface XXObject : NSObject @end

@implementation XXObject

+ (void)initialize {
    NSLog(@"XXObject initialize");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool { }
    return 0;
}

主函数中的代码为空,如果我们运行这个程序:

你会发现与 load 方法不同的是,虽然我们在 initialize 方法中调用了 NSLog。但是程序运行之后没有任何输出。

如果,我们在自动释放池中加入以下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __unused XXObject *object = [[XXObject alloc] init];
    }
    return 0;
}

再运行程序:

你会发现,虽然我们没有直接调用 initialize 方法。但是,这里也打印出了 XXObject
initialize
 字符串。

initialize 只会在对应类的方法第一次被调用时,才会调用。

我们在 initialize 方法中打一个断点,来查看这个方法的调用栈:

0 +[XXObject initialize]
1 _class_initialize
2 lookUpImpOrForward
3 _class_lookupMethodAndLoadCache3
4 objc_msgSend
5 main
6 start

直接来看调用栈中的 lookUpImpOrForward 方法,lookUpImpOrForward 方法只会在向对象发送消息,并且在类的缓存中没有找到消息的选择子时才会调用,具体可以看这篇文章,从源代码看
ObjC 中消息的发送

在这里,我们知道 lookUpImpOrForward 方法是 objc_msgSend 触发的就够了。

在 lldb 中输入 p sel 打印选择子,会发现当前调用的方法是 alloc 方法,也就是说,initialize 方法是在 alloc 方法之前调用的,alloc 的调用导致了前者的执行。

其中,使用 if (initialize && !cls->isInitialized()) 来判断当前类是否初始化过:

bool isInitialized() {
   return getMeta()->data()->flags & RW_INITIALIZED;
}

当前类是否初始化过的信息就保存在元类的 class_rw_t 结构体中的 flags 中。

这是 flags 中保存的信息,它记录着跟当前类的元数据,其中第 16-31 位有如下的作用:

flags 的第 29 位 RW_INITIALIZED 就保存了当前类是否初始化过的信息。

_class_initialize 方法

在 initialize 的调用栈中,直接调用其方法的是下面的这个 C 语言函数:

void _class_initialize(Class cls)
{
    Class supercls;
    BOOL reallyInitialize = NO;

    // 1. 强制父类先调用 initialize 方法
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }

    {
        // 2. 通过加锁来设置 RW_INITIALIZING 标志位
        monitor_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {
            cls->setInitializing();
            reallyInitialize = YES;
        }
    }

    if (reallyInitialize) {
        // 3. 成功设置标志位,向当前类发送 +initialize 消息
        _setThisThreadIsInitializingClass(cls);

        ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);

        // 4. 完成初始化,如果父类已经初始化完成,设置 RW_INITIALIZED 标志位,
        //    否则,在父类初始化完成之后再设置标志位。
        monitor_locker_t lock(classInitLock);
        if (!supercls  ||  supercls->isInitialized()) {
            _finishInitializing(cls, supercls);
        } else {
            _finishInitializingAfter(cls, supercls);
        }
        return;
    } else if (cls->isInitializing()) {
        // 5. 当前线程正在初始化当前类,直接返回,否则,会等待其它线程初始化结束后,再返回
        if (_thisThreadIsInitializingClass(cls)) {
            return;
        } else {
            monitor_locker_t lock(classInitLock);
            while (!cls->isInitialized()) {
                classInitLock.wait();
            }
            return;
        }
    } else if (cls->isInitialized()) {
        // 6. 初始化成功后,直接返回
        return;
    } else {
        _objc_fatal("thread-safe class init in objc runtime is buggy!");
    }
}

方法的主要作用自然是向未初始化的类发送 +initialize 消息,不过会强制父类先发送 +initialize

  1. 强制未初始化过的父类调用 initialize 方法

    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }
    
  2. 通过加锁来设置 RW_INITIALIZING 标志位
    monitor_locker_t lock(classInitLock);
    if (!cls->isInitialized() && !cls->isInitializing()) {
        cls->setInitializing();
        reallyInitialize = YES;
    }
    
  3. 成功设置标志位、向当前类发送 +initialize 消息

    objectivec((void(*)(Class,
    SEL))objc_msgSend)(cls, SEL_initialize);

  4. 完成初始化,如果父类已经初始化完成,设置 RW_INITIALIZED 标志位。否则,在父类初始化完成之后再设置标志位
    monitor_locker_t lock(classInitLock);
    if (!supercls  ||  supercls->isInitialized()) {
        _finishInitializing(cls, supercls);
    } else {
        _finishInitializingAfter(cls, supercls);
    }
    
  5. 如果当前线程正在初始化当前类,直接返回,否则,会等待其它线程初始化结束后,再返回,保证线程安全
    if (_thisThreadIsInitializingClass(cls)) {
        return;
    } else {
        monitor_locker_t lock(classInitLock);
        while (!cls->isInitialized()) {
            classInitLock.wait();
        }
        return;
    }
    
  6. 初始化成功后,直接返回
    return;
    

管理初始化队列

因为我们始终要保证父类的初始化方法要在子类之前调用,所以我们需要维护一个 PendingInitializeMap 的数据结构来存储当前的类初始化需要哪个父类先初始化完成。

这个数据结构中的信息会被两个方法改变:

if (!supercls  ||  supercls->isInitialized()) {
  _finishInitializing(cls, supercls);
} else {
  _finishInitializingAfter(cls, supercls);
}

分别是 _finishInitializing 以及 _finishInitializingAfter,先来看一下后者是怎么实现的,也就是在父类没有完成初始化的时候调用的方法:

static void _finishInitializingAfter(Class cls, Class supercls)
{
    PendingInitialize *pending;
    pending = (PendingInitialize *)malloc(sizeof(*pending));
    pending->subclass = cls;
    pending->next = (PendingInitialize *)NXMapGet(pendingInitializeMap, supercls);
    NXMapInsert(pendingInitializeMap, supercls, pending);
}

因为当前类的父类没有初始化,所以会将子类加入一个数据结构 PendingInitialize中,这个数据结构其实就类似于一个保存子类的链表。这个链表会以父类为键存储到 pendingInitializeMap 中。

NXMapInsert(pendingInitializeMap, supercls, pending);

而在父类已经调用了初始化方法的情况下,对应方法 _finishInitializing 的实现就稍微有些复杂了:

static void _finishInitializing(Class cls, Class supercls)
{
    PendingInitialize *pending;

    cls->setInitialized();

    if (!pendingInitializeMap) return;
    pending = (PendingInitialize *)NXMapGet(pendingInitializeMap, cls);
    if (!pending) return;

    NXMapRemove(pendingInitializeMap, cls);

    while (pending) {
        PendingInitialize *next = pending->next;
        if (pending->subclass) _finishInitializing(pending->subclass, cls);
        free(pending);
        pending = next;
    }
}

首先,由于父类已经完成了初始化,在这里直接将当前类标记成已经初始化,然后递归地将被当前类 block 的子类标记为已初始化,再把这些当类移除 pendingInitializeMap

小结

到这里,我们对 initialize 方法的研究基本上已经结束了,这里会总结一下关于其方法的特性:

  1. initialize 的调用是惰性的,它会在第一次调用当前类的方法时被调用
  2. 与 load 不同,initialize 方法调用时,所有的类都已经加载到了内存中
  3. initialize 的运行是线程安全的
  4. 子类会继承父类的 initialize 方法

而其作用也非常局限,一般我们只会在 initialize 方法中进行一些常量的初始化。

参考资料

关注仓库,及时获得更新:iOS-Source-Code-Analyze

时间: 2024-11-07 01:56:44

懒惰的 initialize 方法的相关文章

Load与initialize方法

为了讲清楚饿汉式单例模式实现需要了解一下这两个方法.它们的特别之处,在于iOS会在运行期提前并且自动调用这两个方法,而且很多对于类方法的规则(比如继承,类别(Category))都有不同的处理因为这两个方法是在程序运行一开始就被调用的方法,我们可以利用他们在类被使用前,做一些预处理工作.比如我碰到的就是让类自动将自身类名保存到一个NSDictionary中.Apple的文档就不再这里给出了,可以自己去看看. Apple的文档很清楚地说明了initialize和load的区别在于:load是只要类

NSObject的load和initialize方法

load和initialize的共同特点 在不考虑开发者主动使用的情况下,系统最多会调用一次 如果父类和子类都被调用,父类的调用一定在子类之前 都是为了应用运行提前创建合适的运行环境 在使用时都不要过重地依赖于这两个方法,除非真正必要 load和initialize的区别 load方法 调用时机比较早,运行环境有不确定因素.具体说来,在iOS上通常就是App启动时进行加载,但当load调用的时候,并不能保证所有类都加载完成且可用,必要时还要自己负责做auto release处理.对于有依赖关系的

细说OC中的load和initialize方法

OC中有两个特殊的类方法,分别是load和initialize.本文总结一下这两个方法的区别于联系.使用场景和注意事项.Demo可以在我的Github上找到——load和initialize,如果觉得有帮助还望点个star以示支持,总结在文章末尾. load 顾名思义,load方法在这个文件被程序装载时调用.只要是在Compile Sources中出现的文件总是会被装载,这与这个类是否被用到无关,因此load方法总是在main函数之前调用. 调用规则 如果一个类实现了load方法,在调用这个方法

iOS开发-OC篇 load方法 和 initialize方法比较

Load方法 和 initialize方法的比较    在OC语言中,我们相比之下对于load和initialize方法的使用比较少,所以会不是很清楚的了解二者的用途和区别,所以整理了一下,和大家进行分享,有所得不对的地方,希望能够指出来,多谢! 1.load方法特点: 1> 当类被引用进程序的时候会执行这个函数 2> 一个类的load方法不用写明[super load],父类就会收到调用,并且在子类之前. 3> Category的load也会收到调用,但顺序上在主类的load调用之后.

黑马程序员-OC类的本质,深入探讨,load方法和initialize方法

1:类的本质:类也是一种类,可以叫做类类,类对象,类类型: 2:类和对象在内存中分配问题(注意区分类的对象和类对象的概念) 类对象在内存中只有一份,且只加载一次,类对象中存放了类中定义的方法: 而成员变量和isa指针,存放在了类的对象中;isa指针指向了类对象:如图: 3:类本身也是对象,是class类型的对象: // 以person为例 Person *p1 = [[Person alloc] init]; Person *p1 = [[Person alloc] init]; // 获取类对

iOS Load方法 和 initialize方法的比较

一.load方法特点: 1. 当类被引用进程序的时候会执行这个函数 2.一个类的load方法不用写明[super load],父类就会收到调用,并且在子类之前. 3.Category的load也会收到调用,但顺序上在主类的load调用之后. 二.initialize方法特点: 1. initialize的自然调用是在第一次主动使用当前类的时候 2.和load不同,即使子类不实现initialize方法,会把父类的实现继承过来调用一遍.注意的是在此之前,父类的方法已经被执行过一次了,同样不需要su

load和initialize方法

  一.load 方法什么时候调用: 在main方法还没执行的时候 就会 加载所有类,调用所有类的load方法. load方法是线程安全的,它使用了锁,我们应该避免线程阻塞在load方法. 在项目中使用的一些场景: 比如我们要统计所有页面(UIViewController.UITableViewController)的数据,可以在UIViewController的类别里的load方法里实现Method Swizzle @implementation UIViewController (BaseM

NSObject的load和initialize方法(转)

全文转载自:http://www.cocoachina.com/ios/20150104/10826.html 在Objective-C中,NSObject是根类,而NSObject.h的头文件中前两个方法就是load和initialize两个类方法,本篇文章就对这两个方法做下说明和整理. 1.概述 Objective-C作为一门面向对象语言,有类和对象的概念.编译后,类相关的数据结构会保留在目标文件中,在运行时得到解析和使用. 在应用程序运行起来的时候,类的信息会有加载和初始化过程.其实在Ja

Orchard源码分析(5.1):Host初始化(DefaultOrchardHost.Initialize方法)

概述 Orchard作为一个可扩展的CMS系统,是由一系列的模块(Modules)或主题(Themes)组成,这些模块或主题统称为扩展(Extensions).在初始化或运行时需要对扩展进行安装:DefaultOrchardHost.SetupExtensions方法. 当添加新的扩展.删除扩展或修改扩展源码后,需要通知扩展加载器(Extension Loader)重新加载或完成一些清理工作,所以需要进行监视:DefaultOrchardHost.MonitorExtensions方法. Orc