和谐学习!不急不躁!!我是你们的老朋友小青龙~
上篇文章,我们讲到了,通过isa+掩码
就可以访问到类的信息。直接上图:
那么类是否也像对象一样,有isa、有它的上一层结构呢?继续操作:
看到这里,我们发现虽然打印的两个都叫Direction,但是地址不一样,那么下面那个3d810地址是什么东西呢?我们打开Xcode工程(isa分析)-》Products-》isa分析.app-》show in finder -》右键显示包内容。把isa分析可执行文件拖到MachOView
工具里展开,找到Symbol Table
->*Symbols,右边搜索我们的类名“Direction
”,如图:
我们会发现在符号表里,多了一个叫METACLASS
,翻译过来就是“元类
”的意思。这个东西是编译的时候,系统帮我们生成的。所以我们上面打印的3d810就是Direction的元类。
在元类的基础上,继续重复上面的操作,我们会得到一个根元类地址,而且发现元类的isa指向的就是根元类;顺便打印了一下根元类地址,发现打印出来是NSObject,我们针对NSObject类重复上面操作,发现NSObject类的isa指向的就是根元类地址:
由此我们可以得到这样一副关系:
1、对象的isa -》类,类的isa -》元类,元类的isa -》根元类
2、根类(NSObject类)的isa -》根元类
为了验证以上的猜测,我们写了一些代码:
- 定义了两个类
Direction
和定义DirectionChild
//定义Direction,继承自 - NSObject
@interface Direction:NSObject
@property (nonatomic,strong) NSString *name;
@property (nonatomic,assign) NSInteger indexValue;
@end
@implementation Direction
@end
//定义DirectionChild,继承自 - Direction
@interface DirectionChild:Direction
@property (nonatomic,strong) NSString *hobby;
@property (nonatomic,assign) NSInteger runSpeed;
@end
@implementation DirectionChild
@end
- 在main函数里实现:
int main(int argc, char * argv[]) {
// Direction *st = [Direction alloc];
// NSLog(@"%@",st);
//NSObject对象
NSObject *obj01 = [NSObject new];
//NSObject类
Class obj02 = NSObject.class;
//NSObject元类
Class obj03 = object_getClass(obj02);
//根元类
Class obj04 = object_getClass(obj03);
NSLog(@"\n NSObject对象-%p\n NSObject类-%p\n NSObject元类-%p\n NSObject根元类-%p\n\n ",obj01,obj02,obj03,obj04);
//Direction的元类
Class dMetaClass = object_getClass(Direction.class);
//元类的父类
Class dSuperClass = class_getSuperclass(dMetaClass);
NSLog(@"\n Direction情况打印 --> \n 元类-%p\n 元类的父类-%p %@\n \n ",dMetaClass,dSuperClass,dSuperClass);
//DirectionChild的元类
Class dChildMetaClass = object_getClass(DirectionChild.class);
//根元类
Class dChildSuperClass = class_getSuperclass(dChildMetaClass);
NSLog(@"\n DirectionChild情况打印 --> \n 元类-%p\n 元类的父类-%p %@\n ",dChildMetaClass,dChildSuperClass,dChildSuperClass);
//打印NSObject类的父类
Class nSuperClass = class_getSuperclass(NSObject.class);
NSLog(@"打印NSObject类的父类-->%@ - %p",nSuperClass,nSuperClass);
//打印根元类的父类
Class objectSuperClass = class_getSuperclass(obj04);
NSLog(@"打印根元类的父类-->%@ - %p",objectSuperClass,objectSuperClass);
...
...
}
输出结果如下:
由此可以得出结论:
- 如果一个类继承自NSObject,这个类的元类指向的是根元类;
- 如果一个类继承自NSObject的子类,这个类指向的是它父类的元类;
- NSObject没有父类
- 根元类的父类是NSObject类 由上可知,元类直接也存在着继承链关系。
下面我们来看一张关系图:
解释如下:
- 1 -》2 -》3 -》4 -》4 (isa走位图)
实例对象isa -》类,isa -》元类,isa -》NSObject根元类,isa -》NSObject类
- 7 -》8 -》9 -》4 -》4 (isa走位图)
父类实例对象isa -》父类,isa -》父类元类,isa -》NSObject根元类,isa -》NSObject类
- 6 -》5-》4 -》4 (isa走位图)
NSObject实例对象isa -》NSObject类,isa -》NSObject根元类,isa -》NSObject类
- 2 -》8 -》 5(类的继承链)
子类 -》父类 -》NSObject类 -》nil
- 3 -》9 -》4 -》5 (元类的继承链)
子类元类 -》NSObject根元类 -》NSObject类;
父类元类 -》NSObject根元类 -》NSObject类;
文章iOS底层实现分析之对象的本质提到过,类对象包含一个isa
,它的类型是Class
,继而我们又知道Class在底层的实现是objc_class
,那么objc_class的内部实现又是怎么样的呢?接下来我们就研究下:
typedef struct objc_class *Class;
打开objc4-818.2源码,搜索“struct objc_class
”
可以看到这样一段代码:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
...
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
#endif
if判断意思是,非2.0版本才会进入这个判断, 很明显我们当前用的都是最新的2.0版本,所以段代码不会走,我们不用管它,继续找下一个搜索结果:
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 vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
Class getSuperclass() const {
...
}
void setSuperclass(Class newSuperclass){
...
}
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
...
};
我们可以看到,它有一个被注释了的CLASS ISA,搜索"objc_object":
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
我们发现objc_object有一个ISA指针,所以可以得出objc_class也有isa 所以objc_class包含的信息大概如下:
1、Class ISA;//
2、Class superclass;//父类
3、cache_t cache; // 方法缓存
4、class_data_bits_t bits; // 用于获取类的信息,通过isa+掩码得到类的地址
我们在struct objc_class : objc_object还看到这样一个方法:
class_rw_t *data() const {
return bits.data();
}
点开data进去:
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
大概意思是bits & FAST_DATA_MASK返回一个class_rw_t结构体。
command
+单机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_ISA
uint16_t index;
...
const class_ro_t *ro() const {...}
//方法列表
const method_array_t methods() const {...}
//属性列表
const property_array_t properties() const {...}
///协议列表
const protocol_array_t protocols() const {...}
};
解释下~
rw:readWrite 可读可写;
ro:readOnly 只读
t:列表
我们可以看到,class_rw_t包含了一些属性、方法列表、属性列表、协议列表,还有class_ro_t
,那么class_ro_t是什么呢?
点进去class_ro_t可以看到:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
...
method_list_t *baseMethods() const {...};
...
}
class_ro_t
里也包含了一些属性和方法,跟class_rw_t
所不同的是,class_ro_t只读。
至此,我们已经对objc_class结构有了基本的认识,这会儿我有个疑问,系统是怎么通过class类找到属性值的呢?这里我们做一个测试,写一段代码:
int a[5] = {11,22,33,44,55};
/** 正常情况下,我们打印输出是这样写的:
for (int i = 0; i<5; i++) {
printf("\n%d", a[i]);
}
*/
///接下来打印一下每个值及值对应的地址
for (int i = 0; i<5; i++) {
printf("\n打印第%d个 -->%d - %p",i,a[i],&a[i]);
}
printf("\na数组的地址 - %p",&a);
打印结果如下:
我们发现,直接打印a的地址,结果和a数组第一个值的地址是一样的,我们还发现每两个值之间的地址都差了4个字节
,这是因为64位下,int类型占用4个字节。
接下来,我们可以做一个猜想,把上面打印的a[i]
换成*(a+i)
:
///这段代码追加到上面的代码后面
printf("\n\n下面是验证猜想:");
for (int i = 0; i<5; i++) {
printf("\n打印第%d个 -->%d - %p",i,*(a+i),&a[i]);
}
打印结果:
由此可以得出结论,数组指针是通过每个元素占用字节的偏移量来访问属性值,这也叫内存偏移。那么问题又来了,类是如何通过内存偏移找到属性值呢?
通过上面,我们知道了一个Class类,它的成员排序是ISA、superclass、cache、bits,我们通过计算前三个大小,就可以得到bits的地址。(属性列表就放在bits里)
- ISA是结构体指针,64位下占用8个字节。
- superclass类型是Class,它是一个结构体指针,也就是8字节大小。
- cache,它是一个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;
};
...
/**
省略部分的代码就是一些static修饰、方法,
前者存放在全局区、后者存放在方法区,都不占用结构体大小,
所以可以忽略那部分,只需要计算上面的大小
*/
/**
分析一下上面:
包含了_bucketsAndMaybeMask和一个联合体,
uintptr_t无符号整型,即_bucketsAndMaybeMask占用8个字节;
根据联合体互斥的特性,_originalPreoptCache是指针,可以得出联合体占用8个字节。
所以cache_t共占用16个字节。
*/
};
打开objc4-818.2源码工程,写入代码,ldb调试一下:
如图所示,一顿ldb命令
下来,得到了class_rw_t
的地址,有同学可能会对$2->data()
有疑问,上面有说到过class_rw_t是通过bits.data()去获取的,而$2明显是一个指针,指针的访问用->
,继续输入ldb命令:
最终,我们打印了DirectionChild的第一个属性hobby。
我们继续打印一下第二个属性:
ldb命令的执行过程中可能很容易出现,内存数据读取失败的提示:
Execution was interrupted, reason: EXC_BAD_ACCESS (code=1, address=0x40).
The process has been returned to the state before expression evaluation.
出现这样的提示不要慌,可以从前面几步重新开始执行一边,实在不行就重新build一下再走一遍流程。
接下来,我们按照读取属性的方式去读取一下class_rw_t里的方法,在那之前,我们先给DirectionChild类添加几个方法:
//定义DirectionChild,继承自 - Direction
@interface DirectionChild:Direction
@property (nonatomic,strong) NSString *hobby;
@property (nonatomic,assign) NSInteger runSpeed;
///方法
-(void)runSpeedNonTime;
+(void)runSpeedWithTime:(NSString *)time;
-(void)eatSomethingNonTime;
+(void)eatSomethingWithTime:(NSString *)time;
@end
@implementation DirectionChild
-(void)runSpeedNonTime{}
+(void)runSpeedWithTime:(NSString *)time{}
-(void)eatSomethingNonTime{}
+(void)eatSomethingWithTime:(NSString *)time{}
@end
ldb命令调试:
到这里,似乎翻车了?打印的方法信息都为空,为什么这样的方式打印,属性信息会出来呢? 我们打印属性信息是通过打印property_t,打印方法是通过打印method_t,我们来看下这两位哥们儿的结构:
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) { }
};
//看这里 <--
struct property_t {
const char *name;
const char *attributes;
};
class method_array_t :
public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{
typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super;
public:
method_array_t() : Super() { }
method_array_t(method_list_t *l) : Super(l) { }
const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const {
return beginLists();
}
const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const;
};
//看这里 <--
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;
};
...
};
我们发现method_t
并不像property_t
一样直接把属性暴露在外,而是用big结构体
再包一层,那么既然如此,我们就再多访问一层big()
:
如图所示,方法列表里包含了以下6个方法:
- runSpeedNonTime
- eatSomethingNonTime
- hobby
- setHobby
- runSpeed
- setRunSpeed
但是没有下面的
类方法
: - +(void)runSpeedWithTime:(NSString *)time;
- +(void)eatSomethingWithTime:(NSString *)time;
1、那么类方法
存放在哪里呢?
2、于此同时,我又给DirectionChild增加了一个成员变量bgColorValue
,按照上面的操作,发现属性列表里也没有打印出bgColorValue,它又存放到哪里了呢?
@interface DirectionChild:Direction
{
NSString *bgColorValue;
}
@property (nonatomic,strong) NSString *hobby;
@property (nonatomic,assign) NSInteger runSpeed;
///方法
-(void)runSpeedNonTime;
+(void)runSpeedWithTime:(NSString *)time;
-(void)eatSomethingNonTime;
+(void)eatSomethingWithTime:(NSString *)time;
@end
相关代码已经上传百度网盘:
- isa分析 0620 提取码:k7n2
- objc4-818.2 提取码:q71k