你的記憶體洩漏了嗎?幾個容易被忽視的保留環
保留環高危地帶-Block
block隱性強引用self,這個大家都有警惕了,以至於發展出規範的寫法:
__weak __typeof(self)weakSelf = self; __strong __typeof(weakSelf)strongSelf = weakSelf;
(不知道如何使用weakSelf的請後面留言)
所以現在已經較少出錯了,本篇列舉是一些大家開發中意識不到的地方,各位iOSer可以對照檢查自己的專案,看看是否引發了相同的保留環:
NSTimer 強引用
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerEvent:) userInfo:nil repeats:YES];
如上面程式碼,這個timer並沒有被self引用,那麼為什麼self不會被釋放呢?因為timer被加到了runloop中( timer必須加到Runloop的原因 ),timer又強引用了self,所以timer一直存在的話,self也不會釋放:

NSTimer強引用self
嚴格來說,這不是一個保留環,而是一個隱性的強引用鏈。
解決的方法是找個地方釋放timer:
- (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; [self.timer invalidate]; self.timer = nil; }
這裡要注意 repeat = YES 才會導致強引用
應該是蘋果意識到了timer的潛在危險,所以iOS10以後,NSTimer提供了block方法:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
這樣就可以使用“標準的weakSelf”來防止強引用self了,如果沒有低版本相容要求,強烈推薦使用這種方式啟用timer。
CAAnimation 的 delegate 是強引用
以下程式碼會導致ViewController 不能被釋放:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"]; animation.fromValue = [NSNumber numberWithFloat:0.2f]; animation.toValue = [NSNumber numberWithFloat:1.0f]; animation.autoreverses = YES; animation.removedOnCompletion = NO; animation.duration = 1; animation.repeatCount = HUGE_VALF; animation.delegate = self; [self.view.layer addAnimation:animation forKey:@"animation"];
原因是蘋果違反了自己定下的規則:
凡是代理都要設定為weak
蘋果給出的理由是 CAAnimation 的事件是非同步的,必須強引用才能保證事件發生時執行事件,那麼如何解決呢?
這裡提供一個簡單易用的方法:動畫結束時,將 CAAnimation 設為 nil
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { [self.view.layer removeAnimationForKey:@"animation"]; anim = nil; }
另外可以使用NSProxy等第三方代理來解決,請根據自己的實際需要選擇。
NSArray 強引用成員
考慮以下場景,有一組QQ使用者和一組QQ群,一個使用者可以加到幾個群中,群當然也包含很多成員,設計使用者和群的類如下:
@class Group; @interface Person : NSObject @property(nonatomic, strong) NSMutableArray<Group *> *myGroups; @end
@class Person; @interface Group : NSObject @property(nonatomic, strong) NSMutableArray<Person *> *persons; @end
加群的過程如下
Group *group = [[Group alloc] init]; Person *person = [[Person alloc] init]; person.myGroups = [NSMutableArray array]; [person.myGroups addObject:group]; group.persons = [NSMutableArray array]; [group.persons addObject:person];
這時候, person 和 group 都不能被釋放
,也無法將myGroups 或 persons 宣告為weak來打破迴圈,因為這樣陣列一旦分配就會被立即釋放,也就起不了儲存物件的作用了,這下怎麼辦呢?
我們需要找個中介者,可以使用 NSValue,打破互相引用的狀態:
[person.myGroups addObject:[NSValue valueWithNonretainedObject:(id)group]]; [group.persons addObject:[NSValue valueWithNonretainedObject:(id)person]];
Storyboard 上的按鈕等控制元件要宣告為 weak
正確的寫法如下:
@property(nonatomic, weak) IBOutlet UIButton *btn;
從Storyboard拖出控制元件宣告時,系統會自動宣告為weak,但有時自己手動宣告時候要注意檢查。
小結:
本篇列舉了4種容易忽視的強引用導致的記憶體洩漏:
- NSTimer的target
- CAAnimation的delegate
- NSArray陣列成員
- Storyboard的IBOutlet
以上情形都分別給出了簡便的解決方案,當然都不是唯一的方法,只要能在適當的時機打破強引用都是可以考慮的,如果你有更“優雅”的解決方案,歡迎在留言寫出使用情形和具體解決方案供大家一同學習。
:rose::rose::rose:贈人玫瑰,手有餘香。:rose::rose::rose: