小整内存管理

482 阅读14分钟

基本知识

内存管理

程序运行过程中,对系统分配资源的管理技术。

内存的分配

内存中分为5大区

  • 栈区:编译器分配和自动释放,地址从高到低
  • 堆区:由程序员释放,如果不释放,待程序结束后,由系统释放,地址从低到高
  • 全局区:全局变量+静态变量,由系统释放
  • 常量区:常量字符串等,由系统释放
  • 程序代码区:二进制代码

iOS中的内存管理

iOS中说的内存管理,管理的就是堆上的内存。

常见的内存管理方式有垃圾回收机制、自动引用计数。

在iOS中采用的是引用计数方式。

引用计数是指将资源(如对象)被引用的次数进行保存,当有新的对象引用时次数递增,有对象解除引用时次数递减,当对象的被引用次数为0时,释放该对象。

<iOS5之前,采用的是MRC,即手动管理引用计数。>=iOS5之后,采用ARC,即自动管理引用计数。

引用计数retainCount存储在引用计数表(散列表)中。

内存管理的思考方式

  • 自己生成对象,自己持有。
  • 非自己生成的对象,自己也可以持有
  • 不再需要自己持有的对象时释放
  • 非自己持有的对象无法释放

自己生成对象,自己持有

以 alloc、new、copy、mutableCopy 开头的方法,要注意是驼峰式命名。

id a = [[NSObject alloc] init];
id b = [NSObject new];

非自己生成的对象,自己也可以持有

id a = [NSMutableArray array]; //取得对象存在,但自己不持有
[a retain]; //持有对象

不再需要自己持有的对象时释放

id a = [[NSObject alloc] init];
[a release];

非自己持有的对象无法释放

id a = [[NSObject alloc] init];
[a release];
[a release]; //自己已经不持有

MRC

关键字有alloc,retain,release,retainCount,autorelease,dealloc。

ARC

所有权修饰符

  • __strong

    id类型和对象类型默认的所有权修饰符,对对象强引用

  • __weak

    提供弱引用,不能持有对象实例

  • __unsafe_unretained

    类似于weak,但是当对象被释放后,指针仍然保存之前的地址,被释放后的地址变为僵尸对象,访问被释放的地址就会出问题,所以说是不安全的。修饰的变量不属于编译器的内存管理对象。

  • __autoreleasing

    对象被注册到autoreleasepool。 编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是,则自动将返回值的对象注册到 autoreleasepool。 id的指针(id *obj)或对象的指针(NSString**a),在没有显式指定时,会被附加上__autorelease修饰符。

几个例子,arc下编译器是如何转换源代码。

{
    id __strong obj = [[NSObject alloc] init];
}
=>
{
    id obj = objc_msgSend(NSObject, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    objc_release(obj);
}
{
    id __strong obj = [NSMutableArray array];
}
=>
{
    id obj = objc_msgSend(NSMutableArray, @selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_release(obj);
}
+ (id)array {
    return [[NSMutableArray alloc] init];
}
=>
+ (id)array {
    id obj=objc_msgSend(NSMutableArray, @selector(alloc));
    objc_msgSend(obj, @selector(init));
    return objc_autoreleaseReturnValue(obj);
}

objc_retainAutoreleasedReturnValue函数主要用于最优化程序运行。用于 自己持有对象的函数,但持有的对象应为返回注册在autoreleasepool中对象的方法或者函数的返回值。在调用alloc/new/copy/mutableCopy以外的方法调用之后。

objc_retainAutoreleasedReturnValue成对的是objc_autoreleaseReturnValue,用于alloc/new/copy/mutableCopy以外的方法返回对象的实现上。

高级编程中的图

属性声明的属性与所有权修饰符

  • assign --- __unsafe_unretained
  • copy --- __strong(赋值的是被复制的对象)
  • strong --- __strong
  • unsafe_unretained --- __unsafe_unretained
  • weak --- __weak

默认关键字

MRC: @property (atomic, readWrite, retain) UIView *view;

ARC: @property (atomic, readWrite, strong) UIView *view;

如果是 基本类型,则为 assign

ARC下的规则

  • 不能使用 retain/release/retainCount/autorelease

  • 不能使用 NSAllocateObject/NSDeallocateObject

  • 须遵守内存管理的方法命名规则

  • 不要显式的调用dealloc

  • 使用@autoreleasepool块 替代 NSAutoreleasePool

  • 不能使用区域NSZone

    区域是为了防止内存碎片化引入,现在已经很效率,如果继续使用,反而会引起内存使用效率低下

  • 对象型变量不能作为C语言结构体struct/union的成员

    C语言无法管理结构体成员的生存周期,arc下内存管理的工作在编译器,编译器需要知道并管理对象的生存周期。可以强制转换为 void* 或 __unsafe_retained struct { NSMutableArray __unsafe_retained *array; };

  • 显式转换 id 和 void *

    __bridge void *

ARC简化引用计数

ARC在调用retain、release等这些内存管理的方法时,并不走消息派发机制,而是直接调用底层的C函数,这样做性能更好,所以,开发者不需要覆写retain等这些方法。

  • ARC如何清理实例变量?

借用Objective-C++的 特性,回收Objective-C++对象时,会直接调用所有C++对象的析构函数。生成.c**_destruct方法非Objective-C对象,如 CF中的对象 或者是由 malloc分配在堆中的对象,仍需要清理。

循环引用

实质:多个对象之间 相互强引用,不能释放回收

如何避免?

1、strong引用 改为 weak 引用
2、适当的时机 手动断开引用

几种常见的循环引用

1、delegate
    weak
2、NSTimer
    invalidate手动置为nil    
    使用block来打破环循环引用,block内部weakself,直接target设置为weakself并不能解决问题,因为timer内部会对target引用+1
3、block
    weak
    并不是所有的block都会引起循环引用,只有被强引用的block才会
    block执行时,self未被释放,block执行完后,self被释放,此时访问 weakself会发生错误(nil),可以在block中strong一下,使得在block期间持有对象,block结束后,解除持有

copy

主要涉及到深拷贝和浅拷贝。

深拷贝指的是内容拷贝,浅拷贝指的是指针拷贝。

非集合类对象

可变,copy-内容拷贝,mutablecopy-内容拷贝
不可变,copy-指针拷贝,mutablecopy-内容拷贝

集合类对象

可变,copy-内容拷贝,mutablecopy-内容拷贝
不可变,copy-指针拷贝,mutablecopy-内容拷贝

对于集合类对象的内容复制,仅仅针对的是对象本身,对象中的元素还是指针拷贝。

要想复制整个集合对象,就要用集合深复制的方法,有两种:

1、使用initWithArray:copyItems:方法,将第二个参数设置为YES即可: NSDictionary shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:YES];

2、将集合对象进行归档(archive)然后解归档(unarchive): NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];

举例:

//0x0000000100f3e4e0
NSArray *origin = @[]; 
//NSArrayO,指针拷贝,0x0000000100f3e4e0
NSArray *a = origin.copy; 
//NSArrayM,但是 不能 [b addobject];内容拷贝,0x0000000100f3ec40
NSArray *b = origin.mutableCopy; 
//NSArrayO,不可修改,指针拷贝,0x0000000100f3e4e0
NSMutableArray *c = origin.copy; 
//NSArrayM,可修改 [d addObject:@1];内容拷贝,0x0000000102d001d0
NSMutableArray *d = origin.mutableCopy;
//NSArrayO,不可修改,指向a,0x0000000100f3e4e0
NSMutableArray *e = a; 
//NSArrayM, 可修改,b中的内容 也会改变,0x0000000100f3ec40
NSMutableArray *f = b;

内存管理方案

实际上是3种方案的结合。

TaggedPointer

主要针对类似于NSNumber的小对象类型,解决对象浪费内存的现象。

存储一个NSNumber对象,值是一个整数。在64位CPU下,需要8(指针)+8(值)=16个字节,引入TaggedPointer后,将指针分成2部分,一部分存储数据,一部分作为标记,表示这是一个特别的指针,不指向任何一个地址。所以64位下只需要 8个字节。

TaggedPointer专门用来存储小的对象,例如NSNumberNSDate

TaggedPointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。(不能 访问 isa)

比如在源码中,判断是不是一个class。

inline bool
objc_object::isClass()
{
    if (isTaggedPointer()) return false;
    return ISA()->isMetaClass();
}

判断的依据就是标记。

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。

苹果引入Tagged Pointer,不但减少了64位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。

NONPOINTER_ISA(64位系统下)

# if __arm64__
    #   define ISA_MASK        0x0000000ffffffff8ULL
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    #   define ISA_BITFIELD                                                     
          uintptr_t nonpointer        : 1;  // 是否对isa开启指针优化 。0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等
          uintptr_t has_assoc         : 1;  // 关联对象标志位
          uintptr_t has_cxx_dtor      : 1;  // 该对象是否有C++或Objc的析构器,如果有析构函数,则需要做一些析构的逻辑处理,如果没有,则可以更快的释放对象
          uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \ //存在类指针的值,开启指针优化的情况下,arm64位中有33位来存储类的指针
          uintptr_t magic             : 6;   // 判断当前对象是真的对象还是一段没有初始化的空间
          uintptr_t weakly_referenced : 1;   // 是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象释放的更快
          uintptr_t deallocating      : 1;   // 是否正在释放
          uintptr_t has_sidetable_rc  : 1;                                       \
          uintptr_t extra_rc          : 19
    #   define RC_ONE   (1ULL<<45)
    #   define RC_HALF  (1ULL<<18)
    # elif __x86_64__
    #   define ISA_MASK        0x00007ffffffffff8ULL
    #   define ISA_MAGIC_MASK  0x001f800000000001ULL
    #   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    #   define ISA_BITFIELD                                                        \
          uintptr_t nonpointer        : 1;                                         \
          uintptr_t has_assoc         : 1;                                         \
          uintptr_t has_cxx_dtor      : 1;                                         \
          uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
          uintptr_t magic             : 6;                                         \
          uintptr_t weakly_referenced : 1;                                         \
          uintptr_t deallocating      : 1;                                         \
          uintptr_t has_sidetable_rc  : 1;                                         \
          uintptr_t extra_rc          : 8
    #   define RC_ONE   (1ULL<<56)
    #   define RC_HALF  (1ULL<<7)
    # else
    #   error unknown architecture for packed isa
    # endif

散列表SideTable

文章Runtime-SideTable

dealloc

在dealloc中,只释放引用并解除监听。 仅执行一次,保留计数为0时,但 具体何时,则 无法保证。

不要自己主动调用,运行期系统会在合适的时候调用。

如果是MRC,则 还要 [super dealloc];

开销较大或系统内稀缺的资源,如 文件描述符、套接字、大块内存等,最好不要放在 dealloc中释放,因为 保留时间有些过长,比较好的做法是,应用完 资源后,就释放。

系统 并不能 保证 每个 对象的 dealloc方法 都会 执行。应用程序终止时,这些对象 还处于 存活状态,因为 这些 对象 没有 收到 dealloc消息。由于 应用程序终止后,会将 占用的资源 返还给 os,因此 这些 对象 也就 等于是 消亡了。

dealloc的流程,见文章runtime。

异常安全代码

在编写“异常安全代码”时留意内存管理问题。

MRC下,要注意捕获异常,在@finally中 release

默认情况下,ARC不生成 安全处理异常所需的清理代码。@try{}@catch,因为这样做要加入大量的样板代码 进行 跟踪清理的对象,会 严重影响运行期的性能。还会增加应用程序的大小。

使用 -fobjc-arc-exceptions 这个 编译器标志来 开启此 功能。

默认不开启的原因是:在Objective-C代码中,只有 应用程序必须因异常状况而终止时才应该抛出异常。

处于 Objective-C++模式下,会自动开启。因为C++处理异常所用的代码与ARC实现的附加代码类似,性能损失不大。

autorelease

主线程 或者 大中枢派发(GCD)机制中的线程,这些线程默认都是有自动释放池,每次事件循环时,就会 自动清空。无需 创建 自动释放池。

通常 只有 一个 地方 需要 创建自动释放池,那就是 在 main函数里。

如果 main中 不加 autorelease,从技术的角度也是可以的,因为此时应用程序已经要终止,os会把应用程序所占的全部内存都 释放掉。但是如果不加的话,那么是由UIApplicationMain函数释放,没有自动释放池 可以 容纳。所以 可以理解,main中的autorelease,用于在最外层捕获全部自动释放对象所用的池。

@autorelease{}内的对象,会在}处收到release消息

尽量不要建立额外的自动释放池,毕竟 存在 一些 开销。

NSAutoreleasePool,更为重量级,通常用来创建那种偶尔需要情况的池,老式写法。ARC下的@autorelease 更为轻量。

自动释放池排布 在 栈中。

累,借鉴文章iOS AutoreleasePool 深入探究

僵尸对象

运行期系统 会将 所有 已经回收的实例转化成僵尸对象,而不是真正的回收,如果给僵尸对象发送消息,则会在 控制台中打印,应用程序终止。

需要 手动开启,真正上线时并不会开启。

实现代码 在 Objective-C的运行期程序库、Foundation库以及Core Foundation框架中。

系统 在即将回收对象时,会检查环境变量是否开启僵尸对象功能,如果开启,则会执行附加步骤。

_NSZombie_***,从 模版类中 复制出来,copy 整个 _NSZombie_ 类结构,修改 对象的 isa指针,指向 僵尸类,僵尸类 中没有 任何的 实现方法,所有 经过 的 消息,都要 经过 完整的消息转发机制。在 完整的消息转发机制中,__forwarding__是核心,它会 先 检查 接收消息的对象的所属 类名,如果是 _NSZombie_,表明 是僵尸对象,则特殊处理,打印 内容,终止应用程序。

不要使用retainCount

这个数值 只能 反映 某个 时间点的值,并没有 考虑 系统 稍后把 自动释放池 清空。

可能永远不会为0,系统有时候会 优化,在 对象为1的时候就回收。

单例对象,保留值 很大。编译器常量@“aaa”

如何检测内存泄漏

Instructment中的Leaks/Allocations

在官方文档中

Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
Abandoned memory: Memory still referenced by your application that has no useful purpose.
Cached memory: Memory still referenced by your application that might be used again for better performance.

Leaked memory主要是在MRC下,忘记release操作所泄漏的内存,可以用Leaks检查。

Abandoned memory主要是指因为循环引用导致的无法释放的内存。

主要是场景重现,可以借助github上第三方的项目。

常见问题

  • __weak和_unsafe_unretained的区别?

weak 修饰的指针变量,在指向的内存地址销毁后,会在runtime的机制下,自动置为 nil。

_unsafe_unretained不会置为nil,容易出现悬垂指针,发生崩溃。但是_unsafe_unretain 比 __weak 效率高。

  • 什么是悬垂指针?什么是野指针?

悬挂指针: 指针指向的内存,已经被释放,指针还在。(BAD_ACCESS)

野指针: 未进行初始化的指针。

  • MRC下如何重写属性的setter和 getter方法
- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}

- (NSString *)getName {
    return [[_name retain] autorelease];
}

  • assign可以修饰对象吗?为什么?

不可以。对象的内存是分配在堆上,如果用assign修饰,当对象被释放后,指针的地址还在,造成野指针。assign修饰的基本类型,是分配在栈中,由系统统一分配和释放,不会出现 野指针。

  • MRC下,可以重写dealloc方法,但是不能主动调用
- (void)dealloc {
    [_str release];
    [super dealloc]; //必须最后调用super
}
  • MRC下为什么调用 release后,还要再 = nil?

调用release后,计数降至0,对象所占的内存可能会回收。之所以说 可能,是因为,内存 在被 解除分配 之后,只是 放在了可用内存池中。使用时若对象内存没有被覆写,则仍然有效。因此,一般调用 release后,会 清空指针,保证 不会出现 可能指向 无效对象的 指针,这种指针 叫做 悬挂指针。

  • MRC为什么 strong的属性,要先 [aa retain],[_aa release],然后 _aa=aa?

如果先释放,后保留,两个值 如果 指向 同一个对象,那么 先 执行 release的对象 可能 导致 系统将 该对象永久回收,retain无法 复生,实例变量 成为 悬挂指针。