OC类的探索(一)—类的结构分析

一、前言

前边文章我们介绍了OC对象的alloc流程和isa的分析,接下来我们来探索一下OC中的类。

二、内存中的类

我们都知道同一个类的对象在内存中是可以创建多个的,那么类在内存中能不能创建多个呢。写了以下代码验证一下:

//MARK: - 分析类对象内存存在个数
void lgTestClassNum(){
    Class class1 = [LGPerson class];
    Class class2 = [LGPerson alloc].class;
    Class class3 = object_getClass([LGPerson alloc]);
    Class class4 = [LGPerson alloc].class;
    NSLog(@"\n%p-\n%p-\n%p-\n%p",class1,class2,class3,class4);
}
结果

由此可见,每个类在内存中有且只有一个,也就是类对象只会在工程编译时创建一次。

三、类的生成

接下来我们探索一下类是怎样生成的。

  • 我们创建如下类:
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *nickName;
@end

@implementation LGPerson

@end
  • 编译一下,然后使用clang生成cpp文件。

关于clang生成cpp的命令
// 常用方式
clang -rewrite-objc main.m -o main.cpp

// 存在UIKit等其他动态引用库时
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot/Application/Xcode.app/Comtents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m

// 模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

// 真机
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

  • 打开cpp文件,寻找LGPerson,可以看到下方内容:
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_LGPerson$_nickName;
struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_nickName;
};

// @property (nonatomic, copy) NSString *nickName;
/* @end */


// @implementation LGPerson


static NSString * _I_LGPerson_nickName(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_LGPerson_setNickName_(LGPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _nickName), (id)nickName, 0, 1); }
// @end

NSObject_IVARS的结构。生成NSObject类的时候,如下:

#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif

struct NSObject_IMPL {
    Class isa;
};

因此我们可以得出,类的本质是继承于objc_objectstruct结构体,而类的属性会在编译时生成ivarssettergetter,还生成了一个带下划线的_nickName。而NSObject_IVARS则是继承与NSObject_IMPL的一个结构体,结构体内存在一个Class类型的isa,也就是说每个类在创建的时候,都会从NSObject继承一个isa

四、类对象的结构

在源码objc-750里我们可以通过寻找到类Class的父类objc_classobjc_class是继承于objc_object的结构体(万物皆对象)。

  • objc_objectobjc_class的结构如下:
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
    // Class ISA;  //NSObject 继承的isa, 8字节
    Class superclass; // 8字节
    cache_t cache;     //结构体,32字节        // 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();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    /*下方省略部分代码*/
  • cache_t的结构,注意结构体指针和结构体的区别。结构体的大小和结构体内的内容大小有关,结构体指针就是8字节,struct声明的带*的属于结构体指针。
struct cache_t {
    struct bucket_t *_buckets; //结构体指针, 8字节
    mask_t _mask; // uint32_t类型,4字节
    mask_t _occupied; // uint32_t类型,4字节
    /**下方都是函数实现,不占用内存,不显示了*/
  • 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_ro_t结构
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;//ivar列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties; //属性列表

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

五、类对象结构的验证

接下来我们就验证一下类的属性和方法的存储的位置。

  • 首先,我们创建类LGPerson的.h文件如下,包括一些属性和方法。
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *name;

//方法在.m中都有实现
- (void)sayHello;
+ (void)sayHappy;

@end

NS_ASSUME_NONNULL_END

main函数里打断点,控制台使用lldb打印LGPerson类信息如下:

(lldb) x/4gx LGPerson.class  // 输出前四个内存值
0x1000023a8: 0x001d800100002381 0x0000000100b37140
0x1000023b8: 0x00000001003da280 0x0000000000000000
//根据objc_class的结构,bits在第四个位置
//前边三个isa、superclass、cache共占用32字节
//那么需要首地址平移32字节到bits,32转为16进制就是0x20
(lldb) p/x 0x1000023a8 + 0x20
(long) $1 = 0x00000001000023c8  //获取到bits
(lldb) p (class_data_bits_t *)$1  //强转
(class_data_bits_t *) $2 = 0x00000001000023c8
(lldb) p $2->data()
(class_rw_t *) $3 = 0x000000010194f5b0 //获取rw
(lldb) p *$3    //输出rw
(class_rw_t) $4 = {
  flags = 2148139008
  version = 0
  ro = 0x00000001000022f8
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100002230
        arrayAndFlag = 4294976048
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x00000001000022e0
        arrayAndFlag = 4294976224
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
  demangledName = 0x0000000000000000
}
(lldb) p $4.properties      //输出properties
(property_array_t) $5 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x00000001000022e0
      arrayAndFlag = 4294976224
    }
  }
}
(lldb) p $5.list   
(property_list_t *) $6 = 0x00000001000022e0
(lldb) p *$6   //输出properties的list
//在这里只获取到了属性name,count =1 ,那么hobby在哪啊
(property_list_t) $7 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
  }
}//未找到hobby
(lldb) p $4.methods //获取方法列表
(method_array_t) $8 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002230
      arrayAndFlag = 4294976048
    }
  }
}
(lldb) p $8.list
(method_list_t *) $9 = 0x0000000100002230
(lldb) p *$9
(method_list_t) $10 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
//第一个方法是sayHello
    first = {
      name = "sayHello"
      types = 0x0000000100001f8d "v16@0:8"
      imp = 0x0000000100001bc0 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
    }
  }
}
(lldb) p $10->get(1)//第2个方法是cxx_destruct
(method_t) $11 = {
  name = ".cxx_destruct"
  types = 0x0000000100001f8d "v16@0:8"
  imp = 0x0000000100001c90 (LGTest`-[LGPerson .cxx_destruct] at LGPerson.m:11)
}
  Fix-it applied, fixed expression was: 
    $10.get(1)
(lldb) p $10->get(2) //第3个方法是name的getter
(method_t) $12 = {
  name = "name"
  types = 0x0000000100001f95 "@16@0:8"
  imp = 0x0000000100001c20 (LGTest`-[LGPerson name] at LGPerson.h:16)
}
  Fix-it applied, fixed expression was: 
    $10.get(2)
(lldb) p $10->get(3) //第4个方法是name的setter
(method_t) $13 = {
  name = "setName:"
  types = 0x0000000100001f9d "v24@0:8@16"
  imp = 0x0000000100001c50 (LGTest`-[LGPerson setName:] at LGPerson.h:16)
}
  Fix-it applied, fixed expression was: 
    $10.get(3)
(lldb) //没找到sayHappy方法

从上边的lldb打印结果(上边每一步都有注释)看来,在class_rw_tproperties里并未找到hobby,在class_rw_tmethods里并未找到方法sayHappy。我们知道class_rw_tclass_ro_t也有属性和方法列表,那么我们在class_ro_t里找一下吧。结果如下:

(lldb) x/4gx LGPerson.class
0x1000023a8: 0x001d800100002381 0x0000000100b37140
0x1000023b8: 0x00000001003da280 0x0000000000000000
(lldb) p/x 0x1000023a8 + 0x20
(long) $1 = 0x00000001000023c8
(lldb) p (class_data_bits_t *)$1
(class_data_bits_t *) $2 = 0x00000001000023c8
(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000100f42b80
(lldb) p $3->ro  //获取到ro
(const class_ro_t *) $4 = 0x00000001000022f8
(lldb) p *$4
(const class_ro_t) $5 = {
  flags = 388
  instanceStart = 8
  instanceSize = 24
  reserved = 0
  ivarLayout = 0x0000000100001f8b "\x02"
  name = 0x0000000100001f82 "LGPerson"
  baseMethodList = 0x0000000100002230
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000100002298
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000022e0
}
(lldb) p $5.baseProperties
(property_list_t *const) $6 = 0x00000001000022e0
(lldb) p *$6
(property_list_t) $7 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
//属性列表里获取到name
    first = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
  }
}
(lldb) p $5.ivars
(const ivar_list_t *const) $8 = 0x0000000100002298
(lldb) p *$8
(const ivar_list_t) $9 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x0000000100002370
//ivars里获取到hobby
      name = 0x0000000100001e72 "hobby"
      type = 0x0000000100001fa8 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}
(lldb) p $9.get(1)  //ivars里获取到_name
(ivar_t) $10 = {
  offset = 0x0000000100002378
  name = 0x0000000100001e78 "_name"
  type = 0x0000000100001fa8 "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $5.baseMethodList
(method_list_t *const) $11 = 0x0000000100002230
(lldb) p *$11
(method_list_t) $12 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
//sayHello方法
      name = "sayHello"
      types = 0x0000000100001f8d "v16@0:8"
      imp = 0x0000000100001bc0 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
    }
  }
}
(lldb) p $12.get(1) 
(method_t) $13 = {
  name = ".cxx_destruct"
  types = 0x0000000100001f8d "v16@0:8"
  imp = 0x0000000100001c90 (LGTest`-[LGPerson .cxx_destruct] at LGPerson.m:11)
}
(lldb) p $12.get(2)  //name的getter方法
(method_t) $14 = {
  name = "name"
  types = 0x0000000100001f95 "@16@0:8"
  imp = 0x0000000100001c20 (LGTest`-[LGPerson name] at LGPerson.h:16)
}
(lldb) p $12.get(3)  //name的setter方法
(method_t) $15 = {
  name = "setName:"
  types = 0x0000000100001f9d "v24@0:8@16"
  imp = 0x0000000100001c50 (LGTest`-[LGPerson setName:] at LGPerson.h:16)
}
(lldb) 

由上边结果可以看出,在class_rw_tclass_ro_t里找到了hobby_namenamesayHelloname:setName:,就差sayHappy方法,其实我们也发现sayHellosayHappy是不同的,sayHappy是类方法,那么我们对比一下对象方法和类方法的调用,这个方法是不是应该存在LGPerson的元类里,接下来我们验证一下。

  • 获取LGPerson的元类。
Class metaPerson = objc_getMetaClass("LGPerson");
        NSLog(@"~~~~~%@",metaPerson);
  • lldb打印
(lldb) x/4gx metaPerson   //获取元类的前四个内存地址
0x100002370: 0x001d800100b370f1 0x0000000100b370f0
0x100002380: 0x00000001003da280 0x0000000000000000
(lldb) p/x 0x100002370 + 0x20 //内存平移到bits
(long) $1 = 0x0000000100002390
(lldb) p (class_data_bits_t *)$1
(class_data_bits_t *) $2 = 0x0000000100002390
(lldb) p $2->data()  //获取data,即rw
(class_rw_t *) $3 = 0x00000001021bafc0
(lldb) p $3->ro  //获取ro
(const class_ro_t *) $4 = 0x00000001000021e8
(lldb) p *$4
(const class_ro_t) $5 = {
  flags = 389
  instanceStart = 40
  instanceSize = 40
  reserved = 0
  ivarLayout = 0x0000000000000000
  name = 0x0000000100001f85 "LGPerson"
  baseMethodList = 0x00000001000021c8
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000000000000
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000000000000
}
(lldb) p $5.baseMethodList
(method_list_t *const) $6 = 0x00000001000021c8
(lldb) p *$6  //找到了sayHappy方法
(method_list_t) $7 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "sayHappy"
      types = 0x0000000100001f90 "v16@0:8"
      imp = 0x0000000100001bf0 (LGTest`+[LGPerson sayHappy] at LGPerson.m:17)
    }
  }
}
(lldb) 

LGPerson的元类里找到了sayHappy方法,也就验证了我们的猜想。

六、总结

1、每个类在内存中有且只有一个,占用一份内存。
2、类是在编译期生成的。类的本质是结构体objc_classobjc_class继承于objc_object,验证了万物皆对象。
3、类的属性在编译期会生成ivarssettergetter,还生成了一个带下划线的实例变量。
4、类的结构里边包括isasuperclasscachebits
4、类对象的对象方法、属性和成员变量会存在与类的class_ro_t里。类对象的类方法则会存在于类的元类class_ro_t里。

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