前言
我前面的博客探讨对象的本质与isa我们发现成员变量底层实现只是添加了一个变量而没有实现get和set方法,而属性变量底层实现会变成_变量,并且会实现get和set方法,但是底层set方法有些是通过objc_setProperty设置属性值,有些是通过首地址+偏移量的方式,当时我们留了一个悬念。同样在博客类的结构分析上中我们发现类的实例方法在类中,但是类的类方法在哪里呢?今天就深入探讨下这两个问题。
1.0 类的内存优化
探讨类的结构之前先来总结一下WWDC2020中关于runtime对类的内存优化。首先了解两个概念
Clean Memory:"干净内存",被加载后不会再变化的内存,类从磁盘第一次加载到内存中就是Clean Memory,class_ro_t就是clean Memory,因为是只读的,ro就是read only。Dirty Memory:"脏内存",在进程运行是会发生变化的内存,类一旦被使用就是Dirty Memory,因为运行时会写入新的数据,添加方法等。class_rw_t就是Dirty Memory,rw就是read write。- 区别:
clean Memory可以进行移除从而节省更多内存空间,因为如果需要clean memory系统就可以从磁盘中重新加载;Dirty Memory只要进程在运行,它就必须一只存在,所以特别消耗内存,那么就需要尽可能的把Dirty Memory转化成clean Memory。
ios14之前
类对象包含了最常用的信息:指向元类、父类、以及方法的缓存。它还有一个指针指向更多的额外信息class_ro_t,其中 ro表示read only 。这部分信息是只读的,其中包含了
类名、方法、协议、实例变量和属性等信息。Swift类和Objective-C类均使用这个结构。但类一旦被使用,就会产生一些变化。这是ios14之前内存的变化。
ios14之后
First Subclass和Next Sibling Class指针让运行时可以遍历当前使用的所有类Methods 、 Properties 、 Protocols,这部分也是可以在运行时进行修改的。在实践中发现,其实只有大约10%类的方法会发生变化,所以这部分内存可以得到优化,滕出一些空间- Demangled Name 只会被Swift类所使用,而且除非有需要获取它们的Objective-C名称,甚至都不会用到。
所以后两个不常用的部分,我们又可以拆分出来。这样就把
class_rw_t,拆成了2部分,如果确实有需要,我们才会这部分class_rw_ext_t结构分配内存,大约90%的类都不需要这部分额外的数据。这是runtime对类结构做的优化,目的是尽可能的减少Dirty Memory。
2.0 objc_setProperty
我们用对象的本质与isa中的示例main.cpp接着分析。
main.m如下:
@interface LGWPerson:NSObject{
NSString* sex;
}
@property(nonatomic,copy)NSString* name;
@property(nonatomic,strong)NSString* nickname;
@property(nonatomic,assign)int age;
@end
@implementation LGWPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
main.cpp如下:
typedef struct objc_object LGWPerson;
typedef struct {} _objc_exc_LGWPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_name;
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_nickname;
extern "C" unsigned long OBJC_IVAR_$_LGWPerson$_age;
typedef struct objc_object *id;
typedef struct objc_selector *SEL;
typedef struct objc_class *Class;
struct NSObject_IMPL {
Class isa;
};
struct LGWPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *sex;
int _age;
NSString *_name;
NSString *_nickname;
};
// @implementation LGWPerson
static NSString * _I_LGWPerson_name(LGWPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGWPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_LGWPerson_setName_(LGWPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGWPerson, _name), (id)name, 0, 1); }
static NSString * _I_LGWPerson_nickname(LGWPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGWPerson$_nickname)); }
static void _I_LGWPerson_setNickname_(LGWPerson * self, SEL _cmd, NSString *nickname) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGWPerson$_nickname)) = nickname; }
static int _I_LGWPerson_age(LGWPerson * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_LGWPerson$_age)); }
static void _I_LGWPerson_setAge_(LGWPerson * self, SEL _cmd, int age) { (*(int *)((char *)self + OBJC_IVAR_$_LGWPerson$_age)) = age; }
// @end
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
return 0;
}
分析:属性变量name的set方法是通过objc_setProperty设置值的,而属性nickname和age的set方法是通过首地址+偏移量设置值的。说明代码编译的时候就已经确定了使用哪种方式设置属性的值。
1.0.1 LLVM探索set_Property
既然编译的时候就确定了属性变量set方法,那就在LLVM源码中探索一下set_Property
分析:
CGM.CreateRuntimeFunction(FTy, "objc_setProperty"),创建了objc_setProperty方法。说明创建objc_setProperty方法llvm需要调用getSetPropertyFn()函数。源码查找调用getSetPropertyFn()函数的地方,向上查找最后定位到GetPropertySetFunction()函数,即调用此函数就生成objc_setProperty方法。
分析:strategy.getkind()等于
GetSetProperty时会调用GetPropertySetFunction()方法。那么strategy.getkind()什么情况下等于GetSetProperty呢?
分析:当属性被
copy修饰是kind=GetSetProperty,也就是说属性被copy修饰时,设置属性的值才会调用set_Property,否则设置属性的值为首地址+偏移量。
1.0.2 objc源码探索objc_setProperty
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) __attribute__((always_inline));
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();
}
//release旧值
objc_release(oldValue);
}
分析:objc_setProperty设置属性值的过程就是release旧值,retain新值,
copyWithZone会开辟新的内存空间。
1.0.3 copy属性修饰符拓展
NSString不可变对象实例变量直接赋值
@interface ViewController()
@property(nonatomic,strong)NSString* strongstr;
@property(nonatomic,copy)NSString* copystr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString* originstr=@"hello";
_strongstr=originstr;
_copystr=originstr;
NSLog(@"origin:%p,%p,%@",originstr,&originstr,originstr);
NSLog(@"strongstr:%p,%p,%@",_strongstr,&_strongstr,_strongstr);
NSLog(@"copystr:%p,%p,%@",_copystr,&_copystr,_copystr);
}
输出:
origin:0x1049ee010,0x7ffeeb214c58,hello
strongstr:0x1049ee010,0x600003a708e8,hello
copystr:0x1049ee010,0x600003a708f0,hello
分析:origin为不可变对象NSString时,origin、strongstr、copystr三个不同的指针但指向同一个内存空间0x1049ee010
NSMutableString可变对象实例变量直接赋值
@interface ViewController()
@property(nonatomic,strong)NSString* strongstr;
@property(nonatomic,copy)NSString* copystr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableString * originstr=[NSMutableString stringWithFormat:@"hello"];
_strongstr=originstr;
_copystr=originstr;
[originstr setString:@"hello word"];
NSLog(@"origin:%p,%p,%@",originstr,&originstr,originstr);
NSLog(@"strongstr:%p,%p,%@",_strongstr,&_strongstr,_strongstr);
NSLog(@"copystr:%p,%p,%@",_copystr,&_copystr,_copystr);
}
输出:
origin:0x600000d0cc60,0x7ffeee772c58,hello
strongstr:0x600000d0cc60,0x600003174fa8,hello
copystr:0x600000d0cc60,0x600003174fb0,hello
分析:origin为可变对象NSMutableString时,origin、strongstr、copystr三个不同的指针但指向同一个内存空间0x600000d0cc60。copy和strong修饰符没有任何影响?继续探索一下
NSString不可变对象点语法赋值
@interface ViewController()
@property(nonatomic,strong)NSString* strongstr;
@property(nonatomic,copy)NSString* copystr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString * originstr=@"hello";
self.strongstr=originstr;
self.copystr=originstr;
NSLog(@"origin:%p,%p,%@",originstr,&originstr,originstr);
NSLog(@"strongstr:%p,%p,%@",_strongstr,&_strongstr,_strongstr);
NSLog(@"copystr:%p,%p,%@",_copystr,&_copystr,_copystr);
}
输出:
origin:0x10f19c018,0x7ffee0a66c48,hello
strongstr:0x10f19c018,0x6000011e07c8,hello
copystr:0x10f19c018,0x6000011e07d0,hello
分析:origin为不可变对象NSString时,origin、strongstr、copystr三个不同的指针但指向同一个内存空间0x10f19c018
NSMutableString可变对象点语法赋值
#import "ViewController.h"
@interface ViewController()
@property(nonatomic,strong)NSString* strongstr;
@property(nonatomic,copy)NSString* copystr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableString * originstr=[NSMutableString stringWithFormat:@"hello"];
self.strongstr=originstr;
self.copystr=originstr;
[originstr setString:@"hello word"];
NSLog(@"origin:%p,%p,%@",originstr,&originstr,originstr);
NSLog(@"strongstr:%p,%p,%@",_strongstr,&_strongstr,_strongstr);
NSLog(@"copystr:%p,%p,%@",_copystr,&_copystr,_copystr);
}
输出:
origin:0x6000015d0f60,0x7ffeea73fc48,hello word
strongstr:0x6000015d0f60,0x6000029a4bb8,hello word
copystr:0xec3c9c9f9ae973a1,0x6000029a4bc0,hello
分析:origin为可变对象NSMutableString时,strongstr和origin指向同一个内存空间0x6000015d0f60,而copystr却指向的是0xec3c9c9f9ae973a1,说明开辟了新内存。看来点语法赋值和实例变量直接赋值是有本质区别的,对容器对象NSArray和NSMutableArray同样进行上面的测试,结果也是一样,copy修饰时,可变容器对象点语法赋值时会进行内存的拷贝,strong只是把引用计数加1。
总结:
- 当被
copy修饰时,点语法赋值才会调用objc_setProperty,而下划线变量的方式赋值,只是成员变量的直接赋值。 - 当被
copy修饰时,如果赋值的对象是不可变对象,那么只是进行指针的拷贝即浅拷贝,不会进行内存拷贝。如果赋值的对象是可变对象,那么进行了内存的拷贝即深拷贝。 - 当被
strong修饰时,无论赋值的对象是可变还是不可变,只进行了指针拷贝即浅拷贝,只是将源字符串的引用计数加1。 - 一般声明一个NSString或者NSArray对象是
不希望它改变的,所以使用copy修饰,这样当被可变对象NSMutableString或者NSMutableArray赋值时会进行深拷贝,被赋值的对象是一份新的内存,可变对象的改变不会影响到新的内存空间。 assign修饰对象会造成野指针,因为assign设置新值时不会release旧值,assign一般用于修饰基本数据类型,基本数据类型的变量在栈中,栈中内存由系统管理分配和释放,不会造成野指针。- 顺便提一下
weak,用weak来修饰的话,对象释放的时候会把指针置为nil,从而避免了野指针的出现
1.0.4 copy和mutableCopy方法
上面探索了属性修饰符copy,那么下面看一下copy和mutableCopy方法的区别,一定要自己代码敲一遍才能深刻理解,最好是能看一下源码copy和mutableCopy的实现,但是Foundation没有开源,后面有机会反编译一下这个框架深入研究一下。
不可变对象NSString和NSArray
NSLog(@"----非容器不可变对象-----");
NSString * originstr=@"hello";
NSString * copystr=[originstr copy];
NSString* mutablestr=[originstr mutableCopy];
NSLog(@"originstr:%p--%@",originstr,originstr);
NSLog(@"copystr:%p--%@",copystr,copystr);
NSLog(@"mutablestr:%p--%@",mutablestr,mutablestr);
NSLog(@"----容器不可变对象-----");
NSArray* originArray=@[@"hello",@"word"];
NSArray* copyArray=[originArray copy];
NSArray* mytableArray=[originArray mutableCopy];
NSLog(@"originArray:%p--%@",originArray,originArray);
NSLog(@"copyArray:%p--%@",copyArray,copyArray);
NSLog(@"mytableArray:%p--%@",mytableArray,mytableArray);
输出:
----非容器不可变对象-----
originstr:0x10dcc2040--hello
copystr:0x10dcc2040--hello
mutablestr:0x600000f900c0--hello
----容器不可变对象-----
originArray:0x6000001ca480--(
hello,
word
)
copyArray:0x6000001ca480--(
hello,
word
)
mytableArray:0x600000f90060--(
hello,
word
)
分析:copystr和copyArray指针可以看出,对于不可变对象,copy只是进行了指针的拷贝,并没有拷贝内存即浅拷贝;mutablestr和mytableArray可以看出,对于不可变对象,mutableCopy进行了内存的拷贝即深拷贝。
可变对象NSMutableString和NSMutableArray
NSLog(@"----非容器可变对象-----");
NSMutableString * originstr=[NSMutableString stringWithFormat:@"hello"];
NSString * copystr=[originstr copy];
NSString* mutablestr=[originstr mutableCopy];
[originstr setString:@"word"];
NSLog(@"originstr:%p--%@",originstr,originstr);
NSLog(@"copystr:%p--%@",copystr,copystr);
NSLog(@"mutablestr:%p--%@",mutablestr,mutablestr);
NSLog(@"----容器可变对象-----");
NSMutableArray* originArray=[NSMutableArray arrayWithObjects:@"hello", nil];
NSArray* copyArray=[originArray copy];
NSArray* mytableArray=[originArray mutableCopy];
[originArray addObject:@"word"];
NSLog(@"originArray:%p--%@",originArray,originArray);
NSLog(@"copyArray:%p--%@",copyArray,copyArray);
NSLog(@"mytableArray:%p--%@",mytableArray,mytableArray);
NSLog(@"originArray第一个元素地址:%p",originArray[0]);
NSLog(@"copyArray第一个元素地址:%p",copyArray[0]);
NSLog(@"mytableArray第一个元素地址:%p",mytableArray[0]);
输出:
----非容器可变对象-----
originstr:0x600000196b50--word
copystr:0xf28896a3aa0c427f--hello
mutablestr:0x6000001f0000--hello
----容器可变对象-----
originArray:0x6000001d70c0--(
hello,
word
)
copyArray:0x600000d98380--(
hello
)
mytableArray:0x600000180000--(
hello
)
originArray第一个元素地址:0x107438038
copyArray第一个元素地址:0x107438038
mytableArray第一个元素地址:0x107438038
分析:
- 对于
可变对象originstr和originArray,无论是copy还是mutableCopy都是对内存的拷贝即深拷贝,可变对象的改变不会影响深拷贝的值。 容器对象深拷贝,但是里面的元素是浅拷贝
总结
不可变对象:copy是浅拷贝,mutableCopy是深拷贝可变对象:copy和mutableCopy都是深拷贝容器对象的深拷贝,但是里面的元素是浅拷贝
2.0 类方法归属分析
上一篇文章类的结构分析上我们知道对象是类的实例,类是元类的实例,那么对象的实例方法在类中,那么类方法是否在元类中呢?我们通过runtime探索一下
@interface GyPerson : NSObject
- (void)sayHello;
+ (void)sayHappy;
@end
@implementation GyPerson
- (void)sayHello{
}
+ (void)sayHappy{
}
@end
void lgObjc_copyMethodList(Class pClass){
unsigned int count = 0;
Method *methods = class_copyMethodList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Method const method = methods[i];
//获取方法名
NSString *key = NSStringFromSelector(method_getName(method));
LGLog(@"Method, name: %@", key);
}
free(methods);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
const char * className = class_getName(GyPerson.class);
NSLog(@"----类方法-----");
Class gyperson=objc_getClass(className);
lgObjc_copyMethodList(gyperson);
NSLog(@"----元类方法-----");
Class meteperson=objc_getMetaClass(className);
lgObjc_copyMethodList(meteperson);
}
return 0;
}
输出如下:
----类方法-----
Method, name: sayHello
----元类方法-----
Method, name: sayHappy
分析:通过runtime api class_copyMethodList获取到类和元类的方法列表,并打印出来,发现sayHello()实例方法在GyPerson类中,sayHappy()类方法在GyPerson元类中,这正好验证了我们的猜想。我们还可以利用runtime api class_getInstanceMethod来判断是否存在sayHello()或sayHappy()方法。
int main(int argc, const char * argv[]) {
@autoreleasepool {
const char * className = class_getName(GyPerson.class);
//类
Class gyperson=objc_getClass(className);
//元类
Class meteperson=objc_getMetaClass(className);
//判断类中是否有sayHello
Method method1=class_getInstanceMethod(gyperson, @selector(sayHello));
//判断类中是否有sayHappy
Method method2=class_getInstanceMethod(gyperson, @selector(sayHappy));
//判断元类中是否有sayHello
Method method3=class_getInstanceMethod(meteperson, @selector(sayHello));
//判断元类中是否有sayHappy
Method method4=class_getInstanceMethod(meteperson, @selector(sayHappy));
NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
}
return 0;
}
输出如下:
0x100008110-0x0-0x0-0x1000080a8
分析:0x100008110是方法sayHello()指针地址,0x1000080a8是方法sayHappy()指针地址,0x0代表没有。
我们通过源码看一下Method的结构:
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
分析:Method是一个指向objc_method的结构体指针,有三个属性变量method_name(SEL)、method_types(char*)、method_imp(IMP)。SEL和IMP的关系就像是一本书的目录与页码,SEL是目录名称,IMP是页码,即SEL对应方法名,IMP对应方法实现的指针地址。
2.1 类方法的继承
类方法在元类中,元类是类的实例,所以一切方法都是对象方法,万物皆对象,还记得isa指向图吗,我们再结合isa图探索一下。
这个isa走位和继承图还可以看出,
实例的方法查找流程不会到元类中,只会沿着类的继承关系一直找到NSObject的父类nil,而类方法的查找流程会进入元类,沿着元类的继承关系一直找到NSObject的父类nil,即NSObject是任何对象的根,都会查找到NSObject。
demo测试一下:创建子类Gteacher,创建父类Gperson,在父类中添加实例方法say666(),创建NSObject分类,添加实例方法和类方法say666(),main函数中调用Gteacher类方法say666()
@interface Gteacher: NSObject
@end
@implementation Gteacher
@end
@interface Gperson: NSObject
-(void)say666:(NSString*)str;
@end
@implementation Gperson
-(void)say666:(NSString*)str{
NSLog(@"父类实例方法:%@",str);
}
@end
//NSObject分类方法
+(void)say666:(NSString*)str{
NSLog(@"NSObject元类:%@",str);
}
-(void)say666:(NSString*)str{
NSLog(@"NSObject根类:%@",str);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
[LGTeacher say666:@"ok666"];
}
return 0;
}
输出:
NSObject元类:ok666
分析:类Gteacher方法say666在元类中,那么会进入元类中查找该方法,如果找不到就根据元类Gteacher继承关系在父元类Gperson中查找,而say666在父类中是实例方法,实例方法在类中,所以在父元类中找不到say666,所以根据元类的继承一直找到根元类NSObject,在根元类中找到了say666方法。测试还发现如果把分类NSObject类方法+(void)say666去掉,最后还会找到对象方法-(void)say666,这是因为根元类NSObject的父类是NSObject。
总结:
实例方法在类中,类方法在元类中对象是类的实例,所以对象方法在类中,类是元类的实例,类方法在元类中。- 编译器
自动生成元类,目的是存放类方法 实例方法查找流程不会到元类中,只会沿着类的继承关系一直找到NSObject的父类nil,而类方法的查找流程会进入元类,沿着元类的继承关系一直找到NSObject的父类nil,即NSObject是任何对象的根,都会查找到NSObject。