1. 有哪几种常见的内存管理模式?什么情况会出现内存泄漏?如何避免?
常见的内存管理模式
- ARC
- MRC
ARC和MRC的简单介绍
内存管理是指软件运行时对计算机内存资源的分配和使用的技术,其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。
内存是分配在哪里的?
- iOS中数据是存在在堆和栈中的
- 内存管理管理的是堆上的内存,栈上的内存不需要我们管理
- 非OC对象(基础数据类型)存储在栈上
- OC对象存储在堆上
int a = 10; //栈
int b = 20; //栈
Car *c = [[Car alloc]init]; //堆
如图
引用计数是计算机的一种内存管理技术,是指将资源的被引用次数保存起来,当引用次数变为0时就将该资源释放的过程
iOS常使用的是ARC(自动管理引用计数)
- 会根据引用计数自动监视对象的生命周期,在编译时期自动在已有的代码中插入合适的内存管理代码
MRC是手动管理引用计数
- 四个法则
- 自己生成的对象,自己持有
- 非自己生成的对象,自己也能持有
- 不再需要自己持有对象的时释放
- 无法释放非自己持有的对象
/*
* 自己生成并持有该对象
*/
id obj0 = [[NSObeject alloc] init];
id obj1 = [NSObeject new];
/*
* 持有非自己生成的对象
*/
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj retain]; // 自己持有对象
/*
* 不在需要自己持有的对象的时候,释放
*/
id obj = [[NSObeject alloc] init]; // 此时持有对象
[obj release]; // 释放对象
/*
* 指向对象的指针仍就被保留在obj这个变量中
* 但对象已经释放,不可访问
*/
/*
* 非自己持有的对象无法释放
*/
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
[obj release]; //
~~~此时将运行时crash 或编译器报error~~~
非 ARC 下,调用该方法会导致编译器报 issues。此操作的行为是未定义的,可能会导致运行时 crash 或者其它未知行为
还有一个情况就是自己生成的对象,且该对象存在,但自己不持有
- (id) getAObjNotRetain {
id obj = [[NSObject alloc] init]; // 自己持有对象
[obj autorelease]; // 取得的对象存在,但自己不持有该对象
return obj;
}
使用auturelease方法可以使取得的对象存在,但自己不持有对象
- 调用autorelease,该对象会注册到autoreleasepool中,在超出生命周期后调用release被释放
- 调用release对象会被立即释放
其实比如[NSMutableArray array]、[NSArray array]都可以取得谁都不持有的对象,这些方法都是通过autorelease实现的。
内存泄露+如何避免
内存泄露
- 概念:申请的内存空间使用完毕之后未回收
- 根本原因:发生了循环引用
- 一次内存泄露危害可以忽略,但若一直泄漏,无论有多少内存,迟早都会被占用光,最终导致程序crash
目前,在ARC环境下,循环引用三种常见的情况
- delegate
如果代理用strong来修饰,那么delegate会强引用代理方,代理方强引用委托方,委托方内部对delegate又有一个强引用——造成循环引用
- block
- 理由:如果block被当前ViewController(self)持有,这时,如果block内部再持有ViewController(self),就会造成循环引用。
- 解决方案:在block外部对弱化self,再在block内部强化已经弱化的weakSelf
- NSTimer
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(updateTime:)
userInfo:nil
repeats:YES];
- 理由:这时 target: self,增加了ViewController的retain count, 即self强引用timer,timer强引用self。造成循环引用。
- 解决方案:在恰当时机调用[timer invalidate]即可。
2. 详细说明一下点击按钮之后事件响应的流程?
事件产生和传递
- 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的队列事件中
- UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常会先发送事件给应用程序的主窗口(keyWindow)
- 触摸事件的传递是从父控件传递到子控件
- 也就是UIApplication->window->寻找处理事件最合适的view
UIView不能接收触摸事件的三种情况:
-
不接受用户交互:userInteractionEnabled = NO;
-
隐藏:hidden = YES;
-
透明:alpha = 0.0~0.01
应用如何找到最合适的控件来处理事件?
- 首先判断主窗口(keyWindow)自己是否能接受触摸事件
- 判断触摸点是否在自己身上
- 子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
- view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止
- 如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view
在事件传递寻找最合适的View时,底层到底干了哪些事?
寻找合适的View用到两个重要方法:
hitTest:withEvent:
- 不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法
- 可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view
- 一般是重写自己的父控件的hitTest:withEvent:方法,返回指定的子控件
pointInside:withEvent:
- 该方法判断点在不在当前view上(方法调用者的坐标系上)
- 如果返回YES,代表点在方法调用者的坐标系上
- 返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件
hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上
综上,其实事件的传递的过程是这样的:
产生触摸事件 --> UIApplication事件队列 --> [UIWindow hitTest:withEvent:] --> 返回更合适的view --> [子控件 hitTest:withEvent:] --> 返回最合适的view
事件的响应
响应者链条:
在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”,也可以说,响应者链是由多个响应者对象连接起来的链条
在iOS中响应者链的关系可以用下图表示:
- 事件的传递:
- 当一个事件发生后,事件会从父控件传给子控件
- 也就是说由UIApplication -> UIWindow -> UIView -> initial view
- 以上就是事件的传递,也就是寻找最合适的view的过程。
- 事件的响应:
- 首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView)
- 如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;
- 如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传递;
- (对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理)
- 一直到 window,如果window还是不能处理此事件则继续交给application处理
- 如果最后application还是不能处理此事件则将其丢弃
- 在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[super touches….],就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法
事件的传递和响应也可以简单理解为:
UIApplication-->UIWindow-->递归找到最合适处理的控件-->控件调用touches方法-->判断是否实现touches方法-->没有实现默认会将事件传递给上一个响应者-->找到上一个响应者-->找不到方法作废
事件的传递是从上到下(父控件到子控件)
事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件)