美文网首页堆栈OC底层相关iOS假装进步
编码篇-iOS程序中的内存分配 栈区堆区全局区等相关知识

编码篇-iOS程序中的内存分配 栈区堆区全局区等相关知识

作者: 進无尽 | 来源:发表于2017-01-15 18:16 被阅读137次

    前言

    在计算机的系统中,运行的应用程序中的数据都是保存在内存中,不同类型的数据,保存的内存区域不同。内存区域大致可以分为:栈区、堆区、全局区(静态区)、文字常量区、程序代码区。学习内存相关的知识对我们的日常开发是十分必要的。


    一. 栈区

    (1)栈区(stack) 由编译器自动分配并释放,存放函数的参数值,局部变量等。栈是系统数据结构,对应线程/进程是唯一的。优点是快速高效,缺点时有限制,数据不灵活。【先进后出】

    alloc 在堆上申请一块空间返回一个指针,这个指针在栈上,申请的空间在堆上,
    这里指的局部变量不是对象地址,而是这个对象的指针在栈上。
    

    (2)申请后的系统响应
    栈区存储每一个函数在执行的时候都会向操作系统索要资源,栈区就是函数运行时的内存,栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。

    注意:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
    

    (3)申请大小的限制
    栈:栈是向低地址扩展的数据结构,是一块连续的内存的区域。是栈顶的地址和栈的最大容量是系统预先规定好的,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数 ) ,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

    (4)栈:由系统自动分配,速度较快,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,不会产生内存碎片。

    二. 堆区

    注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

    堆是一种特殊的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆
    堆分为大根堆,小根堆,大根堆就是树的根结点大于叶子结点.

    (1)堆区(heap) 由程序员分配和释放,如果程序员不释放,程序结束时,可能会由操作系统回收 ,比如在ios 中 alloc 都是存放在堆中。
    优点是灵活方便,数据适应面广泛,但是效率有一定降低。【顺序随意】

    堆空间的分配总是动态的虽然程序结束时所有的数据空间都会被释放回系统,
    但是精确的申请内存与释放是优质程序开发者必备的素质。
    

    (2)堆区申请后的系统响应

    1.首先应该知道操作系统有一个记录空闲内存地址的链表。
    2.当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,
      然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
    3 .由于找到的堆结点的大小不一定正好等于申请的大小,
      系统会自动的将多余的那部分重新放入空闲链表中
    

    (3)申请大小的限制
    堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

    (4)是由alloc分配的内存,速度比较慢,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。不过用起来最方便。

    三. 全局区(静态区) (static)

    全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域。程序结束后有系统释放。

    注意:全局区又可分为:
         未初始化全局区: .bss段        
         初始化全局区:data段。
         举例:int a;未初始化的。int a = 10;已初始化的。
    
    四. 文字常量区

    存放常量字符串,程序结束后由系统释放

    五.程序代码区

    存放函数的二进制代码

    补充说明

    栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的。

    例子代码:

    int a = 10;  #全局初始化区 
    char *p;     #全局未初始化区 
    main{ 
          int b;    #栈区 
          char s[] = "abc"   #栈
          char *p1;  #栈
          char *p2 = "123456";       #123456在常量区,p2在栈上。 
          static int c =0;          #全局(静态)初始化区 
          w2 = (char *)malloc(20);   #分配得来得10和20字节的区域就在堆区。
     }
    

    六. 内存分配地址状况图:

    Paste_Image.png Paste_Image.png Paste_Image.png

    七. 字符串内存管理

    NSString是一个不可变的字符串对象。这不是表示这个对象声明的变量的值不可变,而是表示它初始化以后,你不能改变该变量所分配的内存中的值,但你可以重新分配该变量所处的内存空间。copy 和 retain 对它的作用都是浅复制,也就只是单纯地指针复制。

    # 此时 str  是__NSCFConstantString类型。   
     NSString *str1 = @"my string"; 
     
    类函数初始化生成:   
     # 这也会初始化内存空间,但是比较特别的这个方法是autorelease类型,内存由系统释放
     NSString *str2 = [NSString stringWithString:@"my string"];
    
     实例方法初始化生成: 
      NSString *str3 = [[NSString alloc] initWithString:@"my string"];
      NSString *str4 = [[NSString alloc]initWithFormat:@"my string"];
     # str3、str4则必须手动释放
     # 用Format初始化的字符串,需要初始化一段动态内存空间,如:0x6a42a40;
     # initWithString 直接返回字符串常量的地址,而不是重新开辟一块内存空间。所以str3和str1的地址一致
     # 不过我们还是应该遵循内存管理的原则,release 一下 str3,但是str4和str1的地址不一致。
    

    __NSCFConstantString

    这些对象地址相同,是因为他们都是__NSCFConstantString对象,也就是字符串常量对象,可以看到其isa都是__NSCFConstantString,该对象存储在栈上,创建之后由系统来管理内存释放,相同内容的NSCFConstantString对象地址相同。该对象引用计数很大,为固定值不会变化,表示无限运行的retainCount,对其进行retain或release也不会影响其引用计数。

    当创建一个NSCFConstantString对象时,会检测这个字符串内容是否已经存在,如果存在,则直接将地址赋值给变量;不存在的话,则创建新地址,再赋值。

    总的来说,对于NSCFConstantString对象,只要字符串内容不变,就不会分配新的内存地址,无论你是赋值、retain、copy。这种优化在大量使用NSString的情况下可以节省内存,提高性能。

    一个问题:为什么我们在定义NSString时使用Copy而不是 Strong

    strong和retain同义, weak和assign同义, 为什么要采用这种说法, 似乎是ARC出现后为了消除引用计数的观念而采用的做法. 至于为什么要用copy, 由于纯NSString是只读的, 所以strong和copy的结果一样,都是引用计数+1,相当于 retain,但是当是mutable string时,strong是单纯的增加对象的引用计数,而copy操作是执行了一次深拷贝(开出了新的地址,生成了新的对象), NSMutableString是NSString的子类, 因此NSString指针可以持有NSMutableString对象,我们一般不希望因为之前的值变化导致属性值也跟着变化,所以使用Copy是可以兼顾可变字符和不可变字符的

    八. 浅拷贝和深拷贝

    浅拷贝,只是拷贝了对象的指针,而不是拷贝对象本身。 深拷贝,是直接拷贝整个对象的内存到另一块内存中。

    • 浅拷贝(shallow copy):在浅拷贝操作时,对于被拷贝对象的每一层都是指针拷贝。
    • 单层拷贝(one-level-deep copy):在深拷贝操作时,对于被拷贝对象,至少有一层是深拷贝。
    • 深拷贝(real-deep copy):在完全拷贝操作时,对于被拷贝对象的每一层都是对象拷贝

    下面是一个实现
    浅拷贝的例子:

    NSArray *shallowCopyArray = [someArray copyWithZone:nil];
    NSDictionary *shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:NO];
    

    深拷贝

    • 集合的深拷贝有两种方法。可以用 initWithArray:copyItems: 将第二个参数设置为YES即可深拷贝,如:

      NSDictionary shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:YES];
      
    • 归档一个容器类对象(archive)拷贝后,然后解档(unarchive),即可实现里面元素的深拷贝。

    深浅拷贝规律总结如下:

    copy mutableCopy
    不可变对象 对象指针拷贝 对象本身深拷贝
    可变对象 对象本身深拷贝 对象本身深拷贝
    不可变容器对象 对象指针拷贝 对象本身深拷贝
    可变容器对象 对象本身深拷贝 对象本身深拷贝

    在容器类对象中,对immutable对象进行copy,是指针拷贝,mutableCopy是内容拷贝;对mutable对象进行copy和mutableCopy都是内容拷贝。但是:集合对象的内容拷贝仅限于对象本身,对象元素仍然是指针拷贝

    九. 函数参数赋值

    从函数调用的角度理解:

    • 传值:
      函数参数压栈的是参数的副本。
      任何的修改是在副本上作用,没有作用在原来的变量上。

    • 传指针:
      压栈的是指针变量的副本。
      当你对指针解指针操作时,其值是指向原来的那个变量,所以对原来变量操作。

    • 传引用:
      压栈的是引用的副本。由于引用是指向某个变量的,对引用的操作其实就是对他指向的变量的操作。(作用和传指针一样,只是引用少了解指针的草纸)

    从编译的角度来阐述方法中传指针、传引用之间的区别:

    程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

    下面图就是反映传指针的逻辑


    所谓的双指针(参数中传指针):就是新建一个对象p ,再创建一个指针p1指向p,然后再创建一个指针p2,p2内保存了p1指针的地址,取得p2指针的内容,就是拿到了p1指针的地址,然后对其的指向进行修改.

    下面是一个字符串的使用实例

     NSString *abc = @"vvvv";
     [self creat: &abc];
     gloubStr = abc;
     NSLog(@"newStr: %@",gloubStr);
    
     - (void)creat :(NSString **)oriangeStr
    {
          *oriangeStr = @"asd";
    }
    gloubStr是一个未初始化的全局变量,使用这种指针传值不能传入全局的变量,只能传局部变量
    
    会造成
    [self creat: &gloubStr];
    NSLog(@"newStr: %@",gloubStr);
    
     这样直接使用全局变量也不会报错了。
    - (void)creat :(NSString * __strong *)oriangeStr
    {
          *oriangeStr = @"asd";
    }
    

    当对象是 OC对象时:报错

    加()包裹住即可

    UIView *inputArea0 = [self creatInput:titleView0 :&nameTF :@""];
    

    十.局部变量

    在ARC情况下,局部变量离开作用域就被销毁了,所以有些时候要注意,比如UIWebView,设成局部变量,在离开了作用域就被销毁了,但它可能还要执行delegate方法,所以程序就会崩溃。又例如,AVAudioPlayer设置成局部变量时播放不了声音,因为当离开作用域后变量就被销毁了。

    - (void)viewDidLoad
      {
    
          [super viewDidLoad];
    
        SecondViewController *svc = [[SecondViewController alloc]     initWithNibName:@"SecondViewController" bundle:nil];
        svc.delegate = self;
        [self.view addSubview:svc.view];
      }
    

    这个SecondViewController的视图能够显示,但是点击视图上的按钮却不会执行SecondViewController中的方法。
    如果将SecondViewController的一个对象声明为ViewController的一个成员变量就正常。

    这是因为:svc这个指针本身是在栈里分配的出了}就挂了,然后它指向的SecondViewController在堆上生成的对象随后会被析构掉。

    至于视图能够正常显示应该是[self.view addSubview:svc.view]之后self.view中有强引用的指针指向svc.view 所以视图不会挂,但是这个svc已经被销毁了

    小结

    通过以上的描述和比较,我们大致了解了iOS程序中的内存分配、管理问题、方法中参数传递的不同、深浅拷贝、内存泄漏等知识,文中如有阐述错误的地方,欢迎朋友指正。

    相关文章

      网友评论

        本文标题:编码篇-iOS程序中的内存分配 栈区堆区全局区等相关知识

        本文链接:https://www.haomeiwen.com/subject/adrxbttx.html