一、通过isa分析到元类
在《OC底层探究之对象的本质以及isa》文章中我们知道了如何通过isa
找到类
,即isa&掩码
:
那么类
是否还有isa
呢?
我们把类
转为16进制
打印出来:
(lldb) p/x 0x011d8001000080f9 & 0x00007ffffffffff8
(long) $2 = 0x00000001000080f8
复制代码
再打印其内存
:
(lldb) x/4gx 0x00000001000080f8
0x1000080f8: 0x00000001000080d0 0x00007fff8061d008
0x100008108: 0x00007fff2020eaf0 0x0000801000000000
复制代码
与我们打印对象的情况非常相似,那么继续按照isa
转类
的方式进行:
发现类
的isa
中也是类
!
那么这2个类是否一样呢?把新得到的类
转为16进制
打印:
(lldb) p/x 0x00000001000080d0 & 0x00007ffffffffff8
(long) $4 = 0x00000001000080d0
复制代码
发现最开始的类(0x00000001000080f8)
与后面的这个类(0x00000001000080d0)
并不一样!
那么类是否和对象一样在内存
中会不断的开辟内存空间
,会存在多个类
呢?
那么我们来验证
一下:
发现类
都是一样的!且都是第一个类(0x00000001000080f8)
!
那么后面的这个类(0x00000001000080d0)
是什么呢?
把编译好的Mach-O
文件导入到烂苹果
里面去看一看:
在符号表里面就会看到一个METACLASS
,这就是后面的那个类
,也就是元类
!元类是由系统生成和编译的!
整理一下流程
,如下图所示:
二、isa走位图以及继承链
1、isa走位图
那么元类
的isa
中还会有其他的类
吗?
我们继续探索:
(lldb) x/4gx 0x00000001000080d0
0x1000080d0: 0x00007fff8061cfe0 0x00007fff8061cfe0
0x1000080e0: 0x00000001089bab10 0x0002e03100000003
(lldb) p/x 0x00007fff8061cfe0 & 0x00007ffffffffff8
(long) $5 = 0x00007fff8061cfe0
复制代码
发现元类的isa
和isa中的类
是完全一样的!
打印其内存地址
:
(lldb) x/4gx 0x00007fff8061cfe0
0x7fff8061cfe0: 0x00007fff8061cfe0 0x00007fff8061d008
0x7fff8061cff0: 0x0000000108b09aa0 0x0002e03100000007
复制代码
发现这个类
和类的isa
是完全一样的!
打印一下这个特殊的类
,看看是什么:
这个就NSObject
——根元类
!
那么我们再来探索一下NSObject
:
(lldb) p/x NSObject.class
(Class) $8 = 0x00007fff8061d008 NSObject
复制代码
发现NSObject
和我们打印的不一样,继续探索:
(lldb) x/4gx 0x00007fff8061d008
0x7fff8061d008: 0x00007fff8061cfe0 0x0000000000000000
0x7fff8061d018: 0x0000000108b09c70 0x0002801000000003
(lldb) p/x 0x00007fff8061cfe0 & 0x00007ffffffffff8
(long) $10 = 0x00007fff8061cfe0
复制代码
发现NSObject
的isa
中的类
就是isa
自己!
通过代码验证一下:
和我们探索的一致,于是我们可以得出一个isa的走位图
:
2、继承链
OC
中对象
是有继承链
的,那么元类
是否也有继承链
呢?
我们打印一下HPerson
的元类的父类
:
发现HPerson
的元类的父类是NSObject
,即根元类
!
那么是不是元类的父类
就是根元类
呢?
如果HPerson
有父类
呢?HPerson
的元类的父类
又会是什么呢?
新建一个KPerson类
,让KPerson
继承于HPerson
,打印其父类
:
发现KPerson
的元类的父类
是HPerson
!
这意味着元类
也有继承链
!
那么根元类
的父类
又是什么呢?
我们打印一下:
发现根元类
的父类
就是NSObject
,即根类
!
那么根类
的父类
呢?
再来打印一下:
发现根类
没有父类
!
所以我们得出一个继承链
的图:
3、总结
根据isa走位图
和继承链
就可以还原到isa的经典流程图
:
根据图片可以看出一共有3条路线:
- isa路线:对象->类->元类->根元类->根类->nil
- 类的继承:对象->类->父类->根类->nil
- 元类的继承:对象->类->元类->元类的父类->根元类->根类->nil
三、源码分析类的结构
我们已经知道了一个对象
的内存空间
存储着isa
和成员变量
,那么一个类
的内存空间
呢?存储些什么呢?
我们再objc
源码里面搜索objc_class
,就可以看到class
这个结构体:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
struct objc_class : objc_object {
//其余方法等等已省略
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
};
复制代码
可以发现类
继承于objc_object
,拥有4
个成员变量
,其中ISA
是继承于objc_object
。
那么剩下的3
个,superclass
明显可以看出是父类
,另外2
个目前还需要我们继续探索!
先看bits
,注释写着是class_rw_t
!在objc_class
结构体中往下找就可以找到:
class_rw_t *data() const {
return bits.data();
}
复制代码
然后进入到class_rw_t
结构体:
就会发现里面有方法列表
、属性列表
、协议列表
等等!
那我们应该如何去查看类
的内存结构
呢?
四、指针和内存平移
1、指针
-
普通指针:
可以看出,这
2
个内存地址
不一样,但是却是指向了同一个值
!也就是我们经常听到的值copy
! -
对象指针:
可以看出,
指针地址
不同,内存地址
也不同!这
2
种情况可以一个图
来表述: -
数组指针:
可以看出数组
的第一
个元素地址就是首地址
,以及数组指针加减
是依照数组的类型大小
来移动
的!
那么我们是不是可以直接移动指针位置来获取数据呢?
2、内存平移
通过移动指针地址
来取值:
我们发现,内存
是可以进行偏移
的,偏移
之后取地址
的值就可以得到内存
中存储的值
了,这就是取值操作
!
我们可以获取类
的内存地址
,是不是可以通过内存偏移
来获取类
中存储的数据
呢?
五、类的结构内存计算
我们打开objc
源码工程(《OC底层探究之alloc探索》文章中有说明),打印HPerson
类的内存:
(lldb) x/6gx HPerson.class
0x100008238: 0x0000000100008210 0x0000000108679140
0x100008248: 0x0000000108671380 0x0000802c00000000
0x100008258: 0x0000000108f2a234 0x00000002000b9980
复制代码
根据前面探索的类的结构
可知,第一个8
字节是isa
(class类型,即结构体指针),第二个8
字节为superclass
(class类型,即结构体指针),打印一下superclass
:
(lldb) po 0x0000000108679140
NSObject
复制代码
正是HPerson
类的父类NSObject
,我们还可以验证一下:
(lldb) p/x NSObject.class
(Class) $2 = 0x0000000108679140 NSObject
复制代码
和HPerson
类的superclass
内存地址一样!
类结构剩下的2
个元素cache
和bits
的大小
我们并不知道,所以不知道该怎么读!
继续探索一下cache_t
源码:
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
//省略方法、全局变量等等,方法在方法区、全局变量在全局区不占用结构体内存
}
复制代码
发现cache_t
结构体里面有一个explicit_atomic<uintptr_t>
和一个联合体
。
进入explicit_atomic
结构体:
// Version of std::atomic that does not allow implicit conversions
// to/from the wrapped type, and requires an explicit memory order
// be passed to load() and store().
template <typename T>
struct explicit_atomic : public std::atomic<T> {
explicit explicit_atomic(T initial) noexcept : std::atomic<T>(std::move(initial)) {}
operator T() const = delete;
T load(std::memory_order order) const noexcept {
return std::atomic<T>::load(order);
}
void store(T desired, std::memory_order order) noexcept {
std::atomic<T>::store(desired, order);
}
// Convert a normal pointer to an atomic pointer. This is a
// somewhat dodgy thing to do, but if the atomic type is lock
// free and the same size as the non-atomic type, we know the
// representations are the same, and the compiler generates good
// code.
static explicit_atomic<T> *from_pointer(T *ptr) {
static_assert(sizeof(explicit_atomic<T> *) == sizeof(T *),
"Size of atomic must match size of original");
explicit_atomic<T> *atomic = (explicit_atomic<T> *)ptr;
ASSERT(atomic->is_lock_free());
return atomic;
}
};
复制代码
发现explicit_atomic
结构体是一个泛型
类型,它的真正大小是来自于泛型
!
即explicit_atomic<uintptr_t>
的大小取决于uintptr_t
,即为8
字节:
(lldb) po sizeof(uintptr_t)
8
复制代码
再来看联合体
,联合体互斥
,联合体里面一个结构体
和一个explicit_atomic<preopt_cache_t *>
类型,直接看explicit_atomic<preopt_cache_t *>
类型,explicit_atomic<preopt_cache_t *>
的大小取决于preopt_cache_t *
,preopt_cache_t *
是指针
类型,即大小为8
!
所以cache_t
类型的大小为16
!
根据类的结构
可以知道bits
的地址是在类的首地址
加上32
字节,转为16进制
即为20
,然后强转为class_data_bits_t*
类型:
六、lldb分析类的结构
我们之前已经分析了bits
为class_rw_t*
类型:
class_rw_t *data() const {
return bits.data();
}
复制代码
所以可以通过我们强转为class_data_bits_t*
的变量$4
去执行data
方法,因为$4
为指针
,所以使用->
:
(lldb) p $4->data()
(class_rw_t *) $5 = 0x0000000108f2a230
复制代码
得到了class_rw_t *
类型的变量$5
,继续打印$5
:
(lldb) p *$5
(class_rw_t) $6 = {
flags = 2148007936
witness = 0
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000184
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
复制代码
得到了class_rw_t
!
但是没有我们想要的方法列表
、属性列表
等等,怎么办呢?
和刚刚一样,我们调用获取属性列表
的方法:
(lldb) p $6.properties()
(const property_array_t) $7 = {
list_array_tt<property_t, property_list_t, RawPtr> = {
= {
list = {
ptr = 0x00000001000081c0
}
arrayAndFlag = 4295000512
}
}
}
复制代码
但是还是没有属性
,怎么办呢?
我们先看一下property_array_t
,从properties
方法进入:
class property_array_t :
public list_array_tt<property_t, property_list_t, RawPtr>
{
typedef list_array_tt<property_t, property_list_t, RawPtr> Super;
public:
property_array_t() : Super() { }
property_array_t(property_list_t *l) : Super(l) { }
};
复制代码
再进入到list_array_tt
,就看到注释:
/***********************************************************************
* list_array_tt<Element, List, Ptr>
* Generic implementation for metadata that can be augmented by categories.
*
* Element is the underlying metadata type (e.g. method_t)
* List is the metadata's list type (e.g. method_list_t)
* List is a template applied to Element to make Element*. Useful for
* applying qualifiers to the pointer type.
*
* A list_array_tt has one of three values:
* - empty
* - a pointer to a single list
* - an array of pointers to lists
*
* countLists/beginLists/endLists iterate the metadata lists
* count/begin/end iterate the underlying metadata elements
**********************************************************************/
复制代码
可以得出list
是元数据的列表!
那么我们继续在lldb
中查看list
:
(lldb) p $7.list
(const RawPtr<property_list_t>) $8 = {
ptr = 0x00000001000081c0
}
复制代码
发现又进了一层,继续打印ptr
:
(lldb) p $8.ptr
(property_list_t *const) $9 = 0x00000001000081c0
复制代码
然后还原property_list_t
:
(lldb) p *$9
(property_list_t) $10 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 2)
}
复制代码
得到了property_list_t
就是属性列表,count = 2
!
然后打印类的属性
:
(lldb) p $10.get(0)
(property_t) $11 = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
(lldb) p $10.get(1)
(property_t) $12 = (name = "nickName", attributes = "T@\"NSString\",&,N,V_nickName")
复制代码
成功打印出类的属性
,以下是HPerson
类:
展示一下全过程:
七、类的bit数据分析
给类里面添加上类方法
和属性方法
:
按照获取属性
的方式,我们获取一下方法
:
(lldb) x/6gx HPerson.class
0x100008220: 0x00000001000081f8 0x0000000108679140
0x100008230: 0x0000000108671380 0x0000802400000000
0x100008240: 0x0000000108c7d4b4 0x00000002000b9980
(lldb) p (class_data_bits_t *)0x100008240
(class_data_bits_t *) $1 = 0x0000000100008240
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000108c7d4b0
(lldb) p $2.methods()
(const method_array_t) $3 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x00000001000080d8
}
arrayAndFlag = 4295000280
}
}
}
Fix-it applied, fixed expression was:
$2->methods()
(lldb) p $3.list
(const method_list_t_authed_ptr<method_list_t>) $4 = {
ptr = 0x00000001000080d8
}
(lldb) p $4.ptr
(method_list_t *const) $5 = 0x00000001000080d8
(lldb) p *$5
(method_list_t) $6 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 6)
}
复制代码
可以看到有6
个方法,我们来打印一下方法:
(lldb) p $6.get(0)
(method_t) $7 = {}
(lldb) p $6.get(1)
(method_t) $8 = {}
复制代码
发现为空
!
为什么呢?
我们看一看源码
,找到属性列表property_list_t
结构体:
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
复制代码
里面是空的,我们获取的是property_t
,继续找property_t
:
struct property_t {
const char *name;
const char *attributes;
};
复制代码
发现里面只有name
和attributes
这2个成员变量,和我们打印的信息相符!
在看方法列表,找到method_array_t
:
// Two bits of entsize are used for fixup markers.
// Reserve the top half of entsize for more flags. We never
// need entry sizes anywhere close to 64kB.
//
// Currently there is one flag defined: the small method list flag,
// method_t::smallMethodListFlag. Other flags are currently ignored.
// (NOTE: these bits are only ignored on runtimes that support small
// method lists. Older runtimes will treat them as part of the entry
// size!)
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
bool isUniqued() const;
bool isFixedUp() const;
void setFixedUp();
uint32_t indexOfMethod(const method_t *meth) const {
uint32_t i =
(uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
ASSERT(i < count);
return i;
}
bool isSmallList() const {
return flags() & method_t::smallMethodListFlag;
}
bool isExpectedSize() const {
if (isSmallList())
return entsize() == method_t::smallSize;
else
return entsize() == method_t::bigSize;
}
method_list_t *duplicate() const {
method_list_t *dup;
if (isSmallList()) {
dup = (method_list_t *)calloc(byteSize(method_t::bigSize, count), 1);
dup->entsizeAndFlags = method_t::bigSize;
} else {
dup = (method_list_t *)calloc(this->byteSize(), 1);
dup->entsizeAndFlags = this->entsizeAndFlags;
}
dup->count = this->count;
std::copy(begin(), end(), dup->begin());
return dup;
}
};
复制代码
里面有很多内容,但是我们需要获取的是method_t
,继续看method_t
:
struct method_t {
static const uint32_t smallMethodListFlag = 0x80000000;
method_t(const method_t &other) = delete;
// The representation of a "big" method. This is the traditional
// representation of three pointers storing the selector, types
// and implementation.
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
public:
big &big() const {
ASSERT(!isSmall());
return *(struct big *)this;
}
//其余内容省略
}
复制代码
发现里面没有成员变量
,但是有一个big结构体
和big方法
,所以我们应该打印big结构体
:
(lldb) p $6.get(0).big()
(method_t::big) $10 = {
name = "printName"
types = 0x0000000100003f77 "v16@0:8"
imp = 0x0000000100003d80 (HObjectBuild`-[HPerson printName])
}
(lldb) p $6.get(1).big()
(method_t::big) $11 = {
name = "name"
types = 0x0000000100003f8b "@16@0:8"
imp = 0x0000000100003d90 (HObjectBuild`-[HPerson name])
}
(lldb) p $6.get(2).big()
(method_t::big) $12 = {
name = ".cxx_destruct"
types = 0x0000000100003f77 "v16@0:8"
imp = 0x0000000100003e30 (HObjectBuild`-[HPerson .cxx_destruct])
}
(lldb) p $6.get(3).big()
(method_t::big) $13 = {
name = "setName:"
types = 0x0000000100003f93 "v24@0:8@16"
imp = 0x0000000100003db0 (HObjectBuild`-[HPerson setName:])
}
(lldb) p $6.get(4).big()
(method_t::big) $14 = {
name = "nickName"
types = 0x0000000100003f8b "@16@0:8"
imp = 0x0000000100003de0 (HObjectBuild`-[HPerson nickName])
}
(lldb) p $6.get(5).big()
(method_t::big) $15 = {
name = "setNickName:"
types = 0x0000000100003f93 "v24@0:8@16"
imp = 0x0000000100003e00 (HObjectBuild`-[HPerson setNickName:])
}
复制代码
方法
就被正确的打印出来了!
但是,类方法
却没有,为什么呢?因为类方法
在元类
里面!