[iOS开发]引用计数与MRC

文章目录


再了解内存管理这块知识,我认为有必要先了解一下计算机是如何处理内存的

1. 内存分配区域

我们可以简单的将内存区域分为内区和外区

1.1 内区

1.1.1 栈

临时变量由编译器自动分配,在不需要时自动清除的变量存储区,通常是局部变量和函数参数。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。用户栈在程序执行期间可以动态地扩展和收缩。

1.1.2 堆

由new\alloc创建的对象所分配的内存块,它们的释放系统不会主动去管,而是由我们的开发者去告诉系统什么时候释放这块内存(一个对象引用计数为0时系统就会回销毁该内存区域对象)。一般一个 new 就要对应一个 release。在ARC下编译器会自动在合适位置为OC对象添加release操作。会在当前线程Runloop退出或休眠时销毁这些对象,MRC则需程序员手动释放。
堆可以动态地扩展和收缩。
我们讨论的内存管理就是针对堆区进行讨论

1.1.3 全局区

全局变量静态变量被分配到同一块内存中。

1.1.3.1 static静态变量

  • 只能在本文件中访问,修改全局变量的作用域
  • 避免重复定义全局变量

全局静态变量

  • 优点 :不管对象方法还是类方法都可以访问和修改全局静态变量,并且外部类无法调用静态变量,定义后只会指向固定的指针地址,供所有对象使用,节省空间。 并且外部类无法调用静态变量,定义后只会指向固定的指针地址,供所有对象使用,节省空间。
  • 缺点存在的生命周期长,从定义直到程序结束。所以从内存优化和程序编译的角度来说,尽量少用全局静态变量。程序运行时会单独加载一次全局静态变量,过多的全局静态变量会造成程序启动慢。

静态局部变量

  • 优点 :定义后只会存在一份值,每次调用都是使用的同一个对象内存地址的值,并没有重新创建,节省空间,只能在该局部代码块中使用。
  • 缺点 :存在的生命周期长,从定义直到程序结束,只能在该局部代码块中使用。

所以局部和全局静态变量从根本上来说没有什么区别,只是作用域不同。如果仅仅一个类中的对象方法和类方法使用并且值可变,我们就可以定义全局静态变量,如果是多个类使用并可变,建议定义在model作为成员变量使用。如果是不可变值,宏定义即可。

1.1.3.2 extern全局变量

只是用来获取全局变量(包括静态全局变量)的值,不能用于定义变量。现在当前文件查找有没有全局变量,没有找到,才会去其他文件查找。

全局静态变量与全局变量 其实本质上是没有区别的,只是存在修饰区别,一个static让其只能内部使用,一个extern让其可以外部使用

当某个全局变量,没有用static修饰时,其作用域为整个项目文件,若在其他类想引用该变量,则用extern关键字。
例如:想引用其他类的全局变量则在当前类中实现extern int age。如果该作用域不想被外界修改,则用static修饰该变量,则其作用域只限于该文件。
如下:

#import <Foundation/Foundation.h>
#import "Apple.h"
extern int a;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Apple *apple = [[Apple alloc]init];
//        [apple print];
        a = 5;
        NSLog(@"%d",a);
        [apple print];
        
    }
    return 0;
}

在需要引用其他类的全局变量的当前类中实现extern int age,然后把被引用的那个类的static变量删除

#import "Apple.h"
@implementation Apple
int a = 10;
- (void)print {
    NSLog(@"a = %d",a);
}
@end

此时打印结果就为两个都是5.

1.1.3.3 const常量

被const修饰的变量是只读的

  • const的用法:
    const用来修饰右边的值

主要会产生问题的是 * 是指针指向符,我们主要要看 * 与const的关系

  1. const在前,const修饰str这个整体,所以整体不能改变,这个整体是str指向内存中的值。
  2. const在 * 后 表示str指向的地址不能改变
    [iOS开发]引用计数与MRC
  • const与宏有什么区别呢
    [iOS开发]引用计数与MRC
    所以如果使用大量宏容易造成编译时间久

1.1.4 常量区

这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。一般值都是放在这个地方的。常量字符串就是放在这里的。 程序结束后由系统释放。

1.1.5 代码区

存放函数的二进制代码

1.2 外区–*存储区

由malloc等分配的内存快,与堆相似,不过其实用free来结束自己的生命

2.引用计数与MRC部分

2.1 基础的表格

对象 方法 引用计数
生成对象并自己持有 alloc/copy +1(从0变为1)
持有对象 retain +1
释放对象 release -1
废弃对象 dealloc 对象所占内存解除分配 并放回“可用内存池中”

2.2 内存管理的思考方式(四个基本法则)

2.2.1 自己生成的对象,自己持有

  1. alloc\new\copy\mutableCopy 方法名开头来创建的对象意味着自己生成的对象只有自己持有
  2. 持有的本质其实就是强引用

2.2.2 非自己生成的对象,自己也能持有

用 alloc / new / copy / mutableCopy 以外的方法取得的对象,因为非自己生成并持有,所以自己不是该对象的持有者。(比如 NSMutableArray 类的 array方法),但是我们可以通过retain来手动持有对象。

2.2.3 不在需要自己持有对象的时候释放

通过release进行释放

书上还介绍了这样一种用法

- (id)object {
	id object = [[NSObject alloc] init];//自己持有对象
	[obj autorelease]//取得对象存在,但自己不持有对象
	return obj;
}

我们不使用release释放,而是使用autorelease进行释放

autorelease释放与简单的release释放有什么区别?

首先说说什么是自动释放池:自动释放池是OC的一种内存自动回收机制,可以将一些临时变量通过自动释放池来回收统一释放。自动释放池销毁的时候,池子里面所有的对象都会做一次release操作

调用 autorelease 方法,就会把该对象放到离自己最近的自动释放池中(栈顶的释放池,多重自动释放池嵌套是以栈的形式存取的),即:使对象的持有权转移给了自动释放池(即注册到了自动释放池中),调用方拿到了对象,但这个对象还不被调用方所持有。

[iOS开发]引用计数与MRC
其实也就是autorelease 方法不会改变调用者的引用计数,它只是改变了对象释放时机,不再让程序员负责释放这个对象,而是交给自动释放池去处理

autorelease 方法相当于把调用者注册到 autoreleasepool 中,ARC环境下不能显式地调用 autorelease 方法和显式地创建 NSAutoreleasePool 对象,但可以使用@autoreleasepool { }块代替(并不代表块中所有内容都被注册到了自动释放池中)。

我们清楚ARC中我们的块在作用域结束后会自己进行release操作,那么在MRC中呢?
自动释放,听起来和像ARC,但实际上其实更类似于C语言中的局部变量。autorelease会像C语言的自动变量那样来对待对象实例。当超出对象实例的作用域时,对象实例的release方法会被调用。不同于C语言我们也可以自己设定变量的作用域,类似如下
[iOS开发]引用计数与MRC
对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。

int main(int argc, const char * argv[]) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
        id obj = [[NSObject alloc]init];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
        [obj autorelease];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
        [pool drain];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
    return 0;
}

对应结果就是1 1 0
[iOS开发]引用计数与MRC

那么什么时候需要使用自动释放池呢?

  1. If you write a loop that creates many temporary object. 循环中创建了许多临时对象,在循环里使用自动释放池,用来减少高内存占用。
  2. If you spawn a secondary thread. 开启子线程的时候要自己创建自己的释放池,否则可能会发生内存泄漏。

2.2.4 释放非自己持有的对象会导致程序崩溃

释放非自己持有的对象会导致程序崩溃
事实测试并不会… 可能修复了不会崩溃

3. 其他事项

3.1 Effective Objective-C 2.0中提到:不要使用retainCount!

我们在MRC中,有时可能会想要打印引用计数,但retainCount方法并不是很有用,由于对象可能会处于自动释放池中,这会导致打印的引用计数并不精准,而且其他程序库也很有可能自行保留或释放对象,这都会扰乱引用计数的具体值。

3.2 retainCount很大

int main(int argc, const char * argv[]) {
    NSString *firstString = @"你好";
    NSString *secondString = [NSString stringWithFormat:@"hello"];
    NSString *thirdString = [NSString stringWithFormat:@"helloWorld"];
    NSLog(@"%lu,%lu,%lu",(long)[firstString retainCount],(long)[secondString retainCount],(long)[thirdString retainCount]);
    return 0;
}

[iOS开发]引用计数与MRC

可以看到是2的64次方减一
[iOS开发]引用计数与MRC

编译器会把单例对象所表示的数据放在应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无需再创建NSString对象。
测试证明,即便对其进行 release 操作,retainCount 也不会产生任何变化。这个值意味着无限的retainCount,这个对象是不能被释放的。

3.3 三种类型字符串的copy/mutableCopy/retainCount情况

int main(int argc, const char * argv[]) {
    NSString *firstString = @"你好";
    NSString *secondString = [NSString stringWithFormat:@"hello"];
    NSString *thirdString = [NSString stringWithFormat:@"helloWorld"];
    
    NSString *test1 = [firstString copy];
    NSString *test2 = [firstString mutableCopy];
    NSString *test3 = [secondString copy];
    NSString *test4 = [secondString mutableCopy];
    NSString *test5 = [thirdString copy];
    NSString *test6 = [thirdString mutableCopy];
    return 0;
}

用lldb进行调试可以看到
[iOS开发]引用计数与MRC
经过测试

总结就是

无论原来的三个的类型是NSString还是NSMutableString类型

copy 会使原来的对象引用计数加一(当然仅有正常类型的字符串,而不是单例创建的,毕竟那两个引用计数是无限的),并拷贝对象地址给新的指针,所以类型与原类型一致。
mutableCopy 不会改变引用计数,会拷贝内容到堆上,生成一个 __NSCFString 对象,新对象的引用计数为1.

3.4 release自己不持有的对象并没有导致崩溃

[iOS开发]引用计数与MRC
[iOS开发]引用计数与MRC

并不是加了一句NSLog之后就一定会造成程序crash的,如果那句新加的NSLog没有占用原来array的内存,那下一句NSLog依旧能够响应发送给array的消息,结果会类似第一种代码所产生的结果。

所以说,两种情况都是有可能发生的,至于到底发生哪种情况,完全取决于何时系统会清理掉array占用的内存,也可以说取决于“运气”,因为这个时间是不确定的。

还有如果给其一个自动释放池的销毁那加上断点 其输出的结果可能不同应该也是这个原因

上一篇:ios开发中的静态内存分析


下一篇:智能问答机器人综述