iOS 記憶體管理
一、Objective-C 使用引用計數來管理記憶體。
每個物件都有一個計數器,來表示引用該物件的個數;每次引用就加1,用完就減1;當計數為0時表示不再使用該物件,於是就銷燬該物件。
多個物件之間的引用形成閉環會導致迴圈引用,從而不能夠相互釋放,造成記憶體洩漏。
二、ARC 自動引用計數
ARC 自動引用計數,把記憶體管理事宜交由編譯器來處理。
使用 ARC 時,實際上引用計數還是在執行的,只不過保留操作和釋放操作由 ARC 在編譯時自動為你新增。
ARC 環境下特殊情況處理。
- (void)dealloc { // 移除通知中心的監聽者 // 移除 KVO 監聽者 // 關閉 NSTimer,並將定時器置空(nil) // 釋放非 Objective-C 物件的記憶體,如 CFRelease(...), free(...) }
三、自動釋放池
@autoreleasepool {} 自動釋放池。
當向一個物件傳送autorelease訊息時,系統會將該物件放入到最新的自動釋放池。
在自動釋放池的作用域內,池子內的物件依然可以正常使用。
當自動釋放池的作用域結束時,再釋放池子內的物件———每個物件收到幾個autorelease就進行幾次release操作。
常見用途:迴圈內部建立大量臨時物件時,沒有及時釋放導致記憶體使用急劇增加,可以使用自動釋放池在每次迴圈結束時釋放物件。
for (NSInteger i = 0; i < 100000; i++) { @autoreleasepool { NSString *str = @"Hello World"; str = [str stringByAppendingFormat:@"- %ld", i]; str = [str uppercaseString]; } }
四、@property 屬性修飾符
原子性(多執行緒管理):atomic、nonatomic
讀寫屬性:readwrite、readonly
setter 語意:assign、retain、copy
強弱引用:strong、weak
1、NSNumber、NSString、Block 使用 copy。
2、IBOutlet 屬性、引用“引用物件”的屬性(控制器引用view的子檢視)、代理屬性使用 weak。
3、Class、id 型別的屬性使用 retain。
4、NSString 屬性使用 retain 時,可能賦值一個 NSMutableString,導致值為 NSMutableString 型別。
5、NSMutableString 屬性使用 copy 時,導致屬性值為 NSString 型別,有可能出現未識別方法呼叫的異常。
atomic 只是保證了讀寫的執行緒安全,但是在多執行緒環境訪問物件屬性時,訪問結果不一定符合我們的預期。
- (void)setAtomicObj:(NSObject *)atomicObj{
@synchronized(self) {
if (_atomicObj != atomicObj) {
[_atomicObj release];
_atomicObj = [atomicObj retain];
}
}
}
- (NSObject *)atomicObj{
@synchronized(self) {
return _atomicObj;
}
}
ARC下同時重寫getter、setter方法時,需使用關鍵字@synthesize 宣告變數和屬性的關係。
@synthesize name = _name;
注意:@property = ivar + getter + setter;
五、定時器使用時的記憶體管理
通常控制器會擁有一個定時器,如果把控制器設定為定時器的 target,定時器會引用控制器,導致定時器不關閉銷燬時不能釋放控制器。因此,想辦法不要定時器直接或者間接引用控制器即可。
定時器解決迴圈引用思路:
1、iOS10 以後系統提供了block 方法執行定時器操作。
[NSTimer timerWithTimeInterval:(NSTimeInterval) repeats:(BOOL) block:^(NSTimer * _Nonnull timer) {
}];
[NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval) repeats:(BOOL) block:^(NSTimer * _Nonnull timer) {
}];
2、給 NSTimer 提供一個分類,實現自定義的 block 方法。
@interface NSTimer (BBBlocksSupport)
+ (NSTimer *)bb_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)(void))block
repeats:(BOOL)repeats;
@end
@implementation NSTimer (BBBlocksSupport)
+ (NSTimer *)bb_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)(void))block
repeats:(BOOL)repeats {
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(bb_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)bb_blockInvoke:(NSTimer *)timer {
void (^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end
3、中介軟體模式。
設計思路:給中間物件動態新增定時器需要執行的方法。
#import <Foundation/Foundation.h>
@interface BBTimerManager : NSObject
@property (weak, nonatomic) NSTimer *bb_timer;
+ (instancetype)bb_timerManagerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;
@end
#import "BBTimerManager.h"
#import <objc/runtime.h>
@implementation BBTimerManager
+ (instancetype)bb_timerManagerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
Method targetMethod = class_getInstanceMethod([aTarget class], aSelector);
if (!class_addMethod([BBTimerManager class], aSelector, method_getImplementation(targetMethod), method_getTypeEncoding(targetMethod))) {
class_replaceMethod([BBTimerManager class], aSelector, method_getImplementation(targetMethod), method_getTypeEncoding(targetMethod));
}
BBTimerManager *manager = [BBTimerManager new];
manager.bb_timer = [NSTimer scheduledTimerWithTimeInterval:interval target:manager selector:aSelector userInfo:userInfo repeats:repeats];
return manager;
}
@end
- (void)dealloc {
[_timerManager.bb_timer invalidate];
_timerManager.bb_timer = nil;
}
六、優雅使用 KVO
在需要處理屬性變化事件的物件中新增觀察者,並在該物件銷燬時移除觀察者。
@interface RootViewController ()
@property (retain, nonatomic) Focus *focus;
@end
@implementation RootViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.focus = [[Focus alloc] init];
[self.focus addObserver:self
forKeyPath:@"number"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
self.focus.number = @2;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"number"]) {
NSLog(@"%@", change);
}
}
- (void)dealloc {
[self.focus removeObserver:self forKeyPath:@"number"];
}
七、copy 操作
對於不可變物件 NSString、NSArray、NSDictionary 表示引用加1。
對於可變物件 NSMutableString、NSMutableArray、NSMutableDictionary 表示複製物件,返回不可變物件。
mutableCopy 操作:
對於不可變物件、可變物件都是表示複製物件,返回可變物件。
自定義物件使用 copy 和 mutableCopy 需要遵守 NSCopying 和 NSMutableCopying 協議。
- (id)copyWithZone:(nullable NSZone *)zone;
- (id)mutableCopyWithZone:(nullable NSZone *)zone;
八、weak 引用原理
runtime 維護了一個 weak 表,weak 表其實是一個 hash(雜湊)表,其中 Key 是所指物件的地址,Value 是 weak 指標的地址陣列。
weak 的實現原理可以概括一下三步:
1、初始化時:runtime會呼叫objc_initWeak函式,初始化一個新的weak指標指向物件的地址。
2、新增引用時:objc_initWeak函式會呼叫 objc_storeWeak() 函式, objc_storeWeak() 的作用是更新指標指向,建立對應的弱引用表。
3、釋放時,呼叫clearDeallocating函式。clearDeallocating函式首先根據物件地址獲取所有weak指標地址的陣列,然後遍歷這個陣列把其中的資料設為nil,最後把這個entry從weak表中刪除,最後清理物件的記錄。