內存管理(NSTimer、autorelease、weak原理、Tagged Pointer、程序的內存分布)

總結一下我們在內存管理中處理方法,以及出現的常見問題

定時器相關的問題:

  • 1 NSTimer的問題
    看如下的代碼:

@interface ViewController ()

@property (strong, nonatomic) NSTimer *timer;


@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction{
    NSLog(@"%s",__func__);
    
}
-(void)dealloc {
    NSLog(@"%s",__func__);
    [self.timer invalidate];
}
@end

當我們點擊控制器左邊的返回按鈕時,發(fā)現控制器并沒有銷毀,我們簡單的分下一下,應該是我們的timer 強引用了target 而target又強引用了timer,所以導致了循環(huán)引用。其中的target這里指的是self 也就是我們的控制器。抱著嘗試一下的心態(tài)我們這樣修改代碼,看看效果如何:

@interface ViewController ()

@property (weak, nonatomic) NSTimer *timer;


@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
     __weak typeof(self)weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction{
    NSLog(@"%s",__func__);
    
}
-(void)dealloc {
    NSLog(@"%s",__func__);
    [self.timer invalidate];
}
@end

結果發(fā)現當我們點擊返回按鈕的時候,控制器還是沒有銷毀的,也就是還是沒有調用我們的dealloc方法,也就是還是內存泄露了,那么我們分析一下,首先分析一下 __weak typeof(self)weakSelf = self; 我們明明傳進去的是個weak 為什么還泄露了原因是:NSTimer內部相當于有一個強引用的target 外部傳weak 還是沒有用的因為他的內部相當于這樣實現:

@interface NSTimer ()
@property (strong, nonatomic) id target;
@end

所以傳進去即使是weak的,他在內部也強引用了,另外target是作為參數傳進去的不是block,如果我們這個定時器是block的話 那是沒有問題的,因為blcok內部調用外部的變量如果外部是弱引用那么他本身也對他產生弱引用,但是我們這個是參數他沒有這個本事。但是外部@property (weak, nonatomic) NSTimer *timer;明明是弱引用按道理講已經不存在循環(huán)引用了這是什么原因呢?
因為:個人猜測,我們定時器是要加在runloop中,runloop對timer是存在一個強引用的,我們目前知道timer是對target(也就是self)存在強引用的,當我們的控制器pop的時候,因為我們沒有調用定時器的invalidate方法,也就是沒有去除掉runloop對timer的強引用,也就是說timer沒有去除掉對self的強引用,那么我們的控制器就沒有死。
鑒于這種情況我們想到的解決方案是運用這種方式,我們采用的是一個第三方的一個東西,我們用一個對象來進行包裝target,在這個對象中我們弱引用target,然后調用方法進行消息轉發(fā)給給target,那樣的話就完美的解決了這個問題。
例如我們采用一個繼承NSObject的對象來解決這個問題

#import "ViewController.h"
#import "DGProxy.h"

@interface ViewController ()
@property (weak, nonatomic) NSTimer *timer;

@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[DGProxy initWithTarget:self] selector:@selector(timerAction) userInfo:nil repeats:YES];

}
- (void)timerAction{
    NSLog(@"%s",__func__);
    
}
-(void)dealloc {
    NSLog(@"%s",__func__);
    [self.timer invalidate];
}
@end
其中:DGProxy可以這樣實現
#import "DGProxy.h"

@interface DGProxy ()

@property (weak, nonatomic) id target;

@end
@implementation DGProxy

+ (instancetype)initWithTarget:(id)target{
   
    DGProxy *proxy = [[DGProxy alloc] init];
    proxy.target = target;
    return proxy;
    
}
-(id)forwardingTargetForSelector:(SEL)aSelector{
    
    return self.target;
}
@end

我們可以看到運行結果


image.png

可以看到控制器銷毀了也就是說沒有內存泄露了,也就是我們通過一個三方的對象對target進行弱引用然后再通過消息轉發(fā)技術來實現它的方法那么就完美的解決了這個問題。下面還有一個更加完美的方案:
代碼如下:

#import "ViewController.h"
#import "DGSpecifyProxy.h"

@interface ViewController ()
@property (weak, nonatomic) NSTimer *timer;

@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[DGSpecifyProxy initWithTarget:self] selector:@selector(timerAction) userInfo:nil repeats:YES];

}
- (void)timerAction{
    NSLog(@"%s",__func__);
    
}
-(void)dealloc {
    NSLog(@"%s",__func__);
    [self.timer invalidate];
}
@end
DGSpecifyProxy是這么實現的:
#import <Foundation/Foundation.h>

@interface DGSpecifyProxy : NSProxy

+ (instancetype)initWithTarget:(id)target;

@end


#import "DGSpecifyProxy.h"

@interface DGSpecifyProxy ()

@property (weak, nonatomic) id target;

@end

@implementation DGSpecifyProxy

+ (instancetype)initWithTarget:(id)target{
    
    DGSpecifyProxy *proxy = [DGSpecifyProxy alloc];
    proxy.target = target;
    return proxy;
    
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    
    return [self.target methodSignatureForSelector:sel];
}
-(void)forwardInvocation:(NSInvocation *)invocation{
    
    [invocation invokeWithTarget:self.target];
    
}
@end
image.png

可以看到同樣的解決了問題,而且我不得不說2的方式要比1處理的效率要高因為NSProxy我們點進去發(fā)現和NSObject是同一級別的,我們知道我們繼承NSObject然后攔截他的消息轉發(fā)方法實際上他在之前還經過了方法查找等等(如果實在不明白可以看我的runtime的簡書他說了消息轉發(fā)的過程),而我們的NSProxy直接就到了消息轉發(fā)這一步,我們可以這樣看個例子 比如我不寫DGSpecifyProxy中的方法轉發(fā)的方法,我們看一下報錯的是什么?

#import "DGSpecifyProxy.h"

@interface DGSpecifyProxy ()

@property (weak, nonatomic) id target;

@end

@implementation DGSpecifyProxy

+ (instancetype)initWithTarget:(id)target{
    
    DGSpecifyProxy *proxy = [DGSpecifyProxy alloc];
    proxy.target = target;
    return proxy;
    
}
//-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
//
//    return [self.target methodSignatureForSelector:sel];
//}
//-(void)forwardInvocation:(NSInvocation *)invocation{
//
//    [invocation invokeWithTarget:self.target];
//
//}
@end
image.png

可以看到他直接就是我們消息轉發(fā)階段找不到方法而不是我們的不能recognize方法的錯誤,所以從這方面講NSProxy效率更高。

gcd定時器

我封裝了一個關于gcd定時器的一個工具方法,直接粘貼方法也不解釋了,gcd定時器是比較準確的,因為他是基于系統(tǒng)內核的一套定時器,不是基于runloop的也就不存在不準確性。代碼如下:

#import <Foundation/Foundation.h>

@interface DGTimer : NSObject

/**
 定時器的事件

 @param start 開始的時間
 @param interval 中間的間隔
 @param leeway 延時多少秒開始執(zhí)行
 @param repeat 是否重復
 @param isAsync 是否在異步線程
 @param action 事件處理
 */
+ (NSString *)start:(NSTimeInterval)start
     interval:(NSTimeInterval)interval
       leeway:(NSTimeInterval)leeway
       repeat:(BOOL)repeat
        async:(BOOL)isAsync
 handleAction:(void (^)(void))action;

/**
 定時器的事件

 @param start 開始的時間
 @param interval 中間的間隔
 @param leeway 延時多少秒開始執(zhí)行
 @param repeat 是否重復
 @param isAsync 是否在異步線程
 @param target 對象
 @param selector 時間的方法
 */
+ (NSString *)start:(NSTimeInterval)start
           interval:(NSTimeInterval)interval
             leeway:(NSTimeInterval)leeway
             repeat:(BOOL)repeat
              async:(BOOL)isAsync
             target:(id)target
           selector:(SEL)selector;
/**
 取消任務

 @param taskName 任務名稱 開始的時候大爺已經給你返回了
 */
+ (void)cancleTask:(NSString *)taskName;
@end

#import "DGTimer.h"

@implementation DGTimer


static NSMutableDictionary *timer_dic;
static dispatch_semaphore_t gcd_semaphore;

+ (void)initialize{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        timer_dic = [NSMutableDictionary dictionary];
        gcd_semaphore = dispatch_semaphore_create(1);
        
    });
}
/**
 定時器的事件
 
 @param start 開始的時間
 @param interval 中間的間隔
 @param leeway 延時多少秒開始執(zhí)行
 @param repeat 是否重復
 @param isAsync 是否在異步線程
 @param action 事件處理
 */
+ (NSString *)start:(NSTimeInterval)start
     interval:(NSTimeInterval)interval
       leeway:(NSTimeInterval)leeway
       repeat:(BOOL)repeat
        async:(BOOL)isAsync
 handleAction:(void (^)(void))action{
 
    if ((interval <= 0 && repeat) || action == nil) {
        NSAssert((interval <= 0 && repeat) != YES, @"如果重復操作,那么時間間隔不能為0");
        NSAssert(action != nil, @"事件處理不能為空");
        return nil;
    }
    // 創(chuàng)建gcd定時器
    dispatch_queue_t queue = isAsync ?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, leeway *NSEC_PER_SEC);
    // 加鎖
    dispatch_semaphore_wait(gcd_semaphore, DISPATCH_TIME_FOREVER);
    NSString *keyName = [NSString stringWithFormat:@"%zd",timer_dic.count];
    timer_dic[keyName] = timer;
    dispatch_semaphore_signal(gcd_semaphore);
    // 時間處理
    dispatch_source_set_event_handler(timer, ^{
        action();
        if (!repeat) {
            [self cancleTask:keyName];
        }
    });
    dispatch_resume(timer);
    // 這樣的操作相當于強引用了
    return keyName;
}
+ (NSString *)start:(NSTimeInterval)start
           interval:(NSTimeInterval)interval
             leeway:(NSTimeInterval)leeway
             repeat:(BOOL)repeat
              async:(BOOL)isAsync
             target:(id)target
           selector:(SEL)selector{
    
    
  return [DGTimer start:start interval:interval leeway:leeway repeat:repeat async:isAsync handleAction:^{
        
        if ([target respondsToSelector:selector]) {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
             [target performSelector:selector];
    #pragma clang diagnostic pop
        }
    }];
}

/**
 取消任務
 
 @param taskName 任務名稱 開始的時候大爺已經給你返回了
 */
+ (void)cancleTask:(NSString *)taskName{
    
    if (taskName.length == 0 || ![timer_dic.allKeys containsObject:taskName]) {
        return;
    }
    dispatch_semaphore_wait(gcd_semaphore, DISPATCH_TIME_FOREVER);
    
    dispatch_source_cancel(timer_dic[taskName]);
    [timer_dic removeObjectForKey:taskName];
    
    dispatch_semaphore_signal(gcd_semaphore);
}
@end

程序的內存分布

說明:我們ios的內存分布由低到高的分布為保留->代碼段(_text)->數據段(_data)->堆->棧->內核區(qū),其中代碼段為我們編譯之后的代碼。數據段為:我們的字符串常量、已經初始化的數據、沒有初始化的數據,其中字符串常量為:例如NSString *str = @"123";已經初始化的數據為:已經初始話的全局變量和靜態(tài)變量,未初始化的數據為:為初始化的全局變量和靜態(tài)變量。堆區(qū)為:比如alloc、malloc或者calloc等創(chuàng)建的,分配的內存越來越大。棧區(qū):調用函數的開銷、局部變量等等,分配的內存區(qū)域越來越小。
用一張圖來表示為:


image.png

我們寫代碼來證明一下:
代碼如下:


int b;
int d = 4;
static int a;
static int c = 3;
NSString *g = @"asdasdasd";

- (void)viewDidLoad {
    [super viewDidLoad];
    // 內存分布
    NSObject *e = [[NSObject alloc] init];
    int f = 10;
    NSLog(@"\n&a=%p\n&b=%p\n&c=%p\n&d=%p\n&e=%p\n&f=%p\n&g=%p\n",&a,&b,&c,&d,&e,&f,&g);
}
image.png

我們可以按照我們的分析排序
應該是這樣的
&d=0x10ec1a718
&g=0x10ec1a720
&c=0x10ec1a728
&b=0x10ec1a80c
&a=0x10ec1a808
&e=0x7ffee0fe6060
&f=0x7ffee0fe605c
那么內存有小到大確實是這樣的。

Tagged Pointer的簡單介紹

Tagged Pointer是從64位操作系統(tǒng)ios才引用的技術,主要是優(yōu)化比如 NSDate、NSString、NSNumber等一些小對象的存儲,之前的NSDate、NSString、NSNumber還是采用的動態(tài)的分配內存、管理引用計數等等,那么引用了Tagged Pointer他們當他們的值不是很大的時候就會直接存儲在內存地址中,那么我們的runtime開始發(fā)送消息調用方法的時候直接就會從內存地址中進行查找,這樣的話節(jié)省了內存地址的開銷,從某一方面講優(yōu)化了我們的方案。
下面我寫幾個例子

    // Tagged Pointer
    NSNumber *abc = @(10);
    NSNumber *edf = @(0x600000238140ffff);
    NSLog(@"\n %p\n %p\n %@\n %@\n",abc,edf,[abc class],[edf class]);
image.png

我們可以看到內存地址的區(qū)別,可以看到第一個我們寫的是10 他直接就把10放到了我們的內存地址中了 因為出現了a,其中2目前只是系統(tǒng)內部做的處理,我們看我們數子比較大那個可見他的內存地址還是采用原來的動態(tài)分配內存管理引用計數的方式。

一個小小的總結:

一般來說內存地址的最高位如果是1的話那么一定是 Tagged Pointer,不是1也有可能是 Tagged Pointer。我們通過打印是什么類型這種方式不一定是準確的,但是如果打印出來是Tagged Pointer那一定就是Tagged Pointer類型。
下面是字符串的一個個小小的例子:

    NSString *str1 = [NSString stringWithFormat:@"abc"];
    NSString *str2 = [NSString stringWithFormat:@"asdasdasdasdajsdajsdkasdajksdjkakjsdjkaksdjkasdjajksdkjjkasdjkjjkajsd"];
    NSLog(@"\n%p \n%p \n%@ \n%@",str1,str2,[str1 class],[str2 class]);
image.png

我們可以看到我們第一個字符串打印出來的是Tagged Pointer,而且我們可以看到最后一位是3 二進制也就是ob0011那么最高位就是1也能證明他是Tagged Pointer類型。
看下一個很重要的面試題:

 // 字符串1
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (NSInteger index = 0; index < 1000; index ++) {
        dispatch_async(queue, ^{
           self.name = [NSString stringWithFormat:@"asdasdasdasdajsdajs"];
        });
    }
image.png
// 字符串1
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (NSInteger index = 0; index < 1000; index ++) {
        dispatch_async(queue, ^{
           self.name = [NSString stringWithFormat:@"abc"];
        });
    }

這個例子卻不crash。
這個很奇怪:我解釋一下:
我們可以看到這兩個例子除了那個字符串的賦值不一樣以外其他的都一樣 那為什么,因為我們知道第二個字符串是Tagged Pointer類型,也就是他的值直接在內存地址中攜帶我們訪問的時候就會直接訪問內存地址直接拿到值,而不是Tagged Pointer類型那么他會直接調用他的set方法。
我們知道我們目前都是arc環(huán)境,但是arc的本質是修改非arc的情況,那么我們可以大致猜測非arc的寫法。

- (void)setName:(NSString *)name{
    [_name release];
    _name = name;
}

如果像我們所寫的那么我們第一個代碼程序那么就會出現線程安全的問題,因為很有可能沒有釋放,子線程的值又來賦值了,如果我們加了鎖自然就沒有問題的,但是我覺得我們最主要的問題是對Tagged Pointer的理解。

總結:

1.判斷是否為toggle pointer 在ios上第64位也就是最高位為1
2.判斷是否為toggle pointer在mac上最低位為1.

weak指針的實現原理

我們在我們下載的蘋果的源碼的NSObject.mm文件中我們可以看到(https://opensource.apple.com/tarballs/),其實我們可以分析大致的三個步驟,現在我先說出結論然后在說下每段的過程。

首先我們需要runtime維護了weak表,其中weak表其實是一個hash表,key就是對象的地址,value就是存儲的weak指針所指向的數組。
1.首先runtime會調用objc_initWeak函數,初始化一個weak指針指向一個新的對象的地址。
2.objc_initWeak函數會調用objc_storeWeak函數,objc_storeWeak函數干了很多的事情,一會我會放上源碼他的主要作用是改變weak指針的指向,創(chuàng)建對應的弱引用表(hash表)。
3.也就是對象的銷毀,我們知道對象的銷毀要調用dealloc方法,那么我們在dealloc方法中找到他會調用clearDeallocating函數,這個函數他會通過對象的地址找到對應weak表,他會清除weak表中跟這個對象有關的數據,然后設置為nil,然后他在清除這個key和記錄。
下面來解釋具體每一步的執(zhí)行,不會說的很詳細,需要有一些c++的基礎,我會放上源碼。然后不知道的可以具體看下源碼,下載源碼的網站為:(https://opensource.apple.com/tarballs/objc4/)下載最新的 這個基本就是runtime的源碼,現在我們看下第一個步驟:
1.搜索objc_initWeak函數

image.png

objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }
    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

在這里我們需要看到首先他會判斷傳進來的這個對象是否是存在的,如果這個對象不存在那么weak也就沒有意義 直接設置為nil
2.通過第一步我們已經看到他會調用storeWeak函數,那么我們點進去這個函數看一下內部的實現,如下:

storeWeak(id *location, objc_object *newObj)
{
    assert(haveOld  ||  haveNew);
    if (!haveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }

    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // Clean up old value, if any.
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}

其中這個函數干了n多的事情,具體的話看下源碼就知道怎么回事,需要c++的基礎,其中我說下SideTable,這個是個結構體:
摘抄有用的信息:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}

slock是自旋鎖保證線程安全的,
refcnts引用計數hash表
weak_table就是weak的hash表。weak_table_t也是結構體,具體看下怎么實現的

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

其中這是一個全局的weak hash表,他是通過mask&對象的地址作為key,存儲的weak_entry_t結構體做為values,那么weak_entry_t是怎樣的呢?如下:
摘除有用的信息如下:

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    }
}

其實他是存儲了一個對象的所有的弱引用的hash表。他也是通過散列表的方式進行存儲的。
3.刪除我們應該找deallloc方法,通過一層層的查找我們找到了這個方法

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();
    }
    return obj;
}

其中clearDeallocating就是清除weak操作的,查看具體干了什么,我們順著函數的調用一步一步的查找,最后找到這里:

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;
    
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}

大致的意思就是從拿到對象的地址作為key 進行查找value,便利查找設置為nil,從weak中把該記錄刪除,從引用計數表中廢除對象地址作為key的記錄。

autorelease的實現原理 或者在什么時候進行釋放

自動釋放池主要的目錄結構是
首先我們將我們的工程設置為非arc的,那么我們在我們的buildsetting中輸入automatic re搜索結果就會出現如下


image.png

我們將yes改為no,這時候我們的工程就是非arc的了也就是需要我們手動管理內存等等的相關的機制。我們寫如下的代碼

#import "ViewController.h"
#import "DGPerson.h"

@interface ViewController ()

@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    @autoreleasepool{
        DGPerson *person = [[[DGPerson alloc] init] autorelease];
    }
}
@end

將我們以上的代碼轉化為c++的代碼,首先切換到ViewControlle所在的目錄然后執(zhí)行如下的命令

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m

額外的解釋說明這個命令的意思是:xcode下運行 指定sdk iphoneos 指定架構arm64 編寫ViewController.m文件
我們可以看到aurorelease的結構是這個

 /* @autoreleasepool */{ __AtAutoreleasePool __autoreleasepool; 
        DGPerson *person = ((DGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((DGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((DGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DGPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
    }

我們通過打總是匯編的斷點我們可以看到
怎樣設置:


image.png

可以看到結果為:


image.png

image.png

我們可以看到objc_autoreleasePoolPush和objc_autoreleasePoolPop這個方法也就是在我們的括號進入和退出的結構
到我們的源碼中查找objc_autoreleasePoolPush可以看到他是在AutoreleasePoolPage中調用的push方法
image.png

那我們看下AutoreleasePoolPage是個什么結構,點進去我們可以很多東西我們經過分析拿到最有用的東西為

    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

每個AutoreleasePoolPage一共是4096個字節(jié),除了用來存放他的成員變量外其他的都是都是用來存放autorelease對象的內存地址,比如我們這里存放的對象就是person 那么我們這里面存放的就是person對象的地址,AutoreleasePoolPage是通過雙向鏈表的方式進行存儲的,大致的樣子就是這樣的


image.png

他結構中的parent和child就是分別指的是通過parent指向上一個AutoreleasePoolPage和child指向的是下一個AutoreleasePoolPage,因為一個是4096個字節(jié)其中去掉他的成員變量的7*8=56個那么剩下的是4040個字節(jié)我們知道一個person對象是占用16個字節(jié) ,那么如果我for循環(huán)創(chuàng)建1000個的話那么AutoreleasePoolPage是存不下的,所以他的設計就是通過雙向鏈表的方式進行存儲。
其中next是指向下一個存放autorelease存放對象的內存地址。

  • 我們接下來探究一下push方法和pop方法都在干什么
    我們點進去


    image.png

    點進去push方法可以看到

 static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

我們可以看到他在POOL_BOUNDARY這個宏壓入棧,那么我們來看下pop方法都干了什么

{
        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) {
            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();
            }
        }
    }

大致可以看到會從最后一個入棧對象開始發(fā)送release消息,只要遇到POOL_BOUNDARY然后就執(zhí)行release方法將這個對象進行自動釋放。
對于這種情況

 @autoreleasepool{
        DGPerson *person = [[DGPerson alloc] init];
    }

我們通過剛才的研究可以知道他確實是在括號結束的時候刪除的
那么如果是這種情況 我們來看看那么他是什么時候執(zhí)行release方法進行銷毀的呢

#import "ViewController.h"
#import "DGPerson.h"

@interface ViewController ()

@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%s",__func__);
    DGPerson *person = [[[DGPerson alloc] init] autorelease];
    
}
-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    NSLog(@"%s",__func__);
    
}
- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    NSLog(@"%s",__func__);
}

@end
image.png

可以看到這個對象銷毀是在viewwillapper方法之后執(zhí)行的也就是說我們目前可以得出一個結論至少不是在括號完畢執(zhí)行結束的,其實他的銷毀時跟runloop有關的,我們來看下此時的runloop關系

autorelease什么時候進行釋放

在上面的代碼的基礎上我們打印runloop的,可以看到部分代碼如下:


image.png

我們可以看到調用_wrapRunLoopWithAutoreleasePoolHandler這個函數功能的狀態(tài)是activities = 0x1和activities = 0xa0
我們隨便輸入一個runloop的source點進去可以看到每一中source對應的值

/* Run Loop Observer Activities */
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
};

我們可以看到activities = 0x1對應的是kCFRunLoopEntry,activities = 0xa0(160)對應的是128+32 也就是kCFRunLoopExit和kCFRunLoopBeforeWaiting
其實_wrapRunLoopWithAutoreleasePoolHandler就是處理release的push方法和pop方法,具體可以查看runloop的源碼可以看下,這里就不看了。
結論就是:ios在主線程中注冊了兩個source 其中kCFRunLoopEntry是處理push方法,另外的一個source包括kCFRunLoopExit和kCFRunLoopBeforeWaiting,其中監(jiān)聽到kCFRunLoopBeforeWaiting執(zhí)行那個的是push和pop方法,其中監(jiān)聽到kCFRunLoopExit執(zhí)行的pop方法,所以我們可以說DGPerson *person = [[[DGPerson alloc] init] autorelease];這種的release是在runloop退出或者runloop休息之前執(zhí)行的釋放的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容