美文网首页
内存管理(NSTimer、autorelease、weak原理、

内存管理(NSTimer、autorelease、weak原理、

作者: 目前运行时 | 来源:发表于2018-09-11 14:09 被阅读0次

总结一下我们在内存管理中处理方法,以及出现的常见问题

定时器相关的问题:

  • 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

当我们点击控制器左边的返回按钮时,发现控制器并没有销毁,我们简单的分下一下,应该是我们的timer 强引用了target 而target又强引用了timer,所以导致了循环引用。其中的target这里指的是self 也就是我们的控制器。抱着尝试一下的心态我们这样修改代码,看看效果如何:

@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

结果发现当我们点击返回按钮的时候,控制器还是没有销毁的,也就是还是没有调用我们的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;明明是弱引用按道理讲已经不存在循环引用了这是什么原因呢?
因为:个人猜测,我们定时器是要加在runloop中,runloop对timer是存在一个强引用的,我们目前知道timer是对target(也就是self)存在强引用的,当我们的控制器pop的时候,因为我们没有调用定时器的invalidate方法,也就是没有去除掉runloop对timer的强引用,也就是说timer没有去除掉对self的强引用,那么我们的控制器就没有死。
鉴于这种情况我们想到的解决方案是运用这种方式,我们采用的是一个第三方的一个东西,我们用一个对象来进行包装target,在这个对象中我们弱引用target,然后调用方法进行消息转发给给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进行弱引用然后再通过消息转发技术来实现它的方法那么就完美的解决了这个问题。下面还有一个更加完美的方案:
代码如下:

#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我们点进去发现和NSObject是同一级别的,我们知道我们继承NSObject然后拦截他的消息转发方法实际上他在之前还经过了方法查找等等(如果实在不明白可以看我的runtime的简书他说了消息转发的过程),而我们的NSProxy直接就到了消息转发这一步,我们可以这样看个例子 比如我不写DGSpecifyProxy中的方法转发的方法,我们看一下报错的是什么?

#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

可以看到他直接就是我们消息转发阶段找不到方法而不是我们的不能recognize方法的错误,所以从这方面讲NSProxy效率更高。

gcd定时器

我封装了一个关于gcd定时器的一个工具方法,直接粘贴方法也不解释了,gcd定时器是比较准确的,因为他是基于系统内核的一套定时器,不是基于runloop的也就不存在不准确性。代码如下:

#import <Foundation/Foundation.h>

@interface DGTimer : NSObject

/**
 定时器的事件

 @param start 开始的时间
 @param interval 中间的间隔
 @param leeway 延时多少秒开始执行
 @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 延时多少秒开始执行
 @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 延时多少秒开始执行
 @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;
    }
    // 创建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)->堆->栈->内核区,其中代码段为我们编译之后的代码。数据段为:我们的字符串常量、已经初始化的数据、没有初始化的数据,其中字符串常量为:例如NSString *str = @"123";已经初始化的数据为:已经初始话的全局变量和静态变量,未初始化的数据为:为初始化的全局变量和静态变量。堆区为:比如alloc、malloc或者calloc等创建的,分配的内存越来越大。栈区:调用函数的开销、局部变量等等,分配的内存区域越来越小。
用一张图来表示为:


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位操作系统ios才引用的技术,主要是优化比如 NSDate、NSString、NSNumber等一些小对象的存储,之前的NSDate、NSString、NSNumber还是采用的动态的分配内存、管理引用计数等等,那么引用了Tagged Pointer他们当他们的值不是很大的时候就会直接存储在内存地址中,那么我们的runtime开始发送消息调用方法的时候直接就会从内存地址中进行查找,这样的话节省了内存地址的开销,从某一方面讲优化了我们的方案。
下面我写几个例子

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

我们可以看到内存地址的区别,可以看到第一个我们写的是10 他直接就把10放到了我们的内存地址中了 因为出现了a,其中2目前只是系统内部做的处理,我们看我们数子比较大那个可见他的内存地址还是采用原来的动态分配内存管理引用计数的方式。

一个小小的总结:

一般来说内存地址的最高位如果是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环境,但是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指针的指向,创建对应的弱引用表(hash表)。
3.也就是对象的销毁,我们知道对象的销毁要调用dealloc方法,那么我们在dealloc方法中找到他会调用clearDeallocating函数,这个函数他会通过对象的地址找到对应weak表,他会清除weak表中跟这个对象有关的数据,然后设置为nil,然后他在清除这个key和记录。
下面来解释具体每一步的执行,不会说的很详细,需要有一些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所在的目录然后执行如下的命令

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个字节,除了用来存放他的成员变量外其他的都是都是用来存放autorelease对象的内存地址,比如我们这里存放的对象就是person 那么我们这里面存放的就是person对象的地址,AutoreleasePoolPage是通过双向链表的方式进行存储的,大致的样子就是这样的


image.png

他结构中的parent和child就是分别指的是通过parent指向上一个AutoreleasePoolPage和child指向的是下一个AutoreleasePoolPage,因为一个是4096个字节其中去掉他的成员变量的7*8=56个那么剩下的是4040个字节我们知道一个person对象是占用16个字节 ,那么如果我for循环创建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();
            }
        }
    }

大致可以看到会从最后一个入栈对象开始发送release消息,只要遇到POOL_BOUNDARY然后就执行release方法将这个对象进行自动释放。
对于这种情况

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

我们通过刚才的研究可以知道他确实是在括号结束的时候删除的
那么如果是这种情况 我们来看看那么他是什么时候执行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方法之后执行的也就是说我们目前可以得出一个结论至少不是在括号完毕执行结束的,其实他的销毁时跟runloop有关的,我们来看下此时的runloop关系

autorelease什么时候进行释放

在上面的代码的基础上我们打印runloop的,可以看到部分代码如下:


image.png

我们可以看到调用_wrapRunLoopWithAutoreleasePoolHandler这个函数功能的状态是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,其中监听到kCFRunLoopBeforeWaiting执行那个的是push和pop方法,其中监听到kCFRunLoopExit执行的pop方法,所以我们可以说DGPerson *person = [[[DGPerson alloc] init] autorelease];这种的release是在runloop退出或者runloop休息之前执行的释放的。

相关文章

网友评论

      本文标题:内存管理(NSTimer、autorelease、weak原理、

      本文链接:https://www.haomeiwen.com/subject/mtxswftx.html