前言
相关文章:
iOS底层探索一(底层探索方法)
iOS底层探索九(方法的本质下objc_msgSend慢速及方法转发初探)
相关代码:
objc4_752源码
前几篇文章对alloc方法和isa进行了初步探究,了解了类是怎么进过创建的,这篇文章我们对类进行初步分析,类中都有什么,我们创建的成员变量,方法等,类是怎样进行存储的:
上篇文章我们描述了使用clang对main.m文件进行编译,实现了看到底层实现逻辑,这篇我们直接使用objc4_752源码方式来看类结构的底层实现:
类的结构
要查看类的结构,首先我们先声明类
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface XZPerson : NSObject
@end
NS_ASSUME_NONNULL_END
我们在main函数中添加
int main(int argc, const char * argv[]) {
@autoreleasepool {
XZPerson *p1 = [XZPerson alloc];
Class pClass = object_getClass(p1);
NSLog(@"%@ --- %@",p1,pClass);
}
return 0;
}
typedef struct objc_class *Class;
发现类Class对象在底层只是转换为objc_class结构体,继续深入查看objc_class:
struct objc_class : objc_object {
// Class ISA; //8字节
Class superclass; //父类 8字节
cache_t cache; //缓存 结构体所占大小需要看内部定义16字节16字节
// 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);
}
...
}
可以看到继承objc_object类:继续查看objc_object
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 *` */
这里我们发现了里面必定会有isa这个属性,这也就是为什么我们在objc_class 类中第一个属性为什么是isa,而且还被注释掉了,因为isa是从父类继承的,它一定会有这个属性,后面苹果开发人员还亲切的注释了Use `Class` instead of `struct objc_class ,让我们使用Class ,而不要使用 objc_class;
这就是我们声明的类再底层的大致结构,我们看到了这么多东西,但是没有看到我们在意的,我们最在意的是,我们开发人员申明的成员变量,属性,代理,方法都存在了哪里,我们慢慢进行分析
class_rw_t *data() {
return bits.data();
}
进入class_rw_t中进行查看:
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
。。。
}
我们就看到了 methods,properties,protocols等属性,不出意外的话,应该就是这几个东西了,找到了这几个东西,那我们怎么使用代码拿出,或者怎么样在内存中看到这些确实的存储呢,继续分析。
类中属性与成员变量存储位置探索
首先我们需要在XZPerson类中定义好属性,成员变量,方法等
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface XZPerson : NSObject
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
+ (void)sayHappy;
@end
NS_ASSUME_NONNULL_END
#import "XZPerson.h"
@implementation XZPerson
- (void)sayHello{
NSLog(@"XZPerson say : Hello!!!");
}
+ (void)sayHappy{
NSLog(@"XZPerson say : Happy!!!");
}
@end
这里我们需要先进行分析,在内存中我们应该怎样取到bits,这里我们就要对内存进行分析了,这里给大家找了个字节图片
首先拿出objc_class结构体
其中4个属性第一个Class isa 属性,指针(pointer)类型8字节, Class superclass 指针类型8字节,cache_t cache这个是结构体,需要进入内部看
struct cache_t {
struct bucket_t *_buckets; // 结构体指针8字节
mask_t _mask; //typedef uint32_t mask_t; 4字节
mask_t _occupied; // 4字节
public:
// 向下为函数,函数不占内存
struct bucket_t *buckets();
mask_t mask();
。。。
}
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
(这里需要注意的是struct bucket_t *_buckets; 这个是结构体指针,属于指针类型所以占8字节,mask_t _mask; 内部为int 在64位中占4字节,mask_t _occupied也是站4字节)
综合的出cache_t cache这个结构体就是站16字节,class_data_bits_t bits结构体类型进入查看
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits; //8字节
private:
bool getBit(uintptr_t bit)
{
return bits & bit;
}
...
}
typedef unsigned long uintptr_t;
可以看出里面只有一个属性,所以占8字节
综上所述这个时候我们查看类内存就不能值查看4段了,
我们需要使用x/6gx pClass 打印出6段地址 ,
根据上图操作我们获取到了bits同时操作得到其中的class_rw_t;我们主要是找到属性,和方法存在哪里,现在我们已经可以查看到class_rw_t,继续查看属性properties:
class property_array_t :
public list_array_tt<property_t, property_list_t>
{
typedef list_array_tt<property_t, property_list_t> Super;
public:
property_array_t duplicate() {
return Super::duplicate<property_array_t>();
}
};
它继承于list_array_tt,这里我们可以看到它是一个二维数组,继续查看
class list_array_tt {
struct array_t {
uint32_t count;
List* lists[0];//存储数据使用
static size_t byteSize(uint32_t count) {
return sizeof(array_t) + count*sizeof(lists[0]);
}
size_t byteSize() {
return byteSize(count);
}
};
protected:
class iterator {
List **lists;
List **listsEnd;
typename List::iterator m, mEnd;
。。。
//数组方式遍历取值
const iterator& operator ++ () {
assert(m != mEnd);
m++;
if (m == mEnd) {
assert(lists != listsEnd);
lists++;
if (lists != listsEnd) {
m = (*lists)->begin();
mEnd = (*lists)->end();
}
}
return *this;
}
}
我们可以看到,其中只有一个list,显然数据是在这里进行存储的,还有一些方法,有兴趣的可以进入进行自行查看,这里拿出一个方法为遍历取值方法;
我们接着上面的思路先取出list 查看
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; //属性列表
// This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
_objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
...
}
看到这个结构体我们又重燃希望baseProperties;属性可能在这里面,我们继续查看
成员变量存储地方ro -> ivar_list_t
属性存储地方 ro ->baseProperties ;属性会生成一个带下划线的成员变量存储到ro->ivar_list_t;
属性还会存储在class_rw_t->properties
类中实例方法存储位置探索
根据上面属性的分析我们可以看到ro中有baseMethodList,我们进行查看
struct method_t {
SEL name; //方法名
const char *types; //方法签名
MethodListIMP imp; //方法的函数实现
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
name :方法名
types:为方法签名 :@16@0:8
这个@16@0:8为方法签名
@ 返回值 类型: id 16为偏移位置
@ 参数一类型 : id self
: 参数二类型 : sel select
其实iOS中每个方法都会带2个隐式参数为(id self, SEL _cmd)
其中imp:为函数的真正实现,我们开发职工经常会使用的rutime交换方法,实际就是交换2个方法的imp实现,
select与imp之间的关系,就可以认为是一本书的目录,和章节的实际内容,我们可以根据目录找到具体章节的内容,所以我们可以根据select找到imp的具体实现,如果我们将两个方法的imp进行交换相当于就相当于菜谱的:第一页我们本身是红烧肉做法,直接换成了小葱拌豆腐做法,你用Select找到第一页,就找到小葱拌豆腐,不会找到红烧肉做法了。
回到刚才那个话题,我们在baseMethodList找到了nickname的set方法,get方法,包括sayhello方法,我们有想到了rw中有个methods进行查看
发现这里的数据和baseMethodList中数据一样,其实这里就是ro(read only,顾名思义,只读的,这里面是开发人员不能进行修改的)中数据的备份,class_rw_t (rw read write可读可写,其实我们经常在开发中添加动态添加分类,方法,属性等,其实是添加到这个里面的)。
但是我们的类方法呢,sayhappy方法没找到,既然是类方法,就应该存在类对象中嘛,我们就得先用isa找到类对象,然后进行查看
类中类方法存储探索
既然是在类中,我们首先先拿出类对象进行查看
继续查看,我们先查看rw中的method中有没有sayhappy方法
可以看出rw中的Method是存在sayHappy方法的,继续查看ro中是否存在sayHappy方法
根据上图我们可以看出,在ro中也存在sayHappy方法。
根据以上分析,我们可以得出结论,在类方法(+号方法)存储在类对象中。
总结
根据以上我们可以的出结论,在Class中存在bits属性此属性中存储类中声明的实例方法,成员变量,属性代理等其中,
属性:属性声明后会在bits有class_rw_t属性内Method属性中生产set,get方法,properties生成变量,同事在ro中ivars生产带下划线变量(_nickName),baseMethodList属性中生产set,get方法;
成员变量:只在bits中class_rw_t内ro中ivars属性中有变量进行保存
实例方法(-方法):在bits中class_rw_t内的methods,和ro中baseMethodList都有进行保存
类方法(+方法):在类中isa指针指向的类对象的bits中class_rw_t内的methods,和ro中baseMethodList都有进行保存
rw中的内容其实是运行时在DYLD过程中从ro中拷贝过来的;(这里在分析DYLD过程中再进行详细分析)
以上便是我对OC中类的初步探究,如果有错误的地方还请指正,大家一起讨论,开发水平一般(文章中有错误后,我发现会第一时间对博文进行修改),还望大家体谅,欢迎大家点赞,关注我的CSDN,我会定期做一些技术分享!
写给自己:
学会不在意,约束好自己,把该做的事做好,把该走的路走好,保持善良,做到真诚,宽容待别人,严以律自己,其他一切随意就好,未完待续。。。