一、通过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
三、源码分析类的结构
我们已经知道了一个对象的内存空间存储着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:])
}
方法就被正确的打印出来了!
但是,类方法却没有,为什么呢?因为类方法在元类里面!