iOS面试题:iOS 开发中常见的内存问题有哪些?

内存问题主要包括两个部分,一个是iOS中常见循环引用导致的内存泄露 ,另外就是大量数据加载及使用导致的内存警告。

mmap

虽然苹果并没有明确每个 App 在运行期间可以使用的内存最大值,但是有开发者进行了实验和统计,一般在占用系统内存超过 20% 的时候会有内存警告,而超过 50% 的时候,就很容易 Crash 了,所以内存使用率还是尽量要少,对于数据量比较大的应用,可以采用分步加载数据的方式,或者采用 mmap 方式。mmap 是使用逻辑内存对磁盘文件进行映射,中间只是进行映射没有任何拷贝操作,避免了写文件的数据拷贝。 操作内存就相当于在操作文件,避免了内核空间和用户空间的频繁切换,能够提供高性能的写入速度。此外,mmap 可以保持数据的一致性,即使在对应的用户进程崩溃后,内存映射的文件仍然可以落盘。参见:mmap 实现数据一致性。因为,用户进程崩溃后,内核会托管 mmap 的交换区,保证对应的数据能够存盘。sqlite 里也使用 mmap 提高性能防止丢数据。

循环引用

循环引用是 iOS 开发中经常遇到的问题,尤其对于新手来说是个头疼的问题。循环引用对 App 有潜在的危害,会使内存消耗过高,性能变差和 Crash 等,iOS 常见的内存主要以下三种情况:

1)Delegate

代理协议是一个最典型的场景,需要你使用弱引用来避免循环引用。ARC 时代,需要将代理声明为 weak 是一个即好又安全的做法:

@property (nonatomic, weak) id <MyCustomDelegate> delegate;

2)block

Block 的循环引用,主要是发生在 ViewController 中持有了 block,比如:

@property (nonatomic, copy) LFCallbackBlock callbackBlock;

同时在对 callbackBlock 进行赋值的时候又调用了 ViewController 的方法,比如:

    self.callbackBlock = ^{
        [self doSomething];
    }];

就会发生循环引用,因为:ViewController -> 强引用了 callback -> 强引用了 ViewController,解决方法也很简单:

    __weak __typeof(self) weakSelf = self;
    self.callbackBlock = ^{
      [weakSelf doSomething];
    }];

原因是使用 MRC 管理内存时,Block 的内存管理需要区分是 Global(全局)、Stack(栈)还是 Heap(堆),而在使用了 ARC 之后,苹果自动会将所有原本应该放在栈中的 Block 全部放到堆中。全局的 Block 比较简单,凡是没有引用到 Block 作用域外面的参数的 Block 都会放到全局内存块中,在全局内存块的 Block 不用考虑内存管理问题。(放在全局内存块是为了在之后再次调用该 Block 时能快速反应,当然没有调用外部参数的 Block 根本不会出现内存管理问题)。

所以 Block 的内存管理出现问题的,绝大部分都是在堆内存中的 Block 出现了问题。默认情况下,Block 初始化都是在栈上的,但可能随时被收回,通过将 Block 类型声明为 copy 类型,这样对 Block 赋值的时候,会进行 copy 操作,copy 到堆上,如果里面有对 self 的引用,则会有一个强引用的指针指向 self,就会发生循环引用,如果采用 weakSelf,内部不会有强类型的指针,所以可以解决循环引用问题。

那是不是所有的 block 都会发生循环引用呢?其实不然,比如 UIView 的类方法 Block 动画,NSArray 等的类的遍历方法,也都不会发生循环引用,因为当前控制器一般不会强引用一个类。

此外,还有一种情况是在 self.callbackBlock 中使用了 ivar,也会造成循环引用。因为对 ivar 变量的直接访问还是会依赖 self 的编译地址再进行偏移。

3)NSTimer

NSTimer 我们开发中会用到很多,比如下面一段代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomeThing) userInfo:nil repeats:YES];
    }
    - (void)doSomeThing {
    }
    - (void)dealloc {
         [self.timer invalidate];
         self.timer = nil;
    }

这是典型的循环引用,因为 timer 会强引用 self,而 self 又持有了 timer,所有就造成了循环引用。那有人可能会说,我使用一个 weak 指针,比如:

    __weak typeof(self) weakSelf = self;
    self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(doSomeThing) userInfo:nil repeats:YES];

但是其实并没有用,因为不管是 weakSelf 还是 strongSelf,最终在 NSTimer 内部都会重新生成一个新的指针指向 self,这是一个强引用的指针,结果就会导致循环引用。那怎么解决呢?主要有如下三种方式:

3.1)使用中间类

创建一个继承 NSObject 的子类 MyTimerTarget,并创建开启计时器的方法。

    // MyTimerTarget.h
    #import <Foundation/Foundation.h>
    @interface MyTimerTarget : NSObject
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats;
    @end
    // MyTimerTarget.m
    #import "MyTimerTarget.h"
    @interface MyTimerTarget ()
    @property (assign, nonatomic) SEL outSelector;
    @property (weak, nonatomic) id outTarget;
    @end
    @implementation MyTimerTarget
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats {
        MyTimerTarget *timerTarget = [[MyTimerTarget 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]) {
            [self.outTarget performSelector:self.outSelector withObject:timer.userInfo];
        } else {
            [timer invalidate];
        }
    }
    @end
    // 调用方
    @property (strong, nonatomic) NSTimer *myTimer;
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.myTimer = [MyTimerTarget scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
    }
    - (void)doSomeThing {
    }
    - (void)dealloc {
        NSLog(@"MyViewController dealloc");
    }

VC 强引用 timer,因为 timer 的 target 是 MyTimerTarget 实例,所以 timer 强引用 MyTimerTarget 实例,而 MyTimerTarget 实例弱引用 VC,解除循环引用。这种方案 VC 在退出时都不用管 timer,因为自己释放后自然会触发 timerSelector:中的[timer invalidate]逻辑,timer 也会被释放。

3.2)使用类方法

我们还可以对 NSTimer 做一个 category,通过 block 将 timer 的 target 和 selector 绑定到一个类方法上,来实现解除循环引用。

    // NSTimer+MyUtil.h
    #import <Foundation/Foundation.h>
    @interface NSTimer (MyUtil)
    + (NSTimer *)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
    @end
    // NSTimer+MyUtil.m
    #import "NSTimer+MyUtil.h"
    @implementation NSTimer (MyUtil)
    + (NSTimer *)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats {
        return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(MyUtil_blockInvoke:) userInfo:[block copy] repeats:repeats];
    }
    + (void)MyUtil_blockInvoke:(NSTimer *)timer {
        void (^block)() = timer.userInfo;
        if (block) {
            block();
        }
    }
    @end
    // 调用方
    @property (strong, nonatomic) NSTimer *myTimer;
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.myTimer = [NSTimer MyUtil_scheduledTimerWithTimeInterval:1 block:^{
            NSLog(@"doSomething");
        } repeats:YES];
    }
    - (void)dealloc {
        if (_myTimer) {
            [_myTimer invalidate];
        }
        NSLog(@"MyViewController dealloc");
    }

这种方案下,VC 强引用 timer,但是不会被 timer 强引用,但有个问题是 VC 退出被释放时,如果要停掉 timer 需要自己调用一下 timer 的 invalidate 方法。

3.2)使用 weakProxy

创建一个继承 NSProxy 的子类 MyProxy,并实现消息转发的相关方法。NSProxy 是 iOS 开发中一个消息转发的基类,它不继承自 NSObject。因为他也是 Foundation 框架中的基类, 通常用来实现消息转发, 我们可以用它来包装 NSTimer 的 target, 达到弱引用的效果。

    // MyProxy.h
    #import <Foundation/Foundation.h>
    @interface MyProxy : NSProxy
    + (instancetype)proxyWithTarget:(id)target;
    @end
    // MyProxy.m
    #import "MyProxy.h"
    @interface MyProxy ()
    @property (weak, readonly, nonatomic) id weakTarget;
    @end
    @implementation MyProxy
    + (instancetype)proxyWithTarget:(id)target {
        return [[MyProxy 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
    // 调用方
    @property (strong, nonatomic) NSTimer *myTimer;
    - (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");
    }

上面的代码中,了解一下消息转发的过程就可以知道 -forwardInvocation: 是会有一个 NSInvocation 对象,这个 NSInvocation 对象保存了这个方法调用的所有信息,包括 Selector 名,参数和返回值类型,最重要的是有所有参数值,可以从这个 NSInvocation 对象里拿到调用的所有参数值。这时候我们把转发过来的消息和 weakTarget 的 selector 信息做对比,然后转发过去即可。

这里需要注意的是,在调用方的 dealloc 中一定要调用 timer 的 invalidate 方法,因为如果这里不清理 timer,这个调用方 dealloc 被释放后,消息转发就找不到接收方了,就会 crash。

3.3)使用 GCD timer

GCD 提供的定时器叫 dispatch_source_t。使用方式如下:

    // 调用方
    @property (strong, nonatomic) dispatch_source_t myGCDTimer;
    - (void)viewDidLoad {
        [super viewDidLoad];
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
        if (timer) {
            self.myGCDTimer = timer;
            dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 1 * NSEC_PER_SEC, 1ull * NSEC_PER_SEC);
            dispatch_source_set_event_handler(timer, ^ {
                NSLog(@"doSomething");
            });
            dispatch_resume(timer);
        }
    }
    - (void)dealloc {
        if (_myGCDTimer) {
            dispatch_cancel(_myGCDTimer);
        }
        NSLog(@"MyViewController dealloc");
    }

更多详情见:NSTimer 循环引用解决方案

其他内存问题

  • NSNotification addObserver 之后,记得在 dealloc 里面添加 remove。
  • 动画的 repeat count 无限大,而且也不主动停止动画,基本就等于无限循环了。
  • forwardingTargetForSelector 返回了 self。

高性能地使用内存的建议

  • 熟读 ARC 机制原理
  • 使用 weak 修饰替换 unsafe_unretain
  • 小心方法中的 self,在 Objective-C 的方法中隐含的 self 是 __unsafed_unretain 的。
  • 使用 Autorelease Pool 来降低循环中的内存峰值,避免 OOM。
  • 要处理 Memory Warning。
  • 需要在收到内存警告的时候释放的缓存类数据,在选用数据结构时,用 NSCache 代替 NSDictionary,使用 NSPurgableData 代替 NSData。在其他常见的操作系统上,由于局部性原理,OS 会将不常用的内存页面写回磁盘,频繁的写磁盘会缩短磁盘或闪存的生命,iOS 为了提升闪存的生命周期,所以没有交换空间,取而代之的是内存压缩技术,iOS 将不常用到的 dirty 页面压缩以减少页面占用量,在再次访问到的时候重新解压缩。这些都在操作系统层面实现,对进程无感知。倘若在使用 NSDictionary 的时候收到内存警告,然后去释放这个 NSDictionary,如果占据的内存过大,很可能在内存解压的过程中造成内存压力更大而导致 App 就被 JetSam 给 Kill 掉了,如果你的内存只是缓存或者是可重建的数据,就把 NSCache 当初 NSDictionary 用。同理 NSPurableData 也是。
  • UITableView/UICollectionView 的重用不单单是 cell 重用,cell 使用的子 view 也要重用。
  • [UIImage imageNamed:] 适合于 UI 界面中的贴图的读取,较大的资源文件应该尽量避免使用。
  • WKWebView 是跨进程通信的,不会占用我们的 APP 使用的物理内存量。
  • try、catch、finally 一定要清理资源。
  • 对大的内存对象进行懒加载,但是要注意线程安全。

关于 iOS 内存管理更多的内容,参见 iOS Memory Deep Dive

内存解决思路

更多信息参加:iOS App 稳定性指标及监测


更多:iOS面试题合集

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容