![](https://img.haomeiwen.com/i1694726/af525d66164420a7.jpg)
前言
上一片我们用LLDB
和源码探索类的底层实现,这篇文章主要补充一下上一篇以及一些问题,这里先留几个问题。
1、类在内存中会存放几份;
2、成员变量、实例变量和熟悉的关系;
3、为什么我们在class_rw_t
里面打印的firstSubclass
为nil
;
4、isKindOfClass
和isMemberOfClass
的区别;
我们先准备几个类信息,这里笔者就只给出头文件。
//1、CDPerson类
@interface CDPerson : NSObject
{
NSString *_nickName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)sayNB;
+ (void)sayHello;
@end
//2、CDPerson 的扩展
@interface CDPerson (CateA)
@property (nonatomic, strong, nonnull) NSString *hobby;
@property (nonatomic, assign) CGFloat height;
- (void)sing;
+ (void)run;
@end
//3、子类CDTeacher
@interface CDTeacher : CDPerson
@property (nonatomic, copy) NSString *tName;
@end
//4、子类CDStudent
@interface CDStudent : CDPerson
@property (nonatomic, copy) NSString *num;
- (void)doStudy;
+ (void)playBall;
@end
类在内存中会存在几份
1、由于类在地城c/c++
的实现也是对象;struct objc_class : objc_object
这个定义可以看出。由于每个对象都需要申请才有,类只会申请一次所以类对象在内存中的存在只有一份。
2、用例子证明:
CDPerson *p1 = [CDPerson alloc];
CDPerson *p2 = [CDPerson alloc];
NSLog(@"%p", object_getClass(p1));
NSLog(@"%p", [CDPerson class]);
NSLog(@"%p", p1.class);
NSLog(@"%p", p2.class);
NSLog(@"%p", [CDPerson class]);
其他打印结果为:
0x1000084f8
0x1000084f8
0x1000084f8
0x1000084f8
0x1000084f8
通过我们验证得出,内在内存中存储的地址只有一份。
成员变量、实例变量和属性
1、成员变量:和结构体下面的变量是一模一样的,只要还原成 c/c++
后又这么一个变量,那么这个变量就是成员变量。
2、实例变量:实例变量是一种特殊的成员变量,是由于对象类型的成员变量,即除开基本数据类型和 NSString
的成员变量都是实例变量。
3、属性:属性是由成员变量和 setter/getter
方法构成的。如果是只读属性就只有 getter
方法 。
@interface CDTest : NSObject
@end
@interface CDTest2 : NSObject
{
NSInteger _a; //成员变量
CDTest *_b; //实例变量(特殊的成员变量)
id _c; //实例变量(由于id 是 _objc_object* 类型)
}
@property(nonatomic, copy)NSString *d;
@end
属性的修饰符,copy、strong 的区别。我们都知道对属性的setter/getter 就是进行赋值和取之 的操作,那到底是怎么设计的呢?那我们设计一份源码编译来看看结果。
@interface CDTest2 : NSObject
{
NSInteger _a; //成员变量
CDTest *_b; //实例变量(特殊的成员变量)
id _c; //实例变量(由于id 是 _objc_object* 类型)
}
@property(nonatomic, copy)NSString *d;
@property(nonatomic, strong) NSString *e;
@property(nonatomic) NSString *f;
@property(copy) NSString *g;
@property(strong) NSString *h;
@end
编译成c++ 源码后为如下所示。
struct CDTest2_IMPL {
struct NSObject_IMPL NSObject_IVARS; // isa
NSInteger _a;
CDTest *_b;
id _c;
NSString *_d;
NSString *_e;
NSString *_f;
NSString *_g;
NSString *_h;
};
然后是实现文件的源码
//属性d 的getter的源码
static NSString * _I_CDTest2_d(CDTest2 * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_CDTest2$_d)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
// 属性d 的setter方法的源码
static void _I_CDTest2_setD_(CDTest2 * self, SEL _cmd, NSString *d) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct CDTest2, _d), (id)d, 0, 1); }
//属性e的getter/setter方法
static NSString * _I_CDTest2_e(CDTest2 * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_CDTest2$_e)); }
static void _I_CDTest2_setE_(CDTest2 * self, SEL _cmd, NSString *e) { (*(NSString **)((char *)self + OBJC_IVAR_$_CDTest2$_e)) = e; }
//属性f的getter/setter
static NSString * _I_CDTest2_f(CDTest2 * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_CDTest2$_f)); }
static void _I_CDTest2_setF_(CDTest2 * self, SEL _cmd, NSString *f) { (*(NSString **)((char *)self + OBJC_IVAR_$_CDTest2$_f)) = f; }
extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
//属性g的getter/setter
static NSString * _I_CDTest2_g(CDTest2 * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct CDTest2, _g), 1); }
static void _I_CDTest2_setG_(CDTest2 * self, SEL _cmd, NSString *g) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct CDTest2, _g), (id)g, 1, 1); }
//属性h的getter/setter
static NSString * _I_CDTest2_h(CDTest2 * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_CDTest2$_h)); }
static void _I_CDTest2_setH_(CDTest2 * self, SEL _cmd, NSString *h) { (*(NSString **)((char *)self + OBJC_IVAR_$_CDTest2$_h)) = h; }
可以发现在上面的一堆方法里面定义了两个特殊的方法 objc_setProperty 和 objc_getProperty。
可以看出,copy 修饰的属性setter 方法都是用objc_setProperty 实现的,而strong 就是直接对地址赋值。这也就是copy 和 strong 的最大的区别。对于objc_setProperty 方法在底层都是 reallySetProperty 这个方法的实现
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);
}
///这里就是处理原子和非原子的操作
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
对于 objc_getProperty 这个方法,尝试了很多 属性得出在copy的基础上如果是 atomic 才会使用这个方法,否者就是直接通过内存偏移直接取值。
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
:如果每个属性都编译成一份setXxx 和 getXxx 的形式,系统底层必然是无法实现的,因为每个开发者写的东西不一样而且很多。所以在这里就有一个中间封装的操作,把所有的成员变量的相应的值按照参数进行传递。
为什么我们在 class_rw_t 里面打印的 firstSubclass 为nil
编写如下一段代码和调试流程
CDPerson *person = [CDPerson alloc];
person.name = @"CD";
//LLDB 的调试
(lldb) p/x CDPerson.class
(Class) $1 = 0x00000001000084f8 CDPerson
(lldb) x/6gx 0x00000001000084f8
0x1000084f8: 0x00000001000084d0 0x0000000100375140
0x100008508: 0x0001000100875110 0x0002802900000000
0x100008518: 0x00000001008750d4 0x00000001000998e0
(lldb) p (class_data_bits_t *)0x100008518
(class_data_bits_t *) $2 = 0x0000000100008518
(lldb) p $2->data()
(class_rw_t *) $3 = 0x00000001008750d0
(lldb) p *$3
(class_rw_t) $4 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000736
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
这里我们可以看到 firstSubclass
为 nil
。
接下来我们在实例化一个对象 CDStudent *stu = [CDStudent alloc];
然后再来调试一下。
(lldb) p *$3
(class_rw_t) $5 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000736
}
}
firstSubclass = CDStudent
nextSiblingClass = NSUUID
}
可以看到为CDStudent
。
然后我们在实例化一个 CDTeacher
对象 CDTeacher *tea = [CDTeacher alloc];
然后再来调试。
(lldb) p *$3
(class_rw_t) $6 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295000736
}
}
firstSubclass = CDTeacher
nextSiblingClass = NSUUID
}
很明显,结果变成了CDTeacher
。然后从源码里面发现了这样一段代码。
/***********************************************************************
* addSubclass
* Adds subcls as a subclass of supercls.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
static void addSubclass(Class supercls, Class subcls)
{
objc_debug_realized_class_generation_count++;
subcls->data()->nextSiblingClass = supercls->data()->firstSubclass;
supercls->data()->firstSubclass = subcls;
......
}
结论:firstSubclass
为某个类第一次创建的时候去寻找父类的时候绑定给父类的。
类的扩展信息存储在什么地方
前面一篇文章讨论了类的结构和元类,以及类中的属性,成员变量和方法存储位置。
那么我们的扩展里面的属性,方法又是怎么存储的呢?我们还是结合 LLDB
和源码调试
- 我们还是按照之前的操作 打印出
class_rw_t
的结构。
(class_rw_t) $3 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4295003112
}
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
- 打印出方法列表
(lldb) p $3.methods()
(const method_array_t) $4 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x0000000100003428
}
arrayAndFlag = 4294980648
}
}
}
(lldb) p $4.list.ptr
(method_list_t *const) $5 = 0x0000000100003428
(lldb) p *$5
(method_list_t) $6 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 2147483660, count = 14)
}
- 打印出所有的方法
(lldb) p $6.get(0).name
(SEL) $11 = "hobby"
Fix-it applied, fixed expression was:
$6.get(0).name()
(lldb) p $6.get(1).name
(SEL) $12 = "setHobby:"
Fix-it applied, fixed expression was:
$6.get(1).name()
(lldb) p $6.get(2).name
(SEL) $13 = "height"
Fix-it applied, fixed expression was:
$6.get(2).name()
(lldb) p $6.get(3).name
(SEL) $14 = "setHeight:"
Fix-it applied, fixed expression was:
$6.get(3).name()
(lldb) p $6.get(4).name
(SEL) $15 = "sing"
Fix-it applied, fixed expression was:
$6.get(4).name()
(lldb) p $6.get(5).name
(SEL) $16 = "sayNB"
Fix-it applied, fixed expression was:
$6.get(5).name()
(lldb) p $6.get(6).name
(SEL) $17 = "p_method"
Fix-it applied, fixed expression was:
$6.get(6).name()
(lldb) p $6.get(7).name
(SEL) $18 = "p_name"
Fix-it applied, fixed expression was:
$6.get(7).name()
(lldb) p $6.get(8).name
(SEL) $19 = "setP_name:"
Fix-it applied, fixed expression was:
$6.get(8).name()
(lldb) p $6.get(9).name
(SEL) $20 = "name"
Fix-it applied, fixed expression was:
$6.get(9).name()
(lldb) p $6.get(10).name
(SEL) $21 = "setName:"
Fix-it applied, fixed expression was:
$6.get(10).name()
(lldb) p $6.get(11).name
(SEL) $22 = "age"
Fix-it applied, fixed expression was:
$6.get(11).name()
(lldb) p $6.get(12).name
(SEL) $23 = "setAge:"
Fix-it applied, fixed expression was:
$6.get(12).name()
(lldb) p $6.get(13).name
(SEL) $24 = ".cxx_destruct"
Fix-it applied, fixed expression was:
$6.get(13).name()
可以看到,扩展的方法在最前面,这也表明了我们的同名方法扩展的优先级别会高于元类的方法。
- 同理我们获取所有的属性
(lldb) p $3.properties()
(const property_array_t) $7 = {
list_array_tt<property_t, property_list_t, RawPtr> = {
= {
list = {
ptr = 0x0000000100008080
}
arrayAndFlag = 4295000192
}
}
}
(lldb) p *($7.list.ptr)
(property_list_t) $8 = {
entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 9)
}
然后一次打印
(lldb) p $8.get(0)
(property_t) $28 = (name = "hobby", attributes = "T@\"NSString\",&,N")
(lldb) p $8.get(1)
(property_t) $29 = (name = "height", attributes = "Td,N")
(lldb) p $8.get(2)
(property_t) $30 = (name = "name", attributes = "T@\"NSString\",C,N,V_name")
(lldb) p $8.get(3)
(property_t) $31 = (name = "age", attributes = "Tq,N,V_age")
(lldb) p $8.get(4)
(property_t) $32 = (name = "p_name", attributes = "T@\"NSString\",C,N,Vp_name")
(lldb) p $8.get(5)
(property_t) $33 = (name = "hash", attributes = "TQ,R")
(lldb) p $8.get(6)
(property_t) $34 = (name = "superclass", attributes = "T#,R")
(lldb) p $8.get(7)
(property_t) $35 = (name = "description", attributes = "T@\"NSString\",R,C")
(lldb) p $8.get(8)
(property_t) $36 = (name = "debugDescription", attributes = "T@\"NSString\",R,C")
也可以看到,属性的也是扩展的在前面。
结论:扩展里面的属性和方法的优先级会高于原类的属性方法。
isKindOfClass 和 isMemberOfClass 的区别
这两个问题应该是面试的一个比较高频的问题了,那下面这个你能准确的回答出来么?
NSObject *obj = [NSObject alloc];
CDPerson *person = [CDPerson alloc];
BOOL ret1 = [obj isKindOfClass:[NSObject class]];
BOOL ret2 = [obj isMemberOfClass:[NSObject class]];
BOOL ret3 = [person isKindOfClass:[NSObject class]];
BOOL ret4 = [person isKindOfClass:[CDPerson class]];
BOOL ret5 = [person isMemberOfClass:[NSObject class]];
BOOL ret6 = [person isMemberOfClass:[CDPerson class]];
NSLog(@"****** \n %d - %d - %d - %d - %d - %d \n******", ret1, ret2, ret3, ret4, ret5, ret6);
BOOL ret7 = [CDPerson isKindOfClass:[NSObject class]];
BOOL ret8 = [CDPerson isKindOfClass:[CDPerson class]];
BOOL ret9 = [CDPerson isMemberOfClass:[NSObject class]];
BOOL ret10 = [CDPerson isMemberOfClass:[CDPerson class]];
NSLog(@"****** \n %d - %d - %d - %d \n******",ret7, ret8, ret9, ret10);
打印的结果为 1 - 1 - 1 - 1 - 0 - 1
和 1 - 0 - 0 - 0
我们去查看底层源码的实现,结果为下面这样。
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
结合我们上篇文章的内容,无非就是成员方法的 isa
和 类方法的 isa
的问题。
可是结果真的就如同上面的源码一样么?我们如下打断点调试,到底是怎么走的呢?
![](https://img.haomeiwen.com/i1694726/25b7309eace6d874.png)
通过断电调试我们发现:
isKindOfClass
压根就不走断点,只有 isMemberOfClass
才会走断点。这里就很奇怪了,为什么 isKindOfClass
不走断点呢?是底层或者编译器做了什么操作么?接下来我们就从汇编的角度来看看到底是什么原因。。
从汇编看函数
先看几个函数
1、+/- class]
2、isKindOfClass:
3、isMemberOfClass:
- class 方法
我们只看源码就是+ (Class)class { return self; }
,也的确源码就是如此实现的,那咱么到汇编去看看,到底是如何调用的呢?结果为如下一段代码。
class 的汇编实现
从这个方法我们可以看出 +/- class
这个方法调用的 objc_opt_class
这个方法?那咱们搜索一下这个方法的实现。
![](https://img.haomeiwen.com/i1694726/fe3a7d9391e3235c.png)
通过源码可以发现:
-
isKindOfClass
通过汇编可以看到isKindOfClass
的汇编是调用的objc_opt_isKindOfClass
这个方法。
isKindOfClass 的汇编
然后全局搜索一下这个方法,可以找到对应的实现。
objc_opt_isKindOfClass 的实现
这里我们再来分析一下下面这个方法BOOL ret8 = [CDPerson isKindOfClass:[CDPerson class]];
的原理。
1、由于这里的
obj
是CDPerson
类,所以obj->getIsa() = 元类(CDPerson )
, 虽然这里都是CDPerson
但是他们不是一个是类,一个是元类,所以不相等;
2、CDPerson
的superClass
是NSObject
,所以NSObject
的getIsa()
还是NSObject
;而otherClass
是CDPerson
,所以第二次循环依然为false
;
3、由于NSObject
是根类,所以getSuperclass()
为nil
,结束循环,直接返回 false。
- isMemberOfClass
isMemberOfClass 的汇编
通过汇编我们发现isMemberOfClass
的底层实现就是objc_msgSend
;所以直接去找源码,而上面的断点调试也是可以知道isMemberOfClass
确实走了断点。
ret6 结果分析
再看 BOOL ret5 = [person isMemberOfClass:[NSObject class]]; 这个
1、
self
为person
对象,所以class
为CDPerson
(类);
2、[NSObject class]
获取得到的为NSObject
;
3、CDPerson
不等于NSObject
,所以结果为:false
。
结论:这里分析了一部分结果,对于其他的结果,读者可以依据笔者的思路去分析其他的结果。但是无非就是 isa 的 super 的 关系而已。
附一张isa/super 流程图
![](https://img.haomeiwen.com/i1694726/65a855ddd68909f8.png)
网友评论