类拓展和关联对象

能否向编译好的类中增加实例变量,能否向运行时创建的类中添加实力变量
  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进行存储和取值
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,106评论 6 542
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,441评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,211评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,736评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,475评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,834评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,829评论 3 446
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,009评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,559评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,306评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,516评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,038评论 5 363
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,728评论 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,132评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,443评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,249评论 3 399
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,484评论 2 379

推荐阅读更多精彩内容

  • 307、setValue:forKey和setObject:forKey的区别是什么? 答:1, setObjec...
    AlanGe阅读 1,569评论 0 1
  • 一. CALayer和UIView的区别联系 1 UIView是UIKIt的只能在iOS使用, CALayer是Q...
    yhj0129阅读 1,665评论 0 1
  • 重点掌握 3 类对象和方法 对象就是一个物体 类的独特存在就是一个实例,对实例进行操作叫做方法。方法可以应用于类或...
    Coder大雄阅读 1,273评论 0 2
  • 一、分类(类别): 背景: 在大型项目,企业级开发中多人同时维护同一个类,此时程序员A因为某项需求只想给当前类cu...
    水灵芳蕥阅读 2,992评论 1 0
  • 不知是从什么时候起,坚强独立成为了别人评价我的代名词,自己回想,打从记事起就开始分担,帮别人做本该他自己完...
    如风如水阅读 545评论 0 0