iOS底层原理之类的结构分析

类的结构和定义

  • 首先跟踪源码,找到Class的的定义,发现其本质为objc_class类型的指针,并且 objc_class继承自objc_object,其中objc_class中有一个隐藏的isa指针,最后在objc_object中发现了isa的定义

这里要注意的是,在new版本的源码中,objc_class继承自objc_object,在之前的旧版本中,isa指针直接定义在objc_class中,其中OC中的NSObject在编译到底层的时候都会转变成相应的结构体objc_object

// 旧的版本,在OBJC2中已经废弃
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
// 新的版本,也是现在源码编译调试使用的版本
typedef struct objc_class *Class;

// objc_class定义
struct objc_class : objc_object {
    // Class ISA; // 8
    Class superclass; // 8
    cache_t cache;    // 16 不是8         // 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();
    }
  //  下面都是方法 
}

// objc_object定义
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

2 类的成员的存储

回到objc_class,其内部定义了四个成员如下

    1. Class isa
    1. Class superclass
    1. cache_t cache
    1. class_data_bits_t bits
  • 首先我们知道isa的指针是关联对象和类,superClass指向继承类,那么类的成员能够存储的地方就只有cache和bits
  • 先看一下cache的结构体定义(不是一个结构体指针,是一个结构体),其中 mask_t为固定的4字节类型的值,而bucket_t则是一个8字节的指针,都不能存放我们定义的属性值,所以可以排除cache,这里也看出 cache的内存大小只有4+4+8=16字节(64位下)
struct cache_t {
    struct bucket_t *_buckets; // 8
    mask_t _mask;  // 4
    mask_t _occupied; // 4
}
#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif
}
  • 这里我们可以大胆猜测,bits中是否存放有我们定义的成员以及方法
2.1属性的存储探索
  • 首先为LGPerson新建一个成员变量hobby以及属性nickName,并且添加了示例方法和类方法,下面开始代码断点
@interface LGPerson : NSObject{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
+ (void)sayHappy;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        Class pClass     = object_getClass(person);
        NSLog(@"%@ - %p",person,pClass);
    }
    return 0;
}
  • x/4gx 打印pClass的内容,但是除了第一以及第二的内存,是我们熟悉的isa以及superClass指针以外,第三块地址的内容我们完全不知晓,第四块地址直接就不存在
  • 按照Class结构体的成员定义顺序,以及内存对齐原则,我们尝试用指针偏移的方法,来找到第四块地址bits的所在,并且看看bits存放的内容到底为何
// 第一步
(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b37140
0x1000023c0: 0x00000001003da280 0x0000000000000000
(lldb) 
  • 首地址为0x1000023b0,其中isa指针占用8字节,superClass占用8字节,cache占用了16字节(上面计算过),那么按照内存对齐原则,我们用首地址偏移32个字节,就应该能得出bits的内容 ,偏移后得出0x1000023d0为bits的理论地址,但是打印结果很迷茫,这里我们强转一下,再次打印,终于打印出class_data_bits_t的结构体
// 第二步
(lldb) po  0x1000023d0
objc[1017]: mutex incorrectly locked
4294976464

(lldb) p (class_data_bits_t *)0x1000023d0
(class_data_bits_t *) $2 = 0x00000001000023d0
  • class_rw_t为类中存储属性和方法的地方,看一下class_rw_t的实现,返回的是bits.data(),我们这里调用一下data方法之后得出一个class_rw_t类型的指针,直接取值,结果如下
// 第三步
(lldb) p $2->data()
(class_rw_t *) $4 = 0x0000000100fd3150
(lldb) p *$4
(class_rw_t) $5 = {
  flags = 2148139008
  version = 0
  ro = 0x0000000100002308
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100002240
        arrayAndFlag = 4294976064
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x00000001000022f0
        arrayAndFlag = 4294976240
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
  demangledName = 0x0000000000000000
}
  • 使用p命令查看结果中properties的值,此时出现了新的类型property_array_t(在源码的objc-runtime-new.h中有其定义),为一个二维数组,继续探索其内部list,进行 p $6.list,此时出现property_list_t类型,继承自entsize_list_tt,在其内部发现first方法,尝试打印,最后找到了属性nickName
(lldb) p $5.properties
(property_array_t) $6 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x00000001000022f0
      arrayAndFlag = 4294976240
    }
  }
}
(lldb) p $6.list
(property_list_t *) $7 = 0x00000001000022f0
(lldb) 
(lldb) p $7.first
(property_t) $8 = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
  Fix-it applied, fixed expression was: 
    $7->first
(lldb) 
// entsize_list_tt and property_list_t
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;
}
2.2 成员变量的存储
  • 为了直观的体现,我们先重新编译运行一下项目,断点打上,先直接跳到输出class_rw_t
(lldb) x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b37140
0x1000023c0: 0x00000001003da280 0x0000000000000000
(lldb) p (class_data_bits_t *)0x1000023d0
(class_data_bits_t *) $1 = 0x00000001000023d0
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000102139a80
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148139008
  version = 0
  ro = 0x0000000100002308
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100002240
        arrayAndFlag = 4294976064
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x00000001000022f0
        arrayAndFlag = 4294976240
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
  demangledName = 0x0000000000000000
}
  • 我们已经知道properties存放的是类的属性,结合class_rw_t里的方法名称,可以先尝试探索一下ro部分
  • 先p出ro的地址,得出一个class_ro_t类型的结构体指针,我们直接取值,拿到class_ro_t结构体的内容,从中可以找到ivars成员,根据名字可以猜测,成员变量有可能在其中
(lldb) p $3.ro
(const class_ro_t *) $4 = 0x0000000100002308
(lldb) p *$4
(const class_ro_t) $5 = {
  flags = 388
  instanceStart = 8
  instanceSize = 24
  reserved = 0
  ivarLayout = 0x0000000100001f89 "\x02"
  name = 0x0000000100001f80 "LGPerson"
  baseMethodList = 0x0000000100002240
  baseProtocols = 0x0000000000000000
  ivars = 0x00000001000022a8
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000022f0
}
(lldb) 
  • 继续寻找ivar的值,打印得到ivar_list_t类型的指针,依旧取值,输出内容,发现了我们定义的成员变量hobby
(lldb) p $5.ivars
(const ivar_list_t *const) $7 = 0x00000001000022a8
(lldb) p *$7
(const ivar_list_t) $8 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x0000000100002378
      name = 0x0000000100001e64 "hobby"
      type = 0x0000000100001fa6 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}
(lldb) 
  • 到这里,成员变量存放的位置我们也已经找到,但是我们只是定义了一个hobby属性,但是count显示个数为2,我们用get方法拿到剩余的一个值_nickName,这里也证明了属性的定义会自动生成对应的成员变量
(lldb) p $8.get(1)
(ivar_t) $9 = {
  offset = 0x0000000100002380
  name = 0x0000000100001e6a "_nickName"
  type = 0x0000000100001fa6 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) 
2.3 方法的存储
  • 依然按照上面寻找成员变量存储位置之时运行的代码,从打印出$3,也就是class_rw_t的结构开始进行方法的查找,可以发现,class_rw_t中的properties代表着属性的存储,ro代表着成员变量的存储,那么可以推断,methods则应该存放类的方法

  • 先执行 p $3.methods方法,获得一个method_array_t类型的结构体,打印出其中的list地址,并且取值得到一个entsize_list_tt,内部的第一个元素存放着我们的sayHello方法

(lldb) p $3.methods
(method_array_t) $12 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002240
      arrayAndFlag = 4294976064
    }
  }
}
(lldb) p $12.list
(method_list_t *) $13 = 0x0000000100002240
(lldb) p *$13
(method_list_t) $14 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayHello"
      types = 0x0000000100001f8b "v16@0:8"
      imp = 0x0000000100001b90 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
    }
  }
}
(lldb) 
  • 但是可以发现,其数组个数count=4,也就是除了我们定义的sayHello方法之外还有另外的三个方法,输出打印其他三个方法名称依次为C++的析构函数destruct方法,属性nickName的gettersetter方法
(lldb) p $14.get(1)
(method_t) $15 = {
  name = ".cxx_destruct"
  types = 0x0000000100001f8b "v16@0:8"
  imp = 0x0000000100001c60 (LGTest`-[LGPerson .cxx_destruct] at LGPerson.m:11)
}
(lldb) p $14.get(2)
(method_t) $16 = {
  name = "nickName"
  types = 0x0000000100001f93 "@16@0:8"
  imp = 0x0000000100001bf0 (LGTest`-[LGPerson nickName] at LGPerson.h:17)
}
(lldb) p $14.get(3)
(method_t) $17 = {
  name = "setNickName:"
  types = 0x0000000100001f9b "v24@0:8@16"
  imp = 0x0000000100001c20 (LGTest`-[LGPerson setNickName:] at LGPerson.h:17)
}
  • 至此,我们已经探索出了类的示例方法存放在类的class_rw_t结构体中的methods里面
2.4 类方法的存储
  • 通过上面的步骤,我们已经可以了解到类的实例方法的存储,但是并没有发现类方法sayHello的存储,通过class_rw_t结构体内部的名称分析,基本可以判断并没有适合存放类方法的位置

  • 那么再回到Class的基本结构成员,isa,superClass,cache,bits四个成员,其中bits.data就是我们一直在寻找的class_rw_t结构体,已经证明其内部不可能存放类方法,cache内部的8字节的bucket_t指针存放的是key和imp的键值对,和我们了解的方法的存储结构并不一样,所以我们暂时先跳过。而superClass则是其父类,LGPerson的父类为NSObject,但是NSObject内部并没有sayHello方法,所以也可以排除在外

  • 最后剩下isa指针,在之前的文章中isa指针走向
    ,我们探索过了isa指针的走向,了解到了类的isa指针,指向的是一个同名类,我们把它叫做元类,那么类方法会不会保存在元类中,我们测试一下

  • lldb控制台输入命令 x/4gx pClass之后,先通过isa指针查找到LGPerson的元类

(lldb)  x/4gx pClass
0x1000023b0: 0x001d800100002389 0x0000000100b37140
0x1000023c0: 0x00000001003da280 0x0000000000000000
(lldb) p/x 0x001d800100002389&0x00007ffffffffff8ULL
(unsigned long long) $19 = 0x0000000100002388
(lldb) po 0x0000000100002388
LGPerson

(lldb) 
  • 因为元类也是类的一种,也是继承自NSObject的一种特殊结构,所以我们也可以依旧按照对类的查找方法来进行元类的结构探索,其中元类的地址为0x0000000100002388,依次找出class_data_bits_t,通过->data()方法找到class_rw_t结构体,打印出里面的methods,获取其中的list数组,最后找到了我们定义的类方法sayHappy流程和查找类的实例方法一样,所以直接看结果
(lldb) p $26.list
(method_list_t *) $27 = 0x00000001000021d8
(lldb) p *$27
(method_list_t) $28 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "sayHappy"
      types = 0x0000000100001f8b "v16@0:8"
      imp = 0x0000000100001bc0 (LGTest`+[LGPerson sayHappy] at LGPerson.m:17)
    }
  }
}
(lldb) 

总结

  • 类之间除了基本的继承关系之外,还依靠isa指针进行对象和类的关联,也就是*对象-类-元类-根源类-根源类这一组isa关系图
  • 其中类的属性和成员变量都存放在类的class_rw_t结构体中
  • 属性的定义,还伴随着成员变量以及其getter和setter的自动生成
  • 类的类方法,则以实例方法的形式,存放在元类中,而元类又是继承自NSObject,形成一个闭环
  • 至此,类的基本结构以及其成员变量,属性和方法的存储也基本探索清楚了,如果有失误或者补足的地方,还望留言一起讨论~
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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