iOS底層原理 記憶體管理 那些你不知道的原理彙總 — (12)

fgyong發表於2019-08-03

上篇文章講了各種鎖的使用和讀寫鎖的應用, 看完本文章你將瞭解到

  1. DisplayLink和timer的使用和原理
  2. 記憶體分配和記憶體管理
  3. 自動釋放池原理
  4. weak指標原理和釋放時機
  5. 引用計數原理

DisplayLink

CADisplayLink是將任務新增到runloop中,loop每次迴圈便會呼叫targetselector,使用這個也能監測卡頓問題。首先介紹下API

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
//runloop沒迴圈一圈都會呼叫
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
//從runloop中刪除
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
//取消
- (void)invalidate;
複製程式碼

我們在一個需要pushVC中執行來觀察宣告週期

@property (nonatomic,strong) CADisplayLink *link;

//初始化
self.link = [FYDisplayLink displayLinkWithTarget:self selector:@selector(test)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
	@synchronized (self) {
		NSLog(@"FPS:%d",fps);
		fps = 0;
	}
});
dispatch_resume(timer);
//全域性變數
dispatch_source_t timer;
static int fps;

- (void)test{
	
	@synchronized (self) {
		fps += 1;
	}
}
- (void)dealloc{
	[self.link invalidate];
	NSLog(@"%s",__func__);
}
//log
2019-07-30 17:44:37.217781+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:38.212477+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:39.706000+0800 day17-定時器[29637:6504821] FPS:89
2019-07-30 17:44:40.706064+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:41.705589+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:42.706268+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:43.705942+0800 day17-定時器[29637:6504821] FPS:60
2019-07-30 17:44:44.705792+0800 day17-定時器[29637:6504821] FPS:60
複製程式碼

初始化之後,對fps使用了簡單版本的讀寫鎖,可以看到fps基本穩定在60左右,點選按鈕返回之後,linkVC並沒有正常銷燬。我們分析一下,VC(self)->link->target(self),導致了死迴圈,釋放的時候,無法釋放selflink,那麼我們改動一下link->target(self)中的強引用,改成弱引用,程式碼改成下面的

@interface FYTimerTarget : NSObject
@property (nonatomic,weak) id target;
@end

@implementation FYTimerTarget
-(id)forwardingTargetForSelector:(SEL)aSelector{
	return self.target;
}
- (void)dealloc{
	NSLog(@"%s",__func__);
}
@end


FYProxy *proxy=[FYProxy proxyWithTarget:self];
self.link = [FYDisplayLink displayLinkWithTarget:self selector:@selector(test)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

- (void)test{
	NSLog(@"%s",__func__);
}

//log
2019-07-30 17:59:04.339934 -[ViewController test]
2019-07-30 17:59:04.356292 -[ViewController test]
2019-07-30 17:59:04.371428 -[FYTimerTarget dealloc]
2019-07-30 17:59:04.371634 -[ViewController dealloc]
複製程式碼

FYTimerTargettarget進行了弱引用,selfFYTimerTarget進行強引用,在銷燬了的時候,先釋放self,然後檢查selfFYTimerTarget,FYTimerTarget只有一個引數weak屬性,可以直接釋放,釋放完FYTimerTarget,然後釋放self(VC),最終可以正常。

NSTimer

使用NSTimer的時候,timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo會對aTarget進行強引用,所以我們對這個aTarget進行一個簡單的封裝

@interface FYProxy : NSProxy
@property (nonatomic,weak) id target;

+(instancetype)proxyWithTarget:(id)target;
@end
@implementation FYProxy
- (void)dealloc{
	NSLog(@"%s",__func__);
}
+ (instancetype)proxyWithTarget:(id)target{
	FYProxy *obj=[FYProxy alloc];
	obj.target = target;
	return obj;
}
//轉發
- (void)forwardInvocation:(NSInvocation *)invocation{
	[invocation invokeWithTarget:self.target];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
	return [self.target methodSignatureForSelector:sel];
}
@end
複製程式碼

FYProxy是繼承NSProxy,而NSProxy不是繼承NSObject的,而是另外一種基類,不會走objc_msgSend()的三大步驟,當找不到函式的時候直接執行- (void)forwardInvocation:(NSInvocation *)invocation,和- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel直接進入訊息轉發階段。或者將繼承關係改成FYTimerTarget : NSObject,這樣子target找不到的函式還是會走訊息轉發的三大步驟,我們再FYTimerTarget新增訊息動態解析

-(id)forwardingTargetForSelector:(SEL)aSelector{
	return self.target;
}
複製程式碼

這樣子targetaSelector轉發給了self.target處理,成功弱引用了self和函式的轉發處理。

FYTimerTarget *obj =[FYTimerTarget new];
obj.target = self;

self.timer = [NSTimer timerWithTimeInterval:1.0f
									target:obj
								   selector:@selector(test)
								   userInfo:nil
									repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
[self.timer setFireDate:[NSDate distantPast]];

//log
2019-07-30 18:03:08.723433+0800 day17-定時器[30877:6556631] -[ViewController test]
2019-07-30 18:03:09.722611+0800 day17-定時器[30877:6556631] -[ViewController test]
2019-07-30 18:03:09.847540+0800 day17-定時器[30877:6556631] -[FYTimerTarget dealloc]
2019-07-30 18:03:09.847677+0800 day17-定時器[30877:6556631] -[ViewController dealloc]
複製程式碼

或者使用timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block,然後外部使用__weak self呼叫函式,也不會產生迴圈引用。 使用block的情況,釋放正常。

self.timer=[NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
	NSLog(@"123");
}];

//log
2019-07-30 18:08:24.678789+0800 day17-定時器[31126:6566530] 123
2019-07-30 18:08:25.659127+0800 day17-定時器[31126:6566530] 123
2019-07-30 18:08:26.107643+0800 day17-定時器[31126:6566530] -[ViewController dealloc]
複製程式碼

由於linktimer是新增到runloop中使用的,每次一個迴圈則訪問timer或者link,然後執行對應的函式,在時間上有相對少許誤差的,每此迴圈,要重新整理UI(在主執行緒),要執行其他函式,要處理系統埠事件,要處理其他的計算。。。總的來說,誤差還是有的。

GCD中timer

GCD中的dispatch_source_t的定時器是基於核心的,時間誤差相對較少。

//timer 需要強引用 或者設定成全域性變數
    timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
    //設定
    dispatch_source_set_event_handler(timer, ^{
  //code 定時器執行的程式碼
 
    });
    //開始定時器
    dispatch_resume(timer);
複製程式碼

或者使用函式dispatch_source_set_event_handler_f(timer, function_t);

dispatch_source_set_event_handler_f(timer, function_t);
void function_t(void * p){
    //code here    
}
複製程式碼

業務經常使用定時器的話,還是封裝一個簡單的功能比較好,封裝首先從需求開始分析,我們使用定時器常用的引數都哪些?需要哪些功能?

首先需要開始的時間,然後執行的頻率,執行的任務(函式或block),是否重複執行,這些都是需要的。 先定義一個函式

+ (NSString *)exeTask:(dispatch_block_t)block
    	  start:(NSTimeInterval)time
       interval:(NSTimeInterval)interval
    	 repeat:(BOOL)repeat
    	  async:(BOOL)async;
+ (NSString *)exeTask:(id)target
		  sel:(SEL)aciton
		start:(NSTimeInterval)time
	 interval:(NSTimeInterval)interval
	   repeat:(BOOL)repeat
		async:(BOOL)async;
//取消
+ (void)exeCancelTask:(NSString *)key;
複製程式碼

然後將剛才寫的拿過來,增加了一些判斷。有任務的時候才會執行,否則直接返回nil,當迴圈的時候,需要間隔大於0,否則返回,同步或非同步,就或者主佇列或者非同步佇列,然後用生成的key,timervalue儲存到全域性變數中,在取消的時候直接用key取出timer取消,這裡使用了訊號量,限制單執行緒操作。在儲存和取出(取消timer)的時候進行限制,提高其他程式碼執行的效率。

+ (NSString *)exeTask:(dispatch_block_t)block start:(NSTimeInterval)time interval:(NSTimeInterval)interval repeat:(BOOL)repeat async:(BOOL)async{
	if (block == nil) {
		return nil;
	}
	if (repeat && interval <= 0) {
		return nil;
	}
	
	NSString *name =[NSString stringWithFormat:@"%d",i];
	//主佇列
	dispatch_queue_t queue = dispatch_get_main_queue();
	if (async) {
		queue = dispatch_queue_create("async.com", DISPATCH_QUEUE_CONCURRENT);
	}
	//建立定時器
	dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
	//設定啟動時間
	dispatch_source_set_timer(_timer,
							  dispatch_time(DISPATCH_TIME_NOW, time*NSEC_PER_SEC), interval*NSEC_PER_SEC, 0);
	//設定回撥
	dispatch_source_set_event_handler(_timer, ^{
		block();
		if (repeat == NO) {
			dispatch_source_cancel(_timer);
		}
	});
	//啟動定時器
	dispatch_resume(_timer);
	//存放到字典
	if (name.length && _timer) {
		dispatch_semaphore_wait(samephore, DISPATCH_TIME_FOREVER);
		timers[name] = _timer;
		dispatch_semaphore_signal(samephore);
	}
	return name;
}



+ (NSString *)exeTask:(id)target
				  sel:(SEL)aciton
				start:(NSTimeInterval)time
			 interval:(NSTimeInterval)interval
			   repeat:(BOOL)repeat
				async:(BOOL)async{
	if (target == nil || aciton == NULL) {
		return nil;
	}
	if (repeat && interval <= 0) {
		return nil;
	}
	
	NSString *name =[NSString stringWithFormat:@"%d",i];
	//主佇列
	dispatch_queue_t queue = dispatch_get_main_queue();
	if (async) {
		queue = dispatch_queue_create("async.com", DISPATCH_QUEUE_CONCURRENT);
	}
	//建立定時器
	dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
	//設定啟動時間
	dispatch_source_set_timer(_timer,
							  dispatch_time(DISPATCH_TIME_NOW, time*NSEC_PER_SEC), interval*NSEC_PER_SEC, 0);
	//設定回撥
	dispatch_source_set_event_handler(_timer, ^{
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
		//這裡是會報警告的程式碼
		if ([target respondsToSelector:aciton]) {
			[target performSelector:aciton];
		}
#pragma clang diagnostic pop

		if (repeat == NO) {
			dispatch_source_cancel(_timer);
		}
	});
	//啟動定時器
	dispatch_resume(_timer);
	//存放到字典
	if (name.length && _timer) {
		dispatch_semaphore_wait(samephore, DISPATCH_TIME_FOREVER);
		timers[name] = _timer;
		dispatch_semaphore_signal(samephore);
	}
	return name;
}
+ (void)exeCancelTask:(NSString *)key{
	if (key.length == 0) {
		return;
	}
	dispatch_semaphore_wait(samephore, DISPATCH_TIME_FOREVER);
	if ([timers.allKeys containsObject:key]) {
		dispatch_source_cancel(timers[key]);
		[timers removeObjectForKey:key];
	}
	dispatch_semaphore_signal(samephore);
}
複製程式碼

用的時候很簡單

key = [FYTimer exeTask:^{
        NSLog(@"123");
    } start:1
    interval:1 
    repeat:YES 
    async:NO];
複製程式碼

或者

key = [FYTimer exeTask:self sel:@selector(test) start:0 interval:1 repeat:YES async:YES];
複製程式碼

取消執行的時候

[FYTimer exeCancelTask:key];
複製程式碼

測試封裝的定時器

- (void)viewDidLoad {
	[super viewDidLoad];
	key = [FYTimer exeTask:self sel:@selector(test) start:0 interval:1 repeat:YES async:YES];
}
-(void)test{
	NSLog(@"%@",[NSThread currentThread]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
	[FYTimer exeCancelTask:key];
}
//log
2019-07-30 21:16:48.639486+0800 day17-定時器2[48817:1300897] <NSThread: 0x6000010ec000>{number = 4, name = (null)}
2019-07-30 21:16:49.640177+0800 day17-定時器2[48817:1300897] <NSThread: 0x6000010ec000>{number = 4, name = (null)}
2019-07-30 21:16:50.639668+0800 day17-定時器2[48817:1300897] <NSThread: 0x6000010ec000>{number = 4, name = (null)}
2019-07-30 21:16:51.639590+0800 day17-定時器2[48817:1300897] <NSThread: 0x6000010ec000>{number = 4, name = (null)}
2019-07-30 21:16:52.156004+0800 day17-定時器2[48817:1300845] -[ViewController touchesBegan:withEvent:]
複製程式碼

在點選VC的時候進行取消操作,timer停止。

NSProxy實戰

NSProxy其實是除了NSObject的另外一個基類,方法比較少,當找不到方法的時候執行訊息轉發階段(因為沒有父類),呼叫函式的流程更短,效能則更好。

問題:ret1ret2分別是多少?

ViewController *vc1 =[[ViewController alloc]init];
FYProxy *pro1 =[FYProxy proxyWithTarget:vc1];

FYTimerTarget *tar =[FYTimerTarget proxyWithTarget:vc1];
BOOL ret1 = [pro1 isKindOfClass:ViewController.class];
BOOL ret2 = [tar isKindOfClass:ViewController.class];
NSLog(@"%d %d",ret1,ret2);
複製程式碼

我們來分析一下,-(bool)isKindOfClass:(cls)物件函式是判斷該物件是否的cls的子類或者該類的例項,這點不容置疑,那麼ret1應該是0,ret2應該也是0

首先看FYProxy的實現,forwardInvocationmethodSignatureForSelector,在沒有該函式的時候進行訊息轉發,轉發物件是self.target,在該例子中isKindOfClass不存在與FYProxy,所以講該函式轉發給了VC,則BOOL ret1 = [pro1 isKindOfClass:ViewController.class];相當於BOOL ret1 = [ViewController.class isKindOfClass:ViewController.class];,所以答案是1

然後ret2是0,tar是繼承於NSObject的,本身有-(bool)isKindOfClass:(cls)函式,所以答案是0。

答案是:ret11ret20

記憶體分配

記憶體分為保留段、資料段、堆(↓)、棧(↑)、核心區。

資料段包括

  • 字串常量:比如NSString * str = @"11"
  • 已初始化資料:已初始化的全域性變數、靜態變數等
  • 未初始化資料:未初始化的全域性變數、靜態變數等

棧:函式呼叫開銷、比如區域性變數,分配的記憶體空間地址越來越小。

堆:通過alloc、malloc、calloc等動態分配的空間,分配的空間地址越來越大。

驗證:

int a = 10;
int b ;
int main(int argc, char * argv[]) {
    @autoreleasepool {
        static int c = 20;
        static int d;
        int e = 10;
        int f;
        NSString * str = @"123";
        NSObject *obj =[[NSObject alloc]init];
        NSLog(@"\na:%p \nb:%p \nc:%p \nd:%p \ne:%p \nf:%p \nobj:%p\n str:%p",&a,&b,&c,&d,&e,&f,obj,str);
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

//log

a:0x1063e0d98 
b:0x1063e0e64 
c:0x1063e0d9c 
d:0x1063e0e60 
e:0x7ffee9820efc 
f:0x7ffee9820ef8 
obj:0x6000013541a0
str:0x1063e0068
複製程式碼

Tagged Pointer

從64bit開始,iOS引入Tagged Pointer技術,用於優化NSNumber、NSDate、NSString等小物件的儲存,在沒有使用之前,他們需要動態分配記憶體,維護計數,使用Tagged Pointer之後,NSNumber指標裡面的資料變成了Tag+Data,也就是將數值直接儲存在了指標中,只有當指標不夠儲存資料時,才會動態分配記憶體的方式來儲存資料,而且objc_msgSend()能夠識別出Tagged Pointer,比如NSNumberintValue方法,直接從指標提取資料,節省了以前的呼叫的開銷。 在iOS中,最高位是1(第64bit),在Mac中,最低有效位是1。 在runtime原始碼中objc-internal.h 370行判斷是否使用了優化技術

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
複製程式碼

我們拿來這個可以判斷物件是否使用了優化技術。

NSNumbe Tagged Pointer

我們使用幾個NSNumber的大小數字來驗證

#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__ //mac開發
// 64-bit Mac - tag bit is LSB
#   define OBJC_MSB_TAGGED_POINTERS 0
#else
// Everything else - tag bit is MSB
#   define OBJC_MSB_TAGGED_POINTERS 1//iOS開發
#endif

#if OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
#endif
bool objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSNumber *n1 = @2;
        NSNumber *n2 = @3;
        NSNumber *n3 = @(4);
        NSNumber *n4 = @(0x4fffffffff);
        NSLog(@"\n%p \n%p \n%p \n%p",n1,n2,n3,n4);
        BOOL n1_tag = objc_isTaggedPointer((__bridge const void * _Nullable)(n1));
        BOOL n2_tag = objc_isTaggedPointer((__bridge const void * _Nullable)(n2));
        BOOL n3_tag = objc_isTaggedPointer((__bridge const void * _Nullable)(n3));
        BOOL n4_tag = objc_isTaggedPointer((__bridge const void * _Nullable)(n4));

        NSLog(@"\nn1:%d \nn2:%d \nn3:%d \nn4:%d ",n1_tag,n2_tag,n3_tag,n4_tag);
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
//log

0xbf4071e2657ccb95 
0xbf4071e2657ccb85 
0xbf4071e2657ccbf5 
0xbf40751d9a833444
2019-07-30 21:55:52.626317+0800 day17-TaggedPointer[49770:1328036] 
n1:1 
n2:1 
n3:1 
n4:0
複製程式碼

可以看到n1 n2 n3是經過優化的,而n4是大數字,指標容不下該數值,不能優化。

NSString Tagged Pointer

看下面一道題,執行test1test2會出現什麼問題?

- (void)test1{
	dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
	for (NSInteger i = 0; i < 1000; i ++) {
		dispatch_async(queue, ^{
			self.name = [NSString stringWithFormat:@"abc"];
		});
	}
}
- (void)test2{
	dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
	for (NSInteger i = 0; i < 1000; i ++) {
		dispatch_async(queue, ^{
			self.name = [NSString stringWithFormat:@"abcsefafaefafafaefe"];
		});
	}
}
複製程式碼

我們先不執行,先分析一下。

首先全域性佇列非同步新增任務會出現多執行緒併發問題,在併發的時候進行寫操作會出現資源競爭問題,另外一個小字串會出現指標優化問題,小字串和大字串切換導致_name結構變化,多執行緒同時寫入和讀會導致訪問壞記憶體問題,我們來執行一下

Thread: EXC_BAD_ACCESS(code = 1)
複製程式碼

直接在子執行緒崩潰了,崩潰函式是objc_release。符合我們的猜想。

驗證NSString Tagged Pointer

- (void)test{
	dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
	for (NSInteger i = 0; i < 1; i ++) {
		dispatch_async(queue, ^{
			self.name = [NSString stringWithFormat:@"abc"];
			NSLog(@"test1 class:%@",self.name.class);
		});
	}
}
- (void)test2{
	dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
	for (NSInteger i = 0; i < 1; i ++) {
		dispatch_async(queue, ^{
			self.name = [NSString stringWithFormat:@"abcsefafaefafafaefe"];
			NSLog(@"test2 class:%@",self.name.class);
		});
	}
}
//log
test1 class:NSTaggedPointerString
test2 class:__NSCFString
複製程式碼

可以看到NSString Tagged Pointer在小字串的時候類是NSTaggedPointerString,經過優化的類,大字串的類是__NSCFString

copy

拷貝分為淺拷貝和深拷貝,淺拷貝只是引用計數+1,深拷貝是拷貝了一個物件,和之前的 互不影響, 引用計數互不影響。

拷貝目的:產生一個副本物件,跟源物件互不影響 修改源物件,不會影響到副本物件 修改副本物件,不會影響源物件

iOS提供了2中拷貝方法

  1. copy 拷貝出來不可變物件
  2. mutableCopy 拷貝出來可變物件
void test1(){
	NSString *str = @"strstrstrstr";
	NSMutableString *mut1 =[str mutableCopy];
	[mut1 appendFormat:@"123"];
	NSString *str2 = [str copy];
	NSLog(@"%p %p %p",str,mut1,str2);
}
//log
str:0x100001040 
mut1:0x1007385f0 
str2:0x100001040
複製程式碼

可以看到strstr2地址一樣,沒有重新複製出來一份,mut1地址和str不一致,是深拷貝,重新拷貝了一份。

我們把字串換成其他常用的陣列

void test2(){
	NSArray *array = @[@"123",@"123",@"123",@"123",@"123",@"123",@"123"];
	NSMutableArray *mut =[array mutableCopy];
	NSString *array2 = [array copy];
	NSLog(@"\n%p \n%p\n%p",array,mut,array2);
}
//log
0x102840800 
0x1028408a0
0x102840800

void test3(){
	NSArray *array = [@[@"123",@"123",@"123",@"123",@"123",@"123",@"123"] mutableCopy];
	NSMutableArray *mut =[array mutableCopy];
	NSString *array2 = [array copy];
	NSLog(@"\n%p \n%p\n%p",array,mut,array2);
}
//log
0x102808720 
0x1028088a0
0x1028089a0
複製程式碼

從上面可以總結看出來,不變陣列拷貝出來不變陣列,地址不改變,拷貝出來可變陣列地址改變,可變陣列拷貝出來不可變陣列和可變陣列,地址會改變。

我們再換成其他的常用的字典

void test4(){
	NSDictionary *item = @{@"key":@"value"};
	NSMutableDictionary *mut =[item mutableCopy];
	NSDictionary *item2 = [item copy];
	NSLog(@"\n%p \n%p\n%p",item,mut,item2);
}

//log
0x1007789c0 
0x100779190
0x1007789c0

void test5(){
	NSDictionary *item = [@{@"key":@"value"}mutableCopy];
	NSMutableDictionary *mut =[item mutableCopy];
	NSDictionary *item2 = [item copy];
	NSLog(@"\n%p \n%p\n%p",item,mut,item2);
}
//log

0x1007041d0 
0x1007042b0
0x1007043a0
複製程式碼

從上面可以總結看出來,不變字典拷貝出來不變字典,地址不改變,拷貝出來可變字典地址改變,可變字典拷貝出來不可變字典和可變字典,地址會改變。

由這幾個看出來,總結出來下表

型別 copy mutableCopy
NSString 淺拷貝 深拷貝
NSMutableString 淺拷貝 深拷貝
NSArray 淺拷貝 深拷貝
NSMutableArray 深拷貝 深拷貝
NSDictionary 淺拷貝 深拷貝
NSMutableDictionary 深拷貝 深拷貝

自定義物件實現協議NSCoping

自定義的物件使用copy呢?系統的已經實現了,我們自定義的需要自己去實現,自定義的類繼承NSCopying

@protocol NSCopying

- (id)copyWithZone:(nullable NSZone *)zone;

@end

@protocol NSMutableCopying

- (id)mutableCopyWithZone:(nullable NSZone *)zone;

@end

複製程式碼

看到NSCopyingNSMutableCopying這兩個協議,對於自定義的可變物件,其實沒什麼意義,本來自定義的物件的屬性,基本都是可變的,所以只需要實現NSCopying協議就好了。

@interface FYPerson : NSObject
@property (nonatomic,assign) int age;
@property (nonatomic,assign) int level;

@end

@interface FYPerson()<NSCopying>
@end

@implementation FYPerson
-(instancetype)copyWithZone:(NSZone *)zone{
	FYPerson *p=[[FYPerson alloc]init];
	p.age = self.age;
	p.level = self.level;
	return p;
}

@end


FYPerson *p =[[FYPerson alloc]init];
p.age = 10;
p.level = 11;
FYPerson *p2 =[p copy];
NSLog(@"%d %d",p2.age,p2.level);
//log
10 11
複製程式碼

自己實現了NSCoping協議完成了對物件的深拷貝,成功將物件的屬性複製過去了,當屬性多了怎麼辦?我們可以利用runtime實現一個一勞永逸的方案。

然後將copyWithZone利用runtime遍歷所有的成員變數,將所有的變數都賦值,當變數多的時候,這裡也不用修改。

@implementation NSObject (add)
-(instancetype)copyWithZone:(NSZone *)zone{
    Class cls = [self class];
    NSObject * p=[cls new];
    //成員變數個數
    unsigned int count;
    //賦值成員變數陣列
    Ivar *ivars = class_copyIvarList(self.class, &count);
    //遍歷陣列
    for (int i = 0; i < count; i ++) {
        Ivar var = ivars[i];
        //獲取成員變數名字
        const char * name = ivar_getName(var);
        if (name != nil) {
            NSString *v = [NSString stringWithUTF8String:name];
            id value = [self valueForKey:v];
            //給新的物件賦值
            if (value != NULL) {
                [p setValue:value forKey:v];
            }
        }
    }
    free(ivars);
    return p;
}
@end

FYPerson *p =[[FYPerson alloc]init];
p.age = 10;
p.level = 11;
p.name = @"xiaowang";
FYPerson *p2 =[p copy];
NSLog(@"%d %d %@",p2.age,p2.level,p2.name);
		
//log
10 
11 
xiaowang
複製程式碼

根據啟動順序,類別的方法在類的方法載入後邊,類別中的方法會覆蓋類的方法,所以 在基類NSObject在類別中重寫了-(instancetype)copyWithZone:(NSZone *)zone方法,子類就不用重寫了。達成了一勞永逸的方案。

引用計數原理

摘自百度百科

引用計數是計算機程式語言中的一種記憶體管理技術,是指將資源(可以是物件、記憶體或磁碟空間等等)的被引用次數儲存起來,當被引用次數變為零時就將其釋放的過程。使用引用計數技術可以實現自動資源管理的目的。同時引用計數還可以指使用引用計數技術回收未使用資源的垃圾回收演算法

在iOS中,使用引用計數來管理OC物件記憶體,一個新建立的OC物件的引用計數預設是1,當引用計數減為0,OC物件就會銷燬,釋放其他記憶體空間,呼叫retain會讓OC物件的引用計數+1,呼叫release會讓OC物件的引用計數-1。 當呼叫alloc、new、copy、mutableCopy方法返回一個物件,在不需要這個物件時,要呼叫release或者autorelease來釋放它,想擁有某個物件,就讓他的引用計數+1,不再擁有某個物件,就讓他引用計數-1.

在MRC中我們經常都是這樣子使用的

FYPerson *p=[[FYPerson alloc]init];
FYPerson *p2 =[p retain];
//code here
[p release];
[p2 release];
複製程式碼

但是在ARC中是系統幫我們做了自動引用計數,不用開發者做很多繁瑣的事情了,我們就探究下引用計數是怎麼實現的。

引用計數儲存在isa指標中的extra_rc,儲存值大於這個範圍的時候,則bits.has_sidetable_rc=1然後將剩餘的RetainCount儲存到全域性的tablekeyself對應的值。

Retainruntime原始碼查詢函式路徑objc_object::retain()->objc_object::rootRetain()->objc_object::rootRetain(bool, bool)

//大概率x==1 提高讀取指令的效率
#define fastpath(x) (__builtin_expect(bool(x), 1))
//大概率x==0 提高讀取指令的效率
#define slowpath(x) (__builtin_expect(bool(x), 0))


//引用計數+1
//tryRetain 嘗試+1
//handleOverflow 是否覆蓋
ALWAYS_INLINE id  objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
	//優化的指標 返回this
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
		//old bits
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
		//使用聯合體技術
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);//nothing
            if (!tryRetain && sideTableLocked) sidetable_unlock();//解鎖
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
			else return sidetable_retain();////sidetable 引用計數+1
        }
        // don't check newisa.fast_rr; we already called any RR overrides
		//不嘗試retain 和 正在銷燬 什麼都不做 返回 nil
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
		//引用計數+1 (bits.extra_rc++;)
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ 溢位處理
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
			//為拷貝到side table 做準備
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
		//拷貝 平外一半的 引用計數到 side table
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

//sidetable 引用計數+1
id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
	//取出table key=this
    SideTable& table = SideTables()[this];
    
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}
複製程式碼

引用計數+1,判斷了需要是指標沒有優化和isa有沒有使用的聯合體技術,然後將判斷是否溢位,溢位的話,將extra_rc的值複製到side table中,設定引數isa->has_sidetable_rc=true

引用計數-1,在runtime原始碼中查詢路徑是objc_object::release()->objc_object::rootRelease()->objc_object::rootRelease(bool performDealloc, bool handleUnderflow),我們進入到函式內部

ALWAYS_INLINE bool  objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;//指標優化的不存在計數器

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {//isa
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
			//side table -1
            return sidetable_release(performDealloc);
        }
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    newisa = oldisa;

    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            goto retry;
        }

		//side table 引用計數-1
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        if (borrowed > 0) {
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

	//真正的銷燬

    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
	//設定正在銷燬
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __sync_synchronize();
    if (performDealloc) {
		//銷燬
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}
複製程式碼

看了上邊瞭解到引用計數分兩部分,extra_rcside table,探究一下 rootRetainCount()的實現

inline uintptr_t  objc_object::rootRetainCount()
{
	//優化指標 直接返回
    if (isTaggedPointer()) return (uintptr_t)this;
//沒優化則 到SideTable 讀取
    sidetable_lock();
	//isa指標
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);//啥都沒做
    if (bits.nonpointer) {//使用聯合體儲存更多的資料 
        uintptr_t rc = 1 + bits.extra_rc;//計數數量
        if (bits.has_sidetable_rc) {//當大過於 聯合體儲存的值 則另外在SideTable讀取資料
	//讀取table的值 相加
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
	//在sidetable 中儲存的count
    return sidetable_retainCount();
}
複製程式碼

當是儲存小資料的時候,指標優化,則直接返回self,大資料的話,則table加鎖, class優化的之後使用聯合體儲存更多的資料,class沒有優化則直接去sizedable讀取資料。 優化了則在sidetable_getExtraRC_nolock()讀取資料

//使用聯合體
size_t  objc_object::sidetable_getExtraRC_nolock()
{
	//不是聯合體技術 則報錯
    assert(isa.nonpointer);
	//key是 this,儲存了每個物件的table
    SideTable& table = SideTables()[this];
	//找到 it 否則返回0
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}
複製程式碼

沒有優化的是直接讀取

//未使用聯合體的情況,
uintptr_t objc_object::sidetable_retainCount()
{//沒有聯合體儲存的計數器則直接在table中取出來
    SideTable& table = SideTables()[this];
    size_t refcnt_result = 1;
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}
複製程式碼

weak指標原理

當一個物件要銷燬的時候會呼叫dealloc,呼叫軌跡是dealloc->_objc_rootDealloc->object_dispose->objc_destructInstance->free 我們進入到objc_destructInstance內部

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
		//c++解構函式
        bool cxx = obj->hasCxxDtor();
		//關聯函式
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }
    return obj;
}
複製程式碼

銷燬了c++解構函式和關聯函式最後進入到clearDeallocating,我們進入到函式內部

//正在清除side table 和weakly referenced
inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
		//釋放weak
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
		//釋放weak 和引用計數
        clearDeallocating_slow();
    }
    assert(!sidetable_present());
}
複製程式碼

最終呼叫了sidetable_clearDeallocatingclearDeallocating_slow實現銷燬weak和引用計數side table

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
	//清除weak
    if (isa.weakly_referenced) {
		//table.weak_table 弱引用表
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
	//引用計數
    if (isa.has_sidetable_rc) {
		//擦除 this
        table.refcnts.erase(this);
    }
    table.unlock();
}
複製程式碼

其實weak修飾的物件會儲存在全域性的SideTable,當物件銷燬的時候會在SideTable進行查詢,時候有weak物件,有的話則進行銷燬。

Autoreleasepool 原理

Autoreleasepool中文名自動釋放池,裡邊裝著一些變數,當池子不需要(銷燬)的時候,release裡邊的物件(引用計數-1)。 我們將下邊的程式碼轉化成c++

@autoreleasepool {
		FYPerson *p = [[FYPerson alloc]init];
	}
複製程式碼

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -f main.m 轉成c++

 /* @autoreleasepool */ {
  __AtAutoreleasePool __autoreleasepool;
  FYPerson *p = ((FYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((FYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("FYPerson"), sel_registerName("alloc")), sel_registerName("init"));

 }
複製程式碼

__AtAutoreleasePool是一個結構體

struct __AtAutoreleasePool {
	__AtAutoreleasePool() {//建構函式 生成結構體變數的時候呼叫
		atautoreleasepoolobj = objc_autoreleasePoolPush();
	}
	~__AtAutoreleasePool() {//解構函式 銷燬的時候呼叫
		objc_autoreleasePoolPop(atautoreleasepoolobj);
	}
	void * atautoreleasepoolobj;
};
複製程式碼

然後將上邊的程式碼和c++整合到一起就是這樣子

{
    __AtAutoreleasePool pool = objc_autoreleasePoolPush();
    FYPerson *p = [[FYPerson alloc]init];
    objc_autoreleasePoolPop(pool)
}
複製程式碼

在進入大括號生成一個釋放池,離開大括號則釋放釋放池,我們再看一下釋放函式是怎麼工作的,在runtime原始碼中NSObject.mm 1848 行

void objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}
複製程式碼

pop實現了AutoreleasePoolPage中的物件的釋放,想了解怎麼釋放的可以研究下原始碼runtime NSObject.mm 1063行

其實AutoreleasePoolAutoreleasePoolPage來管理的,AutoreleasePoolpage結構如下

class AutoreleasePoolPage {
    magic_t const magic;
    id *next;//下一個存放aotoreleass物件的地址
    pthread_t const thread;//執行緒
    AutoreleasePoolPage * const parent; //父節點
    AutoreleasePoolPage *child;//子節點
    uint32_t const depth;//深度
    uint32_t hiwat;
}
複製程式碼

AutoreleasePoolPage在初始化在autoreleaseNewPage申請了4096位元組除了自己變數的空間,AutoreleasePoolPage是一個C++實現的類

  • 內部使用id *next指向了棧頂最新add進來的autorelease物件的下一個位置
  • 一個AutoreleasePoolPage的空間被佔滿時,會新建一個AutoreleasePoolPage物件,連線連結串列,後來的autorelease物件在新的page加入
  • AutoreleasePoolPage每個物件會開闢4096位元組記憶體(也就是虛擬記憶體一頁的大小),除了上面的例項變數所佔空間,剩下的空間全部用來儲存autorelease物件的地址
  • AutoreleasePool是按執行緒一一對應的(結構中的thread指標指向當前執行緒)
  • AutoreleasePool並沒有單獨的結構,而是由若干個AutoreleasePoolPage以雙向連結串列的形式組合而成(分別對應結構中的parent指標和child指標)

其他的都是自動釋放池的其他物件的指標,我們使用_objc_autoreleasePoolPrint()可以檢視釋放池的儲存內容

extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
	@autoreleasepool {//r1 = push()

		FYPerson *p = [[FYPerson alloc]init];
		_objc_autoreleasePoolPrint();
		printf("\n--------------\n");
	}//pop(r1)
	return 0;
}
//log

objc[23958]: ##############
objc[23958]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[23958]: 3 releases pending.
objc[23958]: [0x101000000]  ................  PAGE  (hot) (cold)
objc[23958]: [0x101000038]  ################  POOL 0x101000038
objc[23958]: [0x101000040]       0x10050cfa0  FYPerson
objc[23958]: [0x101000048]       0x10050cdb0  FYPerson
objc[23958]: ##############

--------------
複製程式碼

可以看到儲存了3 releases pending一個物件,而且大小都8位元組。再看一個複雜的,自動釋放池巢狀自動釋放池

int main(int argc, const char * argv[]) {
	@autoreleasepool {//r1 = push()

		FYPerson *p = [[[FYPerson alloc]init] autorelease];
		FYPerson *p2 = [[[FYPerson alloc]init] autorelease];
		@autoreleasepool {//r1 = push()
			
			FYPerson *p3 = [[[FYPerson alloc]init] autorelease];
			FYPerson *p4 = [[[FYPerson alloc]init] autorelease];
			
			_objc_autoreleasePoolPrint();
			printf("\n--------------\n");
		}//pop(r1)
	}//pop(r1)
	return 0;
}
//log
objc[24025]: ##############
objc[24025]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[24025]: 6 releases pending.
objc[24025]: [0x100803000]  ................  PAGE  (hot) (cold)
objc[24025]: [0x100803038]  ################  POOL 0x100803038
objc[24025]: [0x100803040]       0x100721580  FYPerson
objc[24025]: [0x100803048]       0x100721b10  FYPerson
objc[24025]: [0x100803050]  ################  POOL 0x100803050
objc[24025]: [0x100803058]       0x100721390  FYPerson
objc[24025]: [0x100803060]       0x100717620  FYPerson
objc[24025]: ##############
複製程式碼

看到了2個POOL和四個FYPerson物件,一共是6個物件,當出了釋放池會執行release

當無優化的指標呼叫autorelease其實是呼叫了AutoreleasePoolPage::autorelease((id)this)->autoreleaseFast(obj)

   static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
        //當有分頁而且分頁沒有滿就新增
        if (page && !page->full()) {
            return page->add(obj);
        } else if (page) {
            //滿則新建一個page進行新增obj和設定hotpage
            return autoreleaseFullPage(obj, page);
        } else {
            //沒有page則新建page進行新增
            return autoreleaseNoPage(obj);
        }
    }
複製程式碼

MRCautorealease修飾的是的物件在沒有外部新增到自動釋放池的時候,在runloop迴圈的時候會銷燬


typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

//activities = 0xa0轉化成二進位制 0b101 0000
系統監聽了mainRunloop 的 kCFRunLoopBeforeWaiting 和kCFRunLoopExit兩種狀態來更新autorelease的資料
//回撥函式是 _wrapRunLoopWithAutoreleasePoolHandler

"<CFRunLoopObserver 0x600002538320 [0x10ce45ae8]>{valid = Yes, activities = 0xa0, 
repeats = Yes, order = 2147483647, 
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10f94087d), 
context = <CFArray 0x600001a373f0 [0x10ce45ae8]>{type = mutable-small, count = 1, 
values = (\n\t0 : <0x7fb6dc004058>\n)}}"
複製程式碼

activities = 0xa0轉化成二進位制 0b101 0000 系統監聽了mainRunloopkCFRunLoopBeforeWaitingkCFRunLoopExit兩種狀態來更新autorelease的資料 回撥函式是 _wrapRunLoopWithAutoreleasePoolHandler

void test(){
    FYPerson *p =[[FYPerson alloc]init];
}
複製程式碼

p物件在某次迴圈中push,在迴圈到kCFRunLoopBeforeWaiting進行一次pop,則上次迴圈的autolease物件沒有其他物件retain的進行釋放。並不是出了test()立馬釋放。

在ARC中則執行完畢test()會馬上釋放。

總結

  • 當重複建立物件或者程式碼段不容易管理生命週期使用自動釋放池是不錯的選擇。
  • 存在在全域性的SideTable中weak修飾的物件會在dealloc函式執行過程中檢測或銷燬該物件。
  • 可變物件拷貝一定會生成已新物件,不可變物件拷貝成不可變物件則是引用計數+1。
  • 優化的指向物件的指標,不用走objc_msgSend()的訊息流程從而提高效能。
  • CADisplayLinkTimer本質是加到loop迴圈當中,依附於迴圈,沒有runloop,則不能正確執行,使用runloop需要注意迴圈引用和runloop所在的執行緒的釋放問題。

參考資料

資料下載


最怕一生碌碌無為,還安慰自己平凡可貴。

廣告時間

iOS底層原理 記憶體管理 那些你不知道的原理彙總 — (12)

相關文章