私有类__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合适。