iOS 类与对象底层原理
iOS 类与对象底层原理
文章目录
- iOS 类与对象底层原理
- 探索对象本质
- objc_setProperty 源码
- cls与类的关联原理
- 联合体
- isa的类型isa_t
- 原理探索
- initIsa方法
- 通过setClass方法中的shiftcls来验证绑定的一个流程
- 通过 isa & ISA_MSAK
- 通过object_getClass
- 通过位运算
- 类&类的结构
- 从终端调试开始认识调用逻辑
- 什么是元类
- 小结
- NSObject到底有几个?
- 类存在几份
- isa走位继承图
- object_class & objc_object
- 这里的objc_class 与objc_object有什么关系?
- objc_object与对象的关系
- 总结
- 类结构分析
- 类信息中有哪些内容
- 计算cache类的内存大小
- bits
- 探索property_list(属性列表)
- 探索methods_list
- 从编译期开始介绍class_ro_t,class_rw_t,class_rw_ext_t
- class_ro_t
- class_rw_t
- 区别
- class_rw_ext_t
- 总结
探索对象本质
- 在main中自定义一个类GCObject,有一个属性name
@interface GGObject : NSObject
@property (nonatomic, copy) NSString* name;
@end@implementation GGObject@end
通过终端将main.m编译成main.cpp:
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp//2、将 ViewController.m 编译成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp //4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
在编译之后,我们可以开始看这里的类的一个定义:
//NSObject的定义
@interface NSObject <NSObject> {Class isa OBJC_ISA_AVAILABILITY;
}//NSObject 的底层编译
struct NSObject_IMPL {Class isa;
};struct GGObject_IMPL {struct NSObject_IMPL NSObject_IVARS; // 这里NSString *_name;
};static NSString * _I_GGObject_name(GGObject * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_GGObject$_name)); } //get方法
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);static void _I_GGObject_setName_(GGObject * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct GGObject, _name), (id)name, 0, 1); } //set方法
这里我们可以看到NSObject的定义会产生一个问题,isa的类型居然是Class,这里我们定义alloc放的核心之一的initInstance方法,通过拆看这个方法的源码实现,我们发现在初始化isa指针的时候,是通过isa_t类型初始化的。
这里为了让开发人员更加清晰明确,需要在isa返回的时候做了一个类型强制转换.
这里其实我们可以看出:
- OC对象的本质就是结构体
- CGObject中的isa就是继承自NSObject中的isa
objc_setProperty 源码
在这之前我们可以看到这里的上面的一个set方法:
static void _I_GGObject_setName_(GGObject * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct GGObject, _name), (id)name, 0, 1); } //set方法
这里面可以看到他里面调用了一个objc_setProperty
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);bool mutableCopy = (shouldCopy == MUTABLE_COPY);reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy); //跳转到真正的一个调用方法
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{if (offset == 0) {object_setClass(self, newValue);return;}id oldValue;id *slot = (id*) ((char*)self + offset);if (copy) {newValue = [newValue copyWithZone:nil];} else if (mutableCopy) {newValue = [newValue mutableCopyWithZone:nil];} else {if (*slot == newValue) return;newValue = objc_retain(newValue); // 新增retain}if (!atomic) {oldValue = *slot;*slot = newValue;} else {spinlock_t& slotlock = PropertyLocks[slot];slotlock.lock();oldValue = *slot;*slot = newValue; slotlock.unlock();}objc_release(oldValue); //release 旧值
}
小结
- objc_setProperty 方法的目的适用于关联上层的set方法以及底层的set方法,其本质就是一接口
- 这么设计的原因是,上层的set方法很多,如果直接调用底层set方法中,会有很多临时变量。当你吵着一个set的时候非常麻烦
- 这里其实采用了一种设计模式:适配器设计模式(即将底层接口适配位客户段需要的接口),对外提供一个接口共上层的set方法使用,对内调用底层的set方法。让他们两者互不影响)
cls与类的关联原理
联合体
联合体是指把不同的数据合成一个整体,但是所有变量是互斥的,所有的成员共占一段内存
- 缺点:包容性弱
- 优点:成员共用一段内存,是内存的使用更为精细灵活,同时节省了一个内存空间
共用体的所有成员占用同一段内存,修改一个成员影响其他所有成员变量。
结构体各个成员会占用不同的内存,互相之间没有影响。
结构体内存>=所有成员占用的内存总和。
共用体占用的内存等于最大成员占用的内存
isa的类型isa_t
以下isa指针的类型isa_t
的定义,从定义中可以看到是通过联合体来定义的。
union isa_t {isa_t() { } // 默认的一个构造函数isa_t(uintptr_t value) : bits(value) { }uintptr_t bits; // 这个和cls是一个互斥类型private:// Accessing the class requires custom ptrauth operations, so// force clients to go through setClass/getClass by making this// private.Class cls; // 这里把cls指针私有化,禁止直接访问cls,必须通过getClass()和setClass()方法操作public:
#if defined(ISA_BITFIELD)struct {ISA_BITFIELD; // defined in isa.h 这里是一个位域设置一个结构体};bool isDeallocating() {return extra_rc == 0 && has_sidetable_rc == 0; //判断位域对应的一个标志位}void setDeallocating() {extra_rc = 0; has_sidetable_rc = 0;}
#endifvoid setClass(Class cls, objc_object *obj); //提供set方法获取cls指针更加安全Class getClass(bool authenticated); //提供get方法获取cls指针Class getDecodedClass(bool authenticated); 。。
}; //这里是因为C++支持里一个联合体方法,c++11以上支持联合体里面放方法,这里的方法并会被存储到我们的一个代码段,不会影响联合体的一个内存
我们现在来简单分析一个这个isa_t
的一个定义可以看出:
- 提供了两个成员变量
cls
和bits
由联合体的一个定义所知,这里两个成员是互斥的,所以这里有俩种初始化方式:- 通过cls初始化
- 通过bits初始化
- 这里提供了一个位域的内容,用于存储类信息以及其他信息,结构体的成员
ISA_BITFIEld
这里我们就看一下他是怎么定义位域的:
有两个版本 __arm64__
(对应ios 移动端) 和 __x86_64__
(对应macOS)
# define ISA_BITFIELD // __x86_64__ \uintptr_t nonpointer : 1; \ //是否开启isa指针开启指针优化 uintptr_t has_assoc : 1; \//是否有关联对象 uintptr_t has_cxx_dtor : 1; \ // 是否有C++相关实现 uintptr_t shiftcls : 44; \/*MACH_VM_MAX_ADDRESS 0x7fffffe00000存储类信息*/ uintptr_t magic : 6; \//判断对象是真对象还是未初始化对象uintptr_t weakly_referenced : 1; \//对象是否被指向一个或者曾经指向ARC的一个弱变量uintptr_t unused : 1; \ //未被使用uintptr_t has_sidetable_rc : 1; \ //是否外挂一个sidetbaleuintptr_t extra_rc : 8 //额外的引用计数define ISA_BITFIELD //__arm64__ \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 unused : 1; \uintptr_t has_sidetable_rc : 1; \uintptr_t extra_rc : 19
nonpointer
有两个值,表示自定义的类等占1位- 0:纯isa指针
- 1:不只是类对象地址,isa中包含了类对象,对象的引用计数等
has_assoc
表示关联对象标志位- 0:没有关联对象
- 1: 存在关联对象
has_cxx_dtor
表示是否有C++/OC析构器:类似于dealloc,占一位- 如果有则需要做析构逻辑
- 没有就直接可以释放对象
shifcls
白哦是存储类的指针的值arm64
占33位x86_64
中占44位
magic
用于调试器判断当前对象是真的对象还是没有初始化的空间,6位weakly_refrenced
是指对象是否被指向或者曾经指向一个ARC的弱变量has_sidetable_rc
表示 当对象引用计数大于10
时,则需要借用该变量存储进位
extra_rc
(额外的引用计数) ,表示该对象的引用计数值
,实际上是引用计数值减1
原理探索
我们在之前讲过alloc的一个调用流程,在里面有一个initInstanceIsa
,现在我们来看这部分内容:
inline voi
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{ASSERT(!cls->instancesRequireRawIsa());ASSERT(hasCxxDtor == cls->hasCxxDtor());initIsa(cls, true, hasCxxDtor);
}
initIsa方法
这里面也和之前,有一个中间层调用这部分内容,也就是真正开始绑定的一部分内容
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ //这里ASSERT(!isTaggedPointer()); isa_t newisa(0);if (!nonpointer) {newisa.setClass(cls, this); // 初始化cls指针,用之前的那个set方法} else {ASSERT(!DisableNonpointerIsa);ASSERT(!cls->instancesRequireRawIsa());#if SUPPORT_INDEXED_ISA //即isa由cls定义ASSERT(cls->classArrayIndex() > 0);newisa.bits = ISA_INDEX_MAGIC_VALUE;// isa.magic is part of ISA_MAGIC_VALUE// isa.nonpointer is part of ISA_MAGIC_VALUEnewisa.has_cxx_dtor = hasCxxDtor;newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else //bits指向的流程newisa.bits = ISA_MAGIC_VALUE; // 对bits进行一个赋值// isa.magic is part of ISA_MAGIC_VALUE// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BITnewisa.has_cxx_dtor = hasCxxDtor;
# endifnewisa.setClass(cls, this);
#endifnewisa.extra_rc = 1;}// This write must be performed in a single store in some cases// (for example when realizing a class because other threads// may simultaneously try to use the class).// fixme use atomics here to guarantee single-store and to// guarantee memory order w.r.t. the class index table// ...but not too atomic because we don't want to hurt instantiationisa = newisa;
}
我们这里是通过InitIsa来绑定isa指针的,但objc838在这里把绑定具体存储类信息的内容放在我们的setClass
这个函数中.下面我们来看这部分代吗内容.
cls
与 isa
关联原理
就是isa
指针中的shiftcls
位域中存储了类信息
,其中initInstanceIsa
的过程是将 calloc
指针 和当前的 类cls
关联起来
通过setClass方法中的shiftcls来验证绑定的一个流程
这里我们接着看setClass这个方法:
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{// Match the conditional in isa.h.
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
# if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE// No signing, just use the raw pointer.uintptr_t signedCls = (uintptr_t)newCls;# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT// We're only signing Swift classes. Non-Swift classes just use// the raw pointeruintptr_t signedCls = (uintptr_t)newCls;if (newCls->isSwiftStable())signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));# elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL// We're signing everythinguintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));# else
# error Unknown isa signing mode.
# endifshiftcls_and_sig = signedCls >> 3;#elif SUPPORT_INDEXED_ISA// Indexed isa only uses this method to set a raw pointer class.// Setting an indexed class is handled separately.cls = newCls;#else // Nonpointer isa, no ptrauthshiftcls = (uintptr_t)newCls >> 3; // 这里会有shiftcls赋值的逻辑是将LGPerson进行编码后,右移3位
#endif
}
从上面这张图可以看出我们这里是在shiftcls
中存储一个类信息的
运行至newisa.shiftcls = (uintptr_t)cls >> 3
其中shiftcls存储当前类信息部分
- shiftcls赋值的逻辑是将LGPerson进行编码后,右移3位,这里为什么可以保证后三位没有信息的原因是内存对齐后从二进制层面来说他一定是后三位为000的,(因为结构体内存对齐的一个机制,导致了我们的结构体的二进制后三位一定为0);
这个时候我们再运行会我们的一个initIsa
这里的一个创建isa的内容:isa() = newisa;
然后观察这里的一个内存的情况:
这里我们可以清楚的看到一个内容这里一个cls完美的指向了我们的一个类.
这里为什么shiftcls
为什么会出现一个强制类型转化的原因是,内存不可以存储字符串,机械码只可以识别01这种数字所以才强制类型转化成一个uintptr_t数据类型
在64位的机器上,intptr_t和uintptr_t分别是long int、unsigned long int的别名;在32位的机器上,intptr_t和uintptr_t分别是int、unsigned int的别名。
这里为什么需要向右移动三位,是因为他的前面还有三个位域,需要右移动将其抹零.
主要是由于shiftcls
处于isa
指针地址的中间
部分,前面还有3
个位域,为了不影响前面的3个位域
的数据,需要右移
将其抹零
。
通过 isa & ISA_MSAK
- 在方式一后,继续执行,回到
_class_createInstanceFromZone
方法,此时cls 与 isa已经关联完成
,执行po objc
将isa
指针地址 & ISA_MASK
(处于macOS
,使用x86_64
中的宏
定义),即 po 0x0000000100008358 & 0x00007ffffffffff8 & 0x00007ffffffffff8
,得出CGObject
通过object_getClass
查看object_getClass
的源码实现,同样可以验证isa与类关联的原理,这里的原理和上面的大致一样,所以笔者就不多赘述了.
通过位运算
我们之前提到过shiftcls
只有44位的大小.这44位其实存储了一个类信息,需要经过位运算,将右边三位,和去除44位的位置都抹零
所以我们只有这样处理之后也可以得到shiftcls里面保存的是一个类,说明isa已经关联成功了.
类&类的结构
这里主要分析的isa走向和继承关系
在分析之前,我们先定义两个类:
@interface CJLPerson : NSObject
{NSString *hobby;
}
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end
@interface CJLTeacher : CJLPerson
@end@implementation CJLTeacher
@end
从终端调试开始认识调用逻辑
根图中的p/x 0x0000000100008320 & 0x00007ffffffffff8ULL
与 p/x 0x00000001000082f8 & 0x00007ffffffffff8ULL
中的类信息打印出来都是CJLPerson
?
前者打印的是person对象的一个isa指针地址,他&后的结果是创建person的类CJLPerson
后者打印的是isa中类信息所指的类的isa的指针地址,即通过CJLPerson类的类的isa地址,在苹果中,我们把一个类的类叫做元类
什么是元类
- 对象的isa是指向类,类其实也是一个对象,可以叫做类对象.类对象的位域3指向平果定义的元类
- 元类是系统给的,他的定义和创建都是由编译器玩测的,这个过程中,类的归属来自于元类
- 元类是类对象的类,每一个类都有独一无二的元类来存储类方法的相国信息
- 元类本身是没有名字的,只是因为类与之关联,所以直接采用了类的一个名字
这里我们继续用调试来观察
我们可以发现NSObject的元类指向了自己,CJLPerson元类的元类指向的是NSObject,从上面的调试信息我们其实可以得到下面的图片:
小结
- 对象的isa指向类
- 类的isa指向元类
- 元类的isa指向根元类
- 根元类的isa指向自己
NSObject到底有几个?
其实NSObject只有一个,我们可以打印出NSObject的元类的地址和前面的CJLPerson元类的地址,发现他其实都是同一个,所以可以得出一个结论,内存中只存在一份根元类NSObject,根元类是指向他自己
类存在几份
由于类的信息在内存中只有一份,所以类对象只有一份
isa走位继承图
isa走位
isa的走向有以下几点说明:
- 实例对象(Instance of Subclass 的
isa
指向类(class)
- 类对象(class)
isa
指向元类(Meta class)
- 元类(Meta class)的
isa
指向根元类(Root metal class)
- 根元类(Root metal class) 的
isa
指向它自己
本身,形成闭环
,这里的根元类
就是NSObject
superclass走位
类之间的继承关系:
- 类继承自父类superClass
- 父类继承自根类rootClass
- 根力类继承自nil,即根类可以理解为万物起源,无中生有
元类也存在继承,元类之间的继承关系如下:
- 自子类的元类元类继承自父类的元类
- 父类的元类继承自根元类
- 根元类继承自根类,这里的根类是指NSObject
实例对象之间没有继承关系,类之间由继承关系
object_class & objc_object
isa走位我们理清楚了,又来了一个新的问题:为什么 对象
和 类
都有isa属性
呢?这里就不得不提到两个结构体类型:objc_class
& objc_object
这里我们重新回顾我们的一个NSObject定义的内容:
struct NSObject_IMPL {Class isa;
};
typedef struct objc_class *Class;
其中Class是用isa指针类型,是由objc_class定义的一个类型
objc_class是一个结构体,iOS中兵所有的Class都是以objc_class为模板创建的.
这里我们先认识一下这两个结构体:
这里我们可以简单看一下这两个结构体:
struct objc_object {Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
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 vtableclass_data_bits_t bits;//这里代码太长了就不展示了
这里的objc_class 与objc_object有什么关系?
- 结构体类型objc_class继承自objec_class类型.其中objc_obejct是一个结构体,他又一个isa属性,所以objc_class也有了isa属性
- NSObject中的isa在底层是由Class 定义的,其中class的底层编码来自 objc_class类型,所以NSObject也拥有了isa属性
- NSObject是一个类,用它初始化一个实例对象objc,objc满足objc_object的一个特性,主要还是因为isa是由objc_class继承过来的,而objce_class继承自objc_object,因为objc_object由isa属性,所以每一个对象都有一个isa,isa表示一个指向,来自当前的objc_object.
- objc_object是当前的根对象,缩由对象都有这样一个特性,即拥有isa属性
objc_object与对象的关系
- 所有的对象都是以objc_object继承过来的
- 所有的对象都是来源于NSObject,但是到真正的底层就是一个objc_object的一个结构体类型了
所以可以说对象是继承于objc_object的
总结
所有的对象都是由objc_object继承过来的
所有的对象,类,元类都有isa属性.
简单概括就是万物都是对象,所有都来源于objc_object,有一下两点结论:
- 以objc_object为模板创建的对象,都有isa
- 以objc_class为模板创建的类,都有isa
在结构层面可以通俗的理解为上层OC与底层对接:
- 下层就是通过结构体定义的模板,例如objc_class,objc_object
- 上层就是通过底层模板创建的一些类型
类结构分析
类信息中有哪些内容
探索类信息中有什么的时.事先我们并不清楚类的结构是什么样,但是我们可以通过类得到一个首地址,然后通过地址平移去获取里面所有的值.
开始认识我们的一个结构体:
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 vtableclass_data_bits_t bits;
- isa属性:继承自objec_object的isa,占8个字节
- superclass属性:Class类型,Class实有objc_object定义的,是一个指针
- cache方法缓存,提高方法的性能
- bits属性:只有首地址经过上面三尔个属性的内存大小总和的平移,才能获得到bits
计算cache类的内存大小
进入cache类cache_t
的定义(只贴出了结构体中非static修饰的属性,主要是因为static类型的属性 不存在结构体的内存中)
struct cache_t {
private:explicit_atomic<uintptr_t> _bucketsAndMaybeMask; //是一个结构体指针类型,占8个字节 explicit_atomic 是将普通指针转换为原子指针的操作,为了线程安全. _bucketsAndMaybeMask 字段通过预处理宏和 buckets() 方法间接实现了 _buckets 的功能 这种设计是为了 优化内存(共享 _bucketsAndMaybeMask 字段)并支持不同架构的掩码存储模式。union {struct {explicit_atomic<mask_t> _maybeMask;
#if __LP64__uint16_t _flags;
#endifuint16_t _occupied;};explicit_atomic<preopt_cache_t *> _originalPreoptCache;};
这个类在LP64位的一个情况应该是只有16个字节,上面包含一个一个结构体指针类型,和一个结构体类型.这个类的结构可以大致的简单理解为下面这个样式:
这里展示一下,这里和objc838有所出入,在objc838版本中把bucket和mask合并了,然后通过掩码的一个方式去访问这部分内存.
bits
bits
:封装了类的其他信息,例如成员变量,方法列表,协议,属性
这里我们可以看到这里的一个中的bits的内容,这里我们先来认识bits中非常重要的一个类别class_rw_t
这是一个结构体类型,这里我们呢还没有看到属性列表以及方法列表:
所以我们还需要进一步探索,所以我们找一下这里最重要的内容bits中最重要的两个方法:safe_ro
data
,后者返回的是class_rw_t
前者返回的是class_ro_t
在64位架构CPU下,bits
的第3到第46字节存储 class_rw_t
。class_rw_t
中存储 flags 、witness、firstSubclass、nextSiblingClass 以及 class_rw_ext_t
。
在本文的最后几章内容中会讲几个的一个区别
探索property_list(属性列表)
通过查看class_rw_t
这个类.我们可以发现这里的一个属性列表和方法列表等多个列表
这里注意一个内容,这里的存储在class_rw_t这个类是存储一个属性的,并不存储一个成员变量成员变量是存储在另一个类中:class_ro_t
的这里我们对比看一下这两个类的区别:
这个类也有方法,属性,协议和成员变量.但方法,属性,协议的开头是用base
开头的
这里有一个成员变量列表也就是我们这里的ivars
,这个列表包含的内容不仅仅包含一个成员变量列表,除了包括在{}
中定义的一个成员变量,还包括通过属性定义的成员变量.bits --> data() -->ro() --> ivars
通过这个流程来获取成员变量表.
通过@property
定义的属性,也会存储在bits属性中,通过bits --> data() --> properties() --> list
获取属性列表
,其中只包含属性
探索methods_list
这里的methods_list是不存储类方法的,他只存储一个实例方法.那我们的类方法存储在哪里呢?
类方法其实存储在我们的一个元类的方法列表里面:
-
类的实例方法存储在类的bits属性中,通过
bits --> methods() --> list
获取实例方法列表
,例如CJLPersong
类的实例方法sayHello
就存储在CJLPerson类的bits
属性中,类中的方法列表
除了包括实例方法
,还包括属性的set方法
和 `get方法 -
类的类方法存储在元类的bits属性中,通过
元类bits --> methods() --> list
获取类方法列表
,例如CJLPerson
中的类方法sayBye
就存储在CJLPerson
类的元类
(名称也是CJLPerson)的bits
属性中
从编译期开始介绍class_ro_t,class_rw_t,class_rw_ext_t
首先我们从编译期程序做了什么事情开始介绍:
当类被编译的时候,二进制类在磁盘中的表示如下:
首先是类对象本身,包含最常访问的信息:指向元类(isa),超类(superclass)和方法缓存(cache)的指针,它还具有指向包含更多数据的结构体
class_ro_t
的指针,包含了类的名称,方法,协议,实例变量等等编译期确定
的信息。其中ro
表示read only
的意思。当类第一次从磁盘加载到内存时,它们总是以这样的形式开始布局的,但是一旦使用它们,就会发生改变:
当类被 Runtime 加载之后,类的结构会发生一些变化,在了解这些变化之前,我们需要知道2个概念:
Clean Memory:加载后不会发生更改的内存块,
class_ro_t
属于 Clean Memory,因为它是只读的。
Dirty Memory:运行时会发生更改的内存块,类结构一旦被加载,就会变成 Dirty Memory,因为运行时会向它写入新的数据。例如,我们可以通过 Runtime 给类动态的添加方法。这里要明确,Dirty Memory 比 Clean Memory 要昂贵得多。因为它需要更多的内存信息,并且只要进程正在运行,就必须保留它。另一方面, Clean Memory 可以进行移除,从而节省更多的内存空间,因为如果你需要 Clean Memory ,系统可以从磁盘中重新加载。
Dirty Memory 是这个类数据 被分成两部分的原因。
对于我们来说,越多的 Clean Memory 显然是更好的,因为它可以
节约更多的内存
。我们可以通过分离出永不更改的数据部分,将大多数类数据保留为 Clean Memory,应该怎么做呢?在介绍优化方法之前,我们先来看一下,在类加载之后,类的结构会变成如何呢?
类分析
在类首次被使用的时候,runtime会为它分配额外的存储容量,用于 读取/写入 数据的一个结构体 class_rw_t
。
这个结构体用来存储只有在运行时才会生成的新信息比方说所有类都会连接成一个树状结构,这里是通过firstSubclass和newxtSon;欧美化Class指针实现的,这样runtime可以遍历当前使用的所有的类.
这里之所以还会有该类的方法列表和属性列表的信息是因为它们在运行时是可以更改的.当category被加载的时候,它可以想类中添加新的方法.而且程序员也可以通过runtimeAPI动态的添加.
class_ro_t
是只读的,存放的是 编译期间就确定 的字段信息;而class_rw_t
是在 runtime 时才创建的,它会先将class_ro_t
的内容拷贝一份,再将类的分类的属性、方法、协议等信息添加进去,之所以要这么设计是因为 Objective-C 是动态语言,你可以在运行时更改它们方法,属性等,并且分类可以在不改变类设计的前提下,将新方法添加到类中。
在实际生活中,我们会发现class_rw_t
会占用比class_ro_t
更多的内存所以诞生了一个新的结构体class_rw_ext_t
大约只有10%左右的类实际会存在动态的一个更改行为,这样减少了一个class_rw_t
的大小减小了一半,所以会变成下面这个结构
class_ro_t
这里先展示这个类的样式
struct class_ro_t {uint32_t flags;uint32_t instanceStart;uint32_t instanceSize;
#ifdef __LP64__uint32_t reserved;
#endifunion {const uint8_t * ivarLayout;Class nonMetaclass;};explicit_atomic<const char *> name;WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;protocol_list_t * baseProtocols;const ivar_list_t * ivars;const uint8_t * weakIvarLayout;property_list_t *baseProperties;
这个类我们可以很清楚的看到他有属性列表,方法列表.
这个类其实是在编译期的时候就产生了,他会将本来编译好的内容,放到我们的ro中.
class_rw_t
先展示一下这个结构体的一个样式:
struct class_rw_t {// Be warned that Symbolication knows the layout of this structure.uint32_t flags;uint16_t witness;
#if SUPPORT_INDEXED_ISAuint16_t index;
#endifexplicit_atomic<uintptr_t> ro_or_rw_ext; //按需存储静态或动态元数据,兼顾性能和内存。//作用原子化指针,指向类的 只读元数据(class_ro_t)或 动态扩展数据(class_rw_ext_t)。Class firstSubclass; //指向当前类的 第一个直接子类Class nextSiblingClass; //指向当前类的 下一个兄弟类,与 firstSubclass 共同维护类的 链表结构。
从他两者的一个成员变量其实就可以看出两者的一个区别,这里里面之所以没有属性的一个内容,是因为在iOS14之后属性的部分被放到了我们之前提到的class_rw_ext_t
中.
区别
从生成时机的角度来说, ro
编译阶段生成,rw
运行的时候生成。从存储的内容角度来讲,ro
中有方法、属性、协议和成员变量,而rw
中并没有成员变量。rw
中的方法属性协议的取值方法中,也是通过取ro
或者rwe
中的值来获得。ro
中的方法、属性、协议都是base,也就是只有本类中的方法属性和协议。
class_rw_ext_t
这里我们来看一下这个结构体的一个样式:
struct class_rw_ext_t {DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)class_ro_t_authed_ptr<const class_ro_t> ro;method_array_t methods;property_array_t properties;protocol_array_t protocols;char *demangledName;uint32_t version;
};
这个结构体存储了class_ro_t
和 methods
(方法列表)、properties
(属性列表)、protocols
(协议列表)等信息。
而 class_ro_t
中也存储了 baseMethodList
(方法列表)、baseProperties
(属性列表)、baseProtocols
(协议列表) 以及 实例变量、类的名称、大小 等等信息。
但是我们这里要注意一下他的一个创建的前提条件:
- 使用分类的类
- 使用runtime API动态修改类的结构的时候
在遇到以上2种情况的时候,类的结构(属性、协议、方法)发生改变,原有的ro
(Claer Memory,便宜)已经不能继续记录类的属性、协议、方法信息了,于是系统重新生成可读可写的内存结构rw_ext
(Dirty Memory, 比较贵),来存放新的类结构。
这时候我们就很好理解下面这个方法列表了:
const method_array_t methods() const {auto v = get_ro_or_rwe();if (v.is<class_rw_ext_t *>()) { // 判断有没有创建一个re_extreturn v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods; //如果有就在这里创建} else {return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods}; //如果没有就在ro中读取}}
总结
这是笔者对于iOS类与对象底层原理的一个学习,这里可能会有什么纰漏或者错误,还请不吝指出.
参考博客:
iOS-底层原理 08:类 & 类结构分析
iOS 类的结构分析
类结构中的class_rw_t与class_ro_t
【iOS】类与对象底层探索
相关文章:
iOS 类与对象底层原理
iOS 类与对象底层原理 文章目录 iOS 类与对象底层原理探索对象本质objc_setProperty 源码cls与类的关联原理联合体isa的类型isa_t 原理探索initIsa方法通过setClass方法中的shiftcls来验证绑定的一个流程通过 isa & ISA_MSAK通过object_getClass通过位运算 类&类的结构…...
Babel、core-js、Loader之间的关系和作用全解析
在现代前端开发中,Babel、polyfill(如 core-js)和 Loader 是非常常见又容易混淆的几个概念。为了彻底搞明白它们的作用、关系和使用方法,下面一篇文章详细梳理。 一、Babel的作用 Babel 是一个 JavaScript 的编译器,主…...
总线位宽不变,有效数据位宽变化的缓存方案
总线位宽不变,有效数据位宽变化的缓存方案 譬如总线位宽为64bit,但是有时候只有高32bit有效,有时只有低32bit有效,有时64bit都有效。总线上收到的数据要先缓存到FIFO中,那么这个FIFO的宽度和深度如何设置呢࿱…...
若依脱敏功能升级:接口返回想脱就脱,想不脱就不脱(实现灵活可控制的数据脱敏)
若依原生框架中的脱敏功能不够灵活(默认超级管理员不脱敏,其他则脱敏)。 有时候,我们有些接口想要脱敏,但是有些接口又不想脱敏。(例如列表查询的时候脱敏。修改的时候,不想数据脱敏࿰…...
【Azure Redis 缓存】在Azure Redis中,如何限制只允许Azure App Service访问?
问题描述 在Azure Redis服务中,如何实现只允许Azure App Service访问呢? 问题解答 Azure Redis 开启 防火墙的功能,并在防火墙中添加上App Service的出口IP地址即可。两步即可实现此目的! 1)查询 App Service 的出口IP…...
如何解决无训练数据问题:一种更为智能化的解决方案
手动标注数据真的很费时间,而且买数据集又贵得要命,还不一定能完全符合你的需求。但这里有个令人兴奋的好消息,为啥不用 AI 来解决这个问题呢? 别再依赖传统方法了,你可以用像 LLM(大型语言模型)和图像生成器这样的 AI 工具,为你的特定目标创建合成训练数据。如今有那…...
AI 应用同质化:一场看不见的资源 “吞噬战”
大家好,我是涛涛,今天聊聊令人担心的事情。 一、同质化的“繁荣”背后 当ChatGPT在2022年掀起全球AI热潮时,中国互联网行业迅速进入“All in AI”模式。根据艾瑞咨询数据,2023年国内AI应用市场新增注册企业超2.3万家,…...
Java + Spring Boot + MyBatis获取以及持久化sql语句的方法
在Java的Spring Boot项目中结合MyBatis获取实际执行的SQL语句,可以通过以下几种方法实现: 方法一:配置MyBatis日志级别 通过调整日志级别,MyBatis会输出执行的SQL语句及参数,适用于快速调试。 修改application.prope…...
「浏览器即OS」:WebVM技术栈如何用Wasm字节码重构冯·诺依曼体系?
一、冯诺依曼架构的维度坍塌 1. 传统计算模型的能量耗散 浏览器执行效率瓶颈分析: 操作x86指令周期Wasm指令周期能效比提升矩阵乘法3894.2x内存访问1234x系统调用120012100x 二、WebVM的量子纠缠架构 1. 浏览器内核的重构 // 基于WASI的系统调用处理 #[no_mangl…...
Vue3项目目录结构规范建议
以下是一个推荐的 Vue 3 项目目录结构规范,适用于中大型项目并遵循最佳实践: 基础目录结构 bash src/ ├─ assets/ # 静态资源 │ ├─ images/ # 图片文件 │ ├─ fonts/ # 字体文件 │ └─ styles/ …...
【计算机视觉】CV实战项目- Four-Flower:基于TensorFlow的花朵分类实战指南
深度解析Four-Flower:基于TensorFlow的花朵分类实战指南 项目概述与技术背景技术栈组成 完整实战流程环境配置1. 基础环境安装2. 项目环境搭建3. 环境验证 数据准备模型架构解析训练过程优化1. 训练配置2. 关键参数建议3. 训练监控 常见问题与解决方案1. 内存不足错…...
4.27 JavaScript核心语法+事件监听
JavaScript负责网页的行为(交互行为) JS基本语法: 引用方式 变量&常量&数据类型: alert()标签输出弹出框,如以上代码会输出true。 函数: 自定义对象: 属性方法行为 JS中的全局变量是window。 js…...
于键值(KV)的表
基于键值(KV)的表 将行编码为键值(KVs) 索引查询:点查询和范围查询 在关系型数据库中,数据被建模为由行和列组成的二维表。用户通过SQL表达他们的意图,而数据库则神奇地提供结果。不那么神奇的…...
Matlab算例运行
1. 使用终端命令运行算例: 2. 如果点击Run 按钮就是会一直报错,所以直接改成终端运行算例...
package.json script 中的 prepare 脚本的作用是什么
在 package.json 的 scripts 中,prepare 脚本是一个特殊的生命周期脚本,主要作用和执行时机如下: prepare 脚本的作用和执行时机 执行时机: 在执行 npm publish 命令之前运行。在执行不带参数的 npm install 命令时运行ÿ…...
图论---最大流(Dinic)
最大流一定是阻塞流,阻塞流不一定是最大流。 阻塞流---从起点到终点的管道已经阻塞了。 时间复杂度: 一般情况:O(n2m)O(n2m)(但实际运行效率较高,尤其在稀疏图上)。 使用当前弧优化后,效率接近…...
FastAPI系列06:FastAPI响应(Response)
FastAPI响应(Response) 1、Response入门2、Response基本操作设置响应体(返回数据)设置状态码设置响应头设置 Cookies 3、响应模型 response_model4、响应类型 response_classResponse派生类自定义response_class 在“FastAPI系列0…...
双目RealSense系统配置rs_camera.launch----实现D435i自制rosbag数据集到离线场景的slam建图
引言 Intel RealSense系列相机因其出色的深度感知能力和灵活的配置选项,在机器视觉与应用中得到广泛应用。大家在后期的slam学习中,无论是对算法本身的性能要求还是实验的泛化性都有一定的要求,那么公开的数据集如kitti、tum、Eourc不能满足…...
【MCP-2】MCP是什么,利用智普大模型在MaxKB中调用自己开发的MCP服务
在上一篇【MCP-1】MCP是什么,从DEMO入手文章中我们介绍了MCP是什么、他能干啥,以及简单的Demo示例等,这篇文章我们使用MaxKB这个工具,利用智普大模型,看看MCP到底怎么用。 创建SSE协议的MCP服务 在上篇文章中的Demo是…...
Allegro23.1新功能之如何单独关闭铜皮显示效果操作指导
Allegro23.1新功能之如何单独关闭铜皮显示效果操作指导 Allegro升级到了23.1的时候,支持单独关闭铜皮显示 ,如下图 如何仅关闭shape的显示,单独显示线,具体操作如下 点击setup...
《从分遗产说起:JS 原型与继承详解》
“天天开心就好” 先来讲讲概念: 原型(Prototype) 什么是原型? 原型是 JavaScript 中实现对象间共享属性和方法的机制。每个 JavaScript 对象(除了 null)都有一个内部链接指向另一个对象,这…...
【Part 2安卓原生360°VR播放器开发实战】第二节|基于等距圆柱投影方式实现全景视频渲染
《VR 360全景视频开发》专栏 将带你深入探索从全景视频制作到Unity眼镜端应用开发的全流程技术。专栏内容涵盖安卓原生VR播放器开发、Unity VR视频渲染与手势交互、360全景视频制作与优化,以及高分辨率视频性能优化等实战技巧。 📝 希望通过这个专栏&am…...
Android——RecyclerView
RecyclerView的使用 依赖 implementation("androidx.recyclerview:recyclerview:1.4.0")activity_recyclerview.xml <androidx.recyclerview.widget.RecyclerViewandroid:id"id/rv"android:layout_width"match_parent"android:layout_height…...
跨域问题(Cross-Origin Problem)
跨域问题(Cross-Origin Problem)是浏览器出于安全考虑,对不同源(协议、域名、端口)之间的资源访问进行限制而引发的限制。以下是详细解释: 1. 核心定义 跨域:当一个网页(源A&#x…...
阿里云直接对系统云盘扩容
阿里云直接对系统云盘扩容 登录阿里云控制台,进入ECS实例管理页面,检查目标磁盘的容量是否已更新为扩容后的数值。通过SSH远程连接服务器,使用命令 lsblk 或 fdisk -l 查看当前磁盘分区和容量,确认扩容后的物理磁盘已被系统识别。…...
Java大厂面试突击:从Spring Boot自动配置到Kafka分区策略实战解析
第一轮核心知识 面试官:请解释Spring Boot中自动配置的工作原理并演示如何自定义一个ConfigurationProperties组件? xbhog:自动配置通过EnableAutoConfiguration注解触发,结合当前环境判断(如是否检测到MyBatis依赖&…...
【python】lambda用法(结合例子理解)
目录 lambda 是什么? 为什么叫 lambda? 语法 举例 1. 最简单的 lambda:单个数字处理 2. 用 lambda 排序一组字符串(按照长度排序) 3. 在列表里找出绝对值最小的数字 4. 给 map() 用 lambda 5. 组合使用:筛选出偶数 lambda 和 def 的对比 lambda 适合用在什么地…...
前端Ui设计工具
PS 稿、蓝湖、Sketch 和 Figma 前端 UI 设计工具的对比分析 PS 稿(Adobe Photoshop) 提供精准设计细节:PS 稿能让前端更精准地理解页面布局、元素尺寸、颜色等,通过精确测量和查看信息面板,把握设计元素的空间关系、…...
深入探索Python Pandas:解锁数据分析的无限可能
放在前头 深入探索Python Pandas:解锁数据分析的无限可能 深入探索Python Pandas:解锁数据分析的无限可能 在当今数据驱动的时代,高效且准确地处理和分析数据成为了各个领域的关键需求。而Python作为一门强大且灵活的编程语言,…...
django admin 设置字段不可编辑
在Django中,如果你想让管理员在后台管理界面中无法编辑某个字段,你可以通过在模型的Meta类中设置editable属性为False,或者在admin.py文件中使用readonly_fields属性来实现。 方法1:在模型中使用Meta类设置 你可以在模型的Meta类…...
AI在医疗领域的10大应用:从疾病预测到手术机器人
AI在医疗领域的10大应用:从疾病预测到手术机器人 系统化学习人工智能网站(收藏):https://www.captainbed.cn/flu 文章目录 AI在医疗领域的10大应用:从疾病预测到手术机器人摘要引言1. 医学影像诊断:从静态…...
深入理解 Java 单例模式:从基础到最佳实践
单例(Singleton)模式是 Java 中最基本、最常用的设计模式之一。它确保一个类在任何情况下都只有一个实例,并提供一个全局访问点来获取这个唯一的实例。 一、为什么需要单例模式?(使用场景) 单例模式主要适…...
Rust:安全与性能兼得的现代系统编程语言
一、起源与设计理念 Rust 是由 Mozilla 研究院 Graydon Hoare 于 2006 年发起设计的系统级编程语言,其诞生源于传统系统语言(如 C/C)在内存安全与并发编程方面的缺陷。经过近十年的迭代,Rust 1.0 稳定版于 2015 年正式发布&#…...
AI赋能智慧医疗新范式:小天互连即时通讯打造高效、安全的医疗通讯平台
在医疗行业,高效的信息协作与严格的数据安全不仅直接关系患者诊疗效率,更是医院现代化管理的核心命题。小天互连即时通讯系统通过将智能化功能与医疗场景深度结合,打造出全链路数字化协作平台,有效破解了传统沟通模式的效率瓶颈&a…...
图像生成新势力:GPT-Image-1 与 GPT-4o 在智创聚合 API 的较量
在人工智能领域,图像生成技术正迅速发展,OpenAI 推出的 GPT-Image-1 和 GPT-4o 在图像生成方面展现出了强大的能力。智创聚合 API 平台已支持这两个模型,并且其图片生成 / 编辑工作台支持图片的循环编辑等功能,为用户提供了更便捷…...
如何避免爬虫因Cookie过期导致登录失效
1. Cookie的作用及其过期机制 1.1 什么是Cookie? Cookie是服务器发送到用户浏览器并保存在本地的一小段数据,用于维持用户会话状态。爬虫在模拟登录后,通常需要携带Cookie访问后续页面。 1.2 Cookie为什么会过期? 会话Cookie&…...
集成方案 | Docusign + 甄零科技,赋能企业海外业务高效增长!
本文将详细介绍 Docusign 与甄零科技的集成步骤及其效果,并通过实际应用场景来展示 Docusign 的强大集成能力,以证明 Docusign 集成功能的高效性和实用性。 甄零科技是一家专注于数字化合同管理系统的 SaaS 解决方案提供商,致力于为企业打造“…...
【Arxiv 2025】Single Image Iterative Subject-driven Generation and Editing
文章目录 文章标题作者及研究团队介绍01 在论文所属的研究领域,有哪些待解决的问题或者现有的研究工作仍有哪些不足?02 这篇论文主要解决了什么问题?03 这篇论文解决问题采用的关键解决方案是什么?04 这篇论文的主要贡献是什么&am…...
CoOAG:首个捕捉学术研究兴趣动态演变的数据集
2025-04-24,由西安交通大学基于学术合作网络构建一种新的动态图数据集CoOAG,用于研究动态图中的节点分类问题。该数据集通过捕捉作者研究兴趣的动态变化,为动态图学习领域提供了新的研究方向和测试平台,特别是在标签受限的动态节点…...
决策树随机深林
决策树和随机森林是机器学习中常用的两种模型,以下是对它们的简单介绍: 决策树 - 原理:通过一系列的条件判断对样本进行分类或预测。它由节点(内部节点是属性上的测试,叶节点是类别或值)和边组成࿰…...
Unity 和 Unreal Engine(UE) 两大主流游戏引擎的核心使用方法
以下是 Unity 和 Unreal Engine(UE) 两大主流游戏引擎的核心使用方法和对比分析,帮助开发者快速上手并根据项目需求选择合适工具: 一、Unity 使用指南 1. 安装与配置 安装:从 Unity Hub 下载,选择长期支持…...
Maven 依赖范围(Scope)详解
Maven 依赖范围(Scope)详解 Maven 是一个强大的项目管理工具,广泛用于 Java 开发中构建、管理和部署应用程序。在使用 Maven 构建项目时,我们经常需要引入各种第三方库或框架作为项目的依赖项。通过在 pom.xml 文件中的 <depe…...
博物馆除湿控湿保卫战:M-5J1R 电解除湿科技如何重塑文物守护的未来
在卢浮宫幽深的长廊里,达芬奇的《蒙娜丽莎》正经历着一场看不见的战争——不是来自时间的侵蚀,而是空气中无形的水分子。每一件文物都在与湿度进行着无声的抗争,这场抗争关乎人类文明的延续。湿度,这个看不见的文物杀手࿰…...
消防应急物资智能调用立库:豪越科技助力消防“速战速决”
在消防救援的战场上,时间就是生命,每一秒都关乎着人民群众的生命财产安全。然而,在过去的紧急救援中,应急物资无法及时到位的情况时有发生,成为制约救援效率的关键难题,给救援工作带来了巨大的困境。 想象一…...
机器学习基础理论 - 分类问题评估指标
几个定义:混淆矩阵 TP: True Positives, 表示实际为正例且被分类器判定为正例的样本数FP: False Positives, 表示实际为负例且被分类器判定为正例的样本数FN: False Negatives, 表示实际为正例但被分类器判定为负例的样本数TN: True Negatives, 表示实际为负例且被分类…...
深度学习4.1 多层感知机
基本概念 多层感知机(Multilayer Perceptron, MLP)是一种前馈人工神经网络,由输入层、至少一个隐藏层和输出层组成。 核心特点: 采用全连接结构(相邻层神经元全部相连; 通过非线性激活函数…...
解决两个技术问题后小有感触-QZ Tray使用经验小总结
老朋友都知道,我现在是一家软件公司销售部门的项目经理和全栈开发工程师,就是这么“奇怪”的岗位,大概我是公司销售团队里比较少有技术背景、销售业绩又不那么理想的销售。 近期在某个票务系统项目上驻场,原来我是这个项目的项目…...
非计算机专业如何利用AI开展跨学科和交叉研究
对于非计算机专业的研究者,利用AI开展跨学科研究既充满机遇也面临挑战。以下是一份系统化的指南,帮助您高效入门并找到交叉研究的突破口: 一、认知重塑:理解AI的本质与局限 AI不是“黑箱”:现代AI以数据驱动为核心&a…...
Python 数据可视化进阶:精准插入图表到指定 Excel 工作表
Python 数据可视化进阶:精准插入图表到指定 Excel 工作表 在处理数据的过程中,我们常常需要将生成的图表精准地插入到已存在数据的 Excel 文件的指定工作表中。借助 Python 的强大库组合,这一操作得以高效实现。以下是经过优化和注释补充的代…...
MQTT - MQTT 实践(Windows EMQX、MQTTX、客户端认证、连接与主题)
概述 -说明概括MQTT消息队列遥测传输协议一种规则EMQX一款大规模分布式物联网接入平台一个平台MQTTXMQTT 客户端一个工具 工具(MQTTX)和平台(EMQX)间遵循规则(MQTT)即可进行双向通信 一、Windows EMQX 下…...