iOS 內存管理:Weak、Autorelease、Copy、Tagged Pointer、Timer問題

一:面試題:

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

關於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的支持,是在程序運行過程中,檢測到對象銷燬的時候,就會把對象對應的弱引用都會銷燬掉。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章