ios 类的结构分析下

428 阅读14分钟

前言

我前面的博客探讨对象的本质与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之前

截屏2021-07-09 下午6.23.50.png 类对象包含了最常用的信息:指向元类、父类、以及方法的缓存。它还有一个指针指向更多的额外信息class_ro_t,其中 ro表示read only 。这部分信息是只读的,其中包含了类名、方法、协议、实例变量和属性等信息。Swift类和Objective-C类均使用这个结构。但类一旦被使用,就会产生一些变化。这是ios14之前内存的变化。

ios14之后

未命名文件.jpg

  • First SubclassNext 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

7871624877600_.pic_hd.jpg 分析:CGM.CreateRuntimeFunction(FTy, "objc_setProperty"),创建了objc_setProperty方法。说明创建objc_setProperty方法llvm需要调用getSetPropertyFn()函数。源码查找调用getSetPropertyFn()函数的地方,向上查找最后定位到GetPropertySetFunction()函数,即调用此函数就生成objc_setProperty方法。

7891624879147_.pic_hd.jpg 分析:strategy.getkind()等于GetSetProperty时会调用GetPropertySetFunction()方法。那么strategy.getkind()什么情况下等于GetSetProperty呢? WeChate21c1e3151282ff48e433aa759c2000f.png 分析:当属性被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:0x1049ee0100x7ffeeb214c58,hello
strongstr:0x1049ee0100x600003a708e8,hello
copystr:0x1049ee0100x600003a708f0,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:0x600000d0cc600x7ffeee772c58,hello
strongstr:0x600000d0cc600x600003174fa8,hello
copystr:0x600000d0cc600x600003174fb0,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:0x10f19c0180x7ffee0a66c48,hello
strongstr:0x10f19c0180x6000011e07c8,hello
copystr:0x10f19c0180x6000011e07d0,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:0x6000015d0f600x7ffeea73fc48,hello word
strongstr:0x6000015d0f600x6000029a4bb8,hello word
copystr:0xec3c9c9f9ae973a10x6000029a4bc0,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,那么下面看一下copymutableCopy方法的区别,一定要自己代码敲一遍才能深刻理解,最好是能看一下源码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)。SELIMP的关系就像是一本书的目录页码,SEL是目录名称,IMP是页码,即SEL对应方法名,IMP对应方法实现的指针地址

2.1 类方法的继承

类方法在元类中,元类是类的实例,所以一切方法都是对象方法,万物皆对象,还记得isa指向图吗,我们再结合isa图探索一下。

656418fd0e244c558eb0d852cc0ba299~tplv-k3u1fbpfcp-watermark.image.png 这个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。