iOS 内存管理机制

ARC 和 MRC 的区别?

MRC ARC
strong ARC特有,在MRC时代没有,相当于MRC模式下的retain
retain MRC、ARC两种内存管理方式下相同 MRC、ARC两种内存管理方式下相同
assign 可以用来修饰对象类型,也可以用来修饰基本数据类型。修饰对象类型的时候,对象的引用计数不会随着引用次数的增加而增加,也就是说被释放之前,引用计数永远是1。 只能用来修饰基本数据类型,不能用来修饰对象类型。除此之外,还用来修饰代理对象。
weak 相当于MRC模式下的assign
copy 1. block访问外部局部变量,block存放在栈里面 2. 只要block访问整个APP都存在的变量,肯定是在全局区 3. 不能使用retain引用block,因为block不在堆里面,只有使用copy才会把block放在堆区里面 1. 只要block访问外部局部变量,block就会存放在堆区 2. 可以使用strong去引用,因为本身就已经存放在堆区的 3. 也可以使用copy进行修饰,但是strong性能更好

strong:强引用,它是ARC特有。在MRC时代没有,相当于retain。由于MRC时代是靠引用计数器来管理对象什么时候被销毁所以用retain,而ARC时代管理对象的销毁是有系统自动判断,判断的依据就是该对象是否有强引用对象。如果对象没有被任何地方强引用就会被销毁。所以在ARC时代基本都用的strong来声明代替了retain。只能用于声明OC对象(ARC特有)

ARC

ARC默认属性修饰符

  • ARC下对象类型属性:(atomic, readwrite, strong)
  • ARC下非对象类型:(atomic, readwrite, unsafe_unretained)

ARC属性的所有权修饰符

属性声明的属性 所有权修饰符
assign _unsafe_unretained 修饰符
copy _strong 修饰符 (但是赋值的是被复制的对象)
retain _strong 修饰符
strong _strong 修饰符
_unsafe_unretained _unsafe_unretained修饰符
weak _weak 修饰符
_autoreleasing 修饰符
  • assignassign 只可以用来修饰基本数据类型,该方式会对象直接赋值而不会进行 retain 操作。

  • copy:表⽰重新建立一个新的计数为1的对象,然后释放掉旧的值。NSString、NSArray、NSDictionary 等经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,为确保对象中的属性值不会无意间变动,应该在设置新属性值时拷贝一份。而 NSMutableString、NSMutableArray、NSMutableDictionary 则往往使用 strong 关键字更为妥当。

    浅copy,类似strong,持有原始对象的指针,会使 retainCount 加一。

    深copy,会创建一个新的对象,不会对原始对象的 retainCount 变化。

  • retain和strong区别strong 修饰 block,相当于 copy,此时 block 是放在堆上的,生命周期不会随函数周期结束而出栈,但是 retain 修饰的 block 是存放在栈上, 因此 block 在函数调用结束时,对象会变成 nil,对象的指针会变成野指针,因此对象继续调用会产生异常。

  • _autoreleasing: _autoreleasing 修饰符变量引用的对象,相当于在 MRC 情况下调用对象的 autorelease 方法

WWDC2011iOS5 所引入自动管理机制——自动引用计数(ARC),它不是垃圾回收机制而是编译器的一种特性。ARC管理机制MRC手动机制 差不多,只是不再需要手动调用retain、release、autorelease;当你使用ARC时,编译器会在在适当位置插入 releaseautoreleaseARC 时代引入了 strong强引用 来带代替 retain,引入了weak弱引用

RunLoop

apple 官方文档(多线程编程指南)描述: “runloop 是用来在线程上管理事件异步到达的基础设施……

runloop 在没有任何事件处理的时候会把它的线程置于休眠状态,它消除了消耗CPU周期轮询,并防止处理器本身进入休眠状态并节省电源。” 看见没,消除CPU空转才是它最大的用处。

runloop.png

图中第1步 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前

图中第6步 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observerorder2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

图中第10 Observer 监视事件是exit(即讲退出runloop),其回调内会调用 _objc_autoreleasePoolpop() 释放自动释放池。

从上面就能看出,Runloop 中系统自动创建的 @autoreleasepool 是在准备进入休眠状态才被销毁的。所以在 ARC 下,在线程中的临时对象是在当前线程的 Runloop 进入休眠或者退出 loop 或者退出线程时被执行 release 的。

AutoreleasePool

自动释放池是OC中的一种内存自动回收机制,它可以将加入 AutoreleasePool 中的变量 release 的时机延迟,简单来说,就是当创建一个对象,在正常情况下,变量会在超出其作用域的时立即 release。如果将对象加入到了自动释放池中,这个对象并不会立即释放,会等到 runloop 休眠/超出 autoreleasepool 作用域{}之后才会被释放。其机制如下图所示

Autoreleasepool.png

  1. 从程序启动到加载完成,主线程对应的 runloop 会处于休眠状态,等待用户交互来唤醒 runloop

  2. 用户的每一次交互都会启动一次 runloop,用于处理用户的所有点击、触摸事件等

  3. runloop 在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中

  4. 在一次完整的 runloop 结束之前,会向自动释放池中所有对象发送 release 消息,然后销毁自动释放池

MRC

MRC默认属性修饰符:

MRC:(atomic, readwrite, assign)

MRC手动内存管理

引用计数器:在MRC时代,系统判定一个对象是否销毁是根据这个对象的引用计数器来判断的。

  • 每个对象被创建时引用计数都为1
  • 每当对象被其他指针引用时,需要手动使用[obj retain];让该对象引用计数+1。
  • 当指针变量不在使用这个对象的时候,需要手动释放release这个对象。 让其的引用计数-1.
  • 当一个对象的引用计数为0的时候,系统就会销毁这个对象。
  • 在MRC模式下必须遵循谁创建,谁释放,谁引用,谁管理

内存泄漏

内存泄漏就是应当废弃的对象在超出其生存周期后继续存在。该被释放的时候未释放,一直被其内部的对象所持有,循环引用就属于内存泄漏。

block的属性修饰词为什么是copy?使用block有哪些使用注意?

block一旦没有进行copy操作,就不会在堆上
使用注意:循环引用问题

block在修改NSMutableArray,需不需要添加__block?

不需要
当变量是一个指针的时候,block里只是复制了一份这个指针,两个指针指向同一个地址。所以,在block里面对指针指向内容做的修改,在block外面也一样生效。

野指针和悬垂指针

野指针是指向“垃圾”内存(不可用内存)的指针
产生原因:
指针创建时未初始化。指针变量刚被创建时不会自动成为NULL指针,它会随机指向一个内存地址。

悬垂指针:指针所指向的对象已经被释放或者回收了,但是指向该对象的指针没有作任何的修改,仍旧指向已经回收的内存地址。 此类指针称为垂悬指针。

@property 属性声明的关键字:
weak 表示非持有特性,为属性设置新值的时候,设置方法既不会保留新值,也不会释放旧值。当属性所指的对象释放的时候,属性也会被置为 nil
assign用来修饰基本数据类型和对象。当 assign 用来修饰对象的时候,和 weak 类似。唯一的区别就是当属性所指的对象释放的时候,属性指针不会被置为 nil,这就会产生悬垂指针
unsafe_unretained 用来修饰属性的时候,和 assign 修饰对象的时候是一模一样的。为属性设置新值的时候,设置方法既不会保留新值,也不会释放旧值。唯一的区别就是当属性所指的对象释放的时候,属性不会被置为 nil,这就会产生 悬垂指针,所以是不安全的。

情形一
weak和assign的区别:
释放对象是否产生野指针,适用 OC 类型还是基本类型。

1. 修饰类型不同

  • weak只能用于修饰OC对象类型;
  • assign既可以修饰OC类型也可以修饰基本类型;

2. 释放是是否产生野指针
weak 对象被释放是,对象的指针会被设置为 nil,因此再次去对该对象发送消息,不会崩溃,因此 weak 是安全的;
assign 如果修饰对象,会产生野指针问题;如果修饰基本数据类型则是安全的。 当对象被释放的时候,对象的指针不会置空,该对象会变成野指针,再次对该对象发送消息,会直接崩溃。

总结:
assign:适用于基本数据类型(int),因为基本数据类型存放在栈中,采用先进先出的原则,用系统自动分配释放管理内存。如果使用在对象类型,存放在堆中,需要考虑野指针的问题,则程序员要手动分配释放或者 ARC 下内存自动管理分配。
weak:适用于 OC 对象类型,同时也适用于 delegate,不会产生野指针,也不会循环引用,非常安全。
情形二
block为什么用copy修饰 ?
默认情况下,block 是存放在栈中即 NSStackBlock ,因此 block 在函数调用结束时,对象会变成 nil,但是对象的指针变成野指针,因此对象继续调用会产生异常。使用 copy 修饰之后,会将 block 对象保存到堆中 NSMallocBlock,它的生命周期会随着对象的销毁而结束的。所以函数调用结束之后指针也会被设置为 nil,再次调用该对象也不会产生异常。

解决循环引用常见的方式

  • weak-strong-dance

  • __block修饰对象(需要注意的是在block内部需要置空对象,而且 block 必须调用)

  • 传递对象 self 作为 block 的参数,提供给 block内部 使用

  • 使用NSProxy

定时器 NSTimer 中的循环引用

首先控制器 self 强引用 timer,然后timer 又强引用我们的控制器 self,这就造成了循环引用,释放不掉造成内存泄露。

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
复制代码

NSTimer循环引用解决方案:

方案一(失败):weak-strong-dance

一想到循环引用的解决方案,我们首先想到的肯定是 weak-strong-dance,那么我们来尝试一下:

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
复制代码

我们再次运行程序,进行 push-pop 跳转。发现问题还是存在,即定时器方法仍然在执行,并没有执行控制器 selfdealloc 方法,为什么呢?

我们使用 __weak 虽然打破了 self -> timer -> self 之前的循环引用,即引用链变成了self -> timer -> weakSelf -> self。但是在这里我们的分析并不全面,此时还有一个 Runlooptimer 的强持有,因为 Runloop 的生命周期比控制器 self 界面更长,所以导致了 timer 无法释放,同时也导致了控制器 self 界面也无法释放。它们之间的引用链如下图所示:

timer引用链.png

我们的定时器 timer 捕获的是控制器 self,是一个对象,其引用链关系为:NSRunLoop -> timer -> weakSelf -> self。所以RunLoop对整个对象的空间有强持有,runloop没停,timer 和 weakSelf是无法释放的。

方案二(成功):pop时在其他方法中销毁timer

根据前面的解释,我们知道由于 Runlooptimer 的强持有,导致了 Runloop 间接的强持有了self(因为 timer 中捕获的是 self 对象)。所以导致 dealloc 方法无法执行。需要查看在 pop 时,是否还有其他方法可以销毁 timer。这个方法就是 didMoveToParentViewController

didMoveToParentViewController方法,是用于当一个视图控制器中添加或者移除 viewController 后,必须调用的方法。目的是为了告诉iOS,已经完成添加/删除子控制器的操作。

在控制器 self中重写 didMoveToParentViewController方法

- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 无论push 进来 还是 pop 出去 正常跑
    // 就算继续push 到下一层 pop 回去还是继续
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}
复制代码

方案三(成功):中介者模式,即不使用self,依赖于其他对象

timer 模式中,我们重点关注的是 fireInTheHole 能执行,并不关心 timer 捕获的 target 是谁,由于这里不方便使用 self(因为会有强持有问题),所以可以将 target 换成其他对象,例如将 target 换成NSObject 对象,将 fireInTheHole 交给 target 执行

timertargetself 改成 objc

@property (nonatomic, strong) id target;

//**********1、修改target**********
    self.target = [[NSObject alloc] init];
    class_addMethod([NSObject class], @selector(fireInTheHole), (IMP)fireInTheHoleObjc, "v@:");
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireInTheHole) userInfo:nil repeats:YES];
    
//**********imp**********
void fireInTheHoleObjc(id obj){
    NSLog(@"%s -- %@",__func__,obj);
}

- (void)fireInTheHole {
    NSLog(@"fire in the hole");
}
    
复制代码

运行结果如下:

中介者模式1.png

运行发现执行 dealloc 之后,timer还是会继续执行。原因是解决了中介者的释放,但是没有解决中介者的回收,即 self.target 的回收。所以这种方式有缺陷

可以通过在 dealloc 方法中,取消定时器来解决,代码如下:

-(void)dealloc {
    NSLog(@"%s",__func__);
    [self.timer invalidate];
    self.timer = nil;
}
复制代码

发现pop之后,timer释放,从而中介者也会进行回收释放,运行结果如下:

中介者模式.png

方案四(成功):NSProxy虚基类的方式

NSProxy 是一个虚基类,它的地位等同于 NSObject
command+shift+0 打开 Xcode 参考文档搜索 NSProxy,说明如下:

NSProxy

An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.

Declaration

@interface NSProxy

Overview

Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent distributed messaging (for example, NSDistant<wbr>Object) or for lazy instantiation of objects that are expensive to create.
NSProxy implements the basic methods required of a root class, including those defined in the NSObject protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override the forward<wbr>Invocation: and method<wbr>Signature<wbr>For<wbr>Selector: methods to handle messages that it doesn’t implement itself. A subclass’s implementation of forward<wbr>Invocation: should do whatever is needed to process the invocation, such as forwarding the invocation over the network or loading the real object and passing it the invocation. method<wbr>Signature<wbr>For<wbr>Selector: is required to provide argument type information for a given message; a subclass’s implementation should be able to determine the argument types for the messages it needs to forward and should construct an NSMethod<wbr>Signatureobject accordingly. See the NSDistant<wbr>Object, NSInvocation, and NSMethod<wbr>Signature class specifications for more information.

我们不用 self 来响应 timer 方法的 target,而是用 NSProxy来响应。

首先定义一个继承自 NSProxy 的子类

FXProxy.h

@interface FXProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
复制代码

FXProxy.m

@interface FXProxy ()
@property (nonatomic, weak) id object;
@end

@implementation FXProxy
+ (instancetype)proxyWithTransformObject:(id)object{
    FXProxy *proxy = [FXProxy alloc];
    proxy.object = object;
    return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}
@end
复制代码

timer 中的 target 传入 NSProxy 子类对象,即 timer 持有 NSProxy 子类对象

//************解决timer强持有问题************
self.proxy = [FXProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireInTheHole) userInfo:nil repeats:YES];
    
-(void)dealloc {
  NSLog(@"%s",__func__);
  [self.timer invalidate];
  self.timer = nil;
}  
复制代码

这样做的主要目的是将强引用的注意力转移成了消息转发。虚基类只负责消息转发,即使用 NSProxy 作为中间代理、中间者
这里有个疑问,定义的 proxy 对象,在 dealloc 释放时,还存在吗?

proxy 对象会正常释放,因为 self 正常释放了,所以可以释放其持有者,即 timerproxytimer 的释放也打破了 runLoopproxy 的强持有。完美的达到了两层释放,解释如下:

  • self释放,导致了 proxy 的释放

  • dealloc 方法中,timer 进行了释放,所以 runloop 强引用也释放了

它们之间的引用链如下图所示:

proxy.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享