一:面試題:
1.1:CADisplayLink、NSTimer
1.1.1:CADisplayLink、NSTimer循環引用
1.1.2:NSProxy小問題
1.1.3:GCD定時器
1.2:iOS程序的內存佈局
1.2.1:Tagged Pointer
1.3:OC對象的內存管理
1.3.1:copy:
1.3.2:引用計數的存儲
1.3.3:weak指針的原理
1.3.4:autorelease
1.3.5:autorelease釋放時機
一:面試題
1.1:CADisplayLink、NSTimer
1.1.1:CADisplayLink、NSTimer循環引用
- 使用CADisplayLink、NSTimer有什麼注意點?
CADisplayLink、NSTimer會對target產生強引用,如果target又對它們產生強引用,那麼就會引發循環引用
這兩個定時器底層是基於runloop(有事情做事情,沒事情休眠)來實現的,所以有可能並不準時。
CADisplayLink:不用設置時間,保證調用頻率和屏幕的刷幀頻率一致,60FPS
- (void)viewDidLoad {
[super viewDidLoad];
// 保證調用頻率和屏幕的刷幀頻率一致,60FPS
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
// 這個self是自動加入到runloop中的
// self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)linkTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
// 循環引用導致這裏沒有調用
NSLog(@"%s", __func__);
[self.link invalidate];
// [self.timer invalidate];
}
看這個 如果傳入的是弱指針 也無用, 因爲傳入的 不論是弱指針還是強指針,都是一個內存地址,這個是一個形參,傳入到NSTimer內部,它內部是肯定有一個強指針的屬性或者變量 來保存這個形參,重點在於NSTimer內部的那個是強指針,所以這裏傳弱的還是強的都無所謂。
- (void)viewDidLoad {
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target: weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
}
但是如果block內部用的是弱指針,則對外部的是弱引用,這是它的特點,因爲block對外的指針強弱的關係跟傳入的強弱有關係,這裏不懂的,請看前面block,裏面講解很透徹。
解決方案
方法1:使用block:NSTimer對block產生強引用,block對self產生弱引用,self強引用Timer,所以沒有循環引用。
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
方法2:使用代理對象(NSProxy:NSObject)
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
// self.link = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self] selector:@selector(linkTest)];
// [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)linkTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.link invalidate];
// [self.timer invalidate];
}
#import <Foundation/Foundation.h>
@interface MJProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#import "MJProxy.h"
@implementation MJProxy
+ (instancetype)proxyWithTarget:(id)target
{
MJProxy *proxy = [[MJProxy alloc] init];
proxy.target = target;
return proxy;
}
// 消息轉發:如果不實現,會爆消息找不到的錯誤,這樣返回它自己,就找到方法了。
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
NSTimer的那根線不開源,所以只能退而求其次。
方法三:特殊類:NSProxy
@interface NSProxy <NSObject> {
Class isa;
}
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
可以看到NSProxy跟NSObject一樣,都是基類。
NSProxy 沒有init方法。存在的意義就是用來解決代理行爲、轉發行爲。只要去調用NSProxy的某一個方法的時候,馬上就會直接調用它的另外一個方法:- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
而它是沒有- (id)forwardingTargetForSelector:(SEL)aSelector這個方法的。
同樣用這個也可以實現上面的循環引用
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
#import <Foundation/Foundation.h>
@interface MJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#import "MJProxy.h"
@implementation MJProxy
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy對象不需要調用init,因爲它本來就沒有init方法
MJProxy *proxy = [MJProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
(MJProxy繼承自NSProxy)會發現控制器會dealloc。因爲定時器引用的 是MJProxy,而MJProxy裏面引用着是target(弱引用),而且一旦調用MJProxy的某一個方法的時候,就會馬上來到methodSignatureForSelector方法進行消息轉發,方法簽名用原來的方法簽名,對上面來說,就是調用控制器對象的methodSignatureForSelector這個方法,返回控制器timerTest方法的簽名,方法簽名有了,會接着調用forwardInvocation方法,把方法簽名包裝成一個NSInvocation給你,再用這個target調用這個。
跟上面用NSObject的區別和優勢是什麼呢?
好處在於比NSObject的效率高,專門用來做消息轉發。
看一下兩個類 不實現消息轉發,報錯是什麼。先看NSObject
2018-10-09 11:03:37.781345+0800 Interview03-定時器[2088:194218] -[MJProxy1 timerTest]: unrecognized selector sent to instance 0x60c00000b9f0
2018-10-09 11:03:37.787655+0800 Interview03-定時器[2088:194218] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MJProxy1 timerTest]: unrecognized selector sent to instance 0x60c00000b9f0'
*** First throw call stack:
(
0 CoreFoundation 0x0000000107b7b1e6 __exceptionPreprocess + 294
1 libobjc.A.dylib 0x0000000107210031 objc_exception_throw + 48
2 CoreFoundation 0x0000000107bfc784 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
3 CoreFoundation 0x0000000107afd898 ___forwarding___ + 1432
4 CoreFoundation 0x0000000107afd278 _CF_forwarding_prep_0 + 120
5 Foundation 0x0000000106c7a4dd __NSFireTimer + 83
6 CoreFoundation 0x0000000107b0ae64 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
7 CoreFoundation 0x0000000107b0aa52 __CFRunLoopDoTimer + 1026
8 CoreFoundation 0x0000000107b0a60a __CFRunLoopDoTimers + 266
9 CoreFoundation 0x0000000107b01e4c __CFRunLoopRun + 2252
10 CoreFoundation 0x0000000107b0130b CFRunLoopRunSpecific + 635
11 GraphicsServices 0x000000010ccefa73 GSEventRunModal + 62
12 UIKit 0x0000000107ff8057 UIApplicationMain + 159
13 Interview03-定时器 0x000000010690e5ef main + 111
14 libdyld.dylib 0x000000010b5d8955 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)
再看一下用NSProxy的
2018-10-09 11:05:16.053761+0800 Interview03-定時器[2142:200608] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSProxy methodSignatureForSelector:] called!'
*** First throw call stack:
(
0 CoreFoundation 0x000000010796a1e6 __exceptionPreprocess + 294
1 libobjc.A.dylib 0x0000000106fff031 objc_exception_throw + 48
2 CoreFoundation 0x00000001079df975 +[NSException raise:format:] + 197
3 Foundation 0x0000000106b12b22 -[NSProxy methodSignatureForSelector:] + 43
4 CoreFoundation 0x00000001078ec45a ___forwarding___ + 346
5 CoreFoundation 0x00000001078ec278 _CF_forwarding_prep_0 + 120
6 Foundation 0x0000000106a694dd __NSFireTimer + 83
7 CoreFoundation 0x00000001078f9e64 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
8 CoreFoundation 0x00000001078f9a52 __CFRunLoopDoTimer + 1026
9 CoreFoundation 0x00000001078f960a __CFRunLoopDoTimers + 266
10 CoreFoundation 0x00000001078f0e4c __CFRunLoopRun + 2252
11 CoreFoundation 0x00000001078f030b CFRunLoopRunSpecific + 635
12 GraphicsServices 0x000000010c6aca73 GSEventRunModal + 62
13 UIKit 0x0000000107de7057 UIApplicationMain + 159
14 Interview03-定时器 0x00000001066fd61f main + 111
15 libdyld.dylib 0x000000010b3c7955 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)
可以明顯看到執行流程是不同的,
如果是NSObject,執行流程就是三大階段:消息發送、動態解析、消息轉發,這裏需要進行消息發送,裏面需要遞歸查找方法。如果是NSProxy,是一步到位,先看一下自己類中有沒有這個方法,不會去父類中查找方法,直接進入消息轉發,省略了去父類中找方法的過程和動態解析過程。
這個對NSTimer和CADisplayLink一樣有用。
1.1.2:NSProxy小問題
#import <Foundation/Foundation.h>
@interface MJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#import "MJProxy.h"
@implementation MJProxy
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy對象不需要調用init,因爲它本來就沒有init方法
MJProxy *proxy = [MJProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
#import <Foundation/Foundation.h>
@interface MJProxy1 : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#import "MJProxy1.h"
@implementation MJProxy1
+ (instancetype)proxyWithTarget:(id)target
{
MJProxy1 *proxy = [[MJProxy1 alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
MJProxy *proxy = [MJProxy proxyWithTarget:self];
MJProxy1 *proxy1 = [MJProxy proxyWithTarget:self];
NSLog(@"%d %d",
[proxy isKindOfClass:[ViewController class]],
[proxy1 isKindOfClass:[ViewController class]]
);
}
2018-10-09 11:21:27.580295+0800 Interview03-定時器[2337:231792] 1 0
proxy1是繼承自NSObject,所以按照NSObject的方式來判斷,判斷proxy1既不是ViewController,又不是它的子類,所以爲0;
但是proxy是繼承自NSProxy,這個在找不到方法的時候,就會進行消息轉發,而這個MJProxy我們寫的消息轉發是轉發到傳入對象自己身上,就會變成ViewController在調用isKingOfClass,所以是自己這個類,所以打印爲1。
源碼:由於是Fontdation的,所以源碼不開源,需要看gnustep,(講fondatoin的很多類重寫了一下,可以做參考。)裏面對ismemberclass和iskindofclass都進行了消息轉發。
1.1.3:GCD定時器
CADisplayLink、NSTimer這兩個定時器底層是基於runloop(有事情做事情,沒事情休眠)來實現的,所以有可能並不準時。
NSTimer依賴於RunLoop,如果RunLoop的任務過於繁重,可能會導致NSTimer不準時
(runloop:每跑一圈 看一下時間,而每跑一圈所花費的時間是不一定的,因爲任務不一定,所以導致可能不準時)
而GCD的定時器會更加準時(跟系統內核掛鉤,不依賴runloop)
拖拽模式下都不影響GCD的定時器。時間非常準確,可以在任意線程工作,可以在當時或者延後時間工作,很強大
- (void)test
{
// 隊列
// dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_queue_t queue = dispatch_queue_create("timers", DISPATCH_QUEUE_SERIAL);
// 創建定時器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設置時間
uint64_t start = 2.0; // 2秒後開始執行
uint64_t interval = 1.0; // 每隔1秒執行
// NSEC_PER_SEC:納秒 這個是CGD的定時器的需要傳入的單位
// dispatch_time:從第一個參數開始算時間 第二個參數也就是多長時間後執行
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
// 設置回調1
// dispatch_source_set_event_handler(timer, ^{
// NSLog(@"1111");
// });
// 設置回調2
dispatch_source_set_event_handler_f(timer, timerFire);
// 啓動定時器
dispatch_resume(timer);
// 需要用強引用保留這個時間
self.timer = timer;
}
void timerFire(void *param)
{
NSLog(@"2222 - %@", [NSThread currentThread]);
}
2018-10-09 14:44:22.014101+0800 Interview02-GCD定時器[3835:374964] begin
2018-10-09 14:44:24.015619+0800 Interview02-GCD定時器[3835:375012] 2222 - <NSThread: 0x608000461b40>{number = 3, name = (null)}
2018-10-09 14:44:25.015591+0800 Interview02-GCD定時器[3835:375012] 2222 - <NSThread: 0x608000461b40>{number = 3, name = (null)}
2018-10-09 14:44:26.015557+0800 Interview02-GCD定時器[3835:375012] 2222 - <NSThread: 0x608000461b40>{number = 3, name = (null)}
在arc環境下 GCD創建出來的對象 都不需要銷燬。
封裝
#import <Foundation/Foundation.h>
@interface MJTimer : NSObject
+ (NSString *)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (NSString *)execTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (void)cancelTask:(NSString *)name;
@end
#import "MJTimer.h"
@implementation MJTimer
// 自己創建的唯一標識,一個定時器對應一個,這樣防止亂掉
static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers_ = [NSMutableDictionary dictionary];
semaphore_ = dispatch_semaphore_create(1);
});
}
+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
// 隊列
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
// 創建定時器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設置時間
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
// 防止在多線程下執行 這個範圍:無所謂的就不要加鎖,防止影響效率
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
// 定時器的唯一標識
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[name] = timer;
// 解鎖
dispatch_semaphore_signal(semaphore_);
// 設置回調
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) { // 不重複的任務
[self cancelTask:name];
}
});
// 啓動定時器
dispatch_resume(timer);
return name;
}
+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!target || !selector) return nil;
// 這個target會不會對這個block造成循環引用呢?
// 因爲這個block並不被外面的self所擁有,所以self不會對立面的block產生強引用,
// 而裏面的block可能會對傳進來的target產生強引用,也就是對self產生強引用,
// 所以這是單方面強引用,就好像寫了一個GCD裏面的block一樣,
// 外部selftask強引用的是timer內部的字典內部的字符串,並不是timer,所以不會產生強引用。
return [self execTask:^{
if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push // xcode去掉警告 紅色的字是xcode給的警告語
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:selector];
#pragma clang diagnostic pop
}
} start:start interval:interval repeats:repeats async:async];
}
+ (void)cancelTask:(NSString *)name
{
if (name.length == 0) return;
// 防止在多線程下執行
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
dispatch_source_t timer = timers_[name];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:name];
}
dispatch_semaphore_signal(semaphore_);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"begin");
// 接口設計
self.task = [MJTimer execTask:self
selector:@selector(doTask)
start:2.0
interval:1.0
repeats:YES
async:NO];
// self.task = [MJTimer execTask:^{
// NSLog(@"111111 - %@", [NSThread currentThread]);
// } start:2.0 interval:-10 repeats:NO async:NO];
}
- (void)doTask
{
NSLog(@"doTask - %@", [NSThread currentThread]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[MJTimer cancelTask:self.task];
}
注意上面target和block不會產生循環引用。上面已經解釋清楚。
1.2:iOS程序的內存佈局
- 代碼段:編譯之後的代碼
- 數據段:
- 字符串常量:比如NSString *str = @“123" @“123”會在常量區
- 已初始化數據:已初始化的全局變量、靜態變量等 int a = 10;
- 未初始化數據:未初始化的全局變量、靜態變量等 int a;
- 棧:函數調用開銷,比如局部變量。分配的內存空間地址越來越小 。 棧內存的地址比較大
- 堆:通過alloc、malloc、calloc等動態分配的空間,分配的內存空間地址越來越大
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
// 已初始化的全局變量
int a = 10;
// 未初始化的全局變量
int b;
int main(int argc, char * argv[]) {
@autoreleasepool {
// 已初始化的靜態變量
static int c = 20;
// 未初始化靜態變量
static int d;
// 棧 :地址會越來越小
int e; // 內存地址比f要大 因爲是從高往低處放的
int f = 20;
// 字符串常量
NSString *str = @"123";
// 堆 地址會越來越大
NSObject *obj = [[NSObject alloc] init];
// 驗證:
NSLog(@"\n&a=%p\n&b=%p\n&c=%p\n&d=%p\n&e=%p\n&f=%p\nstr=%p\nobj=%p\n",
&a, &b, &c, &d, &e, &f, str, obj);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
/*
數據區:
字符串常量 :內存地址最低
str =0x10dfa0068
已初始化的全局變量、靜態變量
&a =0x10dfa0db8
&c =0x10dfa0dbc // c是12
未初始化的全局變量、靜態變量 這個d和b沒有按照寫的順序,這個是編譯器行爲。
&d =0x10dfa0e80
&b =0x10dfa0e84 // 從這可以跟堆這裏對比可以看出,這個空間很大
堆:
obj=0x608000012210 // 看堆6和棧7 也差的很大 堆和棧中間的一段有可能是堆也有可能是棧,因爲堆是越來越大,棧是越來越小。
棧:先分配給e再分配給f,地址是越來越小的
&f =0x7ffee1c60fe0
&e =0x7ffee1c60fe4
*/
1.2.1:Tagged Pointer
從64bit開始,iOS引入了Tagged Pointer技術,用於優化NSNumber、NSDate、NSString等小對象的存儲
在沒有使用Tagged Pointer之前, NSNumber等對象需要動態分配內存、維護引用計數等,NSNumber指針存儲的是堆中NSNumber對象的地址值
使用Tagged Pointer之後,NSNumber指針裏面存儲的數據變成了:Tag + Data,也就是將數據直接存儲在了指針中(tag標記可以知道是number還是date還是string,標記在最後面面)
當指針不夠存儲數據時,纔會使用動態分配內存的方式來存儲數據
objc_msgSend能識別Tagged Pointer,比如NSNumber的intValue方法,直接從指針提取數據,節省了以前的調用開銷
==》 比以前少耗性能,直接使用指針,優化了內存,並且在使用上,直接用objc_msgSend可以讀取出來,還有使用方面的優化。
如何判斷一個指針是否爲Tagged Pointer?
iOS平臺,最高有效位是1(第64bit) (是 1UL<<63,)
Mac平臺,最低有效位是1
這個平臺的源碼判斷是
#import <Foundation/Foundation.h>
// 0b1110001
// &0b0000001
//-----------
// 0000001
BOOL isTaggedPointer(id pointer)
{
// 看最低有效位是不是1, 這裏不是很嚴謹,也有可能是6,這個僅僅是一個驗證
return (long)(__bridge void *)pointer & 1;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 下面三種寫法 等價
// NSNumber *number = [NSNumber numberWithInt:10];
// NSNumber *number = @(10);
NSNumber *number1 = @4;
NSNumber *number2 = @5;
NSNumber *number3 = @(0xFFFFFFFFFFFFFFF);
// 這是調用objc_msgSend() 就是用的oc方法,通過isa找到類對象,
// 類對象去搜索intvalue這個方法,但是number1 是採取 targe pointer技術實現的,
// 這個沒有堆空間,也沒有isa,就會找不到這個方法,
//但是好處是objc_msgSend這個方法內部會判斷這個number是不是一個target pointer,
//一旦發現時target pointer,會直接從指針中取出它的值
number1.intValue;
// 只要是堆空間的地址,最後一位一定是0.因爲oc對象有內存對齊的概念,用16來對齊,都是16的倍數。所以爲0
NSLog(@"%d %d %d", isTaggedPointer(number1), isTaggedPointer(number2), isTaggedPointer(number3));
NSLog(@"=====");
NSLog(@"%p %p %p", number1, number2, number3);
NSLog(@"=====%d",nu);
}
return 0;
}
2018-10-09 16:25:23.509030+0800 Interview04-TaggedPointer[4747:517412] 1 1 0
2018-10-09 16:25:23.509250+0800 Interview04-TaggedPointer[4747:517412] =====
2018-10-09 16:25:23.509259+0800 Interview04-TaggedPointer[4747:517412] 0x427 0x527 0x100404c40
2018-10-09 16:25:23.509267+0800 Interview04-TaggedPointer[4747:517412] =====4
講個例子:看下面的代碼會發生什麼問題?
@interface ViewController ()
@property (strong, nonatomic) NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
// 加鎖
self.name = [NSString stringWithFormat:@"abcdefghijk"];
// 解鎖
});
}
}
@end
以前的name會被釋放兩次,造成壞內存訪問。
原因:set方法的本質是
- (void)setName:(NSString *)name
{
if (_name != name) {
[_name release];
_name = [name retain];
}
}
而賦值之前,會先release掉舊值,因爲上面是多線程,所以就會發生同時訪問的問題,那本身已經release掉一次後,如果爲0,再relsease一次,那就是壞內存訪問了。
解決方式:
多線程加鎖,atomic(這個不推薦使用,因爲在get方法處或者其他地方都用到這個屬性,我們只需要在這段代碼中加鎖,意義不大。)
因爲運行在堆內存,釋放,
那我們改一下,改成下面的形式,再看一下結果
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abc"];
});
}
這種情況下,就不會崩潰。爲啥?
因爲是target pointer,不會像oc對象一樣 有set方法,不會有release這些,只是對指針進行的賦值。
NSString *str1 = [NSString stringWithFormat:@"abcdefghijk"];
NSString *str2 = [NSString stringWithFormat:@"123abc"];
NSLog(@"%@ %@", [str1 class], [str2 class]);
NSLog(@"%p", str2);
2018-10-11 09:49:50.135962+0800 Interview05-TaggedPointer面試題[1475:71624] __NSCFString NSTaggedPointerString
2018-10-11 09:49:50.136063+0800 Interview05-TaggedPointer面試題[1475:71624] 0xa006362613332316
可以看到最低位 不是0。如果是0就是oc對象了。爲16的倍數。
0xa006362613332316最低有效位不是1,但是最高位第64位是1,這是ios程序的特點,可以看一下
0x a006 3626 1333 2316:一共4組,這個裏面每一個16進制位,代表4個2進制位,所以就是16 * 4 = 64位。所以a的位置是低64位,a -> 10 -> 8 + 2 ->1010, 所以a最高位就是1,所以是target poniter。
1.3:OC對象的內存管理
那普通的oc對象是內存對象管理,不是上面的target pointer(後來新改的)
在iOS中,使用引用計數來管理OC對象的內存
因爲我們是runloop運行的程序,如果不釋放的話,越堆越多,內存越來越少,導致內幕才能泄漏。
一個新創建的OC對象引用計數默認是1,當引用計數減爲0,OC對象就會銷燬,釋放其佔用的內存空間
調用retain會讓OC對象的引用計數+1 ,調用release會讓OC對象的引用計數-1
copy也能讓計數器+1,當調用者是不可變的,得到的就是+1的不可變對象
內存管理的經驗總結
當調用alloc、new、copy、mutableCopy方法返回了一個對象,在不需要這個對象時,要調用release或者autorelease來釋放它
想擁有某個對象,就讓它的引用計數+1;不想再擁有某個對象,就讓它的引用計數-1
可以通過以下私有函數來查看自動釋放池的情況
extern void _objc_autoreleasePoolPrint(void);
現在我們使用的都是arc,前身是mrc。
所以我們先來看一下mrc的使用和演變過程。
#import <Foundation/Foundation.h>
#import "MJDog.h"
@interface MJPerson : NSObject
{
MJDog *_dog;
int _age;
}
- (void)setAge:(int)age;
- (int)age;
- (void)setDog:(MJDog *)dog;
- (MJDog *)dog;
@end
#import "MJPerson.h"
@implementation MJPerson
// 基本數據類型
- (void)setAge:(int)age
{
_age = age;
}
- (int)age
{
return _age;
}
// oc對象:需要擁有它,進行內存管理
- (void)setDog:(MJDog *)dog
{
if (_dog != dog) { // 爲了防止賦重複的值,這裏內存就出亂了。
[_dog release]; // 爲了防止賦值不同的實例對象,舊的實例對象沒有銷燬。
_dog = [dog retain]; // 這一步是爲了把dog的內存管理retain一份在person的管理中,擁有這個屬性,因爲第一次賦值_dog爲nil,這個+1,後面再無論重複賦值多少次,都不用重複+1。
}
}
- (MJDog *)dog
{
return _dog;
}
- (void)dealloc
{
// [_dog release];
// _dog = nil;
self.dog = nil; // 這個代替上面兩行,因爲調用的是set方法
self.car = nil;
NSLog(@"%s", __func__);
// 父類的dealloc放到最後
[super dealloc];
}
@end
可以看出,對oc對象的set方法進行了內存管理,因爲這樣纔好控制這個成員的聲明週期,不至於在自己使用的時候就釋放掉了,產生壞內存訪問,所以需要retain。
對舊值進行release是因爲,前面會賦值不同的實例對象,那舊的實例對象就會產生內存泄漏。
加上if判斷,就可以阻止賦同樣的實例對象,重複釋放的問題,賦不同的值,同樣可以達到銷燬舊對象,對新對象的內存管理問題。
===》緊接着編譯器進行發展,不再這麼麻煩了,所以產生property的寫法。
簡化升級2:編譯器幫我們做了操作。幫我們進行set和get的聲明,
synthesize:自動生成成員變量和屬性的setter、getter實現
但是需要自己寫dealloc對對象進行釋放和管理。
#import <Foundation/Foundation.h>
#import "MJDog.h"
@interface MJPerson : NSObject
// 簡化升級2:編譯器幫我們做了操作。幫我們進行set和get的聲明
@property (nonatomic, assign) int age;
@property (nonatomic, retain) MJDog *dog;
@end
#import "MJPerson.h"
@implementation MJPerson
// 升級附屬2:自動生成成員變量和屬性的setter、getter實現
@synthesize age = _age;
- (void)setAge:(int)age
{
_age = age;
}
- (int)age
{
return _age;
}
- (void)setDog:(MJDog *)dog
{
if (_dog != dog) {
[_dog release];
_dog = [dog retain];
}
}
- (MJDog *)dog
{
return _dog;
}
- (void)dealloc
{
self.dog = nil;
NSLog(@"%s", __func__);
[super dealloc];
}
@end
===》再緊接着編譯器進行發展,
升級 3:隨着編譯器發展,@synthesize不用寫了,property的作用就變成了:自動生成set和get的聲明,並且_的成員變量,並且set和get方法的實現。在arc下,還是需要在dealloc中自己釋放。也就是現在的使用方法
所以我們對於@property的關鍵字 一定要用好,不能瞎用,傳入什麼,就會對應做操作和處理。
同樣也需要自己處理dealloc。
@interface MJPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, retain) MJDog *dog;
@property (nonatomic, retain) MJCar *car;
+ (instancetype)person;
@end
1.3.1:copy
拷貝的目的:產生一個副本對象,跟源對象互不影響
修改了源對象,不會影響副本對象
修改了副本對象,不會影響源對象
iOS提供了2個拷貝方法
1.copy,不可變拷貝,產生不可變副本 (不論調用者是誰,產生結果均是不可變的, (如果調用者是不可變的,不產生新對象,因爲指向同一個對象,都是不可變的,所以就不再開闢內存了。這個對NSArray、NSDictionary都是一樣的,同時retaincout + 1 ))
2.mutableCopy,可變拷貝,產生可變副本
深拷貝和淺拷貝
1.深拷貝:內容拷貝,產生新的對象(mutableCopy)
2.淺拷貝:指針拷貝,沒有產生新的對象(調用copy不一定都得到的是淺拷貝,比方說調用者是可變的,得到的就是深拷貝的不可變的對象)
void test2()
{
NSString *str1 = [[NSString alloc] initWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 淺拷貝,指針拷貝,沒有產生新對象 這個cocy相當於是retain
NSMutableString *str3 = [str1 mutableCopy]; // 深拷貝,內容拷貝,有產生新對象
NSLog(@"%@ %@ %@", str1, str2, str3);
NSLog(@"%p %p %p", str1, str2, str3);
[str3 release];
[str2 release];
[str1 release];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 深拷貝
NSMutableString *str3 = [str1 mutableCopy]; // 深拷貝
// [str1 appendString:@"111"];
// [str3 appendString:@"333"];
//
// NSLog(@"%@ %@ %@", str1, str2, str3);
[str1 release];
[str2 release];
[str3 release];
test();
}
return 0;
}
屬性copy:從上面得知的面試題
@interface MJPerson : NSObject
//@property (copy, nonatomic) NSMutableArray *data; // 產生的是不可變數組,往裏面添加數據會報錯,
@property (copy, nonatomic) NSArray *data;
@end
#import "MJPerson.h"
@implementation MJPerson
- (void)setData:(NSArray *)data
{
if (_data != data) {
[_data release];
_data = [data copy];
}
}
- (void)dealloc
{
self.data = nil;
[super dealloc];
}
@end
so:
字符串:一般用copy,
數組和字典用:strong。
OC對象的Copy:
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject <NSCopying>
@property (assign, nonatomic) int age;
@property (assign, nonatomic) double weight;
@end
#import "MJPerson.h"
@implementation MJPerson
// copy底層實現的方法
- (id)copyWithZone:(NSZone *)zone
{
MJPerson *person = [[MJPerson allocWithZone:zone] init];
person.age = self.age;
// person.weight = self.weight;
return person;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"age = %d, weight = %f", self.age, self.weight];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *p1 = [[MJPerson alloc] init];
p1.age = 20;
p1.weight = 50;
MJPerson *p2 = [p1 copy];
// p2.age = 30;
NSLog(@"%@", p1);
NSLog(@"%@", p2);
[p2 release];
[p1 release];
// NSString *str;
// [str copy];
// [str mutableCopy];
// mutableCopy只給fondation自帶的類的特性
// NSArray, NSMutableArray;
// NSDictionary, NSMutableDictionary;
// NSString, NSMutableString;
// NSData, NSMutableData;
// NSSet, NSMutableSet;
}
return 0;
}
1.3.2:引用計數的存儲
rc:retaincout
詳情裏在runtime的詳解裏,有關於isa的引用計數的詳解
extra_rc:裏面存儲的值是引用計數器減1 (一共是19位,如果這19位不夠存儲,下面的has_sidetable_rc就會變爲1)
has_sidetable_rc:引用計數器是否過大無法存儲在isa中 ,值就會爲1,如果爲1,那麼引用計數會存儲在一個叫SideTable的類的屬性中
在64bit中,引用計數可以直接存儲在優化過的isa指針中,也可能存儲在SideTable類中
refcnts是一個存放着對象引用計數的散列表
weak+table:弱引用表
看一下內部源碼結構
retainCout:
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) { // 看是否是非指針類型:是否是優化過的isa指針
uintptr_t rc = 1 + bits.extra_rc; // + 1
if (bits.has_sidetable_rc) { // 是否爲1:引用計數不是存儲在isa中,而是存儲在sidetable中
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
size_t
objc_object::sidetable_getExtraRC_nolock()
{
assert(isa.nonpointer);
SideTable& table = SideTables()[this]; // 根據key取出value。
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) return 0;
else return it->second >> SIDE_TABLE_RC_SHIFT;
}
release
- (oneway void)release {
((id)self)->rootRelease();
}
ALWAYS_INLINE bool
objc_object::rootRelease()
{
return rootRelease(true, false);
}
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 {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) { // 判斷nonpointer
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
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;
.......
}
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
bool do_dealloc = false;
table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) { // 是否要進行dealloc
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}
retain:
- (id)retain {
return ((id)self)->rootRetain();
}
ALWAYS_INLINE id
objc_object::rootRetain()
{
return rootRetain(false, false);
}
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
........
}
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
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.3.3:weak指針的原理
將弱引用存到哈希表中,對象要銷燬的時候,就會取出當前對象對應的弱引用表,把裏面的弱引用都清除掉。
看源碼 看dealloc,銷燬的時候 詳細請看這篇weak
- (void)dealloc {
_objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
if (fastpath(isa.nonpointer && // 是否是一個優化過的isa指針
!isa.weakly_referenced && // 是否有弱引用指向
!isa.has_assoc && // 是否有關聯對象
!isa.has_cxx_dtor && // 是否有c++的析構函數/銷燬函數
!isa.has_sidetable_rc)) // 是否有另外一個結構存儲引用計數
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this); // 上面有
}
}
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
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(); // 指向當前對象的弱引指針置爲nil
}
return obj;
}
inline void
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
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.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this]; // key value 還有弱引用表
table.lock();
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this); // 清除弱引用
}
if (isa.has_sidetable_rc) { // 將引用技術表中的引用計數數據擦除掉
table.refcnts.erase(this);
}
table.unlock();
}
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
objc_object *referent = (objc_object *)referent_id;
// 傳入弱引用表和當前對象
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
.....
// 找到之後從這個表中移除
weak_entry_remove(weak_table, entry);
}
// 這裏可以發現弱引用表使用了哈希表
static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
assert(referent);
weak_entry_t *weak_entries = weak_table->weak_entries;
if (!weak_entries) return nil;
// 哈希表的索引,根據對象的地址值
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}
return &weak_table->weak_entries[index];
}
1.3.4:autorelease
我們會發現,有一些oc對象創建出來,系統有些類不需要我們釋放,比方說下面這樣
self.data = [NSMutableArray array];
其實是因爲系統內部對類方法這種形式已經做了處理,不需要我們再release。對於mrc下的oc對象,我們只需要注意一點,誰創建(alloc、new),誰釋放.
@interface MJPerson : NSObject
+ (instancetype)person;
@end
#import "MJPerson.h"
@implementation MJPerson
+ (instancetype)person
{
return [[[self alloc] init] autorelease];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[super dealloc];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [MJPerson person];
}
return 0;
}
那autorelease什麼時候釋放的呢?
用終端轉一下下面代碼 轉 源碼 看發生了什麼
@autoreleasepool {
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
{
__AtAutoreleasePool __autoreleasepool; // 這個一執行,就會調用這個結構體的構造函數objc_autoreleasePoolPush();
MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
}
// c++的結構體跟類很像。 當我們創建出來一個結構體變量的時候,自動調用結構體的構造函數
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 構造函數,在創建結構體的時候調用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析構函數,在結構體銷燬的時候調用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
本質上講,就是在大括號開始的時候,調用一個構造函數,這個構造函數會執行一個C語言函數 叫objc_autoreleasePoolPush,在大括號即將結束的時候,就會調用這個結構體的析構函數 也就是objc_autoreleasePoolPop,而且這個參數也就是當初push的出來的值。
objc_autoreleasePoolPush:剛開始可能會創建AutoreleasePoolPage對象
objc_autoreleasePoolPop
我們去objc源碼中找objc_autoreleasePoolPush和objc_autoreleasePoolPop,會發現一個結構體對象AutoreleasePoolPage
static inline void pop(void *token)
{
AutoreleasePoolPage *page; // 自動釋放池頁
id *stop;
........
}
page結構體關鍵部分如下
class AutoreleasePoolPage
{
magic_t const magic; //
id *next;
pthread_t const thread; // 這個page對象可能是專屬於某一個線程的
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
}
自動釋放池的主要底層數據結構是:__AtAutoreleasePool、AutoreleasePoolPage
調用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的
AutoreleasePoolPage的結構
Autorelease內部的創建的調用autorelease的對象的地址值就是存放在AutoreleasePoolPage的內存中
每個AutoreleasePoolPage對象佔用4096字節內存,除了用來存放它內部的成員變量,剩下的空間用來存放autorelease對象(就是調用了autorelease的對象)的地址
因爲AutoreleasePoolPage內存是有限的,如果4069存滿了,就會創建一個新的AutoreleasePoolPage,在程序運行過程中,可能有多個AutoreleasePoolPage對象的,而這些所有的AutoreleasePoolPage對象通過雙向鏈表的形式連接在一起。
調用begin會返回0x1038這個地址給你,返回的就是那裏開始存放AutoreleasePoolPage對象的地址。
end:算出來AutoreleasePoolPage的結束地址在哪裏:自己的地址(起始地址)+4096。
多個AutoreleasePoolPage的聯繫:child指針放着下一個AutoreleasePoolPage對象的地址值,而每一個AutoreleasePoolPage都有自己的對象,多出來的空間就用來存儲:調用autorelease對象的地址值。
parent:指向上一個對象,如果是第一個AutoreleasePoolPage,則parent是空的,如果是最後一個AutoreleasePoolPage對象,則child也是空的。
objc_autoreleasePoolPush:
(創建完page)調用push方法會將一個POOL_BOUNDARY(這個值爲0)入棧(放入begin的位置)(這個棧說的是數據結構的棧,先進後出,這個操作的內存區域還是在堆區),並且返回其存放的內存地址(這個值是要給pop函數傳入的),,緊接着,一旦有一個對象調用autorelease,就會將這個對象的內存地址存放到下一個位置。按順序一個一個往下存。(所以這個內存會存兩個內容:一個是POOL_BOUNDARY,一個是對象指針)
objc_autoreleasePoolPop:
調用pop方法時傳入一個POOL_BOUNDARY的內存地址(這個值就是上面的返回的地址值),會從最後一個入棧的對象開始發送release消息,直到遇到這個POOL_BOUNDARY,結束。
next:
id *next指向了下一個能存放autorelease對象地址的區域
剛開始什麼都沒有的時候,指向0x1038,緊接着調用push,就會將POOL_BOUNDARY存到剛纔的位置,所以next指向下一個位置。此時調用第一個autorelease對象,地址也放進去了,next就會又動一個,指向下一個位置。
好,緊接着講一個更復雜的嵌套情況
int main(int argc, const char * argv[]) {
@autoreleasepool { // r1 = push()
MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
@autoreleasepool { // r2 = push()
for (int i = 0; i < 600; i++) {
MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
}
@autoreleasepool { // r3 = push()
MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint();
} // pop(r3)
} // pop(r2)
} // pop(r1)
return 0;
}
上面的代碼是下面圖片的結構
上面的過程解釋1:
可以通過以下私有函數來查看自動釋放池的情況
extern void _objc_autoreleasePoolPrint(void);
// fondation內部的函數
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool { // r1 = push()
MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
@autoreleasepool { // r2 = push()
MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
// for (int i = 0; i < 600; i++) {
// MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
// }
@autoreleasepool { // r3 = push()
MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
// 直接調用,編譯器回去尋找這個函數,並調用 c語言的特性
_objc_autoreleasePoolPrint();
} // pop(r3)
} // pop(r2)
} // pop(r1)
return 0;
}
objc[8529]: ##############
objc[8529]: AUTORELEASE POOLS for thread 0x100390380
objc[8529]: 7 releases pending.
objc[8529]: [0x10100a000] ................ PAGE (hot) (cold) // 只有一個page對象。hot:當前page,正在使用的page
objc[8529]: [0x10100a038] ################ POOL 0x10100a038
objc[8529]: [0x10100a040] 0x10050f420 MJPerson
objc[8529]: [0x10100a048] 0x100506a30 MJPerson
objc[8529]: [0x10100a050] ################ POOL 0x10100a050
objc[8529]: [0x10100a058] 0x1005096b0 MJPerson
objc[8529]: [0x10100a060] ################ POOL 0x10100a060
objc[8529]: [0x10100a068] 0x1005096e0 MJPerson
objc[8529]: ##############
上面的過程解釋2:
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj); // 沒有滿的情況下添加
} else if (page) {
return autoreleaseFullPage(obj, page); // 滿了就再創建一個
} else { // 如果沒有page,就創建一個新的
return autoreleaseNoPage(obj);
}
}
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
autorelease
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this); // 看這裏哪個對象調用autorelease,就把哪個對象內存地址放進去
}
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj); // 講對象地址放進去
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
autoreleasePoolPush源碼
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) { // 剛開始沒有poolpage的情況
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY); // 創建一個新的page並且放入POOL_BOUNDARY
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page) return autoreleaseFullPage(obj, page);
else return autoreleaseNoPage(obj);
}
autoreleasePoolPop
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
static inline void pop(void *token)
{
AutoreleasePoolPage *page; // 自動釋放池頁
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
pop(coldPage()->begin());
} else {
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
return;
}
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {// 判斷節點POOL_BOUNDARY
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
1.3.5:autorelease釋放時機
mrc:
1:如果代碼中被autoreleasepool代碼塊包住的話,release釋放時機是就是大括號結束。也就是AutoreleasePoolPage調用pop方法的時候。
@autoreleasepool {
}
2:如果是下面的情況呢?
- (void)viewDidLoad { // 在source0中處理的
[super viewDidLoad];
NSLog(@"11");
// 這個Person什麼時候調用release,是由RunLoop來控制的
// 它可能是在某次RunLoop循環中,RunLoop休眠之前調用了release
// 打印的viewWillAppear和viewDidAppear 同時打印,說明是在同一次循環中
MJPerson *person = [[[MJPerson alloc] init] autorelease];
NSLog(@"222");
NSLog(@"%@",[NSRunLoop currentRunLoop]);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
2018-10-15 10:15:05.169044+0800 Interview18-autorelease時機[1715:136709] -[ViewController viewWillAppear:]
2018-10-15 10:15:05.170537+0800 Interview18-autorelease時機[1715:136709] -[MJPerson dealloc]
2018-10-15 10:15:05.171813+0800 Interview18-autorelease時機[1715:136709] -[ViewController viewDidAppear:]
在上面打印runloop;會發現有observer,可以看一下
observers = (
"<CFRunLoopObserver 0x60c000139a00 [0x1084f7c80]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1086a5d92), context = <CFArray 0x60c000045c70 [0x1084f7c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7feb71002048>\n)}}",
"<CFRunLoopObserver 0x60c0001396e0 [0x1084f7c80]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x108c8b6b3), context = <CFRunLoopObserver context 0x60c0000d5310>}",
"<CFRunLoopObserver 0x60c000139780 [0x1084f7c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x1086d4da1), context = <CFRunLoopObserver context 0x7feb70d01de0>}",
"<CFRunLoopObserver 0x600000139f00 [0x1084f7c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x10e1d34ce), context = <CFRunLoopObserver context 0x0>}",
"<CFRunLoopObserver 0x60c0001398c0 [0x1084f7c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x1086d4e1c), context = <CFRunLoopObserver context 0x7feb70d01de0>}",
"<CFRunLoopObserver 0x60c000139aa0 [0x1084f7c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1086a5d92), context = <CFArray 0x60c000045c70 [0x1084f7c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7feb71002048>\n)}}"
),
而這個observer,跟這個autorelease有關係,請看
/*
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 1
kCFRunLoopBeforeTimers = (1UL << 1), 2
kCFRunLoopBeforeSources = (1UL << 2), 4
kCFRunLoopBeforeWaiting = (1UL << 5), 32
kCFRunLoopAfterWaiting = (1UL << 6), 64
kCFRunLoopExit = (1UL << 7), 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
*/
/*
kCFRunLoopEntry push :(activities = 0x1)
<CFRunLoopObserver 0x60000013f220 [0x1031c8c80]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x103376df2), context = <CFArray 0x60000025aa00 [0x1031c8c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fd0bf802048>\n)}}
kCFRunLoopBeforeWaiting | kCFRunLoopExit (activities = 0xa0 :a:10:160)
kCFRunLoopBeforeWaiting pop、push
kCFRunLoopExit pop
<CFRunLoopObserver 0x60000013f0e0 [0x1031c8c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x103376df2), context = <CFArray 0x60000025aa00 [0x1031c8c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fd0bf802048>\n)}}
*/
so:
iOS在主線程的Runloop中註冊了2個Observer
a:第1個Observer監聽了kCFRunLoopEntry事件,會調用objc_autoreleasePoolPush()
b:第2個Observer
監聽了kCFRunLoopBeforeWaiting事件,會調用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
監聽了kCFRunLoopBeforeExit事件,會調用objc_autoreleasePoolPop()
過程:
1:剛進入runloop就來了一次objc_autoreleasePoolPush操作,往下走,
2:在休眠之前,調用objc_autoreleasePoolPop(跟一開始的push進行對應,他們之前有autorelease的操作,就會在此次休眠之前進行釋放),緊接着調用了一次objc_autoreleasePoolPush操作。接着被喚醒,做各種操作,再盡心新的一次循環,當即將又進入休眠的時候再次進行第二部的操作,先pop,再push。
3:接着往下走,如果沒有循環了,在退出runloop之前,pop。
- autorelease對象在什麼時機會被調用release?
在處理runloop循環中,一開始進行了push操作,進入循環,接着處理source0的時候,處理viewdidload,這個viewdidload中有個對象(person)調用了autorelease,那麼就會交給AutoreleasePoolPage處理,緊接着調用viewwillappear(在同一次循環中),接着往下走,開始休眠,當即將進入休眠之前,會調用objc_autoreleasePoolPop,來對應上一次的push(上面的 person對象就release了)。緊接着會再次調用objc_autoreleasePoolPush。
- 介紹下內存的幾大區域
- 講一下你對 iOS 內存管理的理解
- LLVM + Runtime
- weak指針的實現原理
- 方法裏有局部對象, 出了方法後會立即釋放嗎
mrc:如果對person對象用的是autorelease,不會馬上釋放,那麼會在runloop的循環結束休眠前進行釋放,經驗證是在viewwillappear之後。
mrc:如果是在person對象的局部變量裏直接用的release,那麼就在局部變量裏調用release之後就釋放了。
arc:經驗證,person釋放在viewwillappear之前,所以我們猜測,在局部變量大括號執行之前,加入了release。也就是馬上釋放。
- ARC 都幫我們做了什麼?
LLVM(編譯器)+ Runtime :ARC是LLVM編譯器和Runtime系統相互協作的一個結果
ARC利用LLVM編譯器自動幫我們生成release、retain和autorelease的代碼。
像弱引用這樣的是需要Runtime的支持,是在程序運行過程中,檢測到對象銷燬的時候,就會把對象對應的弱引用都會銷燬掉。