前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >类的加载(三)

类的加载(三)

作者头像
拉维
发布2021-03-10 14:30:12
4460
发布2021-03-10 14:30:12
举报
文章被收录于专栏:iOS小生活

今天来聊聊类的扩展。

首先来看看扩展和分类的区别

格式上,扩展是匿名的分类

我们在OC的.m文件中,经常会使用扩展对某类进行私有的属性或者成员变量的声明,如下:

然后我们再来看一下类目的写法:

比较一下扩展和类目的写法,我们会发现它们两个的不同点就在于:类后面的小括号里面是否有内容,这个内容就是类目的名字。

因此,在形式上,我们可以说,扩展是匿名的分类

那么,类扩展的数据,是如何加载进内存的呢?答案是,类扩展中的内容会在编译时作为类的一部分进行编译,因此读取的时候可以直接在ro中获取到

需要注意的是,我们可以在类的.m文件中创建一个扩展,用于声明私有的数据和变量;也可以创建一个专门的扩展文件,这样的话,一个类如果需要相应的扩展,那就引入相应的扩展文件即可(一定要注意,是需要引入的哦~),如果不引入的话,就不会在编译的时候加载进ro的哦~

扩展中可以正常添加属性,分类中添加的属性不会自动生成setter和getter

我之前在关于类目的几点探讨中详细比较过类目和扩展,也介绍过为什么类目中添加的属性不能自动生成setter和getter。今天,我会在一个更底层的维度去解释这个原因。

前面提到了,扩展中的内容和原类中的内容一样,他们都是在编译期就会被直接编译进内存,因此是可以直接在ro中获取到的。而分类在运行的时候才会被加载进rw。

而通过比较ro和rw的数据结构我们发现,存储成员变量的数组ivars只在ro中有,rw中是没有ivars的。因此,分类中是添加不了成员变量的,而属性自动生成的setter和getter是需要生成一个带有下划线的成员变量的,所以分类中声明的属性不会自动生成setter和getter。

类目中关联对象的原理

Runtime——使用类目给某个类添加属性中,我们可以了解到如何在类目中给一个类添加属性,现在我们就来探究一下其底层原理。

我们在自定义的setter中会通过objc_setAssociatedObject函数来设置值:

代码语言:javascript
复制
objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)

它需要传4个参数:

  1. 第一个参数是关联的对象,也就是说,要给哪个对象添加属性
  2. 第二个参数是属性的Key,它会作为该属性的唯一标识,objc_getAssociatedObject函数会通过该Key获得属性的值并返回
  3. 第三个参数是需要保存的属性值
  4. 第四个参数是限定词

我们点进去看其源码:

代码语言:javascript
复制
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}

我们看到,objc_setAssociatedObject内部又调用了_object_set_associative_reference。这里为什么需要再封装一层呢?objc_setAssociatedObject供外界调用,而_object_set_associative_reference是内部实现,万一将来某一天进行了代码调整,_object_set_associative_reference这个函数的函数名变了,那么我在外界调用的objc_setAssociatedObject不会受到丝毫影响,只需要改objc_setAssociatedObject里面调用的_object_set_associative_reference即可。

接下来我们看_object_set_associative_reference的源码:

代码语言:javascript
复制
    // retain the new value (if any) outside the lock.
    // 在锁之外保留新值(如果有)。
    ObjcAssociation old_association(0, nil);
    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);
                }
            }
        }
    }

给一个对象设置关联值的步骤总结如下:

  1. 获取到存储所有关联对象的总表associations(这个总表里面又包含每一个对象的小表),以及当前的对象disguised_object
  2. 判断传入的值new_value是不是nil。如果new_value是nil,那么就看之前在当前对象这个表中是否有关联过这个value,如果关联过,那么就将关联的这个value移除。这也是为什么传入nil就能够将关联的value移除
  3. 如果传入的new_value不是nil,那么就在总表中查找当前对象的小表,看能不能找到。如果没有找到当前对象的小表,那么就说明之前没有存储过该对象的关联信息,此时就为该对象新建一张关联信息表,建好之后就通过传入的Key将new_value存储进去。
  4. 如果在总表中找到了当前对象的小表,那么我就看这张小表中之前有没有关联过标记为Key的value,如果关联过,那么就将之前关联的value替换为new_value;如果没有关联过,那么就直接将new_value通过Key存储进小表。

上面了解了如何给一个对象设置关联值,那么获取对象的关联值是如何进行的呢?

我们在自定义的getter方法中会通过objc_getAssociatedObject函数来获取值:

代码语言:javascript
复制
objc_getAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>)

它有两个参数:

  1. 第一个参数是关联的对象,也就是说,需要在哪个对象中去获取关联值
  2. 第二个参数是属性的Key,它是该属性的唯一标识,objc_getAssociatedObject函数会通过该Key获得属性的值并返回,与objc_setAssociatedObject中的Key相对应。

objc_getAssociatedObject函数的实现如下:

代码语言:javascript
复制
id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}

_object_get_associative_reference的源码如下:

代码语言:javascript
复制
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;
}

据此,我们可以分析出获取一个对象的关联值的步骤:

  1. 获取到存储所有关联对象的总表associations(这个总表里面又包含每一个对象的小表),以及当前的对象disguised_object
  2. 在总表中获取当前对象所在的小表,如果没有获取到,那么就返回nil
  3. 如果在总表中找到了小表,接下来就在小表中通过Key查找关联值,如果没有找到该key所对应的关联值,那么就返回nil;如果找到了,那么就返回找到的这个关联值。

到这里,你可能会有一个疑问,当对象释放的时候,其关联的属性会同步释放吗?答案是会的,下面来分析。

任何对象的释放都会调用到dealloc方法:

代码语言:javascript
复制
- (void)dealloc {
    _objc_rootDealloc(self);
}

_objc_rootDealloc:

代码语言:javascript
复制
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

rootDealloc:

代码语言:javascript
复制
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

object_dispose:

代码语言:javascript
复制
id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

objc_destructInstance:

代码语言:javascript
复制
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;
}

在上面?的第10行,就是释放关联对象的操作。

load_images

_dyld_objc_notify_register这个函数里面会调用load_images函数,我们看其实现:

代码语言:javascript
复制
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();
}

这里面主要是调用了prepare_load_methods和call_load_methods两个函数。

prepare_load_methods

代码语言:javascript
复制
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);
    }
}

上面的第7行,首先是获取到所有非懒加载的类列表classlist。也许你会有疑问,为什么这里必须是非懒加载类呢?我们知道,实现了load方法就是非懒加载类,而我们现在研究的是load方法的加载时机,所以研究的这个类势必是非懒加载类。

获取到所有非懒加载的类列表classlist之后,遍历它,然后在每一次遍历体内都执行schedule_class_load函数。

接下来我们看一下schedule_class_load函数的实现:

代码语言:javascript
复制
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函数:

代码语言:javascript
复制
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++;
}

重点关注一下第23~25行,loadable_classes是一个数组,其元素类型是loadable_class:

代码语言:javascript
复制
struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

23~25行的作用就是将cls的信息加入到loadable_classes数组中

接下来我们回到prepare_load_methods函数的源码,看第13~25行:

首先,会通过_getObjc2NonlazyCategoryList函数获取到非懒加载类目列表categorylist,然后遍历这个列表,在每一次遍历中,通过add_category_to_loadable_list函数,将类目cat的信息加入到loadable_categories数组中,loadable_categories数组的元素类型是loadable_category:

代码语言:javascript
复制
struct loadable_category {
    Category cat;  // may be nil
    IMP method;
};

call_load_methods

前面通过prepare_load_methods函数已经将非懒加载类和非懒加载分类的信息分别加进loadable_classesloadable_categories数组中了,接下来我们就是调用它们。

首先看一下call_load_methods函数的源码:

代码语言:javascript
复制
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;
}

上面源码中的第14~24行,是一个do-while循环遍历,遍历执行原类以及分类中的+load方法

总结

load_images中调用load方法的步骤如下:

  1. 通过add_class_to_loadable_list函数将非懒加载类信息加入到loadable_classes中
  2. 通过add_category_to_loadable_list函数将非懒加载分类信息加入到loadable_categories中
  3. 通过call_class_loads函数调用主类中的load方法
  4. 通过call_category_loads函数调用分类中的load方法

一定要注意哦,不管是主类还是分类中,每一个+load方法都会被调用哦~

这里也引申出来一个面试题:主类和分类中有相同的方法,如何调用?

  1. 对于load方法而言,先调主类中的,再调分类中的
  2. 对于其他的一般方法,会通过attachlist将方法按照编译顺序依次加入到rw中,然后通过消息查找机制找的时候,会找到最后attach进来的那个方法。因此给人的假象就是,“主类的方法被分类给覆盖了”,实际上并没有被覆盖,假象而已。

initialize方法分析

截止到上面所有的load方法调用完毕,整个函数也就执行完毕了。那么initialize方法是在什么时候调用的呢?

关于+load和+initialize的比较,我之前也写过两篇文章,大家可以了解一下:

initialize和load的调用时机

一个Bug所引发的方法交换小讨论

我们应该都知道如下结论:一个类的+initialize方法会在第一次初始化这个类之前被调用,并且只被调用一次。这说明,+initialize方法很有可能是在消息查找期间被调用的,所以,我们就去lookUpImpOrForward函数中找找,看看能不能发现一些蛛丝马迹:

你别说,还真找到了,?上图红框内。

我们看一下initializeAndLeaveLocked函数的源码:

代码语言:javascript
复制
// Locking: caller must hold runtimeLock; this may drop and re-acquire it
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
    return initializeAndMaybeRelock(cls, obj, lock, true);
}

它调用了initializeAndMaybeRelock函数:

代码语言:javascript
复制
static Class initializeAndMaybeRelock(Class cls, id inst,
                                      mutex_t& lock, bool leaveLocked)
{
    lock.assertLocked();
    assert(cls->isRealized());

    if (cls->isInitialized()) {
        if (!leaveLocked) lock.unlock();
        return cls;
    }

    // Find the non-meta class for cls, if it is not already one.
    // The +initialize message is sent to the non-meta class object.
    Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);

    // Realize the non-meta class if necessary.
    if (nonmeta->isRealized()) {
        // nonmeta is cls, which was already realized
        // OR nonmeta is distinct, but is already realized
        // - nothing else to do
        lock.unlock();
    } else {
        nonmeta = realizeClassMaybeSwiftAndUnlock(nonmeta, lock);
        // runtimeLock is now unlocked
        // fixme Swift can't relocate the class today,
        // but someday it will:
        cls = object_getClass(nonmeta);
    }

    // runtimeLock is now unlocked, for +initialize dispatch
    assert(nonmeta->isRealized());
    initializeNonMetaClass(nonmeta);

    if (leaveLocked) runtimeLock.lock();
    return cls;
}

initializeAndMaybeRelock函数里面又调用了callInitialize函数:

接着看callInitialize函数的源码:

代码语言:javascript
复制
void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

这里就是给cls发送SEL_initialize消息,也就是调用+initialize方法

要注意哦,+initialize方法和一般的方法的调用是一样的哦~都是调用的是最后attach进rw中的那一个方法的实现哦~

以上。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-02-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档