Objective-C:内存管理

内存管理的问题#

  先看看下面的几段代码,重温一下使用内存常见的问题。

#include <stdio.h>
#include <stdlib.h>

int a = 2;

void foo() {}

int main() {
    char str1[20] = "Gello, world!";
    char *str2 = "Gello, world!";// 只读数据段,无法更改,内存使用错误
    char *str3 = (char *)malloc(1000);// 申请空间
    //str[0] = 'H';//只读数据段无法更改 bos error
    str1[0] = 'H';
    printf("栈(stack):str1 = %p\n", str1);
    printf("堆(head):str3 = %p\n", str3);
    printf("数据段:a = %p\n", &a);
    printf("只读数据段:str2 = %p\n", str2);
    printf("代码段:str2 = %p\n", foo);
    free(str3);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void get_memory(char *p, int capacity) {
    p = malloc(sizeof *p * capacity);
}

// 任何时候希望通过函数调用修改传入的参数
// 那就不能只传参数的值 而要传参数的地址
// 如果传入的参数本身就是地址 那么就要使用指向指针的指针
// 指针的第一个用途就是实现跨栈的操作
void get_memory2(char **p, int capacity) {
    // 指针的第二个用途就是申请堆空间
    *p = malloc(sizeof **p * capacity);
}

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int a = 5, b = 10;
    printf("a = %d, b = %d\n", a, b);
    swap(&a, &b);// 交换 a 和 b 的值
    printf("a = %d, b = %d\n", a, b);
    char *str = NULL;
    // 未申请到堆空间,只是为形参申请了100字节的空间
    // 为指针申请空间,需要传指针的地址,用二重指针(指针的指针)
    //get_memory(str, 100);
    get_memory2(&str, 100);
    if (str)// 判断是否申请到空间
    {
        strcpy(str, "Hello, world!");
        printf("%s\n", str);
        free(str);// 堆空间不会随着栈的消失而消失,需要手动释放
        str = NULL;
    }
    return 0;
}
#include <stdio.h>

char *get_memory() {
    // c中数组的数据放在栈空间中
    // str是一个局部变量,调用结束后会自动释放
    // 一个函数可以返回栈空间的数据但不能返回栈空间的地址
    char str[] = "hello, world";
    char *str2 = "hello, world";// 放在只读数据段中 
    // return str;// 无法返回栈空间的地址
    return str2;
}

void main() {
    char *str = get_memory();
    if (str != NULL) {
        printf("%s\n", str);
        // 没有申请空间(malloc), 不能释放堆空间
        // free操作跟malloc操作是成对出现的
        //free(str);
    }
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *get_memory(int capacity) {
    char *str = malloc(sizeof *str * capacity);
    return str;
}

void foo(char *str) {
    // ...
    free(str);
    str = NULL;
}

void bar(char *str) {
    strcpy(str, "Hello, world!");
    printf("%s\n", str);
    // 用完内存一定需要释放, 否则有内存泄露的风险
    free(str);
    str = NULL; // important
}

int main() {
    char *str = get_memory(100);
    // 申请内存以后一定要先判断再使用
    if (str) {
        bar(str);// 调用完后若有其他函数调用str,提前释放了str,会出错
        //foo(str);// 重复释放
    }
    return 0;
}

  C语言中内存操作常见错误:

  1. 内存分配未成功就开始使用内存。

  2. 内存分配虽然成功但尚未初始化就使用。

  3. 内存分配成功且已经初始化但访问越界。

  4. 使用realloc()函数不使用备用指针。

  5. 内存泄露(申请了堆空间,但在使用结束后忘记释放)。

  6. 提前释放(释放了内存却仍然在使用的空间,导致数据不安全)。

  7. 重复释放(释放一个已经释放过的空间,导致程序崩溃)。

  上述问题中的第5项和第6项在实际开发中,尤其是遇到模块化的团队开发或者程序中使用多线程的时候,显得尤为难以处理。为了解决上述问题,在Objective-C中引入了引用计数的概念。Objective-C中每个类都是NSObject子类,因此每个对象都有一个内置的计数器,这个计数器称为引用计数(Reference Count),也称保留计数(Retain Count)。所谓Objective-C的内存管理,就是要维护引用计数器正确+1和-1,当引用计数器为0时,对象正确释放。在Objective-C中,每个对象就如同一个QQ讨论组,当有人创建讨论组时,讨论组人数为1(对象创建);每有一个人加入讨论组,该讨论组的人数+1(使用retain增加引用计数),每有一个人离开讨论组,该讨论组的人数-1(使用release减少引用计数);如果讨论组的人数为0,则自动解散。

  和内存管理相关的方法:

  • retain:增加对象的引用计数。

  • release:减少对象的引用计数。

  • autorelease:在自动释放池块结束时减少对象的引用计数。

  • retainCount:引用计数的数量。

  • 将整个项目都改成手动内存管理
    操作:(选择no)

《Objective-C:内存管理》 整个项目改为手动内存管理.png

    • 将项目中的某个文件改成手动内存管理
      操作:选择需要改为手动管理的m文件添加 -fno-objc-arc

《Objective-C:内存管理》 某个文件改成手动内存管理.png

如何有效管理内存##

  C语言中内存操作要牢记以下几点:

  1. 用malloc()/realloc()/calloc()申请内存后,应理解检查是否为NULL。

  2. 不要忘记为数组或动态申请的内存赋值,防止未初始化的内存作为运算的右值。

  3. 内存操作要小心边界,防止操作越界。

  4. 分配内存和释放内存的操作必须配对,防止内存泄露。

  5. 用free()函数释放内存后将指针赋值为NULL,防止产生野指针。

  Objective-C中使用内存的原则基本上是:

  1. 分配内存的操作要和释放内存的操作成对出现。

  2. 谁分配了内存,谁就要负责回收此内存。

  特殊情况:

  1. 成员变量是对象指针,应在析构方法(dealloc)中释放。

  2. 如果发生指针的转移,应释放旧对象,retain新对象。

  3. 从对象持有者(如NSArray、NSDictionary等)中取出对象的指针,如需长期使用,需要retain。

  ARC自动释放池的代码示例

#import <Foundation/Foundation.h>
#import "YHStudent.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 野指针(释放了仍然还在使用)和内存泄露(未正确的释放)
        // 1、创建了一个学生对象(堆上)
        // 2、创建了一个指针(栈上)
        // 3、指针指向了学生能够对象(指针中存储了学生对象的地址)
        __weak YHStudent *stu = [[YHStudent alloc] init];// 弱指针,一初始化就销毁 ,stu直接被销毁
        __strong YHStudent *stu2 = [[YHStudent alloc] init];// 强指针
        YHStudent *student = [[YHStudent alloc] init];// 强指针

        stu2 = nil;//若赋值为空,则在这被释放,stu2在这里被销毁
    }// 在这儿被释放(由于出了强指针(student)的作用域)
    return 0;
}
#import <Foundation/Foundation.h>

@interface YHStudent : NSObject

@property (nonatomic, copy) NSString *name;

@end
#import "YHStudent.h"

@implementation YHStudent

//ARC中可以重写dealloc方法,但是觉对不见而已调用父类的dealloc方法
//在对象将要销毁的时候会自动调用dealloc
- (void) dealloc {
    NSLog(@"学生被销毁!!");
}

@end
  1. 四个关键字的使用

    __strong(强引用):缺省属性,其修饰的对象指针,指向哪个对象,会对该对象retain,离开哪个对象,会对该对象release。

    __weak(弱引用):其修饰的对象指针,指向任何对象都不会retain。这样的指针指向的对象随时可能消失。如果对象消失了,这个指针会自动变成nil。

    __unsafe_unretained:其修饰的对象指针,指向任何对象都不retain。当指向的对象消失,该指针不会变成nil,仍然指向已经释放的对象。

    __autoreleasing:只用来修饰需要被传入地址的指针。

  2. 属性修饰符

    • copy:控制@property实现的set方法,会先创建一个新的对象,将参数的值传给新的对象,最后将新的对象赋值给成员变量.常用来修饰字符串、block、数组、字典、NSData;
    • strong:控制@property实现符合内存管理的set方法,引用计数加1;修饰一般的对象(retain的替代品)
    • weak:控制@property实现一般的set方法(直接赋值),修饰对象用来避免循环引用(最常用的是delegate)
    • assign:控制@property实现一般的set方法(直接赋值);常用来修饰基本数据类型(int、float、char、结构体、枚举、联合体)
    • retain:在MRC中相当于strong(实现的set方法就是旧值release、新值retain)。
  3. 牢记在ARC有效时retain/release/autorelease/retainCount都不能用。

  4. 不能显式调用dealloc析构器,析构器中可以将成员变量中的指针赋值为nil。

  5. 用@autoreleasepool{}替代NSAutoreleasePool对象的创建。

  6. 在ARC有效时id和void *不再等同,需要用__bridge转换。

  7. 不要在C的结构体中声明对象指针,否则无法进行内存管理。

管理内存的一些常见问题##

  • 内存管理的作用:

    • 解决内存泄露和野指针操作
  • 为什么要内存管理,我们要注意的问题是什么?

    • 解决内存泄露和野指针操作
  • 什么是黄金法则?

    • 内存管理原则:谁创建谁释放,在哪儿创建在哪儿释放
  • @property参数(retain)要注意的问题:避免循环引用

  • 什么时候autorelease?与release的区别

    • 对象需要延时销毁的时候使用autorelease
    • autorelease是将对象添加到自动释放池中(延时对象的销毁),release将对象的引用计数器减1
  • 什么是自动释放池

    • 注意:autorelease和autoreleasepool是成对出现的
    • autoreleasepool的原理:当autoreleasepool销毁的时候,会将自动释放池中所有的对象调用一次release方法
    • autorelease的作用:将对象放入自动释放池中(并不是写在自动释放池的大括号中的对象就是在自动释放池中的对象)
  • 一个工程中能有一个自动释放池?

    • 错,可以NSAutoreleasePool或者@Autoreleasepool{}去创建多个自动释放池
  • 在手动内存管理中,尽量都使用autorelease?

    • 错,对象调用autorelease会延迟对象的销毁,如果所有的对象都延迟销毁的话,相当于没有做内存管理
  • 对内存管理的理解?(原理)重点

    • 手动(MRC):1、在创建一个对象的时候系统会自动创建这个对象的引用计数,并且赋值为1;2、当引用计数为0的时候,对象会去调用dealloc方法,来销毁对象;3、对象调用release方法会让引用计数减1,调用retain方法让对象的引用计数加1。

    • 自动(ARC):在ARC中管理内存的实质还是通过引用器去管理的,但是程序员不再去关心引用计数的值。在ARC环境下,系统会在程序编译的时候会自动在合适的地方添加retain、release或者autorelease。

    • 当有强指针指向对象的时候,对象不销毁;弱指针不影响对象的销毁;指针默认都是强指针

    • __weak 使用这个关键字修饰的指针是弱指针;__strong 使用这个关键字修饰的指针是强指针(默认值);

  • 手动内存管理的原则?

    • 程序中如果出现alloc、retain、new必须配对出现一个release或者autorelease(谁创建谁释放,在哪儿创建在哪儿释放)
  • 3、 autoreleasepool的原理和autorelease的作用

    • autoreleasepool的原理:当autoreleasepool销毁的时候,会将自动释放池中所有的对象调用一次release方法
    • autorelease的作用:将对象放入自动释放池中(并不是写在自动释放池的大括号中的对象就是在自动释放池中的对象)
  • 4、 MRC中符合内存管理setter函数 的书写(旧值release,新值retain,然后赋值)

    原文作者:DeanYan
    原文地址: https://www.jianshu.com/p/852d071fca69
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞