Runtime(6)--load和initialize

  • +load
  • +initialize
    +load+initialize这两个方法看起来都是在类初始的时候调用的,其实是在调用时机和runtime底层逻辑上有很大的不同。

+load

我们知道在程序开始运行的时候,在main()函数之前程序会准备很多的工作。其中runtime的初始化_objc_init(),就是在之前调用的。
在前面文章我们看过_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(); //读取影响运行时的环境变量,如果需要,还可以打印环境变量
    tls_init();     //关于线程key的绑定 例如析构函数设置静态密钥key

    //运行c++静态构造函数,libc在dyld调用静态构造函数之前调用_objc_init(),
    //所以我们必须自己做.
    static_init();
    runtime_init();
    exception_init();
    cache_init();
    _imp_implementationWithBlock_init();

    //注册镜像加载通知回调
    /**
    注:仅供objc运行时使用
    映射、未映射和初始化objc映像时要调用的寄存器处理程序
    Dyld将使用包含objc-image-info部分的镜像数组调用“mapped”函数
    那些是dylib的镜像会自动替换ref计数,因此objc不再需要对它们调用dlopen()来防止它们被卸载
    在调用_dyld_objc_notify_register()期间,dyld将调用已经加载了objc镜像的“映射”函数
    在以后的dlopen()调用期间,dyld还将调用“mapped”函数
    当Dyld在该镜像中被称为初始化器时,Dyld将调用“init”函数
    这是objc调用图像中的任何+load方法的时候
    */
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
  }

我们只关注load_images里的逻辑

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

在load_images方法其实就是干了一件事,调用Class的+load()方法。
runtime调用+load()方法分为两步走:

  1. Discover load methods
  2. Call +load methods

接着往下看:
在Discover load methods中,会调用prepare_load_methods 来处理+load方法:


void prepare_load_methods(const headerType *mhdr)
{
    size_t count, I;

    runtimeLock.assertLocked();

    classref_t const *classlist = 
        _getObjc2NonlazyClassList(mhdr, &count);
    for (i = 0; i < count; i++) {
        schedule_class_load(remapClass(classlist[i]));
    }

    category_t * const *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, nil);
        ASSERT(cls->ISA()->isRealized());
        add_category_to_loadable_list(cat);
    }
}

prepare_load_methods 逻辑分为两个部分:
1.先调用schedule_class_load 来组织class+load 方法
2.在调用add_category_to_loadable_list来组织category+load方法

然后schedule_class_load 方法实现如下:

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。把superclass用add_class_to_loadable_list方法到loadable class list中:

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

从上面的方法可以看出,每一个定义了+load的类,都会被放到loadable_classes中。
也可以看出,+load方法并不存在子类重写父类之说。而且父类的+load方法会先于子类加到loadable class list列表当中去,在递归取出调用的时候也是会先调父类的方法,后调用子类的方法。

刚刚是类里面的load方法,下面接着看add_category_to_loadable_list方法:

void add_category_to_loadable_list(Category cat)
{
    IMP method;

    loadMethodLock.assertLocked();

    method = _category_getLoadMethod(cat);

    // Don't bother if cat has no +load method
    if (!method) return;

    if (PrintLoading) {
        _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 
                     _category_getClassName(cat), _category_getName(cat));
    }
    
    if (loadable_categories_used == loadable_categories_allocated) {
        loadable_categories_allocated = loadable_categories_allocated*2 + 16;
        loadable_categories = (struct loadable_category *)
            realloc(loadable_categories,
                              loadable_categories_allocated *
                              sizeof(struct loadable_category));
    }

    loadable_categories[loadable_categories_used].cat = cat;
    loadable_categories[loadable_categories_used].method = method;
    loadable_categories_used++;
}

add_category_to_loadable_list方法中,会将所有定义了+load方法的category都放到loadable_categories队列中。

在所有load方法都放完之后,最后就开始进行挨个调用。
call_load_methods将定义了+load方法的classcategory分别放到loadable_classesloadable_categories 队列后,runtime就会依次读取队列中的classcategory,并为之调用+load方法:

* Locking: loadMethodLock must be held by the caller 
*   All other locks must not be held.
**********************************************************************/
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;
}
static void call_class_loads(void)
{
    int I;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    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, @selector(load));
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

从上面的call_load_methods方法可以看出:
1.super class+load方法调用时机是先于sub class
2.class+load方法调用时机是先于category

+load总结

objc4源码解读过程:objc-os.mm
_objc_init

load_images

prepare_load_methods
schedule_class_load
add_class_to_loadable_list
add_category_to_loadable_list

call_load_methods
call_class_loads
call_category_loads
(*load_method)(cls, SEL_load)

+load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用

1.首先,load方法是一定会在runtime中被调用的,只要类被添加到runtime中了,如果类实现了就一定会调用load方法
2.load方法不会覆盖。如果子类实现了load方法,那么会先调用父类的load方法,然后又去执行子类的load方法。同样的,如果分类实现了load方法,也会先执行主类的load方法,然后再去执行分类的load方法。而且执行顺序是 父类 -> 子类 ->分类。
3.共同父类的各子类之间的load方法的执行顺序,主要依据于文件的编译顺序。先编译的文件中的load方法先调用。
4.同一个类的各分类之间的load方法的执行顺序,也是依据主要依据于文件的编译顺序。先编译的文件中的load方法先调用。(这和各分类之间通过消息发送机制调用同一个方法的逻辑不通,不能混淆。因为在add_category_to_loadable_list 方法内,把category中的load方法添加到loadable_categories中是顺序插入的,所以在取出调用的时候先编译的分类的load方法排在前面。而在类对象的methods中的方法,分类的方法是头插在methods列表中的,所以先编译的先加入,后编译的后加入也就排在了methods列表的前面,在方法调用走消息发送机制的时候,就会在查找到之后被先调用。)
5.要注意在runtime初始化的时候去调用+load方法,在这个时候+load方法是根据方法地址直接调用,并不是经过objc_msgSend函数调用也就是这个时候没有遵循消息的发送机制。

load补充

一:为了安全起见,我们的load里的操作每次都保证只去做一次。比如hook方法的时候。
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"%s",__FUNCTION__);
        Method method1 = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc"));
        Method method2 = class_getInstanceMethod([self class], @selector(deallocSwizzle));
        method_exchangeImplementations(method1, method2);
    });
}
一:对于不同关系的类之间的load的执行顺序。我们来验证一下。
341604308333_.pic.jpg

FatherClass是父类
SubClass1、SubClass2FatherClass的两个子类
Sub1Cate、Sub1Cate2SubClass1的两个分类
Sub2CateSubClass2的分类
FatherCateFatherClass的分类

首先看下第一次的编译顺序:
351604308345_.pic.jpg

父类不管顺序如何,优先级都是最高的,FatherClass的父类肯定是先执行。
然后看两个平级的子类。SubClass2比SubClass1先进行编译,因此SubClass2的load先执行。最后分分类,不管是父类还是子类的分类,都只看编译顺序,和是哪个类的分类没关系,应该是哪个分类先编译,哪个分类的load方法先执行。同样如果没有继承关系的类之间,load的执行顺序也都是按照编译顺序执行的

361604308364_.pic.jpg

看打印,和我们分析的打印结果一致。

我们调整下编译的位置
371604308431_.pic.jpg

看下打印:
381604308441_.pic.jpg

还是如此。

+initialize

+ (void)initialize
{
    if (self == [<#ClassName#> class]) {
        <#statements#>
    }
}
objc4源码解读过程
objc-msg-arm64.s
objc_msgSend

objc-runtime-new.mm
class_getInstanceMethod
lookUpImpOrNil
lookUpImpOrForward
_class_initialize
callInitialize
objc_msgSend(cls, SEL_initialize)

1.+initialize方法会在类第一次接收到消息时调用,相当于系统告诉你需要用到这个类了,把这个类里的相关内容准备好,准备被用到.
2.当第一次子类接收消息的时候。
2-0.如果分类中有+initialize的实现。会在本子类的方法列表中找打最靠前的+initialize的实现,然后依据2-1、2-2的逻辑流程走。
2-1.如果父类中有+initialize的实现。先调用父类的+initialize,再去调用子类的+initialize
2-2如果父类中的+initialize之前调用过了,而且子类中自己也有+initialize的实现,则只会去把自己的+initialize实现就好了。
2-3.如果子类中没有+initialize的实现,则回去调用父类的+initialize的实现,不管父类之前有没有调用过+initialize的实现,其实这是符合objc_msgSend的消息机制的,子类的方法列表中查询不到,就去父类的列表中找(从上面的源码过程中也能知道此时的objc_msgSend(cls, SEL_initialize)中的cls是子类的类对象。)。
(只要父类和子类中+initialize的方法都有实现,就都会被调用(其实是沿着继承连去一直找寻父类的+initialize方法实现,第一次都会被初始化)。先初始化父类,再初始化子类,每个类只会初始化1次,虽然父类里的+initialize会被调用到多次,但是从判断的逻辑看只做了一次本类的初始化操作。)
3.+initialize是通过objc_msgSend进行调用的,(遵循消息发送机制)所以有以下特点:
(3.1)如果子类的方法列表里没有+initialize,会到父类的方法列表里去找+initialize(所以父类的+initialize可能会被调用多次,不代表父类被初始化了多次,这只是消息发送的机制,初始化的还是自身这个类)
(3.2)如果分类实现了+initialize,在类的方法列表methods里顺序会排在类本身的+initialize之前,所以会调用分类中的

总结一下:+initialize方法每个类在第一次接收消息的时候都会被初始化调用一次。遵循runtime的消息发送机制流程,只不过是在本类第一次初始化的时候,沿着继承链顺带着把继承链上的父类也都+initialize初始化了一次,每个类只会初始化1次,但+initialize方法不一定只调用一次。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345