1. 程式人生 > Android開發 >iOS常見記憶體問題分析

iOS常見記憶體問題分析

引用計數

引自維基百科 引用計數是計算機程式語言中的一種記憶體管理技術,是指將資源(可以是物件、記憶體或磁碟空間等等)的被引用次數儲存起來,當被引用次數變為零時就將其釋放的過程。使用引用計數技術可以實現自動資源管理的目的。同時引用計數還可以指使用引用計數技術回收未使用資源的垃圾回收演演算法。 當建立一個物件的例項並在堆上申請記憶體時,物件的引用計數就為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,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,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];
}
複製程式碼