NSTaggedPointerString,__NSCFConstantString,__NSCFString和NSString的关系?NSString为什么用copy?

时间:2021-6-12 作者:qvyue

问题引入:

NSString都存储在堆区吗?会不会存在栈区,或者数据区呢?

NSString用copy修饰还是strong修饰?NSString调用copy和mutableCopy会创建一个新的内存空间吗?NSMutableString用copy修饰会导致什么样的后果?

一.各类型字符串的关系和存储方式

NSString和NSMutableString相信我们平时都用过n遍了,但NSString真的都是一个存储在堆区的对象吗?如果在不同区,对它们进行copy操作,内存地址又会是什么样呢?

几个需要注意的点:

内存地址由低到高分别为:程序区–>数据区–>堆区–>栈区
其中堆区分配内存从低往高分配,栈区分配内存从高往低分配

1.继承关系:

NSTaggedPointerString(栈区)      ---> NSString
__NSCFConstantString(数据常量区)  ---> __NSCFString (堆区) --->NSMutableString --->NSString

2.对于NSStringFromClass()方法,字符串较短的class,系统会对其进行比较特殊的内存管理,NSObject字符串比较短,直接存储在栈区,类型为NSTaggedPointerString,不论你NSStringFromClass多少次,得到的都是同一个内存地址的string;但对于较长的class,则为__NSCFString类型,而NSCFString存储在堆区,每次NSStringFromClass都会得到不同内存地址的string
3.__NSCFConstantString类型的字符串,存储在数据区,即使当前控制器被dealloc释放了,存在于这个控制器的该字符串所在内存仍然不会被销毁.通过快捷方式创建的字符串,无论字符串多长或多短,都是__NSCFConstantString类型,存储在数据区.

下面是实际代码测试的结果,对象内存地址和指针内存地址都作了相应注释:

 NSString *str1 = @"abcdeefghijk";//该种方式无论字符串多长,都是__NSCFConstantString类型,存储在数据区,0x0000000104b2c400
 NSString *strDigital = @"999";//即使很短,也是(__NSCFConstantString *)类型, 0x00000001092b1420,存储在数据区

 
 NSString *str1Copy = [str1 copy];//__NSCFConstantString,NSString类型的str1进行copy是浅拷贝,并不会拷贝一份新的内存地址,0x0000000104b2c400,内存地址和str1相同
 NSMutableString *str1MutableCopy = [str1 mutableCopy];//__NSCFString,NSString类型str1进行mutableCopy后是深拷贝,0x0000600000d7d590
 
 
 NSMutableString *str1MutaCopyCopy = [str1MutableCopy copy];//__NSCFString,mutableString类型的str1MutableCopy进行mutableCopy后是深拷贝,0x0000600000373980
 [str1MutableCopy appendString:@"12"];
[str1MutaCopyCopy appendString:@"34"];//本行代码崩溃,原因是mutableString进行copy虽然是深拷贝,但返回的是不可变类型,只有NSMutableString才有appendString方法,__NSCFString没有appendString方法;虽然用NSMutableString接收,但str1MutaCopyCopy的类型还是由实际运行过程决定
 
 
 NSString * cfStringLongClass = NSStringFromClass([ViewController class]);//较长类情况下,该方法获得的字符串是类型__NSCFString存储在堆区,0x00006000003739a0
 NSString *taggedStrShortClass = NSStringFromClass([NSObject class]);//较短类情况下,该方法获得的字符串是NSTaggedPointerString类型,存储在栈区,0x86b1d57ef6188519
 
 NSString *taggedStrShortClassCopy = [taggedStrShortClass copy];//(NSTaggedPointerString *) $0 = 0xac71e37414f16209 @"NSObject" 内存地址不会变,浅拷贝,值也相同
 
 NSString *taggedStrShortClassMutaCopy = [taggedStrShortClass mutableCopy];//(__NSCFString *) $2 = 0x00006000036f4030 @"NSObject",内存地址变化,深拷贝一份至堆区,值相同,

 NSObject *obj = [[NSObject alloc]init];//对象存储在堆区,0x0000600000c61160
 
 

 
 //总结:stringWithFormat方式,最终字符串的类型由字符串长度决定,少于10个字符类型为NSTaggedPointerString,否则为__NSCFString类型(长度的规律只适用于英文字母和数字,详见文末备注)
 NSString *strFormatLong = [NSString stringWithFormat:@"%@",@"01234567a909"];// ; 当长度超过9时,为__NSCFString,0x00006000003f6680,堆区
 NSString *strFormatShort = [NSString stringWithFormat:@"%@",@"012345678"];//当字符长度位数为9或以下时,strFormat为NSTaggedPointerString,0xfc16f577dc1ba4f7,存储在栈区
 
 NSString *strShortConstantCopy = [@"999" copy];//__NSCFConstantString,0x00000001092b1420,数据区.NSString调用copy是浅拷贝,和strDigital内存地址相同
 NSString *strShortConstantMutaCopy = [@"999" mutableCopy];//__NSCFString,0x0000600003fad410,堆区,NSMutable类型的string调用mutalbeCopy是深拷贝,返回一个b可变类型字符串;调用copy也是深拷贝,返回不可变字符串
 
 
 //从以下两个对象的内存地址可以得出结论:stringWithFormat不论接收什么类型的字符串参数,也无论它在数据区栈区还是堆区,都遵守长度临界值9的规则
 NSString *strShortConstantMutaCopyFormat = [NSString stringWithFormat:@"%@",strShortConstantCopy];//NSTaggedPointerString,0x8b25a274d8732690
 NSString *strShortConstantCopyFormat = [NSString stringWithFormat:@"%@",strShortConstantMutaCopy];//NSTaggedPointerString,0x8b25a274d8732690

二.NSString为什么用copy,copy修饰词到底做了什么工作?NSMutableString用copy修饰又如何?

测试主要注重下面几方面:

1.NSString分别用copy和strong修饰

2.来源为可变字符串和来源为不可变字符串

3.修改字符串之前和修改字符串之后

注意:

1.不可变类型的字符串是无法更改内容的,这里说的修改其实是修改它的指针,让它指向另一块内存,所以不会影响self.string; 而可变字符串是可以更改内容的,对于他的修改是修改这块内存的内容.

2.copy修饰词起作用是在setter方法中发生的,如果通过_string_copy = [NSMutableString new];设置,得到的self.string_copy仍然是浅拷贝.

3.一个对象的类型是由运行时决定的,和@property中的声明无关,OC对于类型不匹配只会报警告,编译可以通过,但并不代表你执行时候就不会出错.

4.NSString +copy ( 浅拷贝,返回不可变对象) NSString+mutableCopy(深拷贝,返回可变对象) NSMutableString+copy(深拷贝,返回不可变对象) NSMutableString+mutableCopy(深拷贝,返回可变对象)

依然先上代码,看运行结果,内存地址和相关注释如下:

@property (nonatomic, copy) NSString *string_copy;
@property (nonatomic, strong) NSString *string_strong;
 
@property (nonatomic, copy) NSMutableString   *string_muta_copy;
@property (nonatomic, strong) NSMutableString   *string_muta_strong;
@property (nonatomic, strong) UIView   *string_obj;
 
 
 NSString *string = [NSString stringWithFormat:@"%@",@"1234567890"];//p/x string (__NSCFString *) $0 = 0x0000600003e31800 @"1234567890"
    self.string_copy = string;// p/x _string_copy (__NSCFString *) $1 = 0x0000600003e31800 @"1234567890"
    self.string_strong = string;//p/x _string_strong (__NSCFString *) $2 = 0x0000600003e31800 @"1234567890"
    
    self.string_muta_copy = string;//(__NSCFString *) $1 = 0x0000600003e31800 @"1234567890"
    self.string_muta_strong = string;//(__NSCFString *) $2 = 0x0000600003e31800 @"1234567890"
    self.string_obj = string;//(__NSCFString *) $2 = 0x0000600003e31800 @"1234567890",类型是由具体运行过程决定的,即便用UIView接收它,也并没有影响string_obj的类型
    
    string = [NSString stringWithFormat:@"%@",@"abcdefghijklmn"];//p/x string (__NSCFString *) 0x0000600002d36dc0 @"abcdefghijklmn"
    /*
     一.来源是不可变字符串(修改前):
     a.当string的来源是非mutable类型时,不论是copy还是strong修饰,最终都只会仅仅拷贝一个指针,并不拷贝这块值的内存,因为这块内存存储的字符串
     本来就是非mutable,即不可修改类型的,深拷贝一份
     (lldb) p/x string
     (__NSCFString *) $0 = 0x0000600002d36da0 @"1234567890"
     (lldb) p/x _string_copy
     (__NSCFString *) $1 = 0x0000600002d36da0 @"1234567890"
     (lldb) p/x _string_strong
     (__NSCFString *) $2 = 0x0000600002d36da0 @"1234567890"
     
     一.来源是不可变字符串(修改后):
 -------->    此处开始修改源串string的值------>string = [NSString stringWithFormat:@"%@",@"abcdefghijklmn"];
     (因为string本身是不可变的,所以:虽然说修改string的值,倒不如说实际上是修改string这个指针,让string这个指针指向另一块内存区域,
     故结果是不会影响self.string_copy,self.string_strong所指向的那块内存)
     
     (lldb) p/x string
     (__NSCFString *) $3 = 0x0000600002d36dc0 @"abcdefghijklmn"
     (lldb) p/x _string_copy
     (__NSCFString *) $4 = 0x0000600002d36da0 @"1234567890"
     (lldb) p/x _string_strong
     (__NSCFString *) $5 = 0x0000600002d36da0 @"1234567890"
     (lldb)
     
     */
        
    
    NSMutableString *strMuta = [[NSMutableString alloc]initWithString:@"一二三四"];//p/x &strMuta  (NSMutableString **) $7 = 0x00007ffee4d919a0
    
    self.string_strong = strMuta;// p/x &_string_strong (NSString **) $6 = 0x00007f8c5fc0d788
    
    self.string_copy = strMuta;//p/x &_string_copy (NSString **) $8 = 0x00007f8c5fc0d780
    
    
    self.string_muta_strong = strMuta;//(__NSCFString *) $0 = 0x000060000032d500
    self.string_muta_copy = strMuta;//(__NSCFString *) $2 = 0x0000600000d264e0
    [self.string_muta_copy appendString:@"1234"];//崩溃,原因是:copy修饰的string_muta_copy在接收到字符串时,无论是可变还是不可变,都给你深拷贝一遍,NSMutableString调用copy方法虽是深拷贝,但返回的是不可变对象,不可变对象是没有appendString方法的
    /*
     二:来源是可变字符串(修改前)
     0.未修改源muta字符串时,可以看出:copy修饰的string_copy拷贝了一份内存,即值和strMuta一样,但实际存储这个字符串值的内存不是同一块.
     而strong修饰的string_strong只拷贝了一份指针,并没有拷贝存储这块值的内存,实际上只是指针不是同一个,指向的内存还是同一块
     (lldb) p/x strMuta
     (__NSCFString *) $0 = 0x000060000318ab20 @"一二三四"
     (lldb) p/x _string_copy
     (__NSCFString *) $1 = 0x0000600003f88120 @"一二三四"
     (lldb) p/x _string_strong
     (__NSCFString *) $2 = 0x000060000318ab20 @"一二三四"
     
     */
    
    [strMuta appendString:@"1234"];
    /*
     二.来源是可变字符串(修改后)
     1.修改源字符串strMuta后,由于string_copy指针指向新拷贝的那一块内存区域-->0x0000600003f88120,所以修改源串strMuta的那
     块内存-->0x000060000318ab20不会影响copy修饰的字符串;而string_strong只是拷贝了一个指针,这个指针仍然指向-->0x000060000318ab20这
     块区域,和源串是同一块内存,唯一不同的就是指针本身地址不同,而指针的内容是相同的,都存储着源串的内存地址,修改源串导致string_strong存储的值也
     随之变化
     
     lldb) p/x strMuta
     (__NSCFString *) $3 = 0x000060000318ab20 @"一二三四1234"
     (lldb) p/x _string_copy
     (__NSCFString *) $4 = 0x0000600003f88120 @"一二三四"
     (lldb) p/x _string_strong
     (__NSCFString *) $5 = 0x000060000318ab20 @"一二三四1234"

     */

从上面代码运行的结果分析,不难得出:

其实copy就是在setter方法里起了特殊作用,利用copy修饰的string,在setter方法中进行一次判断;a.如果来源是可变字符串,就深拷贝一份(为什么深拷贝?因为来源是可变字符串,这串字符就可能会被修改,如果不深拷贝一份的话,将来源串不小心被修改了,你的self.string也会跟着变化,如果发生了不可预见的结果,你肯定要怪xcode了,这个结果是你不希望发生的);b.如果来源是不可变字符串,就直接赋值,不拷贝内容(为什么只拷贝指针不拷贝内容?因为来源为不可变,你就算改变self.string也只能通过重新赋值来改变,赋值肯定是你自己操作的,如果因为重新赋值发生了不可预见的问题,那就是你自己的责任了)
从结论中我们可以推测NSMutableString不用copy修饰的原因了:如果你用copy去修饰一个NSMutableString的字符串属性string_muta_copy,如果将来你把一个不可变来源通过set方法赋值给这个属性,set方法内部会因为这个来源是不可变的进行一次copy,copy后再将新的对象赋给_string_muta_copy,这时候的拷贝是深拷贝,但返回的是不可变对象,而在你声明中你认为这个对象是可变的,编译时期也会被认为是可变的,你向他发送NSMutableString的方法消息不会报错,但到了运行时候回因为string_muta_copy是个不可变字符串而找不到方法.

对于NSMutableString类型的属性,我们在对它赋值时,最好确保是可变类型,防止以后你把它当做可变字符串,进行拼接操作,而实际上它却被你赋值了不可变字符串而出错

至此:大概讲述了NSString和NSMutableString调用copy和mutableCopy.修饰词用copy和strong的原因.

另外:我们通过把代码编译成C++文件,可以看出copy修饰的属性和strong修饰的属性,在底层的处理是不同的,下面是源码节选:

//ViewController对象在底层实则是一个结构体,它的每个属性都将作为该类型结构体的一个成员
struct ViewController_IMPL {
   struct UIViewController_IMPL UIViewController_IVARS;
   NSString *_string_copy;
   NSString *_string_strong;
   NSMutableArray *_array_muta_copy;
   NSMutableArray *_array_muta_strong;
   NSArray *_array_copy;
   NSArray *_array_strong;
};

//分别代表_string_copy和_string_strong在ViewController_IMPL结构体中的偏移量,通过偏移量+结构体内存地址就可以找到这个成员
extern "C" unsigned long OBJC_IVAR_$_ViewController$_string_copy;
extern "C" unsigned long OBJC_IVAR_$_ViewController$_string_strong;

// self.string_copy = string;
 ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setString_copy:"), (NSString *)string);

// self.string_strong = string;
   ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setString_strong:"), (NSString *)string);



//相当于_string_copy的getter方法,取_string_copy时,返回结构体里的该成员
static NSString * _I_ViewController_string_copy(ViewController * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_ViewController$_string_copy)); }

//OC中对copy修饰的属性所做的处理,这个方法下面会有详细说明
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

//相当于_string_copy的setter方法,可以看见,内部是调用了objc_setProperty方法做特殊处理的
static void _I_ViewController_setString_copy_(ViewController * self, SEL _cmd, NSString *string_copy) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ViewController, _string_copy), (id)string_copy, 0, 1); }


//_string_strong的getter方法
static NSString * _I_ViewController_string_strong(ViewController * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_ViewController$_string_strong)); }

//_string_strong的setter方法,可以明显看到和copy修饰的属性处理不一样,没有调用objc_setProperty方法
static void _I_ViewController_setString_strong_(ViewController * self, SEL _cmd, NSString *string_strong) { (*(NSString **)((char *)self + OBJC_IVAR_$_ViewController$_string_strong)) = string_strong; }

关于objc_setProperty:
objc-runtime源码里检索关键词,来到这里:

#define MUTABLE_COPY 2
 
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
//当shouldCopy为真,且不等于2(MUTABLE_COPY)时,copy为YES,一般objc_setProperty方法调用时会给shouldCopy传0,1,2(一般来说,0代表不copy,1代表copy,2代表mutableCopy)
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
 
//当shouldCopy为2时,mutableCopy为YES
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
 
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

对比上面传进来的参数:

objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ViewController, _string_copy), (id)string_copy, 0, 1)
可以看出参数依次传入了self,_cmd,偏移量,string_copy,非atomic,copy为YES

构造好参数之后,实际上是调用了reallySetProperty函数,下面查看该函数:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
//当offset为0时所做的处理,因为结构体第一个成员都是isa,如果offset为0代表是更改isa成员
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }
 
//旧值
    id oldValue;
    id *slot = (id*) ((char*)self + offset);
//当copy为YES时,调用copyWithZone方法
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
//当mutableCopy为YES时,调用mutableCopyWithZone方法
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
//如果objc_setProperty最后一个传0的话,则仅仅对该属性进行一次retain强引用
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }
 
 
 
//设置新值newValue,释放旧值oldValue
    if (!atomic) {
//非原子情况下的操作
        oldValue = *slot;
        *slot = newValue;
    } else {
//原子情况下的操作
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
 
    objc_release(oldValue);
}

感谢:最后,非常感谢有小伙伴向我提出了字符长度影响NSString stringWithFormat得到的字符串类型的问题.实际上系统底层对字符串的处理是按照一个字符对应一个字节(8位),ASCII码恰好用8位存储一个字符,但这些字符仅限英文字母和数字,首位为0,1-7位刚好可表示128个字符,由于字符标准不统一,其它语言的字符不能用标准的ASCII码来存储.所以汉字日文等等,即使是一个字符也用__NSCFString来存储,而不是按照低于10位采用NSTaggedPointerString来优化存储的方式.

原文链接:
NSTaggedPointerString,__NSCFConstantString,__NSCFString和NSString的关系?NSString为什么用copy?

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。