iOS面试题收纳-内存管理

285 阅读15分钟

什么是内存溢出

  • 系统已经不能分配出你所需要的空间

    比如系统仅剩1G的内存空间,而我们需要申请至少2G的内存空间

什么是内存泄漏

  1. 内存泄漏指动态分配内存的对象在使用完后没有被系统回收内存,对象始终占有着内存,属于内存管理出错。
  2. 一次内存泄露危害可以忽略,但内存泄漏堆积后果很严重,无论多少内存,迟早会被耗光
  3. 常见的有ARC下block,delegate,NSTimer等的循环引用造成内存泄漏

什么是Zombie僵尸对象

  1. 内存已经被系统回收的对象就叫僵尸对象
  2. 该对象的数据可能还在内存中,但它已经是一个不稳定对象,不可以再访问或者使用,它的内存是随时可能被别的对象申请而占用的。

Xcode Zombie 监控的原理

blog.csdn.net/lwb102063/a…

mp.weixin.qq.com/s?__biz=Mzg…

  • 首先我们会 hook 基类 NSObject 的 dealloc 方法
  • 当任意 OC 对象被释放的时候,hook 之后的那个 dealloc 方法并不会真正的释放这块内存
  • 同时将这个对象的 ISA 指针指向一个特殊的僵尸类,因为这个特殊的僵尸类没有实现任何方法,所以这个僵尸对象在之后接收到任何消息都会 Crash,与此同时我们会将崩溃现场这个僵尸对象的类名以及当时调用的方法名上报到后台分析
僵尸对象生成过程伪代码
// 1. 获取即将被释放的对象所属的类(Class)
Class cls = object_getClass(self);
// 2. 获取类名
const char *clsName = class_getName(cls);
// 3. 生成僵尸对象类名
const char *zombieClsName = "_NSZomeibe_" + clsName;
// 4. 检查是否存在相同的僵尸对象类名,不存在则创建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
  // 5. 获取僵尸对象类 _NSZombie_
  Class baseZombieCls = objc_lookUpClass("_NSZombie_");
  // 6. 创建zombieClsName类
  zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
// 7. 在对象内存未释放的情况下,销毁对象的成员变量及关联引用
objc_desctuctInstance(self);
// 8. 修改对象的isa指针,令其指向特殊的僵尸类
objc_setClass(self, zombieCls);
僵尸对象被触发伪代码
// 1. 获取对象Class
Class cls = object_getClass(self);
// 2. 获取对象类名
const char *clsName = class_getName(cls);
// 3. 检查是否带有前缀 _NSZombie_
if (string_has_prefix(clasName, "_NSZombie_")) {
  // 4. 获取被野指针对象类名
  const char *originalClsName = substring_from(clsName, 10);
  // 5. 获取当前调用方法名
  const char *selectorName = sel_getName(_cmd);
  // 6. 输出日志
  Log("*** - [%s %s]: message send to deallocted instance %p", originalClsName, selectorName, self);
  // 7. 结束进程
  abort();
}

什么是Core dump

Coredump 是由 lldb 定义的一种特殊的文件格式,Coredump 文件可以还原 App 在运行到某一时刻的完整运行状态(这里的运行状态主要指的是内存状态)

什么是空指针

  1. 空指针是一个没有指向任何内存的指针,空指针是有效指针,值为nil, NULL, Nil, 0
  2. 给空指针发送消息不会报错,不会响应消息

什么是野指针或者悬垂指针

  1. 野指针又叫做悬挂指针

  2. 野指针出现的原因

    1. 指针没有赋值

    2. 指针指向的对象已经释放了, 比如指向僵尸对象;

  3. 野指针可能指向一块垃圾内存,给野指针发送消息会导致程序崩溃

BAD_ACCESS 在什么情况下出现

访问了已经被销毁的内存空间,就会报出这个错误。根本原因是有 悬垂指针 没有被释放

iOS程序的内存布局

objc_memory_0.png

代码段

也叫程序区,存放程序编译产生的函数体的二进制代码数据

常量区

存储常量数据,通常程序结束后由系统自动释放(编译时分配,APP结束由系统释放)

全局区(静态区)

  1. 全局区又可分为未初始化全局区(.bss段)和初始化全局区(data段)
  2. 全局变量和静态变量的存储是放在一块的
    • 初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域
    • 在程序结束后由系统释放(编译时分配,APP结束由系统释放)

堆区(heap)

  1. 一般由程序员分配释放,若程序员不释放,程序结束时由OS回收
  2. 向高地址扩展的数据结构,是不连续的内存区域
    • 这是由于系统是用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址
  3. 容量大,速度慢,无序

栈区(stack)

  1. 由编译器自动分配释放 ,存放方法(函数)的参数值,局部变量的值等
  2. 栈是向低地址扩展的数据结构,是一块连续的内存的区域。即栈顶的地址和栈的最大容量是系统预先规定好的
  3. 容量小速度快,有序

堆和栈的区别

管理方式
  • 对于栈来讲,是由系统编译器自动管理,不需要程序员手动管理
  • 对于堆来讲,释放工作由程序员手动管理,不及时回收容易产生内存泄露
分配方式
  • 堆是动态分配和回收内存的,没有静态分配的堆
  • 栈有两种分配方式:静态分配和动态分配
    • 静态分配是系统编译器完成的,比如局部变量的分配
    • 动态分配由alloc函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现
碎片问题
  • 对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。
  • 对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出
分配效率
  • 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
  • 堆则是C/C++函数库提供的,它的机制是很复杂的。

OC的内存管理机制

  1. 在iOS中,使用引用计数来管理OC对象的内存,主要有手动内存管理、自动内存管理、自动释放池三种方式
  2. 一个新创建的OC对象引用计数默认是1,当引用计数减为0时OC对象就会被销毁,释放其占用的内存空间

手动内存管理 MRC

对象操作OC中对应的方法对应的 retainCount 变化
生成并持有对象alloc/new/copy/mutableCopy等+1
持有对象retain+1
释放对象release-1
废弃对象dealloc0
四个法则
  • 自己生成的对象,自己持有。
  • 非自己生成的对象,自己也能持有。
  • 不在需要自己持有对象的时候,释放。
  • 非自己持有的对象无需释放。
任何以下列名称为前缀的方法,若其返回值为 object,则方法调用者持有该 object:

alloc、new、copy、mutableCopy

还有一个更为严格的规则:任何以 init 为前缀的方法必须遵守下列规则
  • 该方法必须是实例方法;

  • 该方法必须返回类型为id或其所属class、superclass、subclass 的对象;

  • 该方法返回的 object 不能是 autorelese,即方法调用者持有返回的 object。

    • 『方法调用者持有该 object』也就意味着该 object 的内存问题需要调用方管理。

    • 在此之外的任何方法返回的 object,其调用方都不持有,即返回的应该是 autorelease object。

自动内存管理原理

理解ARC实现原理

Clang会自动插入retainrelease语句。Clang编译器有两部分,分别是前端编译器和ARC优化器。

前端编译器

前端编译器会为“拥有的”每一个对象插入相应的release语句。

  • 如果对象的所有权修饰符是__strong,那么它就是被拥有的。
  • 如果在某个方法内创建了一个对象,前端编译器会在方法末尾自动插入release语句以销毁它。而类拥有的对象(实例变量/属性)会在dealloc方法内被释放
  • 在ARC中,没有类可以覆盖release方法,也没有调用它的必要。ARC会通过直接使用objc_release来优化调用过程。
  • 对于retain也是同样的方法。ARC会调用objc_retain来取代保留消息。
ARC优化器

ARC优化器负责移除多余的retainrelease语句,确保生成的代码运行速度高于手动引用计数的代码

使用自动引用计数应遵循的原则
  • 不能使用 retainreleaseretainCountautorelease
  • 不可以使用 NSAllocateObjectNSDeallocateObject
  • 必须遵守内存管理方法的命名规则
  • 不需要显示的调用 Dealloc
  • 使用 @autoreleasePool 来代替 NSAutoreleasePool
  • 不可以使用区域 NSZone
  • 对象性变量不可以作为 C 语言的结构体成员
  • 显示转换 idvoid*

自动释放池

  1. 把需要释放的内存统一放在一个池子中,当池子被抽干后(drain),池子中所有的内存空间也被自动释放掉。
  2. 自动释放池的释放操作分为自动和手动。自动释放受runloop机制影响

两个有用的方法

extern uintptr_t _objc_rootRetainCount(id obj)方法,返回obj的引用计数
extern void _objc_autoreleasePoolPrint(void)方法,打印当前的自动释放池对象

ARC 的 retainCount 怎么存储的?

在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中

存在64张哈希表中,根据哈希算法去查找所在的位置,无需遍历,十分快捷

散列表(引用计数表、weak表)

  • SideTables 表在 非嵌入式的64位系统中,有 64张 SideTable
  • 每一张 SideTable 主要是由三部分组成。自旋锁引用计数表弱引用表
  • 全局的 引用计数 之所以不存在同一张表中,是为了避免资源竞争,解决效率的问题。
  • 引用计数表 中引入了 分离锁的概念,将一张表分拆成多个部分,对他们分别加锁,可以实现并发操作,提升执行效率

非OC对象如何管理内存

  • 非OC对象,需要手动执行释放操作,否则会造成大量的内存泄漏导致程序崩溃

  • 对于CoreFoundation框架下的某些对象或变量需要手动释放、C语言代码中的malloc等需要对应free

retainrelease 的底层

  1. 引用计数存在于SideTable中的refCount表中
  2. 通过第一层 hash 算法,找到对象地址所对应的 sideTable
  3. 然后再通过一层 hash 算法,找到存储 引用计数size_t,然后对其进行增减操作。
  4. retainCount 不是固定的 1,SIZE_TABLE_RC_ONE 是一个宏定义,实际上是一个值为 4 的偏移量

#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit

说一下对 strong,copy,assign,weak,_unsafe_unretain 关键字的理解。

strong

  1. strong 修饰符表示指向并持有该对象,其修饰对象的引用计数会加1。
  2. 该对象只要引用计数不为0就不会被销毁。当然可以通过将变量强制赋值 nil 来进行销毁

weak

  1. weak 修饰符指向但是并不持有该对象,引用计数也不会加1。
  2. Runtime 中对该属性进行了相关操作,无需处理,可以自动销毁。
  3. weak用来修饰对象,多用于避免循环引用的地方。weak 不可以修饰基本数据类型

assign

  1. assign主要用于修饰基本数据类型
  2. assign是可以修饰对象的,但是会出现悬垂指针的问题。当对象释放后继续给对象发送消息,将会造成crash

copy

  1. copy关键字和 strong类似,copy 多用于修饰有可变类型的不可变对象上

__unsafe_unretain

  1. __unsafe_unretain 类似于 weak ,但是当对象被释放后,指针已然保存着之前的地址,被释放后的地址变为 僵尸对象,访问被释放的地址就会出问题,所以说他是不安全的

__autoreleasing

  1. 将对象赋值给附有 __autoreleasing修饰的变量等同于 ARC 无效时调用对象的 autorelease 方法
  2. 实质就是扔进了自动释放池

什么是Tagged Pointer

  • 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储

  • 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

  • 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中

  • 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

  • objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

  • 如何判断一个指针是否为Tagged Pointer?

    • iOS平台,最高有效位是1(第64bit)
    • Mac平台,最低有效位是1

copy和mutableCopy区别

数据类型copymutableCopy
不可变浅拷贝单层深拷贝
可变单层深拷贝单层深拷贝

CADisplayLink、NSTimer会出现的问题以及解决办法

问题

  1. CADisplayLink、NSTimer都是基于 runloop 实现的。runloop 会对CADisplayLink、NSTimer进行强引用,
  2. CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用

解决方案

  • 使用block

    // 内部使用 WeakSelf,并在视图消失前,关闭定时器
    __weak __typeof(self)weakSelf = self;
    NSTimer * timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer");
    }];
    self.timer= timer;
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
  • 使用代理对象(NSProxy)

    @interface TimerProxy : NSProxy
    + (instancetype)proxyWithTarget:(id)target;
    @end
    
    /*
     - (void)viewDidLoad {
     [super viewDidLoad];
     self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:[MyProxy proxyWithTarget:self] selector:@selector(doSomething) userInfo:nil repeats:YES];
     }
     - (void)dealloc {
     if (_myTimer) {
     [_myTimer invalidate];
     }
     NSLog(@"MyViewController dealloc");
     }
     */
    @interface TimerProxy()
    @property (weak, readonly, nonatomic) id weakTarget;
    @end
    
    @implementation TimerProxy
    + (instancetype)proxyWithTarget:(id)target {
        return [[TimerProxy alloc] initWithTarget:target];
    }
    
    - (instancetype)initWithTarget:(id)target {
        _weakTarget = target;
        return self;
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation {
        SEL sel = [invocation selector];
        if (_weakTarget && [self.weakTarget respondsToSelector:sel]) {
            [invocation invokeWithTarget:self.weakTarget];
        }
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
        return [self.weakTarget methodSignatureForSelector:sel];
    }
    
    - (BOOL)respondsToSelector:(SEL)aSelector {
        return [self.weakTarget respondsToSelector:aSelector];
    }
    @end
    
  • 使用工厂方法返回一个timer

    @interface TargetTimer : NSObject
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         target:(id)target
                                       selector:(SEL)selector
                                       userInfo:(id _Nullable)userInfo
                                        repeats:(BOOL)repeats;
    @end
    
    /*
     self.myTimer = [MyTimerTarget scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
     */
    
    #import "TargetTimer.h"
    
    @interface TargetTimer()
    @property (assign, nonatomic) SEL outSelector;
    @property (weak, nonatomic) id outTarget;
    @end
    
    @implementation TargetTimer
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         target:(id)target
                                       selector:(SEL)selector
                                       userInfo:(id _Nullable)userInfo
                                        repeats:(BOOL)repeats {
        TargetTimer *timerTarget = [[TargetTimer alloc] init];
        timerTarget.outTarget = target;
        timerTarget.outSelector = selector;
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval                                                      target:timerTarget                                                    selector:@selector(timerSelector:)                                                    userInfo:userInfo                                                     repeats:repeats];
        return timer;
    }
    
    - (void)timerSelector:(NSTimer *)timer {
        if (self.outTarget && [self.outTarget respondsToSelector:self.outSelector]) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [self.outTarget performSelector:self.outSelector                             withObject:timer.userInfo];
    #pragma clang diagnostic pop
        }
        else {
            [timer invalidate];
        }
    }
    @end
    

方法里有局部对象, 出了方法后会立即释放吗

  • 如果是普通的局部对象,会立即释放
  • 如果是放在了 autoreleasePool 自动释放池,则会等runloop 进入休眠前释放

OC中有GC垃圾回收机制吗,iPhone上有GC吗

  • 垃圾回收(GC)就是程序中用于处理废弃不用的内存对象的机制,防止内存泄露
  • OC本身是支持垃圾回收的,不过只支持MAC OSX平台,iOS 平台不支持

怎么保证多人开发进行内存泄露的检查

  • 使用Analyze进行代码静态分析
  • 为避免不必要的麻烦,多人开发时尽量使用ARC
  • 使用leaks 进行内存泄漏检测
  • 使用一些三方工具

一张图片的内存占用大小是由什么决定的

图片的显示占用内存与图片的硬盘占用大小, 其质量没有关系, 仅仅和其本身的分辨率以及颜色占用字节有关

图片显示占用内存大小 = 图片的宽度 x 图片的高度 x 颜色 RGBA 占用的4个字节;

① 之所以图片的大小与硬盘的占用大小无关是因为硬盘中的图片都是以不同的容器格式被编码了的, 如png, jpeg 等格式, 这些不能被用来直接显示; ② 能够被用来直接显示的只有一种格式就是 bitmap 位图的方式. 要从硬盘中的容器格式变成位图,就会经历 图片解码的过程. 而解码后的位图大小计算方式如上

什么是OOM?怎么检测?

OOM(Out Of Memory)指的是应用内存占用过高被系统强制杀死的情况,通常还会再分为 FOOM (前台 OOM) 和 BOOM (后台 OOM)两种

zhangferry.com/2022/01/20/…

目前有3中主要的检测方法

  • FBAllocationTrackers 通过hook oc的alloc和dealloc追踪分配和释放计数,问题是无法追踪非OC的内存情况,且无法dump堆栈
  • OOMDetector 通过hook 系统底层内存分配的malloc_zone、vm_allocate相关方法,设置一个内存阈值,如果超过阈值之后,定时dump堆栈
  • Memory Graph通过vm_region_recurse扫描进程中dirty memory,通过建立node之间的有向图,分析内存问题