扩展、load、initialize分析

扩展

扩展其实就是匿名的分类,我们知道了分类的加载过程,那么扩展的是在什么时候加载的呢?下面我们来探究下。
我们知道_read_images方法是在dyld加载库完成后调用的,我们可以在这里拦截,然后看看此时ro中是否已经存在扩展的方法。
我们先添加扩展方法和属性:

@interface LGPerson ()
@property (nonatomic, copy) NSString *mName;

- (void)extM_method;
@end

@implementation LGPerson

- (void)extM_method{
    NSLog(@"%s",__func__);
}

@end

然后我们在_read_images中打断点,通过llvm来看下ro中是否有我们通过扩展添加的方法和属性。

(lldb) p ro
(const class_ro_t *) $0 = 0x00000001000012b8
(lldb) p *$0
(const class_ro_t) $1 = {
  flags = 388
  instanceStart = 8
  instanceSize = 32
  reserved = 0
  ivarLayout = 0x0000000100000f8b "\x03"
  name = 0x0000000100000f82 "LGPerson"
  baseMethodList = 0x0000000100001150
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000100001218
  weakIvarLayout = 0x0000000000000000 <no value available>
  baseProperties = 0x0000000100001280
  _swiftMetadataInitializer_NEVER_USE = {}
}
(lldb) p $1.baseMethodList
(method_list_t *const) $2 = 0x0000000100001150
(lldb) p *$2
(method_list_t) $3 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 8
    first = {
      name = "extM_method"
      types = 0x0000000100000f8d "v16@0:8"
      imp = 0x0000000100000ba0 (objc-debug`-[LGPerson extM_method] at LGPerson.m:19)
    }
  }
}
(lldb) p $3.get(0)
(method_t) $4 = {
  name = "extM_method"
  types = 0x0000000100000f8d "v16@0:8"
  imp = 0x0000000100000ba0 (objc-debug`-[LGPerson extM_method] at LGPerson.m:19)
}
(lldb) p $3.get(2)
(method_t) $6 = {
  name = "mName"
  types = 0x0000000100000f95 "@16@0:8"
  imp = 0x0000000100000be0 (objc-debug`-[LGPerson mName] at LGPerson.m:12)
}
(lldb) p $3.get(3)
(method_t) $8 = {
  name = "setMName:"
  types = 0x0000000100000f9d "v24@0:8@16"
  imp = 0x0000000100000c10 (objc-debug`-[LGPerson setMName:] at LGPerson.m:12)
}

然后发现我们添加的方法以及属性的setter和getter方法都已经存在ro中的baseMethodList中,所以可以判定,扩展是在编译期添加到ro中的。
添加扩展的方式除了在LGPerson.m文件中直接添加匿名的分类外,我们还可以创建一个LGPerson+LGExtension.h文件,然后在该文件中添加扩展:

@interface LGPerson ()
@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, copy) NSString *ext_subject;

- (void)extH_method;
@end

添加完成后我们再次在_read_images方法中打印ro,发现我们这次添加的方法和属性并没有在ro中,为什么呢?
我们只需要在LGPerson中添加下面代码,再次运行,打印ro,此时ro中就存在添加的扩展方法和属性了。

#import "LGPerson+LGExtension.h"

因为我们通过扩展添加方法和属性,是针对于LGPerson的,如果没有在LGPerson引入该扩展,编译器就会认为该扩展无效,从而不会编译进去。

问题:我们都知道扩展可以增加属性,但是分类不能添加属性,为什么呢?
通过上面的分析,我们应该可以知道答案了,因为扩展是在编译期间加入到ro中的,而成员变量就是存在ro中,所以扩展可以添加属性。但是分类是在编译后attach到rw中的。rw中不存在ivar成员变量,所以分类就无法添加属性。
当然我们也在运行时通过关联对象实现分类添加属性的目的。

@interface LGPerson (LG)
@property (nonatomic, copy) NSString *cate_name;
@end
@implementation LGPerson (LG)
-(void)setCate_name:(NSString *)cate_name{
    /**
    参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self。
    参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
    参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
    参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。
    */
    objc_setAssociatedObject(self, @"name",cate_name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(NSString *)cate_name{
    return objc_getAssociatedObject(self, @"name");
}
@end

这样我们就可以通过关联对象来实现为分类添加属性的目的。我们来看下底层是怎么处理关联对象的。
首先看下objc_setAssociatedObject的实现

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}

底层接口做了一层适配,我们继续跟踪_object_set_associative_reference方法,

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    //......省略一些代码
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        // 关联对象的管理类
        AssociationsManager manager;
        // 获取关联的 HashMap -> 存储当前关联对象
        AssociationsHashMap &associations(manager.associations());
        // 对当前的对象的地址做按位去反操作 - 就是 HashMap 的key (哈希函数)
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            // 获取 AssociationsHashMap 的迭代器 - (对象的) 进行遍历
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                // 根据key去获取关联属性的迭代器
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    // 替换设置新值
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    // 到最后了 - 直接设置新值
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                // 如果AssociationsHashMap从没有对象的关联信息表,
                // 那么就创建一个map并通过传入的key把value存进去
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            // 如果传入的value是nil,并且之前使用相同的key存储过关联对象,
            // 那么就把这个关联的value移除(这也是为什么传入nil对象能够把对象的关联value移除)
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    // 最后把之前使用传入的这个key存储的关联的value释放(OBJC_ASSOCIATION_SETTER_RETAIN策略存储的)
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

其中的manager对象是管理者

AssociationsManager manager;

AssociationsHashMap``是哈希总表

AssociationsHashMap &associations(manager.associations());

因为我们可能好多对象都有关联属性,所以还需要根据对象的地址找到属于该对象的一个哈希表。

disguised_ptr_t disguised_object = DISGUISE(object);

然后判断if (new_value),如果此时我们关联属性的value值传nil的话,就会走else中的代码。

AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i !=  associations.end()) {
    ObjectAssociationMap *refs = i->second;
    ObjectAssociationMap::iterator j = refs->find(key);
    if (j != refs->end()) {
        old_association = j->second;
        refs->erase(j);
    }
}

这里根据我们上面获取到的对象地址,映射到哈希表中的index,然后获取到属于该对象的哈希表。判断if (i != associations.end())如果没有查找到结尾,就说明成功找到了属于该对象的哈希表。然后通过key值作为index在该对象的哈希表中查找对象的关联属性,然后将该属性删除。所以如果我们之前这个关联属性是有值的,然后我们重新设置为了nil,就会将该关联属性删除。
如果new_value存在的话,就会走if判断中的流程。然后根据对象查找属于该对象的哈希表,如果找到属于该对象的哈希表的话,就在对象的哈希表中查找对应关联属性进行赋值。如果没有找到属于该对象的哈希表,就会创建一张表,然后赋值。
我们来总结下,关联属性的赋值过程:manager管理管理属性的总表,然后我们可以根据对象的地址找到属于该对象的一张哈希表,然后根据关联属性的key找到对应的关联属性,然后就可以赋值处理了。

objc_getAssociatedObject的实现和set方法差不多:从总表中找对象表,然后对象表中找关联属性,然后返回。

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        // 关联对象的管理类
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        // 生成伪装地址。处理参数 object 地址
        disguised_ptr_t disguised_object = DISGUISE(object);
        // 所有对象的额迭代器
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            // 内部对象的迭代器
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                // 找到 - 把值和策略读取出来
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                // OBJC_ASSOCIATION_GETTER_RETAIN - 就会持有一下
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}

我们通过关联对象达到了添加属性的目的,那么如果我们的属性释放了,相应的关联对象是否会释放掉呢?我们只要追踪一下dealloc方法就知道了,通过追踪dealloc方法,我们追踪到下面的方法

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

在上面的方法中判断如果存在关联属性,就会调用_object_remove_assocations,该方法就是移除关联属性的方法,方法详情见下面

void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (associations.size() == 0) return;
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // copy all of the associations that need to be removed.
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            associations.erase(i);
        }
    }
    // the calls to releaseValue() happen outside of the lock.
    for_each(elements.begin(), elements.end(), ReleaseValue());
}

所以结论是:当对象释放的时候,在dealloc方法中会移除关联属性。

+ (void)load方法

我们在日常的开发中,经常用到load方法,那么load方法是在什么时候调用的呢?下面我们来探究下
我们回到_objc_init方法,里面调用了下面的方法

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

我们现在知道了,map_images只要是用来读取各种class、category、protocal等信息,然后存储起来。那么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
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

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

load_images中有两个主要的功能:call_load_methods用来调用load方法,prepare_load_methods为调用load方法做一些准备工作。
首先我们来看看prepare_load_methods方法。

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方法中首先通过_getObjc2NonlazyClassList获取到非懒加载类的列表,因为实现了load的方法的class都是非懒加载类。然后调用了schedule_class_load。在这个方法中又调用了add_class_to_loadable_list(cls);,继续追踪该方法。

method = cls->getLoadMethod();
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;

add_class_to_loadable_list中调用了上面的代码,将cls和method进行存储起来。cls就是我们前面获取到的非懒加载类,method就是对应class的load方法。到此为止,就是获取非懒加载class列表,然后将class和load方法存到loadable_class中。
我们返回到prepare_load_methods,下面还有对category的处理。

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
    realizeClassWithoutSwift(cls);
    assert(cls->ISA()->isRealized());
    add_category_to_loadable_list(cat);
}

和上面类的处理相似,获取到非懒加载的category列表,然后遍历category调用add_category_to_loadable_list进行处理。

method = _category_getLoadMethod(cat);
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;

和class的处理方式一样,获取到cat和cat对应的load方法,然后进行存储到loadable_categories中。
到此为止,准备调用load方法的准备工作已经完毕,分别将class和cate的load方法存储到了不同的地方。
下面我们回到load_images方法查看call_load_methods();是怎么调用load方法的。

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

在该方法中通过一个do while 循环调用call_class_loadscall_category_loads,调用class的load方法以及category的load方法。
我们先来看call_class_loads方法。

struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
// 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, SEL_load);
}

循环处理class,load_method是个调用load方法的函数,接收id和SEL参数。通过SEL就能找到对应的imp进行load的调用。

typedef void(*load_method_t)(id, SEL);

将cls和SEL_load传到上面的函数中,完成了cls的load方法调用。
接下来我们返回到call_load_methods方法中查看call_category_loads();的调用。

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
    Category cat = cats[i].cat;
    load_method_t load_method = (load_method_t)cats[i].method;
    Class cls;
    if (!cat) continue;

    cls = _category_getClass(cat);
    if (cls  &&  cls->isLoadable()) {
        if (PrintLoading) {
            _objc_inform("LOAD: +[%s(%s) load]\n", 
                         cls->nameForLogging(), 
                         _category_getName(cat));
        }
        (*load_method)(cls, SEL_load);
        cats[i].cat = nil;
    }
}

和class的处理方式相同,通过load_method调用cat的load方法。
到此也完成了对分类的load方法调用。

问题:class和category都实现了load方法,会怎么调用呢?
从上面的分析中我们可以知道,在处理load方法的时候,先调用了class的load方法,然后再调用category的load方法。
那么普通的方法呢,class和分类都实现会怎么样呢?
在map_images分析中,category是通过attach到rw中methodList。attach的过程是将新的list添加到栈的头部,所以调用方法的时候从头部开始查找,自然会先找到category中的方法然后进行调用。这就会造成category的中的方法会覆盖class中方法的假像。

initialize方法

我们都知道initialize也是调用比较早的方法,那么它是在什么时候调用的呢?
我们可以通过实验发现,只要class调用方法,不管是哪个方法,都会触发initialize的调用。所以我们可以推测应该是在方法的调用流程中处理了initialize。
我们来的我们熟悉的lookUpImpOrForward方法。发现下面与initialize相关的代码

if (initialize && !cls->isInitialized()) {
    cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    // runtimeLock may have been dropped but is now locked again

    // If sel == initialize, class_initialize will send +initialize and 
    // then the messenger will send +initialize again after this 
    // procedure finishes. Of course, if this is not being called 
    // from the messenger then it won't happen. 2778172
}

跟踪来到initializeAndMaybeRelock方法,在该方法中调用了下面代码

initializeNonMetaClass(nonmeta);

然后initializeNonMetaClass方法中调用了下面的call方法

callInitialize(cls);

callInitialize方法的实现如下

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

给cls发送SEL_initialize消息,走普通的消息发送流程,从而完成了initialize的调用。
总结:initialize方法是在cls第一次接受消息的时候调用的,因为最后调用的时候走的是消息发送流程,所以查找方法进行调用的时候有个优先顺序。如果category实现了initialize,则不会再调用cls中的,子类如果实现了就只会调用子类的,不会调用父类了。

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

推荐阅读更多精彩内容