iOS开发NSString的内存及copy和mutableCopy

私有类__NSCFConstantString,__NSCFString和NSTaggedPointerString

声明一个对象,可以用父类类型声明,子类来初始化,声明只是决定了你使用的时候能用哪个类型的方法,但是这么写是没问题的:

UIView *view = [[UILabel alloc] init];
NSLog(@"%@", NSStringFromClass(view.class));

这里声明用UIView声明,初始化用UILabel初始化,打印的结果UILabel而不是UIView,但是view因为是使用UIVIew类型接收的,它仅能使用UIView的是一些方法和属性而不能使用UIlabel的,当然,由于它实际就是一个UILabel类型的实例,它是可以直接转换的:

 UILabel *realLabel = (UILabel *) view;

现在我们看下NSString的几种情况:

NSString *test0 = @"Jeff";
NSString *test1 = [[NSString alloc] initWithString:@"Jeff"];
NSString *test2 = [NSString stringWithFormat:@"%@", @"Jeff"];
NSString *test3 = [[NSMutableString alloc] initWithString:@"Jeff"];
NSString *test4 = @"Jeff".copy;
NSString *test5 = @"Jeff".mutableCopy;
/**打印测试*/
NSArray<NSString *> *tests = @[test0, test1, test2, test3, test4, test5];
[tests enumerateObjectsUsingBlock:^(NSString *test, NSUInteger idx, BOOL *stop) {
    NSLog(@"test%ld的指针为:%p,类型为%@", idx, test, NSStringFromClass(test.class));
}];

先不说指针的打印,先说打印类型,由于NSMutableString本身是NSString的子类,因此全部用NSString接收是没有问题的,只是如果真正的结果是可变字符串的时候打印类型会才变成NSMutableString(想象中)。
打印结果如下:

2018-06-07 15:07:34.074744+0800 CopyTest[12843:255981] test0的指针为:0x1070090d8,类型为__NSCFConstantString
2018-06-07 15:07:34.074832+0800 CopyTest[12843:255981] test1的指针为:0x1070090d8,类型为__NSCFConstantString
2018-06-07 15:07:34.074951+0800 CopyTest[12843:255981] test2的指针为:0xa0000006666654a4,类型为NSTaggedPointerString
2018-06-07 15:07:34.075055+0800 CopyTest[12843:255981] test3的指针为:0x60c00005a5e0,类型为__NSCFString
2018-06-07 15:07:34.075150+0800 CopyTest[12843:255981] test4的指针为:0x1070090d8,类型为__NSCFConstantString
2018-06-07 15:07:34.075242+0800 CopyTest[12843:255981] test5的指针为:0x60c00005a490,类型为__NSCFString

我们可以看到,它并没有直接打印出NSString或者NSMutableString,而是打印出了3种类型,即_NSCFConstantString,__NSCFString和NSTaggedPointerString。而且内存地址也是有的相同有的不同,长短各异。
即是说,其实系统并不是用的NSString来作为当前的类型,而是内部有私有的类型来作了处理。

__NSCFConstantString

可以看到,此打印的结果的内存地址均相同,该内存地址实际上是在常量区,并不会释放,它是不可变的,拿test0的情况单独举例:

NSString *test0 = @"Jeff";
NSLog(@"变化前内存地址:%p",test0);
test0 = @"Tom";
NSLog(@"变化后内存地址:%p",test0);
NSString *Jeff = @"Jeff";
NSLog(@"新建的Jeff内存地址:%p",Jeff);

打印结果:

2018-06-07 15:36:11.604766+0800 CopyTest[13606:278220] 变化前内存地址:0x105ed90b8
2018-06-07 15:36:11.604917+0800 CopyTest[13606:278220] 变化后内存地址:0x105ed90f8
2018-06-07 15:36:11.605046+0800 CopyTest[13606:278220] 新建的Jeff内存地址:0x105ed90b8

test0改变成@"Tom"后,在常量区为@"Tom"开辟了新了内存地址,并把test0的指针指向了它,于是test0的指向的地址就变了。但是@"Jeff"的地址已经生成过了,且并不会释放掉,这样Jeff变量等于@"Jeff"相当于把指针又指向了最初的@"Jeff"的内存地址,这样它和第一个打印的地址是相同的。而它们的类型之前已经验证过了,是__NSCFConstantString类型,此类型内存地址即在常量区,test0,test1,test4均为此种类型。__NSCFConstantString是不可变的,初始化为@"Jeff"就是@"Jeff",重新赋值为@"Tom"只是把指针指向了一个新的__NSCFConstantString地址,之前的@"Jeff"的地址没有任何变化,看它的名字中带"Constant"也能看出来它的性质。

__NSCFString

测试中可以看出,test3和test5为此类型,test3写法上我们其实是期望它最终类型是NSMutableString类型(虽然接受是用父类NSString接收的),可以看出,系统内部实际上是用__NSCFString类型来处理NSMutableString的,新建一个任何的NSOBject类型并打印地址,可以看出其地址和test3类似,而实际上也正是如此,__NSCFString和对象一样,内存地址是在堆中,而不是常量区,它的内存管理和常规对象类似。test5的情况后面讨论copy和mutableCopy再提。NSMutableString是使用__NSCFString来处理,但不能认为所有的__NSCFString类型均是NSMutableString类型。

NSTaggedPointerString

有兴趣的可以去看下【译】采用Tagged Pointer的字符串
个人觉得其实只要知道此类型的具体的表现即可,此类型字符串地址存放在栈,为不可变的类型,test2如果把@"Jeff"改成一个很长的字符串成(可以尝试使劲复制粘贴),你会发现它又变成了一个__NSCFString类型,但是它还是不可变字符串,这也是为什么说不能认为所有的__NSCFString类型均是NSMutableString类型的原因。

类型小结:

1.NSString对应的私有类主要为 __NSCFConstantString类型,部分情况为NSTaggedPointerString,本来应该为NSTaggedPointerString但因长度过长,也可能为__NSCFString类型。即NSString在私有类体现上可能为3种中的任意一种。
另外,如果字符串私有类型为__NSCFConstantString或者NSTaggedPointerString,那么它实际上应该就体现为一个NSString类型。
2.NSMutableString在私有类体现上均为__NSCFString,但__NSCFString并不一定体现为NSMutableString类型
3.鉴于同一个字符串如"Jeff"的表现可能是任何一种情况,因此我们不能直接用内存地址来判断两个字符串是否相同,而需要用系统提供的isEqualToString方法来判断。

copy和mutableCopy

示例:

NSString *string = @"Jeff";//基础不可变字符串
NSString *stringCopy = string.copy;
NSString *stringMCopy = string.mutableCopy;
NSMutableString *mutableString = [NSMutableString stringWithString:@"Jeff"];//基础可变字符串
NSString *mutableStringCopy = mutableString.copy;
NSString *mutableStringMCopy = mutableString.mutableCopy;

NSArray<NSString *> *tests = @[string, stringCopy, stringMCopy, mutableString, mutableStringCopy, mutableStringMCopy];
for (NSString *test in tests) {
    NSLog(@"指针为:%p,类型为%@", test, NSStringFromClass(test.class));
}

打印结果:

2018-06-07 16:48:49.683133+0800 CopyTest[15592:335287] 指针为:0x10c1ef088,类型为__NSCFConstantString
2018-06-07 16:48:49.683265+0800 CopyTest[15592:335287] 指针为:0x10c1ef088,类型为__NSCFConstantString
2018-06-07 16:48:49.683373+0800 CopyTest[15592:335287] 指针为:0x6080000512e0,类型为__NSCFString
2018-06-07 16:48:49.683458+0800 CopyTest[15592:335287] 指针为:0x608000051280,类型为__NSCFString
2018-06-07 16:48:49.683573+0800 CopyTest[15592:335287] 指针为:0xa0000006666654a4,类型为NSTaggedPointerString
2018-06-07 16:48:49.683691+0800 CopyTest[15592:335287] 指针为:0x608000051340,类型为__NSCFString

分析:

1.声明了一个NSString和一个NSMutableString(备注写了"基础xx字符串")并在其基础上做操作。为了避免接收类型错误,这里还是均用父类NSString来接收copy和mutableCopy的结果。我们发现string和stringCopy内部私有类型为__NSCFConstantString,然后,可变字符串mutableString通过点copy出来的mutableStringCopy私有类型为NSTaggedPointerString类型。而__NSCFConstantString和NSTaggedPointerString具体体现其实就是NSString,即为不可变字符串,区别只是string和stringCopy内存地址相同,在常量区,mutableStringCopy在栈。
2.基础不可变字符串string通过点mutableCopy出来的stringMCopy、基础可变字符串mutableString、以及mutableString.mutableCopy产生的mutableStringMCopy均为__NSCFString类型,但__NSCFString并不能确定其是否是体现为NSMutableString类型,这里可以强制转换下并随便调用下NSMutableString的某个方法验证其确实就是NSMutableString类型。

结论:

1.NSString类型实例通过点copy得到一个内存地址相同的NSString,地址在常量区
2.NSString类型实例通过点mutableCopy得到一个NSMutableString类型
3.NSMutableString通过点copy得到一个NSString类型,但内存地址和1中有所不同,地址在栈区
4.NSMutableString通过mutableCopy得到一个新的NSMutableString类型
5.NSMutableString类型实例内存地址均在堆区

声明为属性时,NSString里copy和strong的区别和注意事项

赋值原理

很多人可能知道,NSString属性一般用copy,为了防止接收NSMutableString后被修改,但是没有深入理解的话,属性虽然写成copy了,但可能还是会犯一些小错误。
原理上来说,copy和strong的区别,其实是在于set方法,如下:

@interface ViewController ()
@property(nonatomic, copy) NSString *stringCopy;
@property(nonatomic, strong) NSString *stringStrong;

@end

@implementation ViewController
- (void)setStringCopy:(NSString *)stringCopy {
    _stringCopy = stringCopy.copy;
}

- (void)setStringStrong:(NSString *)stringStrong {
    _stringStrong = stringStrong;
}

系统内部会根据不同的前缀按照上面的形式来做set方法的处理,这里我重写出来,区别在于,copy修辞的属性,set方法会使用里属性变量会接收传进来的值的copy对象,而不是像strong,直接接收字符串对象,而我们赋值的时候是可以接收一个不可变的NSMutableString类型的字符串的,因为它是NSString的子类。strong修辞的stringStrong属性,接收一个NSStirng字符串类型,自然是没有太大的问题,因为接收的字符串是不会变化一直存在的,好比:

NSString *stringTest = @"test1";
self.stringStrong = stringTest;
stringTest = @"test2";

这里只是stringTest重新指向了一个新的常量区的内存地址(@"test2"),而stringStrong并没有变化,还是之前指向的地址(@"test1"),但如果接收的是一个堆里的NSMutableString,那就不同了,例如:

NSMutableString *test = [NSMutableString stringWithString:@"test"];
self.stringStrong = test;
[test appendString:@"appendString"];

首先是新建了一个可变的字符串,然后stringStrong=test,因为是strong修辞的,所以属性直接指向了它,最后这个可变字符串添加了额外的"appendString",因为属性指向了这个可变字符串,所以属性的值也跟着变化了。同样的,我们用stringCopy来执行同样的逻辑:

NSMutableString *test = [NSMutableString stringWithString:@"test"];
self.stringCopy = test;
[test appendString:@"appendString"];

因为self.stringCopy = test;内部实际上是把_strongCopy赋值为test.copy了,它是一个不可变字符串类型,且是一个区别于变量test的新的内存地址,因此test后面即使修改了,也对stringCopy不会有影响,这也是为什么字符串一般需要用copy的原因。

注意事项!!

重写字符串属性set方法一定要记得先copy再赋值!

上面我们已经知道,copy和strong修辞NSString,其原因在于set方法的不同,而很多时候我们重写set方法都是直接接收入参,例如这样:

@interface ViewController ()
@property(nonatomic, copy) NSString *name;
@property(nonatomic, copy) NSString *father;

@end

@implementation ViewController
- (void)setName:(NSString *)name {
    _name = name;
 // _name = name.copy;//应该这么写
    if ([name isEqualToString:@"Jeff"]) {
        _father = @"Tom";
    }
}

这里只是打个比方,只是说明有些时候会重写set方法来执行一些特定逻辑,但是因为重写的时候,直接用的是_name=name,这样做其实copy的修辞已经没有意义了,这个随便测试一下就能知道结果。当name属性接收一个可变字符串,且可变字符串有变动,name属性也还是会一起变动,这里_name赋值应该是_name=name.copy,而不是直接赋值,直接赋值和strong没有区别。重写set方法的时候一定要注意。

构造方法也一样,需要传入copy后的字符串赋值

如上面的例子,如果仅用name来构造控制器ViewController,且写成这样:

- (instancetype)initWithName:(NSString *)name {
    self = [super init];
    if (self) {
        _name = name;
//       _name = name.copy;//应该这么写
    }

    return self;
}

这样写也是有问题的,因为这里赋值name属性用的是属性的常量_name,没有调用set方法,所以必须把赋值改为_name=name.copy,否则,构造的时候传入的是个不可变字符串,且在构造完后立马修改它,属性也会跟着改变。
这里如果写成self.name = name走set方法来赋值属性本身是可以避免这种情况,但不建议,构造方法一般还是直接操作属性的变量_name合适。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容