iOS底层原理04 - 类的结构

上一篇: iOS底层原理03 - 对象的本质与isa
下一篇:iOS底层原理05 - 属性关键字copy&weak&strong底层分析


0. 补充: 内存平移

在看类的结构之前,先了解一下什么是内存平移。

int a[4] = {1, 2, 3, 4};
NSLog(@"&a=%p, &a[0]=%p, &a[1]=%p", &a, &a[0], &a[1]);
// 打印结果:
// &a=0x7ffeefbff460, &a[0]=0x7ffeefbff460, &a[1]=0x7ffeefbff464

NSString *b[4] = {@"1", @"2", @"3", @"4"};
NSLog(@"&b=%p, &b[0]=%p, &b[1]=%p", &b, &b[0], &b[1]);
// 打印结果:    
// &b=0x7ffeefbff440, &b[0]=0x7ffeefbff440, &b[1]=0x7ffeefbff448     

通过打印结果可以看出:

  • a、b的首地址和第一个元素的指针地址是一致的
  • a的第二个元素与第一个元素地址之前相差4个字节,即int类型所占内存;b的第二个元素与第一个元素相差8字节,即string类型所占内存。

有了这个规律,我们可以通过内存平移,即:首地址+偏移量来访问数组中的其它元素:

1. 准备工作

  • 从GitHub下载可编译的objc4-818.2源码
  • 准备一个类的代码,包含实例方法、类方法、属性、成员变量:
@interface GLPerson : NSObject
{
    NSString *age;
}
@property (nonatomic, strong) NSString *myName;

- (void)sayHi;
+ (void)jump;

@end

2. LLDB调试查看对象和类的内存情况

对象的内存

先创建一个person实例对象,并断点调试

// ISA_MASK = 0x00007ffffffffff8ULL
GLPerson *person = [GLPerson alloc];
person.name = @"Olive";
NSLog(@"person = %p", person);

通过lldb打印person对象的4段内存情况:

(lldb) x/4gx person
0x10140ede0: 0x011d80010000822d 0x0000000000000000
0x10140edf0: 0x00000001000041e0 0x0000000000000000

(lldb) po 0x011d80010000822d & 0x00007ffffffffff8ULL
GLPerson

(lldb) p/x 0x011d80010000822d & 0x00007ffffffffff8ULL
(unsigned long long) $3 = 0x0000000100008228
  • 用对象第一段地址0x011d80010000822d也就是isa和ISA_MASK进行与操作,可以得到isa中存储的类信息 $3
    这里我们可以验证下$3却是就是GLPerson的类信息:
(lldb) p/x GLPerson.class
(Class) $8 = 0x0000000100008228 GLPerson

我们继续对打印$3也就是GLPerson类的内存情况:

(lldb) x/4gx $3
0x100008228: 0x0000000100008250 0x000000010036a140
0x100008238: 0x0000000101230500 0x000880240000000f

(lldb) po 0x0000000100008250
GLPerson
  • GLPerson类的isa打印仍然是GLPerson?我们继续探究0x0000000100008250
(lldb) x/4gx 0x0000000100008250
0x100008250: 0x000000010036a0f0 0x000000010036a0f0
0x100008260: 0x0000000101231180 0x0001e03500000007
(lldb) po 0x000000010036a0f0
NSObject

(lldb) x/4gx 0x000000010036a0f0
0x10036a0f0: 0x000000010036a0f0 0x000000010036a140
0x10036a100: 0x0000000100648100 0x0003e03100000007
  • 通过一步步打印内存,发现0x0000000100008250isa指向了NSObject,而继续打印0x000000010036a0f0的内存,发现其isa仍是自己。

  • 其实在上述步骤中的0x00000001000082280x0000000100008250都打印为GLPerson,但他们并不是一个东西,前者为GLPerson类,后者为其根元类

通过上面的lldb调试分析,也就印证了一副经典的isa走位图:

isa流程图
  • 对象的isa指向类,类的isa指向元类,元类的isa指向根元类,根元类的isa指向自己。
    更通俗点,放到当前案例中就是:
isa走位案例

3. 类的结构

从上面isa走位看出,一切对象最终都会指向NSObject,那么类的结构究竟是怎样的呢?我们类中定义的属性、方法又存储在哪里?

在上一节对象的本质与isa中通过clang编译.cpp文件查看对象结构时发现,最终都来自于一个NSObject_IMPL结构体,其中的Class是以objc_class结构体为模板创建的。

struct NSObject_IMPL {
    __unsafe_unretained Class isa;
};

typedef struct objc_class *Class;

objc_class

打开objc4-818.2源码查看objc_class结构:

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable         // 16字节
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    Class getSuperclass() const {
    ...
    }
    void setSuperclass(Class newSuperclass) {
    ...
    }
    class_rw_t *data() const {
        return bits.data();
    }
    
    ......省略n行代码
}
  • objc_class其实是继承自objc_object,在早期的版本中,isa是直接定义在objc_class结构体中的,但现在已经被注释了,其实isa是来自父类objc_object中:
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

整体关系图:

结构分析

  • class_data_bits_t

我们可以通过类的首地址+偏移量访问bits,偏移量的大小为isa 8字节+superclass 8字节+cache 16字节,即首地址通过地址平移32字节。

(lldb) x/4gx $3
0x100008228: 0x0000000100008250 0x000000010036a140
0x100008238: 0x0000000101230500 0x000880240000000f

上面例子中$3的首地址0x100008228平移32字节后为0x100008248,通过类型强转得到:

// 地址平移得到class_data_bits_t
(lldb) p (class_data_bits_t *)0x100008248
(class_data_bits_t *) $4 = 0x0000000100008248
  • class_rw_t

获取到class_data_bits_t的指针地址$4后,可以通过``中提供的data()方法,直接读取bits中的data数据:

// 获取bits中的class_rw_t数据
(lldb) p $4->data()
(class_rw_t *) $5 = 0x00000001007060a0

// 输出class_rw_t
(lldb) p *$5
(class_rw_t) $6 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4295000144
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}

从上面$6指针的打印中不能明显看出些什么,我们跳转进入class_rw_t结构体,看到里面有几个核心的方法:

class_rw_t

通过上面几个方法的命名,可以猜测后几3个是用来获取方法列表属性列表协议列表的,同样通过lldb验证:

(lldb) p $6.methods()
(const method_array_t) $7 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x0000000100008098
      }
      arrayAndFlag = 4295000216
    }
  }
}

(lldb) p $7.list
(const method_list_t_authed_ptr<method_list_t>) $8 = {
  ptr = 0x0000000100008098
}

(lldb) p $9.ptr
(method_list_t *const) $10 = 0x0000000100008098

(lldb) p *$10
(method_list_t) $11 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 6)
}
// 打印method_list_t中方法个数
(lldb) p $11.count
(uint32_t) $12 = 4

(lldb) p $11.get(0).name()
(SEL) $12 = "sayHi"
(lldb) p $11.get(1).name()
(SEL) $13 = "name"
(lldb) p $11.get(2).name()
(SEL) $14 = ".cxx_destruct"
(lldb) p $11.get(3).name()
(SEL) $15 = "setName:"

method_list_t继承则entsize_list_tt,可以通过count属性获得$10中方法数量为4,以及通过get(n)方法来依次打印它们。

从上面可以看到,实例方法、属性的settergetter方法、cxx_destruct都存储在rwmethods当中。但是一开始我们创建的【类方法】+ (void)jump并没有输出。

我们用类似的方法再去打印属性列表properties:

(lldb) p $6.properties()
(const property_array_t) $16 = {
  list_array_tt<property_t, property_list_t, RawPtr> = {
     = {
      list = {
        ptr = 0x00000001000081a0
      }
      arrayAndFlag = 4295000480
    }
  }
}
(lldb) p $16.list
(const RawPtr<property_list_t>) $17 = {
  ptr = 0x00000001000081a0
}
(lldb) p $17.ptr
(property_list_t *const) $18 = 0x00000001000081a0
(lldb) p *$18
(property_list_t) $19 = {
  entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 1)
}
(lldb) p $19.count
(uint32_t) $20 = 1
(lldb) p $19.get(0)
(property_t) $21 = (name = "name", attributes = "T@\"NSString\",&,N,V_name")

打印得到rwproperties中存储了类的属性,但成员变量NSString *age;并不在里面。那成员变量会存储在哪呢?

  • class_ro_t

class_rw_t中还有一个class_ro_t没有验证,我们来看一下:

(lldb) p $3.ro()
(const class_ro_t *) $22 = 0x00000001000080a8
(lldb) p *$22
(const class_ro_t) $23 = {
  flags = 388
  instanceStart = 8
  instanceSize = 24
  reserved = 0
   = {
    ivarLayout = 0x0000000100003f70 "\U00000002"
    nonMetaclass = 0x0000000100003f70
  }
  name = {
    std::__1::atomic<const char *> = "GLPerson" {
      Value = 0x0000000100003f67 "GLPerson"
    }
  }
  baseMethodList = 0x00000001000080f0
  baseProtocols = nil
  ivars = 0x0000000100008158
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000081a0
  _swiftMetadataInitializer_NEVER_USE = {}
}

class_ro_t中有个ivars字段:

(lldb) p $23.ivars
(const ivar_list_t *const) $24 = 0x0000000100008158
(lldb) p *$24
(const ivar_list_t) $25 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 2)
}

(lldb) p $25.get(0)
(ivar_t) $26 = {
  offset = 0x00000001000081d0
  name = 0x0000000100003e90 "age"
  type = 0x0000000100003f7a "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $25.get(1)
(ivar_t) $27 = {
  offset = 0x00000001000081d8
  name = 0x0000000100003e94 "_name"
  type = 0x0000000100003f7a "@\"NSString\""
  alignment_raw = 3
  size = 8
}

ivar_list_t继承自entsize_list_tt,可以用get(n)方法来获取列表中的值,也就是成功的读取到了age和属性name生成的成员变量_name

最后在看一下刚刚没有获取到的类方法存储在哪里。

类方法的存储

类的isa指向元类,我们来获取元类的内存信息:

我们拿到了元类的首地址0x1000081e0,对此来进行地址平移读取class_data_bits_trw数据:

// 首地址 0x1000081e0 平移32位得到 0x100008200
(lldb) p (class_data_bits_t *)0x100008200
(class_data_bits_t *) $3 = 0x0000000100008200

// 读取class_rw_t
(lldb) p $3->data()
(class_rw_t *) $5 = 0x000000010073fd00
(lldb) p *$5
(class_rw_t) $6 = {
  flags = 2684878849
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4302569489
    }
  }
  firstSubclass = nil
  nextSiblingClass = 0x00007fff80170eb0
}
(lldb) p $6.methods()
(const method_array_t) $7 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x0000000100008088
      }
      arrayAndFlag = 4295000200
    }
  }
}
(lldb) p $7.list
(const method_list_t_authed_ptr<method_list_t>) $8 = {
  ptr = 0x0000000100008088
}
(lldb) p $8.ptr
(method_list_t *const) $9 = 0x0000000100008088
(lldb) p *$9
(method_list_t) $10 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $10.get(0).name()
(SEL) $12 = "jump"

最终在rwmethod中读取到了GLPerson中的类方法+ (void)jump;

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

推荐阅读更多精彩内容