美文网首页OC底层原理
类拓展和关联对象

类拓展和关联对象

作者: 只写Bug程序猿 | 来源:发表于2020-02-27 17:13 被阅读0次
    能否向编译好的类中增加实例变量,能否向运行时创建的类中添加实力变量
    1. 不能向编译好的类中增加实例变量
    2. 只要类没有注册到内存中还是可以添加的
      原因: 我们编译好的实例变量存储在ro中,一旦编译完成,内存结构就完全确定就无法修改,可以通过添加属性+方法实现
    类拓展 extension

    类拓展就是匿名分类,但是跟分类不同的是,拓展可以添加属性和成员变量
    比如

    //
    //  LGPerson+LGExtension.h
    //  objc-debug
    //
    //  Created by Cooci on 2019/12/2.
    //
    
    #import <AppKit/AppKit.h>
    #import "LGPerson.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface LGPerson ()
    @property (nonatomic, copy) NSString *ext_name;
    @property (nonatomic, copy) NSString *ext_subject;
    
    - (void)extH_method;
    @end
    
    NS_ASSUME_NONNULL_END
    
    类拓展的加载时机

    假设我们是一个非懒加载类那么必定会走到下边代码

    static Class realizeClassWithoutSwift(Class cls)
    {
    . ..
     ro = (const class_ro_t *)cls->data();
       
       const char *cname = ro->name;
       const char *oname = "LGPerson";
       if (cname && (strcmp(cname, oname) == 0)) {
           printf("realizeClassWithoutSwift 类名 :%s  - %p\n",cname,cls);
       }
    ...
    
    

    打上断点,我们看下ro里边有没有extension里边的方法或者属性

    (lldb) p *ro
    (const class_ro_t) $0 = {
      flags = 388
      instanceStart = 8
      instanceSize = 40
      reserved = 0
      ivarLayout = 0x0000000100001f82 "\x04"
      name = 0x0000000100001f79 "LGPerson"
      baseMethodList = 0x0000000100002180
      baseProtocols = 0x0000000000000000
      ivars = 0x0000000100002290
      weakIvarLayout = 0x0000000000000000
      baseProperties = 0x0000000100002318
      _swiftMetadataInitializer_NEVER_USE = {}
    }
    (lldb) p $0.baseMethodList
    (method_list_t *const) $1 = 0x0000000100002180
    (lldb) p *$1
    (method_list_t) $2 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 24
        count = 11
        first = {
          name = "extM_method"
          types = 0x0000000100001f84 "v16@0:8"
          imp = 0x0000000100001a90 (objc-debug`-[LGPerson extM_method] at LGPerson.m:23)
        }
      }
    }
    (lldb) p $2.get(1)
    (method_t) $3 = {
      name = "extH_method"
      types = 0x0000000100001f84 "v16@0:8"
      imp = 0x0000000100001ac0 (objc-debug`-[LGPerson extH_method] at LGPerson.m:27)
    }
    (lldb) p $2.get(2)
    (method_t) $4 = {
      name = ".cxx_destruct"
      types = 0x0000000100001f84 "v16@0:8"
      imp = 0x0000000100001cb0 (objc-debug`-[LGPerson .cxx_destruct] at LGPerson.m:17)
    }
    (lldb) p $2.get(3)
    (method_t) $5 = {
      name = "name"
      types = 0x0000000100001f8c "@16@0:8"
      imp = 0x0000000100001af0 (objc-debug`-[LGPerson name] at LGPerson.h:13)
    }
    (lldb) p $2.get(4)
    (method_t) $6 = {
      name = "setName:"
      types = 0x0000000100001f94 "v24@0:8@16"
      imp = 0x0000000100001b20 (objc-debug`-[LGPerson setName:] at LGPerson.h:13)
    }
    (lldb) p $2.get(5)
    (method_t) $7 = {
      name = "mName"
      types = 0x0000000100001f8c "@16@0:8"
      imp = 0x0000000100001b60 (objc-debug`-[LGPerson mName] at LGPerson.m:12)
    }
    (lldb) p $2.get(6)
    (method_t) $8 = {
      name = "setMName:"
      types = 0x0000000100001f94 "v24@0:8@16"
      imp = 0x0000000100001b90 (objc-debug`-[LGPerson setMName:] at LGPerson.m:12)
    }
    (lldb) p $2.get(7)
    (method_t) $9 = {
      name = "ext_name"
      types = 0x0000000100001f8c "@16@0:8"
      imp = 0x0000000100001bd0 (objc-debug`-[LGPerson ext_name] at LGPerson+LGExtension.h:14)
    }
    (lldb) p $2.get(8)
    (method_t) $10 = {
      name = "setExt_name:"
      types = 0x0000000100001f94 "v24@0:8@16"
      imp = 0x0000000100001c00 (objc-debug`-[LGPerson setExt_name:] at LGPerson+LGExtension.h:14)
    }
    (lldb) p $2.get(9)
    (method_t) $11 = {
      name = "ext_subject"
      types = 0x0000000100001f8c "@16@0:8"
      imp = 0x0000000100001c40 (objc-debug`-[LGPerson ext_subject] at LGPerson+LGExtension.h:15)
    }
    (lldb) p $2.get(10)
    (method_t) $12 = {
      name = "setExt_subject:"
      types = 0x0000000100001f94 "v24@0:8@16"
      imp = 0x0000000100001c70 (objc-debug`-[LGPerson setExt_subject:] at LGPerson+LGExtension.h:15)
    }
    (lldb) p $2.get(11)
    Assertion failed: (i < count), function get, file /Users/jasonlee/Desktop/逻辑教育资料/大师班/20200113-大师班-第12节课-load_images&类拓展&关联对象原理分析/01--课堂代码/008-extension的分析/runtime/objc-runtime-new.h, line 140.
    error: Execution was interrupted, reason: signal SIGABRT.
    The process has been returned to the state before expression evaluation.
    (lldb) 
    

    我们发现已经有了ext_name,ext_subjectsettergetter方法
    再来看一下ivars里边,属性和成员变量是否也已经有了

    (lldb) p $0.ivars
    (const ivar_list_t *const) $13 = 0x0000000100002290
    (lldb) p *$13
    (const ivar_list_t) $14 = {
      entsize_list_tt<ivar_t, ivar_list_t, 0> = {
        entsizeAndFlags = 32
        count = 4
        first = {
          offset = 0x00000001000023b8
          name = 0x0000000100001e71 "_name"
          type = 0x0000000100001f9f "@\"NSString\""
          alignment_raw = 3
          size = 8
        }
      }
    }
    (lldb) p $14.get(1)
    (ivar_t) $15 = {
      offset = 0x00000001000023c0
      name = 0x0000000100001e77 "_mName"
      type = 0x0000000100001f9f "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
    (lldb) p $14.get(2)
    (ivar_t) $16 = {
      offset = 0x00000001000023c8
      name = 0x0000000100001e7e "_ext_name"
      type = 0x0000000100001f9f "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
    (lldb) p $14.get(3)
    (ivar_t) $17 = {
      offset = 0x00000001000023d0
      name = 0x0000000100001e88 "_ext_subject"
      type = 0x0000000100001f9f "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
    (lldb)
    

    发现属性和成员变量也已经有了

    • 类拓展在编译时就已经加载,并且分类的属性成员变量等在编译时期就已经加入到ro里边,这就完美解释了,为什么拓展可以添加属性的问题
    • 分类之所以不能添加属性,是因为分类的加载是在运行时,这时候ro已经不能修改,所以不能添加属性
    关联对象

    我们说分类不能直接添加属性,如果直接添加那么底层是不能生成setter和getter方法的,所以我们要手动给他增加setter和getter方法

    //
    //  LGPerson+LG.h
    //  objc-debug
    //
    //  Created by Cooci on 2019/12/3.
    //
    
    #import <AppKit/AppKit.h>
    
    
    #import "LGPerson.h"
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface LGPerson (LG)
    @property (nonatomic, copy) NSString *cate_name;
    @end
    
    NS_ASSUME_NONNULL_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);
        
        /**
         存储
         manager -> hashMap 有所有的关联对象 - 总表 - 千千万的 -> 关联表 -> index -> 属性
         哈希
         哈希函数 -> index - key (属性) + 地址 (对象)
         */
    }
    
    
    -(NSString *)cate_name{
        /**
        参数一:id object : 获取哪个对象里面的关联的属性。
        参数二:void * == id key : 什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。
        */
        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);
    }
    void _object_set_associative_reference(id object, 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;
        
        assert(object);
        
        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));
        
        // 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);
                    }
                }
            }
        }
        // release the old value (outside of the lock).
        // 最后把之前使用传入的这个key存储的关联的value释放(OBJC_ASSOCIATION_SETTER_RETAIN策略存储的)
        if (old_association.hasValue()) ReleaseValue()(old_association);
    }
    

    流程分析:

    1. 先从AssociationsManager中找到一个AssociationsHashMap的总表
    2. 然后根据object的地址按位去反造作,拿到hashMap的key
    3. 这里分两种情况,如果有有值,则创建迭代器进行寻找,
      3.1 找到了就根据key进行赋值,
      3.2 没找到就创建新表并且存值
    4. 如果没有值,比如传进来的是个nil,那么就把关联的value移除

    再看一下objc_getAssociatedObject

    id objc_getAssociatedObject(id object, const void *key) {
        return _object_get_associative_reference(object, (void *)key);
    }
    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;
    }
    
    

    这里借用Cooci老师的一幅原理图


    原理
    关联对象流程分析

    总结

    1. 类拓展是一个匿名的分类,可以添加属性成员变量和方法,在编译期就作为类的一部分写入到类信息
    2. 关联对象是根据key和对象的地址,用两级hash表实现的,先通过manager找到总表,然后根据对象地址找到关联的表,然后根据key进行存储和取值

    相关文章

      网友评论

        本文标题:类拓展和关联对象

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