美文网首页
17.iOS底层学习之关联对象

17.iOS底层学习之关联对象

作者: 牛牛大王奥利给 | 来源:发表于2021-10-20 21:26 被阅读0次

    本篇文章主要研究关联对象,承接上一篇分类的加载,补充一部分关于分类的问题,主要分为以下几个部分:

    • 分类加载是否需要排序?
    • methodList的数据结构?
    • 分类和扩展
    • 关联对象

    分类加载是否需要排序

    关联分类的关键方法是attachCategories,而该方法上面的注释有说明:

    // Attach method lists and properties and protocols from categories to a class.
    // Assumes the categories in cats are all loaded and sorted by load order,
    // oldest categories first.
    static void
    attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
    int flags)
    翻译一下就是:从分类关联方法列表,协议,属性到一个类上,假如所有的类都被如期加载进来了那么分类的顺序就是加载的顺序,最旧的分类是第一个被加载的。

    我们再来调试一下方法attachCategories:

    image.png
    先来到这个方法的是分类B。
    分类A执行attachCategories
    • 第一次进入到方法attachCategories,cats_count=1,cats_list的类型是const locstamped_category_t *
      • 取出const locstamped_category_t里的具体内容发现里面有两个成员变量分别是上面👆🏻打印出来的:
        cat = 0x0000000100008090
        hi = 0x0000000108e0b260
      • 取出cat,发现cat的类型是category_t *const
      • 继续取cat里面装的内容发现如下结构:
    (lldb) p $25.cat
    (category_t *const) $26 = 0x0000000100008090
    (lldb) p *$26
    (category_t) $27 = {
      name = 0x0000000100003e5a "B"
      cls = 0x00000001000084c8
      instanceMethods = {
        ptr = 0x0000000100008038
      }
      classMethods = {
        ptr = 0x0000000100008070
      }
      protocols = nil
      instanceProperties = nil
      _classProperties = nil
    }
    

    其实就是分类的结构体内容,通过打印的内容可以看到现在进来的是分类B,接着打印里面的instanceMethods的ptr,这个ptr的类型是method_list_t,进一步打印得到了对应的两个实例方法:AAAABBBB
    打印classMethods中的ptr,拿到了一个+load方法:

    image.png
    以上的内容是把分类B关联到主类的过程!然后我们接着调试来看下A分类来到方法attachCategories的过程。
    分类A执行attachCategories

    现在的情况是A,B还有主类都是实现了load方法,所以是通过:attachCategories(cls, &lc, 1, ATTACH_EXISTING); 来到的attachCategories方法,那么此时cats_count固定是1。

    image.png
    这个调试的过程和B是一样的。
    不一样的地方是此时B分类中有的方法AAAA已经存在了,我们来看一下A分类插入AAAA方法时的过程。我继续调试,方法通过attachCategories->prepareMethodLists->fixupMethodList->sel_registerName->_ _sel_registerName最终来到方法search_builtins(name)
    static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 
    {
        SEL result = 0;
    
        if (shouldLock) selLock.assertUnlocked();
        else selLock.assertLocked();
    
        if (!name) return (SEL)0;
    
        result = search_builtins(name);
        if (result) return result;
        
        conditional_mutex_locker_t lock(selLock, shouldLock);
        auto it = namedSelectors.get().insert(name);
        if (it.second) {
            // No match. Insert.
            *it.first = (const char *)sel_alloc(name, copy);
        }
        return (SEL)*it.first;
    }
    
    static SEL search_builtins(const char *name) 
    {
    #if SUPPORT_PREOPT
      if (SEL result = (SEL)_dyld_get_objc_selector(name))
        return result;
    #endif
        return nil;
    }
    

    这里方法search_builtins里有一个预编译,我这边跑的Mac 所以直接返回了nil,接下来走的是方法auto it = namedSelectors.get().insert(name),而这个insert方法如下

    insert().png

    这里边的关键方法是try_emplace

    image.png
    所以这个方法会去通过LookupBucketFor方法传进去的key去查找相应的bucket如果找到,那么返回对应的iterator,并且pair的第二个元素设置为false,返回pair。
    根据源码的注释提示:这个要插入的方法已经存在map中了。
    否则的话会继续往下走执行InsertIntoBucket,根据注释提示:插入新的元素,插入新的key到对应的map中并且返回pair,此时的pair的第二个元素是true。
    pair的定义
    这个pair是一个struct,并且带有模板template <class _T1, class _T2>
    pair.png

    所以此时我们再回到方法__sel_registerName,再来看:

    auto it = namedSelectors.get().insert(name);
        if (it.second) {
            // No match. Insert.
            *it.first = (const char *)sel_alloc(name, copy);
        }
    return (SEL)*it.first;
    

    这一段就很清晰了,it的second就是上面pair过后的结果,为false的时候就是原来就有了,直接返回frist就行了因为前面通过pair处理过了,frist里面装的就是对应的value。
    为ture的时候,结合注释,“未匹配到,插入”。


    A分类有重名方法AAAA的时候.png

    综上,这一段流程下来是没有对分类单独进行排序的,目前这种情况的cats_count都是1,是固定的,因为都是从方法 attachCategories(cls, &lc, 1, ATTACH_EXISTING);或者attachCategories(cls->ISA(), &lc, 1, ATTACH_EXISTING | ATTACH_METACLASS);这个进来的,这个cats_count是固定传的1。

    然后我们再来看看从attachToClass-> attachCategories(cls, list.array(), list.count(), flags);这个地方进入的方法attachCategories,这个后面的流程是一样的,都是对对应方法的查询和插入,而进入到attachCategories之后的流程,也是去遍历cats_list每个分类里面的methodList,然后安卓流程attachList,就没有单独对分类排序的部分。

    而分类的顺序之前源码的注释中也提到过,和加载顺序有关。

    编译器分类的顺序.png
    我的顺序是BCA,所以A是最后被load,注释说明,最旧的会被第一个加载。

    这里我的理解是,分类最终要把方法合并到主类的相关关联ro或者是rwe中的方法列表里,每次操作都是对分类里的实质内容进行操作,比如分类里扩展了属性,方法,协议等等,核心操作是把这些相关的联系到主类里。所以分类不会再进行排序。(不管是Person的分类A,还是它的B,或者C,通过load最后关联的结构都是写入到Person类的rwe对应下面的methodlist,方法查找的时候也是从类的methodList找,也没有查找单独分类的方法列表,分类的设计是为了类服务的,为了更好的程序设计,提供了更好的动态性)(要是理解错了希望各位大佬帮忙纠正😊)。

    methodList的数据结构

    前面分析流程的时候,简单的说了下大概的类型和结构,这里总结下。

    • cats_list:locstamped_category_t
    locstamped_category_t
    struct locstamped_category_t {
        category_t *cat;
        struct header_info *hi;
    };
    
    category_t
    struct category_t {
        const char *name;
        classref_t cls;
        WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
        WrappedPtr<method_list_t, PtrauthStrip> classMethods;
        struct protocol_list_t *protocols;
        struct property_list_t *instanceProperties;
        // Fields below this point are not always present on disk.
        struct property_list_t *_classProperties;
    
        method_list_t *methodsForMeta(bool isMeta) {
            if (isMeta) return classMethods;
            else return instanceMethods;
        }
    
        property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
        
        protocol_list_t *protocolsForMeta(bool isMeta) {
            if (isMeta) return nullptr;
            else return protocols;
        }
    };
    
    method_list_t
    struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
        bool isUniqued() const;
        bool isFixedUp() const;
        void setFixedUp();
    
        uint32_t indexOfMethod(const method_t *meth) const {
            uint32_t I = 
                (uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
            ASSERT(i < count);
            return I;
        }
    
        bool isSmallList() const {
            return flags() & method_t::smallMethodListFlag;
        }
    
        bool isExpectedSize() const {
            if (isSmallList())
                return entsize() == method_t::smallSize;
            else
                return entsize() == method_t::bigSize;
        }
    
        method_list_t *duplicate() const {
            method_list_t *dup;
            if (isSmallList()) {
                dup = (method_list_t *)calloc(byteSize(method_t::bigSize, count), 1);
                dup->entsizeAndFlags = method_t::bigSize;
            } else {
                dup = (method_list_t *)calloc(this->byteSize(), 1);
                dup->entsizeAndFlags = this->entsizeAndFlags;
            }
            dup->count = this->count;
            std::copy(begin(), end(), dup->begin());
            return dup;
        }
    };
    

    所以通过cats_list去遍历locstamped_category_t,把locstamped_category_t中的category_t拿到,进而拿到category_t中的method_list_t。

    • cats_list中装的是分类的信息,分类信息里面有method_list信息。


      cats_list.jpg

    分类和扩展

    category(类别、分类)
    • 专门用来给类添加方法。
    • 不能给类添加成员属性,添加了成员变量,也无法取到。
    • 可以通过runtime给分类添加属性。
    • 分类中用@property定义变量,只会生成变量的geter & setter方法的声明,不能生成方法的实现和带下划线的成员变量。
    extension(扩展)
    • 编译时决定的
    • 可以给类添加成员属性,但是是私有变量。
    • 可以给类添加方法,也是私有方法。
    • 类扩展必须与主类在一起,最好定义在主类.m中,单独定义最终还是要合并到主类.m中。(单独写然并卵)

    通过xcrun查看extension的底层实现可以看到extension的内容与类的内容合并到了一起。通过调试去打印ro相关的方法,发现extension中的内容,早就被加到了ro中。在编译的时候就完成了。不会像分类那样在启动的时候再去添加到类的methodList了,所以extension不会影响类的加载。

    关联对象

    首先,我给分类A增加了一个叫aName的属性,然后在main函数中进行实例化主类LGPerson,然后通过实例对象L进行aName的赋值或者进行get的读取,嗯都不行,就报错了报错了报错了!

    set.png get.png

    说明通过分类添加的属性,没有set还有get方法!然后在分类A的实现中发现了这样的警告⚠️

    image.png

    Property 'aName' requires method 'setAName:' to be defined - use @dynamic or provide a method implementation in this category
    aName需要一个方法'setAName:'定义,通过动态性或者在这个分类里提供一个方法实现。于是我就尝试像往常一样实现set方法,然后又报错了。

    image.png
    分类创建的属性没有带下划线的属性名
    然后去查了下资料,这种可以用关联属性实现。
    -(void)setAName:(NSString *)aName{
         objc_setAssociatedObject(self, "aName", aName, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    -(NSString *)aName{
        return objc_getAssociatedObject(self, "aName");
    }
    

    实现完毕这两个方法就可以正常给分类的属性赋值或者读取了。我们来看下关联属性的底层实现。

    objc_setAssociatedObject

    我们跟着这个方法点进去看一下,看到objc_setAssociatedObject调用的是_object_set_associative_reference。
    _object_set_associative_reference源码解读

    //  objc_setAssociatedObject(self, "aName", aName, OBJC_ASSOCIATION_COPY_NONATOMIC);
    //object->self key->"aName" value->aName policy->OBJC_ASSOCIATION_COPY_NONATOMIC
    
    void
    _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
    {
        // This code used to work when nil was passed for object and key. Some code
        // probably relies on that to not crash. Check and handle it explicitly.
        // rdar://problem/44094390
        //判断要设置的对象的还有值都存不存在,有一个不存在就直接返回不再往下执行了。
        if (!object && !value) return;
    
        //判断是不是有元类禁止了关联对象,如果有会打印出来类名
        if (object->getIsa()->forbidsAssociatedObjects())
            _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
        
        //生成DisguisedPtr类的对象 disguised
        DisguisedPtr<objc_object> disguised{(objc_object *)object};
        //生成ObjcAssociation类的对象association
        ObjcAssociation association{policy, value};
    
        // retain the new value (if any) outside the lock.
        //根据_policy的值来确定value的处理方式
        /**acquireValue的实现
            switch (_policy & 0xFF) {
                case OBJC_ASSOCIATION_SETTER_RETAIN:
                    _value = objc_retain(_value);
                    break;
                case OBJC_ASSOCIATION_SETTER_COPY:
                    _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
                    break;
         */
        association.acquireValue();
    
        bool isFirstAssociation = false;
        {
            
            //类AssociationsManager管理锁/哈希表单例对。
            //分配实例将获得锁
            //这里在AssociationsManager创建对象的时候加了锁,在AssociationsManager的对象销毁的时候解锁
            AssociationsManager manager;
            //获取关联对象表
            AssociationsHashMap &associations(manager.get());
            if (value) {
                //然后来到try_emplace方法,这方法前面讲到了,前一个元素是从bucket中查到的结果,后面是查没查到 第二个值second为true的时候是新插入的
                auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
                if (refs_result.second) {
                    /* it's the first association we make */
                    //此时已经为这个key创建了对应bucket但是还没有存值,通过后面打印可以看到是空的
                    isFirstAssociation = true;
                }
    
                /* establish or replace the association */
                //开始创建或者替换association 再拿refs_result的first的second
                auto &refs = refs_result.first->second;
                //往对应的bucket中存值
                auto result = refs.try_emplace(key, std::move(association));
                //因为前面已经执行过了try_emplace,如果这一次执行,返回的result.second为false说明这个已经存在了
                //所以要把存在的这个刚查到的result.first的值和内层的交换一下??(这个地方没有太看懂)
                if (!result.second) {
                    association.swap(result.first->second);
                }
            } else {
                //没有值的情况 进行清空处理
                auto refs_it = associations.find(disguised);
                if (refs_it != associations.end()) {
                    auto &refs = refs_it->second;
                    auto it = refs.find(key);
                    if (it != refs.end()) {
                        association.swap(it->second);
                        //擦除
                        refs.erase(it);
                        if (refs.size() == 0) {
                            associations.erase(refs_it);
    
                        }
                    }
                }
            }
        }
    
        // Call setHasAssociatedObjects outside the lock, since this
        // will call the object's _noteAssociatedObjects method if it
        // has one, and this may trigger +initialize which might do
        // arbitrary stuff, including setting more associated objects.
        if (isFirstAssociation)
            object->setHasAssociatedObjects();
    
        // release the old value (outside of the lock).
        association.releaseHeldValue();
    }
    
    objc_getAssociatedObject

    基于上面关于set的分析,_object_get_associative_reference就好看多了👌🏻!
    _object_get_associative_reference源码解读:

    id
    _object_get_associative_reference(id object, const void *key)
    {
        ObjcAssociation association{};
        {
            AssociationsManager manager;
            AssociationsHashMap &associations(manager.get());
            AssociationsHashMap::iterator i = associations.find((objc_object *)object);
            if (i != associations.end()) {
                ObjectAssociationMap &refs = i->second;
                ObjectAssociationMap::iterator j = refs.find(key);
                if (j != refs.end()) {
                    association = j->second;
                    association.retainReturnedValue();
                }
            }
        }
        return association.autoreleaseReturnedValue();
    }
    

    通过get方法,可以了解到这是个HashMap的嵌套,第一层是AssociationsHashMap,第二层是ObjectAssociationMap。
    最后的结构大致是这样的:


    AssociationsHashMap结构.png

    今天去打羽毛球,然后回来看到大爷发的羽毛球动态:
    贵有恒,何必三更起五更眠;
    最无益,只怕一日曝十日寒。
    大爷永远是大爷🐂🍺😄

    相关文章

      网友评论

          本文标题:17.iOS底层学习之关联对象

          本文链接:https://www.haomeiwen.com/subject/udlliltx.html