(0019)iOS 开发之关于__weak修饰NSString以及内存管理的问题

来源:互联网 发布:滚动屏编辑软件 编辑:程序博客网 时间:2024/06/05 13:26

前言:写这篇文章的初衷,是对阅读别人的博客提出的疑问,一路探索得来的。同时也要加强对内存管理以及block 的管理和使用。

ARC指南1 - strong和weak指针 写到:打印出来是"(null)"  __weak NSString *str = [[NSString alloc] initWithFormat:@"1234"];  

 NSLog(@"%@", str);  

粘贴代码到工程结果: 打印出来并不是(null)。于是又百度一番:发现:转向ARC的说明——翻译Apple官方文档 这样写:

NSString * __weak string2 = [[NSString alloc] initWithFormat:@"First Name"];

NSLog(@"string: %@", string); 

打印的才是 "(null)”。

看到这个结果是不是大吃一惊,__weak 修饰的 string不都是为null吗?为什么会前者是@“1234”。后者是"(null)”,是不是ARC 内存管理有问题呢?


于是我就查看了官方文档:https://developer.apple.com/library/content/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html

Take care when using __weak variables on the stack.  (要注意在栈上的变量)

Although string is used after the initial assignment, there is no other strong reference to the string object at the time of assignment; it is therefore immediately deallocated. The log statement shows that string has a null value. 


结果:__weak 修饰initWithFormat 初始化的对象时,两种写法是都是对的。重点在于打不打印 null 跟@“1234” 这个字符串的长度有关,长的话最好超过10 ,就可以打印 null ,小于9 就能正常打印。为什么会有这样的区别?


结论:

如果:用__weak 修饰 @"" 和WithString 方式构造的字符串会正常打印的,不会@“null”。

因为:这种初始化的string是常量,编译后放到程序的常量区(属于内存静态区),所以引用计数不会生效,放在常量区的数据是跟程序生命周期绑定的,存在内存静态区,不会被销毁,输出自然会有结果。



首先看一下NSString 的关于string初始化的方法

1.

#pragma mark *** Initializers ***

+ (instancetype)string;

+ (instancetype)stringWithString:(NSString *)string;

+ (instancetype)stringWithFormat:(NSString *)format, ...NS_FORMAT_FUNCTION(1,2);


- (instancetype)initWithString:(NSString *)aString;

- (instancetype)initWithFormat:(NSString *)format, ...NS_FORMAT_FUNCTION(1,2);


1.1 准备(引用isa来获取其所属类

关于isa提一点:isa是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。对象所属类的结构体指针类型!更多isa的可以自己去了解。

// 为了方便测试,我先写了个宏,用来打印NSString的值、类、内存地址。

#define TLog1(_var) ({ NSString *name = _var; NSLog(@"%@: %@ -> %p", name, [_var class], _var);})


1.2 NSString的创建

    NSString * temp000 = [NSStringstring];

    temp000 = @"string";

    NSString * temp111 = [[NSStringalloc]initWithString:@"string111111"];

    NSString * temp222 = [NSStringstringWithString:@"string"];

    NSString * temp333 = [[NSStringalloc]initWithFormat:@"First Name"];

    NSString * temp444 = [NSStringstringWithFormat:@"string"];

    

    TLog1(temp000);

    TLog1(temp111);

    TLog1(temp222);

    TLog1(temp333);

    TLog1(temp444);


其实上面的代码:temp111、temp222 会报 Using 'initWithString:' with a literal is redundant ,什么意思尼?

就是用这两种创建字符串的方法是多此一举,因为它有更方便的方法  NSString * temp111 =@"string111111";


以下是控制台的打印:

 :string: __NSCFConstantString -> 0x10f73c410

 :string111111: __NSCFConstantString -> 0x10f73c430

 :string: __NSCFConstantString -> 0x10f73c410

 :First Name: __NSCFString -> 0x600000424800

 :string: NSTaggedPointerString -> 0xa00676e697274736


看到上面这段测试代码,我们可以发现几点同我们想象不同的地方:

  • temp000、temp111、temp222  对象的isa 都是__NSCFConstantString(常量字符串);

  · temp333、temp444对象的 isa 有的 是NSTaggedPointerString,有的是__NSCFString;

  ·   后两种在string的值长度大于某一个阀值(经测试 = 9)时,是__NSCFString类型,(>9)时成了,NSTaggedPointerString。

  ·   temp333、temp444 创建的NSString和创建其他objc对象类似的,在堆上分配内存。

  ·   前3种的 内存地址码 长度短;后两种长。



1.3  测试2:如果把初始化的 string 都是设置 @“string”的话,再次运行,得到以下结论:

  ·  前3种初始化相同的string时,它们的isa和内存地址都是一样的;后2种初始化相同的string时,它们的isa和内存地址也是一样的;

 · 证明两种方式初始化相同的string并不会重新申请新的地址存放同样的字符串的。编译器不再生成一个重复的字符串而是返回该字符串的地址。


结论:

对一个__NSCFConstantString进行retain和copy操作都还是自己,没有任何变化,对其mutableCopy操作可将其拷贝到堆上,retainCount为1。对__NSCFString进行retain和mutableCopy操作时,其特性符合正常的对象特性。但是对其copy时,它却变成了一个_NSCFConstantString对象!(http://www.360doc.com/content/15/0304/17/9200790_452525916.shtml)


用 initWithFormat 初始化字符串:NSString *str = [[NSString alloc] initWithFormat:@"First Name"];

如果字符串的长度较短,那么的指针类型是NSTaggedPointerString,用指针地址的富余位存变量值,

实际上是把常量区的复制了一份在堆中,所以你可以release了。但是 @"First Name" 这个变量本身还是存放在常量区了,所以并没有省内存,反而是增加了内存的使用。

所以,如果直接用一个字符串产生另一个字符串,尽量少用 initWithFormat,而不是initWithString。


指针存在栈,它存的是变量的地址。变量可能在堆,可能在栈,也可能在常量区!

在栈内存。栈内存的变量由系统自动释放回收,无需担心。block在栈,可以copy到堆的,所以self ,@property (copy)  对其引用。



1.4 测试3:把 

NSString * temp555 = [NSString stringWithFormat:@"string"];  

NSString * temp666 = [NSString stringWithFormat:@"stringstring”];    

TLog1(temp555);

TLog1(temp666);


 :string: NSTaggedPointerString -> 0xa00676e697274736

 :stringstring:__NSCFString ->0x608000238880 

__NSCFString对象类型,在堆上,如果用__weak 修饰 stringWithFormat 的对象 ,为什么不管长短也能打印出来尼?


结论:

string的值长度大于某一个阀值(经测试 = 9)时,是NSTaggedPointerString类型,(>9)时成了,__NSCFString。但是stringWithFormat是类办法,内存资源上是autorelease的,不会立即释放,所以能打印。


http://bbs.51cto.com/thread-843658-1.html  这个解释了__weak 修饰stringWithFormat 的对象 ,为什么不管长短也能打印出的问题?

1、initWithFormat是实例办法

只能经由过程 NSString* str = [[NSString alloc] initWithFormat:@"%@",@"Hello World"] 调用,然则必须手动release来开释内存资源

  2、stringWithFormat是类办法

可以直接用 NSString* str = [NSString stringWithFormat:@"%@",@"Hello World"] 调用,内存资源上是autorelease的,并不是立即释放的,所以可以打印出来,不需要手动显式release!



Apple 的 NSTaggedPointerString?

NSTaggedPointerString 它的作用就是:节省短的变量的内存。用指针地址的富余位存变量值。

对象在内存中是对齐的,它们的地址总是指针大小的整数倍,通常为16的倍数。对象指针是一个64位的整数,而为了对齐,一些位将永远是零。Tagged Pointer利用了这一现状,它使对象指针中非零位有了特殊的含义。在苹果的64位Objective-C实现中,若对象指针的最低有效位为1(即奇数),则该指针为Tagged Pointer。这种指针不通过解引用isa来获取其所属类,而是通过接下来三位的一个类表的索引。该索引是用来查找所属类是采用Tagged Pointer的哪个类。剩下的60位则留给类来使用。


应用:Tagged Pointer有一个简单的应用,那就是NSNumber。它使用60位来存储数值。最低位置1。剩下3位为NSNumber的标志。在这个例子中,就可以存储任何所需内存小于60位的数值。从外部看,Tagged Pointer很像一个对象。它能够响应消息,因为objc_msgSend可以识别Tagged Pointer。假设你调用integerValue,它将从那60位中提取数值并返回。这样,每访问一个对象,就省下了一次真正对象的内存分配,省下了一次间接取值的时间。同时引用计数可以是空指令,因为没有内存需要释放。对于常用的类,这将是一个巨大的性能提升。


NSString似乎并不适合Tagged Pointer,因为它的长度即可变,又可远远超过60位。然而,Tagged Pointer是可以与普通类共存的,即对一些值使用Tagged Pointer,另一些则使用一般的指针。例如,对于NSNumber,大于2^60-1的整数就不能采用Tagged Pointer来存储,而需要在内存中分配一个NSNumber的对象来存储。只要创建对象的代码编写正确,就没有问题。

NSString也是如此。对于那些所需内存小于60位的字符串,它可以创建一个Tagged Pointer。其余的则被放置在真正的NSString对象里。这使得常用的短字符串的性能得到明显的提升。实际代码就是如此吗?似乎Apple是这么认为的,因为他们这么做了并实现了它。


关于Tagged Pointer 的知识,这里有一篇文章帮助理解:采用Tagged Pointer的字符串http://www.cocoachina.com/ios/20150918/13449.html



参考链接:

https://blog.cnbluebox.com/blog/2014/04/16/nsstringte-xing-fen-xi-xue-xi/









0 0
原创粉丝点击