iOS常見內(nèi)存問題分析

引用計(jì)數(shù)

引自維基百科
引用計(jì)數(shù)是計(jì)算機(jī)編程語言中的一種內(nèi)存管理技術(shù),是指將資源(可以是對象、內(nèi)存或磁盤空間等等)的被引用次數(shù)保存起來,當(dāng)被引用次數(shù)變?yōu)榱銜r就將其釋放的過程。使用引用計(jì)數(shù)技術(shù)可以實(shí)現(xiàn)自動資源管理的目的。同時引用計(jì)數(shù)還可以指使用引用計(jì)數(shù)技術(shù)回收未使用資源的垃圾回收算法。
當(dāng)創(chuàng)建一個對象的實(shí)例并在堆上申請內(nèi)存時,對象的引用計(jì)數(shù)就為1,在其他對象中需要持有這個對象時,就需要把該對象的引用計(jì)數(shù)加1,需要釋放一個對象時,就將該對象的引用計(jì)數(shù)減1,直至對象的引用計(jì)數(shù)為0,對象的內(nèi)存會被立刻釋放。
使用這種方式進(jìn)行內(nèi)存管理的語言:Objective-C

iOS是使用引用計(jì)數(shù)管理內(nèi)存,非常需要注意的一個點(diǎn)就是持有關(guān)系。持有關(guān)系就是A_View持有B_View,
[B_View removeFromSuperview]釋放A_View對B_View的持有,B_View才會釋放。
如果B_View沒有調(diào)用[B_View removeFromSuperview],即使B_View=nil,也不會釋放。因?yàn)锳_View依然在持有B_View。

所以在iOS里面想要obj釋放,不要使用obj=nil,如果持有關(guān)系沒解除,釋放不掉的。

內(nèi)存問題-定時器

定時器在持有關(guān)系上比較特殊,生成一個定時器并開啟,RunLoop會持有Timer,Timer會持有Target,Timer不關(guān)閉,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,每次觸發(fā)定時器的判斷B_View是否被釋放了,釋放了就停止定時器。這樣B_View在使用定時器的時候,不需要再操心定時器的釋放了。

weakTarget實(shí)現(xiàn)如下

@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

平時使用定時器還需要注意一點(diǎn),就是在starTimer之前,最好先stopTimer一下。有可能starTimer多次,生成了多個Timer對象,造成一堆的Timer在跑,沒釋放。

內(nèi)存問題-延遲執(zhí)行

dispatch延遲3秒執(zhí)行block,要在block里面使用weak_self,如果3秒內(nèi)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秒內(nèi)weak_self釋放了,后面的代碼就不會執(zhí)行了。

注意,要在block里面進(jìn)行weak_self為nil的判斷,如果不做這個判斷,后面的單例依然會執(zhí)行?。?!

內(nèi)存問題-網(wǎng)絡(luò)請求

網(wǎng)絡(luò)請求的問題和上面延遲執(zhí)行的問題類似,由于網(wǎng)絡(luò)是異步加載的,在網(wǎng)絡(luò)環(huán)境很差的時候,如果頁面退出了,由于網(wǎng)絡(luò)請求的block還在持有self頁面,導(dǎo)致頁面不能釋放,直到網(wǎng)絡(luò)請求返回執(zhí)行完block才釋放self。

注意,如果block一直沒有回調(diào),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的判斷,雖然不做判斷也沒問題,因?yàn)閎lock里面不存在單例、after等場景,最好是養(yǎng)成習(xí)慣,對self或weak_self做一次nil判斷,以后即使增加代碼,也不會有隱藏風(fēng)險(xiǎn)。

內(nèi)存問題-代理

其實(shí)代理就是一個對象持有另外一個對象,然后執(zhí)行另外一個對象的方法。如果A持有B,B的delegate剛好是A,那B的delegate要用weak修飾了。

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

內(nèi)存問題-單例

很簡單的道理,因?yàn)閱卫灤┱麄€APP的生命周期的,單例不會釋放。如果單例持有了一個外部傳過來的view,這個view需要用weak修飾,不然view就一直被單例持有,不會釋放。

@interface GiftComboAnimaOperationManager : NSObject

/// 父視圖weak
@property (nonatomic, weak) UIView *parentView;

/// 單例
+ (instancetype)sharedManager;

@end

內(nèi)存問題-動畫

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設(shè)置為NO,CAAnimation執(zhí)行完動畫并不會主動釋放。
這就需要手動釋放CAAnimation。

[CAAnimation removeAnimationForKey:@"key"];

內(nèi)存問題-present vc

[vc_1 presentViewController:vc_2]

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

vc_1調(diào)用

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

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

內(nèi)存問題-KVO

通知

NSNotificationCenter從iOS9之后,會在對象dealloc的時候移除Observer。

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

iOS9之前要手動調(diào)用removeObserver

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

為什么 iOS 9 之前需要手動移除觀察者對象?

觀察者注冊時,通知中心并不會對觀察者對象做 retain 操作,而是對觀察者對象進(jìn)行unsafe_unretained 引用。

什么是unsafe_unretained?因?yàn)?Cocoa 和 Cocoa Touch 中的一些類仍然還沒有支持 weak 引用。所以,當(dāng)我們想對這些類使用弱引用的時候,只能用unsafe_unretained來替代。

// for attribute

@property (unsafe_unretained) NSObject *unsafeProperty;

// for variables

NSObject *__unsafe_unretained unsafeReference;

不安全引用(unsafe reference)和弱引用 (weak reference) 類似,它并不會讓被引用的對象保持存活,但是和弱引用不同的是,當(dāng)被引用的對象釋放的時,不安全引用并不會自動被置為 nil,這就意味著它變成了野指針,而對野指針發(fā)送消息會導(dǎo)致程序崩潰

KVO

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

如果沒有調(diào)用removeObserver,會發(fā)生崩潰。

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"];
}

內(nèi)存問題-關(guān)聯(lián)對象

    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. 

強(qiáng)引用

無論是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,不會造成循環(huán)引用。

總結(jié)

對比以上場景可以發(fā)現(xiàn),對象沒有釋放的根本原因是被持有了,這也是引用計(jì)數(shù)的原理。在代碼中,建議規(guī)范使用block,養(yǎng)成習(xí)慣在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

代碼實(shí)現(xiàn)

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

    [self xxxx];
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容