美文网首页
iOS常见内存问题分析

iOS常见内存问题分析

作者: jackyshan | 来源:发表于2020-06-09 20:42 被阅读0次

引用计数

引自维基百科
引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。
当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。
使用这种方式进行内存管理的语言:Objective-C

iOS是使用引用计数管理内存,非常需要注意的一个点就是持有关系。持有关系就是A_View持有B_View,
[B_View removeFromSuperview]释放A_View对B_View的持有,B_View才会释放。
如果B_View没有调用[B_View removeFromSuperview],即使B_View=nil,也不会释放。因为A_View依然在持有B_View。

所以在iOS里面想要obj释放,不要使用obj=nil,如果持有关系没解除,释放不掉的。

内存问题-定时器

定时器在持有关系上比较特殊,生成一个定时器并开启,RunLoop会持有Timer,Timer会持有Target,Timer不关闭,Target和Timer都不会释放。

释放方式一【手动停止定时器】

- (void)invalidate;
@interface B_View : UIView

/** timer */
@property (nonatomic, strong) NSTimer *timer;

@end

@implementation B_View

- (void)stopTimer {
    [_timer invalidate];
}

B_View手动停止定时器,这样Timer释放了对B_View的持有,B_View就可以dealloc了。

释放方式二【Timer持有中间对象】

中间对象作为Timer的Target,每次触发定时器的判断B_View是否被释放了,释放了就停止定时器。这样B_View在使用定时器的时候,不需要再操心定时器的释放了。

weakTarget实现如下

@interface IKWeakTimerTarget : NSObject

@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer* timer;

@end

@implementation IKWeakTimerTarget

- (void)fire:(NSTimer *)timer {
    if(self.target) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
#pragma clang diagnostic pop
    } else {
        [self.timer invalidate];
    }
}

@end

@implementation IKWeakTimer

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                     target:(id)aTarget
                                   selector:(SEL)aSelector
                                   userInfo:(id)userInfo
                                    repeats:(BOOL)repeats {
    IKWeakTimerTarget *timerTarget = [[IKWeakTimerTarget alloc] init];
    timerTarget.target = aTarget;
    timerTarget.selector = aSelector;
    timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                                         target:timerTarget
                                                       selector:@selector(fire:)
                                                       userInfo:userInfo
                                                        repeats:repeats];
    return timerTarget.timer;
}

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      block:(IKTimerHandler)block
                                   userInfo:(id)userInfo
                                    repeats:(BOOL)repeats {
    NSMutableArray *userInfoArray = [NSMutableArray arrayWithObject:[block copy]];
    if (userInfo != nil) {
        [userInfoArray addObject:userInfo];
    }
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(timerBlockInvoke:)
                                       userInfo:[userInfoArray copy]
                                        repeats:repeats];
}

+ (void)timerBlockInvoke:(NSArray*)userInfo {
    IKTimerHandler block = userInfo[0];
    id info = nil;
    if (userInfo.count == 2) {
        info = userInfo[1];
    }
    
    if (block) {
        block(info);
    }
}

@end

平时使用定时器还需要注意一点,就是在starTimer之前,最好先stopTimer一下。有可能starTimer多次,生成了多个Timer对象,造成一堆的Timer在跑,没释放。

内存问题-延迟执行

dispatch延迟3秒执行block,要在block里面使用weak_self,如果3秒内weak_self释放了,weak_self为nil。

weakify(self)
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
    if (!weak_self) {
        return;
    }
    [weak_self xxx];
    [weak_self aaaaa:@"3"];
    [[CJK_FloatHandle sharedInstance] callingEnd];
});

3秒内weak_self释放了,后面的代码就不会执行了。

注意,要在block里面进行weak_self为nil的判断,如果不做这个判断,后面的单例依然会执行!!!

内存问题-网络请求

网络请求的问题和上面延迟执行的问题类似,由于网络是异步加载的,在网络环境很差的时候,如果页面退出了,由于网络请求的block还在持有self页面,导致页面不能释放,直到网络请求返回执行完block才释放self。

注意,如果block一直没有回调,self就一直不释放!!!

- (void)getBannerData {
    weakify(self)
    [CJActivityBannerService reqBannerList:@{@"type":@(_type)} complete:^(NSArray * _Nonnull arr) {
        strongify(self)
        if (!self) {
            return;
        }
        
        [self _initView];
        [self.bannerView configBannerCellWithModel:arr];
    }];
}

这里对临时变量self做了为nil的判断,虽然不做判断也没问题,因为block里面不存在单例、after等场景,最好是养成习惯,对self或weak_self做一次nil判断,以后即使增加代码,也不会有隐藏风险。

内存问题-代理

其实代理就是一个对象持有另外一个对象,然后执行另外一个对象的方法。如果A持有B,B的delegate刚好是A,那B的delegate要用weak修饰了。

@property (nonatomic, weak, nullable) id <UICollectionViewDelegate> delegate;
@property (nonatomic, weak, nullable) id <UICollectionViewDataSource> dataSource;

内存问题-单例

很简单的道理,因为单例贯穿整个APP的生命周期的,单例不会释放。如果单例持有了一个外部传过来的view,这个view需要用weak修饰,不然view就一直被单例持有,不会释放。

@interface GiftComboAnimaOperationManager : NSObject

/// 父视图weak
@property (nonatomic, weak) UIView *parentView;

/// 单例
+ (instancetype)sharedManager;

@end

内存问题-动画

CAAnimation的delegate为strong,如果CAAnimation不释放,我们的self也不会释放。

/* The delegate of the animation. This object is retained for the
 * lifetime of the animation object. Defaults to nil. See below for the
 * supported delegate methods. */

@property(nullable, strong) id <CAAnimationDelegate> delegate;

/* When true, the animation is removed from the render tree once its
 * active duration has passed. Defaults to YES. */

@property(getter=isRemovedOnCompletion) BOOL removedOnCompletion;

如果removedOnCompletion设置为NO,CAAnimation执行完动画并不会主动释放。
这就需要手动释放CAAnimation。

[CAAnimation removeAnimationForKey:@"key"];

内存问题-present vc

[vc_1 presentViewController:vc_2]

vc_1和vc_2相互持有,在vc_1 pop返回的时候,要先使vc_2 dissmiss,才能释放vc_1vc_2

vc_1调用

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];

    if (self.presentedViewController) {
        [self.presentedViewController dismissViewControllerAnimated:NO completion:nil];
    }
}

内存问题-KVO

通知

NSNotificationCenter从iOS9之后,会在对象dealloc的时候移除Observer。

[[NSNotificationCenter defaultCenter] addObserver: self selector:@selector(appBecomeActive:)
                                             name: UIApplicationDidBecomeActiveNotification
                                           object: nil];

iOS9之前要手动调用removeObserver

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

为什么 iOS 9 之前需要手动移除观察者对象?

观察者注册时,通知中心并不会对观察者对象做 retain 操作,而是对观察者对象进行unsafe_unretained 引用。

什么是unsafe_unretained?因为 Cocoa 和 Cocoa Touch 中的一些类仍然还没有支持 weak 引用。所以,当我们想对这些类使用弱引用的时候,只能用unsafe_unretained来替代。

// for attribute

@property (unsafe_unretained) NSObject *unsafeProperty;

// for variables

NSObject *__unsafe_unretained unsafeReference;

不安全引用(unsafe reference)和弱引用 (weak reference) 类似,它并不会让被引用的对象保持存活,但是和弱引用不同的是,当被引用的对象释放的时,不安全引用并不会自动被置为 nil,这就意味着它变成了野指针,而对野指针发送消息会导致程序崩溃

KVO

[self.marqueeLabel addObserver:self
                    forKeyPath:@"text"
                       options:NSKeyValueObservingOptionNew
                       context:nil
 ];

如果没有调用removeObserver,会发生崩溃。

An instance 0x11d3c9be0 of class UILabel was deallocated
while key value observers were still registered with it.
Current observation info: <NSKeyValueObservationInfo 0x17182bb00> ( <NSKeyValueObservance 0x171c5bd20: Observer: 0x11d3c7ad0, Key path: text, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x171c42220> <NSKeyValueObservance 0x171e400c0: Observer: 0x11d3c7ad0, Key path: attributedText, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x171e40030> )

手动removeObserver

- (void)dealloc {
    [self.marqueeLabel removeObserver:self forKeyPath:@"text"];
}

内存问题-关联对象

    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 

强引用

无论是BOOL,还是Object都是使用OBJC_ASSOCIATION_RETAIN_NONATOMIC

- (void)setAnimationAllowUserInteraction:(BOOL)animationAllowUserInteraction {
    objc_setAssociatedObject(self, @selector(animationAllowUserInteraction), @(animationAllowUserInteraction), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)animationAllowUserInteraction {
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

上面代码等同于

@property (nonatomic, strong) NSNumber *animationAllowUserInteraction;

弱引用

- (void)setDelegate:(id)delegate {
    objc_setAssociatedObject(self,
                             @selector(delegate),
                             delegate,
                             OBJC_ASSOCIATION_ASSIGN);
}

- (id)delegate {
    return objc_getAssociatedObject(self, @selector(delegate));
}

上面代码等同于

@property (nonatomic, weak) id delegate;

weak修饰delegate,不会造成循环引用。

总结

对比以上场景可以发现,对象没有释放的根本原因是被持有了,这也是引用计数的原理。在代码中,建议规范使用block,养成习惯在block里面对self判断nil。如下。

定义weakify、strongify.

/**
 Synthsize a weak or strong reference.
 
 Example:
 weakify(self)
 [self doSomething^{
 strongify(self)
 if (!self) return;
 ...
 }];
 
 */
#ifndef weakify
    #if __has_feature(objc_arc)
    #define weakify(object) __weak __typeof__(object) weak##_##object = object;
    #else
    #define weakify(object) __block __typeof__(object) block##_##object = object;
    #endif
#endif

#ifndef strongify
    #if __has_feature(objc_arc)
    #define strongify(object) __typeof__(object) object = weak##_##object;
    #else
    #define strongify(object) __typeof__(object) object = block##_##object;
    #endif
#endif

代码实现

weakify(self)
^() {//block example
    strongify(self)
    if (!self) {
        return;
    }

    [self xxxx];
}

相关文章

网友评论

      本文标题:iOS常见内存问题分析

      本文链接:https://www.haomeiwen.com/subject/rtastktx.html