前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ObjectC对象内存布局分析

ObjectC对象内存布局分析

作者头像
MelonTeam
发布2018-01-04 15:17:40
2.8K1
发布2018-01-04 15:17:40
举报
文章被收录于专栏:MelonTeam专栏MelonTeam专栏

导语: C语言包括C++对象的内存分布都相当简单,几乎就是一个struct,但OC有Class和MetaClass的设计,本身的内存布局就不太清晰,若要回答一个问题,一个OC对象究竟占用了多少内存?还真没有好好分析过。之前看过一些文章说OC对象内存也是一个struct,然后简单的调试了下,发现并不是这样,更加激发了兴趣,于是抽时间研究了下,由于时间原因,这里的分析还不算太深入,后续再深入分析下。

分析

C++对象的内存布局很简单,比如:

代码语言:javascript
复制
class CMemObject
{
    int value;
    char* pstr;
}

在32bit的模式下,内存直接就是8字节的一个struct,拿到CMemObject的指针以后,可以直接通过相对于this的偏移地址来访问,简单直接。

之前看过一些Runtime的资料,觉得模式不会太复杂,尝试手动猜测和分析,后面才发现想法完全是错的。

网上关于OC的内存分布,大多只停留在这样的资料

代码语言:javascript
复制
struct objc_class {  
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list *methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
#endif
};

很少有对新的结构做分析的。我尝试了用clang编译成c++文件,类似这样

代码语言:javascript
复制
clang -framework Foundation -rewrite-objc MemObject.m

结果得到的是这样的结构

代码语言:javascript
复制
struct _class_t {
    struct _class_t *isa;
    struct _class_t *superclass;
    void *cache;
    void *vtable;
    struct _class_ro_t *ro;
};

里面还有几个关于结构体的定义,但一下看不出头绪。然后我尝试手动分析内存,分析内存在lldb里面主要使用 x 命令。

比如 x/16xg obj意思是,x显示内存,g表示按8字节读取,x表示按16进制显示,16表示读取16个,所以这里会读取16*8个数据,例子

代码语言:javascript
复制
(lldb) x/16xg obj
0x60800001e430: 0x000000010faafe50 0x0000000000000000
0x60800001e440: 0x0000608000283020 0x0000000000000000
0x60800001e450: 0x00006080002815e0 0x0000000000000000
0x60800001e460: 0xbadd907d96eebead 0x000060800001e470
0x60800001e470: 0xbadd907d96eebead 0x0000000000000000
0x60800001e480: 0x00007fa04b504930 0x00007fa04b709c50
0x60800001e490: 0x40383a3040343276 0x0000000000003631
0x60800001e4a0: 0x00000001108dcfb0 0x0000600000057af0

这里走了一些弯路,开始觉得既然分析OC,那么Mac和iOS应该差不多,于是尝试手动在Mac下面分析OC对象的内存结构,遇到了很大的问题,有的数据长得像地址但就是无法访问。光猜是不行的,然后就老老实实按标准流程开始分析。

实践

我实现了一个MemObject的Class

代码语言:javascript
复制
@interface MemObject : NSObject
@property(nonatomic, assign) int intValue;
- (void)function;
@end

有一个属性和一个方法。调用的代码长这样

代码语言:javascript
复制
- (void)memoryAnalyse
{
    MemObject* mem = [[MemObject alloc] init];
    [mem function];
}

调试,直接查看mem的内存

代码语言:javascript
复制
(lldb) x/8xg mem
0x60800001e430: 0x000000010faafe50 0x0000000000000000
0x60800001e440: 0x0000608000283020 0x0000000000000000
0x60800001e450: 0x00006080002815e0 0x0000000000000000
0x60800001e460: 0xbadd907d96eebead 0x000060800001e470

对下面的几个地址尝试分析了下,完全没有头绪。只能拿Objc的源码了 OC的源码地址是 https://opensource.apple.com/tarballs/objc4/ 直接下载最新的就可以。 Class的定义非常简单(抛开类方法的定义,只看属性):

代码语言:javascript
复制
//objc-private.h
struct objc_object {
private:
    isa_t isa;
}

仅有一个isa属性,继续看这个isa

代码语言:javascript
复制
union isa_t 
{
    Class cls;
    uintptr_t bits;
    # if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };

这是一个union,最后面的struct在不同的CPU下面定义还不一样,这个也是为什么我开始在Mac上看到的数据不太像地址的原因。在iOS上面,这个isa属性就直接是Class的地址。参考上面的调试结果,这个Class地址就是 0x000000010faafe50,继续。

代码语言:javascript
复制
(lldb) x/16xg 0x000000010faafe50
0x10faafe50: 0x000000010faafe28 0x000000011044be58
0x10faafe60: 0x0000608000282e40 0x0000000100000003
0x10faafe70: 0x0000608000263c42 0x000000010faafea0
0x10faafe80: 0x0000000111bedec8 0x00007fa04c010a00
0x10faafe90: 0x000000320000007f 0x0000608000262540
0x10faafea0: 0x000000011044be08 0x0000000111bedef0
0x10faafeb0: 0x00006080001184b0 0x0000000500000007
0x10faafec0: 0x0000608000262580 0x000000011044be08

直觉猜第一个和第二个应该是两个Class的地址,果然

代码语言:javascript
复制
(lldb) po (Class)0x000000010faafe28
MemObject
(lldb) po (Class)0x000000011044be58
NSObject

看源码

代码语言:javascript
复制
typedef struct objc_class *Class;
typedef struct objc_object *id;

我们平时用的id时一个objc_object, 而Class是 objc_class,继续看objc_class的定义

代码语言:javascript
复制
struct objc_object {
private:
    isa_t isa;
}

//objc-runtime-new.h
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
}

在OC源码里面,还有一个objc-runtime-old.h,这个是之前的格式,网上大部分都是这种格式。继续看cache_t

代码语言:javascript
复制
struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
}

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;

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

这里是有关方法查找的缓存的,有机会分析下,这里大致可以看出,bucket就是一个key Value的缓存数据存储的地方,而后面两个mask,我们只需要知道数据长度就OK。下面继续看class_data_bits_t,这里也是用掩码来作了一些操作,跟isa的方法类似。

代码语言:javascript
复制
struct class_data_bits_t {
    // Values are the FAST_ flags above.
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }

看下现在的情况:

代码语言:javascript
复制
(lldb) x/16xg 0x000000010faafe50
0x10faafe50: 0x000000010faafe28 0x000000011044be58
             Class              Super Class
0x10faafe60: 0x0000608000282e40 0x0000000100000003
             cache_t.bucket_t*  mask
0x10faafe70: 0x0000608000263c42 0x000000010faafea0
             class_rw_t*

继续看 class_rw_t 的定义

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

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
}

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;
};

终于接近核心了,有两个很重要的类,class_rw_t 和 class_ro_t,从字面意思可以猜,一个是Read & Write,一个是Read Only,显然,因为OC是动态语言,如果要增加属性或者方法,应该是在RW这个类上面添加。我们一步步来验证下。

代码语言:javascript
复制
(lldb) x/32xg 0x608000263c40    //class_rw_t*
0x608000263c40: 0x0000000080080000 0x000000010faaf188
                version   flags    class_ro_t*
0x608000263c50: 0x000000010faaf0f8 0x000000010faaf170
                method_array_t     property_array_t
0x608000263c60: 0x0000000000000000 0x0000000000000000
                protocol_array_t   firstSubclass
0x608000263c70: 0x0000000111c13268 0x0000000000000000
                nextSiblingClass   demangledName

(lldb) x/32xg 0x000000010faaf188 //class_ro_t*
0x10faaf188: 0x0000000800000080 0x000000000000000c
             Start    flags     reserved instanceSize 
0x10faaf198: 0x0000000000000000 0x000000010faadcd8
             ivarLayout         name
0x10faaf1a8: 0x000000010faaf0f8 0x0000000000000000
             baseMethodList     baseProtocols
0x10faaf1b8: 0x000000010faaf148 0x0000000000000000
             ivars              weakIvarLayout
0x10faaf1c8: 0x000000010faaf170 0x0000002800000081
             baseProperties

很明显,RW的 method_array_t 跟 RO 的 baseMethodList 的地址是同样的,RW的 property_array_t 跟 RO 的 baseProperties 也是一致的。我们可以验证一些东西了

代码语言:javascript
复制
(lldb) x/32cb 0x000000010faadcd8 //ClassName
0x10faadcd8: MemObject\0ViewController\0AppDele
//证明ClassName = “MemObject”

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

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

(lldb) x/32xg 0x000000010faaf0f8
0x10faaf0f8: 0x000000030000001a 0x000000010faadd30
             count              "setIntValue"
0x10faaf108: 0x000000010faae783 0x000000010faad760
             type: v20@0:8i16   [MemObject setIntValue:]
0x10faaf118: 0x000000010fdc3a92 0x000000010faae773
             “function”
0x10faaf128: 0x000000010faad710 0x000000011088efad
                                "intValue"
0x10faaf138: 0x000000010faae77b 0x000000010faad740

(lldb) b 0x000000010faad760
Breakpoint 2: where = MemObj`-[MemObject setIntValue:] at MemObject.h, address = 0x000000010faad760
//要确定一个地址是否是一个方法的入口,发现最简单的办法就是下个断点,还可以这样,通过反汇编来确认是不是一个方法入口
(lldb) x/32ig 0x000000010faae783
    0x10faae783: 76 32              jbe    0x10faae7b7               ; "2@0:8@16@24"
    0x10faae785: 30 40 30           xorb   %al, 0x30(%rax)
    0x10faae788: 3a 38              cmpb   (%rax), %bh
    0x10faae78a: 69 31 36 00 69 00  imull  $0x690036, (%rcx), %esi   ; imm = 0x690036 
    0x10faae790: 76 32              jbe    0x10faae7c4               ; "32@0:8@"UIApplication"16@"NSDictionary"24"
    0x10faae792: 34 40              xorb   $0x40, %al
    
(lldb) x/32xg 0x000000010faaf170  //property_array_t
0x10faaf170: 0x0000000100000010 0x000000010faadc45
             count = 1          name="intValue"
0x10faaf180: 0x000000010faadc4e 0x0000000800000080
             attr="Ti,N,V_intValue"
             
还剩下一个感兴趣的字段 

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;
};


(lldb) x/32xg 0x000000010faaf148    //ivars
0x10faaf148: 0x0000000100000020 0x000000010faafe18
                                8
0x10faaf158: 0x000000010faadd3d 0x000000010faae78e      
             "_intValue"        "i"
0x10faaf168: 0x0000000400000002 0x0000000100000010
             sz   alignment_raw  

我很早之前一直疑惑一个问题,就是OC的变量是怎么存的,当看到ivars的结构的时候,恍然大悟,其实跟C++也类似,猜测为这样,可以简单验证下。

MemObject

isa(8byte) _intValue(4byte)

代码语言:javascript
复制
//如果预想得没错,那么如果对intValue赋值,则会直接修改 mem + 8 的值
(lldb) x/16xg mem  
0x60800001e430: 0x000000010faafe50 0x0000000000000000
0x60800001e440: 0x0000608000283020 0x0000000000000000

(lldb) po mem.intValue = 5
5
(lldb) x/16xg mem
0x60800001e430: 0x000000010faafe50 0x0000000000000005
0x60800001e440: 0x0000608000283020 0x0000000000000000
//成功

在 class_ro_t 里面有一个字段 instanceSize ,这里的值是0xc(12),也就是一个8byte的isa指针加一个4byte的int。完全符合设想。

到这里,一个类里面的方法,属性是如何描述的,都已经清楚了,一个类自己所占用的内存,也很清楚了,其实跟C++类似,也是跟属性的定义直接相关。

现在可以画出OC对象的内存布局了。

接下来,还有1个问题:如果动态修改Class的方法或者属性,RW和RO类会如何变化? 修改了下代码

代码语言:javascript
复制
//MemObject.h
#import 

@interface MemObject : NSObject

@property(nonatomic, assign) int intValue;
@property(nonatomic, strong) NSString* strValue;

- (void)function;

@end

//MemObject.m
#import "MemObject.h"

@interface MemObject()
{
    NSString* _privateStr;
}
@end

@implementation MemObject

- (void)function
{
    NSLog(@"function @ memObject");
    self.strValue = @"property";
    _privateStr = @"privateStr";
}

- (void)privateFunction
{
     NSLog(@"function @ privateFunction");
}


- (void)memoryAnalyse
{
    Class clsMemObject = objc_getClass("MemObject");
    int objSize = class_getInstanceSize(clsMemObject);
    
    MemObject* mem1 = [[MemObject alloc] init];
    mem1.intValue = 1;
    [mem1 function];
    
    MemObject* mem2 = [[MemObject alloc] init];
    mem2.intValue = 2;
    [mem2 function];
    
    MemObject* mem3 = [[MemObject alloc] init];
    mem3.intValue = 3;
    [mem3 function];
    
    class_addMethod(clsMemObject, @selector(addValue), (IMP)addedMethod, "v@");
    [mem1 performSelector:@selector(addValue)];
    
    NSString* strAttr = @"strAttr";
    objc_setAssociatedObject(mem1, "attr", (id)strAttr, OBJC_ASSOCIATION_ASSIGN);
}

void addedMethod(id self, SEL _cmd)
{
    NSLog(@"from addedMethod");
}

改动主要为: 1. 添加了私有属性 _privateStr 2. 添加了私有方法 privateFunction 3. 新增加一个属性 strValue 4. 动态添加方法addedMethod 5. 关联一个attr属性

由于调试代码太多,这里仅列出关键的调试步骤。 可以使用class_getInstanceSize获取一个类实例的内存占用大小,这里是 4 * 8=32Byte。 可以直接看见内存布局。

代码语言:javascript
复制
(lldb) x/16xg mem1
0x600000038d80: 0x0000000101c76f50 0x0000000101c760a8
                isa                "privateStr"
0x600000038d90: 0x0000000000000001 0x0000000101c76088
                intValue           "property"

然后分析了下RW类,可以给出内存中实际的布局。

methodList: 7

setStrValue: privateFunction setIntValue: strValue function .cxx_destruct intValue propertyList: 2 — intValue strValue ivarl: 3 — _privateStr _intValue _strValue

在添加方法以后,发现RW的 method_array_t 发生了变化。而且也只有这个发生了变化。随便看了下,比较简单,一下就猜出来了。

代码语言:javascript
复制
(lldb) x/16xg 0x60800003d8c0  //新的method_array_t
0x60800003d8c0: 0x0000000000000002 0x000060800003bb40
                count=2            新添加的方法
0x60800003d8d0: 0x000000010f91a1c0 0x000202030a06060b
                旧的method_array_t指针
(lldb) x/16xg 0x000060800003bb40
0x60800003bb40: 0x000000010000001a 0x000000010f918d39
                                   “addValue”
0x60800003bb50: 0x000000010f918bee 0x000000010f918710

RW 的 method_array_t 变成了两个array,array里面是两个methodList,一个指向新添加的Method,一个指向原来的MethodList。 还有就是关联对象的问题,查看了ivarl没有变化,那么这个 AssociatedObject 又到哪里去了呢?查看了OC的源码,_object_set_associative_reference,原来,OC维护了一个全局的map,维护了对象和关联对象的对应关系,有一个AssociationsManager,还有一个AssociationsHashMap。

OC对象内存布局

到现在为止,我们已经可以比较明确的画出OC的对象内存布局了。

当添加一个Method的时候,变化为

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-07-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 分析
  • 实践
  • MemObject
  • methodList: 7
  • OC对象内存布局
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档