浅谈Objective-C对象二(深入理解isa指针)

在上一篇文章中我们浅谈了Objective-C对象在内存中的基本布局,在文章中的末尾部分我留下了两个疑问,什么是isa?oc中的实例对象方法,类方法,以及协议,属性的名称的都分别存储在哪里?
在开始之前,首先要明白一个概念那就是对象,对象分为以下三种:

  • 实例对象(instance)
  • 类对象(class)
  • 元类对象(meta-class)

instance

实例对象就是通过某个类,调用该类的类方法alloc出来的,每次调用alloc方法都会生成一个全新的instance对象。当然使用alloc+init和使用new是一样的。

@interface Person : NSObject
{
    @public
    int _age;
    int _weight;
}
@end
@implementation Person @end
#define TLog(arg1,arg2) NSLog(@"\n{\narg1:%p\narg2:%p\n}",arg1,arg2)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *instance1 = [[Person alloc] init];
        instance1->_age = 12;
        Person *instance2 = [[Person alloc] init];
        instance2->_age = 15;
        TLog(instance1,instance2);
    }
    return 0;
}

内存布局

instance1.png

instance2.png

通过两张图片可以清晰的看到instance1和instance2的内存地址是不同的,并且各自都包含一个isa和两个成员变量的,此处、细心的同学一定发现了两个对象的isa是一毛毛样。OK,接下来就引入了类对象的概念。

class

其实、实例对象的isa指针,指向的是它的类对象,类对象在内存中的布局:

  • isa指针
  • superclass指针
  • 类的属性列表
  • 成员变量
  • 实例方法列表
  • 协议列表

证明如下:

Class class1 = instance1.class;
Class class2 = instance2.class;
TLog(class1, class2);
// 通过ruantime获取
Class rtClass1 = object_getClass(instance1);
Class rtClass2 = object_getClass(instance2);
TLog(rtClass1, rtClass2);
log:
{
  arg1:0x1000011a0
  arg2:0x1000011a0
}

通过获取到的类对象,打印其内存地址是0x1000011a0,意外的发现这和上图中isa的值(0x001D8001000011A1)并不相等啊。这其实是因为苹果在isa的值上又做了一次位运算(即0x001D8001000011A1 & ISA_MASK)。可通过查看objc4部分源码查看到。
在源码中可以看到Class类对象是这样的结构体

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    class_rw_t *data() { 
        return bits.data();
    }
  ...
 }
struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro; 
    method_array_t methods; //方法列表
    property_array_t properties;  // 属性列表
    protocol_array_t protocols; //协议列表
    ...
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; 
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name; //类名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; // 成员变量
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

objc_class结构体中,class_rw_t这个结构体包含了类对象的基本信息。
搞明白了,实例对象的isa&ISA_MASK指向了自己的类对象,那么类对象的isa指向了谁呢?

meta-class

你猜测的没错,类对象的isa&ISA_MASK就是指向了meta-class。可通过一下代码来证明:

// 可以通过这两种方式获取元类对象
Class metaClass1 = object_getClass([Person class]);
Class metaClass2 = object_getClass(rtClass2);
TLog(metaClass1, metaClass2);
{
arg1:0x100001178
arg2:0x100001178
}

// 在控制台打印如下指令
p/x 0x001D800100001179 & 0x00007ffffffffff8
// 结果是: 0x0000000100001178

元类对象和类对象都是Class类型,所以内存布局也是一样的,但是存储的内容是不一样的。
下面我们证明如此,我们从objc4中抽离出需要的代码来做证明

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
};

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
};

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

struct method_list_t : entsize_list_tt {
    method_t first;
};

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t alignment_raw;
    uint32_t size;
};

struct ivar_list_t : entsize_list_tt {
    ivar_t first;
};

struct property_t {
    const char *name;
    const char *attributes;
};

struct property_list_t : entsize_list_tt {
    property_t first;
};

struct chained_property_list {
    struct chained_property_list *next;
    uint32_t count;
    struct property_t list[0];
};

typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
    uintptr_t count;
    protocol_ref_t list[0];
};

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;  // instance对象占用的内存空间
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;  // 类名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;  // 成员变量列表
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_list_t * methods;    // 方法列表
    property_list_t *properties;    // 属性列表
    const protocol_list_t * protocols;  // 协议列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
};

#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

/* OC对象 */
struct ts_objc_object {
    void *isa;
};

/* 类对象 */
struct ts_objc_class : ts_objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
public:
    class_rw_t* data() {
        return bits.data();
    }
    
    ts_objc_class* metaClass() {
        return (ts_objc_class *)((long long)isa & ISA_MASK);
    }
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      // 实例对象
        ts_objc_object *instance = (__bridge ts_objc_object *)[[Person alloc] init];
        // 类对象
        ts_objc_class *objectClass = (__bridge ts_objc_class *)[Person class];
        class_rw_t *object_rw_data = objectClass->data();
        // 元类对象
        ts_objc_class *metaObjClass = objectClass->metaClass();
        class_rw_t *meta_rw_data = metaObjClass->data(); 
    }
    return 0;
}

通过设置断点,可以清楚的看到,object_rw_data包含的是属性列表、成员变量列表、实例方法列表、协议列表等。
而类方法是存储在元类对象中的。
目前、一个对象在内存中的布局是不是已经很明白了。
下面这张图是不是也明白了?


Snip20180319_18.png

isa的指向:实例对象的isa->类对象、类对象的isa->元类对象、元类对象的isa->基类的元类对象
如果您看明白了,那不妨尝试一下这道面试题吧。
定义一个Person的类继承自NSObject

@interface Person : NSObject
+ (void)test;
@end
@implementation Person
@end

在创建一下NSObject的分类

#import <Foundation/Foundation.h>
@interface NSObject (cc)
+ (void)test;
@end
#import "NSObject+cc.h"
@implementation NSObject (cc)
- (void)test{
    NSLog(@"[%@ cc]",self);
}
@end

猜猜下面打印的是什么

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [Person test];
        [NSObject test];
    }
    return 0;
}

答案是:
[Person test]
1.给Person这个类对象发送类方法、首先会通过Person类对象的isa指针找到Person的元类对象(存储着类方法)
2.Person元类对象并没有实现+ (void)test这个函数,然后会根据superClass指针从父类中找,即NSObject的元类对象。
3.但是NSObject的元类对象中也没有+ (void)test这个函数,所以继续根据其superClass指针,从NSObject的类对象找到了- (void)test函数,直接执行。
所以,第一个打印【Person cc】
[NSObject test]
这个相对就比较直接了
1.NSObject元类对象中没有+ (void)test这个函数,根据superClass指针从NSObject类对象中找到了- (void)test函数。
所以,第二个打印【NSObject cc】

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

推荐阅读更多精彩内容