内存管理

198 阅读7分钟

一:关于定时器

CADisplayLink、NSTimer,GCD都可以实现定时器的功能,但是需要注意循环引用的问题,需要注意:CADisplayLink、NSTimer都是需要依赖于runLoop来实现,正因为如此,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时

CADisplayLink

self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

解决循环引用

使用中间类,运用消息转发机制来进行处理

@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

在控制器找那个这样调用

self.link = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self] selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

NSTimer

下面的方法NSTimer会对target进行一个强引用,如果控制器也声明了一个strongtimer就会造成循环引用的

[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:YES];

如果我们声明一个weakSelfself代入呢?依然无法解决循环引用问题,这种方法只会应用在block内部,这里既是使用weakSelf,只是一个参数,其实找到的依然是self,timer内部可能会有个参数对target进行了强引用。

__weak typeof(self) weakSelf = self;

解决循环引用方案1

其实我们可以用下面👇这个API来创建定时器,在其中使用weakSelf就可以解决循环引用问题

[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"定时器,需要执行的内容");
    }];

解决循环引用方案2

使用中间类,运用消息转发机制来进行处理

@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

在控制器找那个这样调用

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];

解决循环引用方案3

使用NSProxy,专门用来做消息转发

@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

控制器中调用(注意:CADisplayLink也可以使用)

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];

GCD定时器

image.png

注意:

  1. GCD不需要我们去进行销毁
  2. 可以在子线程调用

GCD定时器封装

#import <Foundation/Foundation.h>

@interface WYTimer : 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;



#import "WYTimer.h"

@implementation WYTimer

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;
    
    return [self execTask:^{
        if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#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

控制器中调用

// 接口设计
    self.task = [MJTimer execTask:self
                         selector:@selector(doTask)
                            start:2.0
                         interval:1.0
                          repeats:YES
                            async:NO];

iOS内存布局

image.png

Tagged Pointer

  1. 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储

  2. 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

  3. 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中

  4. 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

  5. objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

如何判断一个指针是否为Tagged Pointer?
iOS平台,最高有效位是1(第64bit)
Mac平台,最低有效位是1

image.png

面试题

下面的两段代码运行结果?

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"];
        });
    }
    
    
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abc"];
        });
    }

第一段崩溃,坏内存访问
第二段没有问题

原因:本质上self.name = [NSString stringWithFormat:@"abc"]这段代码会调用setName的方法,而此时会有多条线程执行这段代码,在调用release的时候,重复释放,导致坏内存访问。

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

解决方案:可以在self.name = [NSString stringWithFormat:@"abc"]进行加锁解锁的操作

第二段没有问题的原因:[NSString stringWithFormat:@"abc"]使用的是Tagged Pointer,将abc直接存储在指针当中,所以就不会调用setName的方法,自然不会出现问题。

OC对象的内存管理

1.在iOS中,使用引用计数来管理OC对象的内存

2.一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间

3.调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

内存管理的经验总结
当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1

可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void)

copy和mutableCopy

image.png

image.png

image.png

关于自定义对象的Copy

自定义对象要想实现copy的拷贝功能,需要遵循NSCopying协议,并实现copyWithZone方法

-(id)copyWithZone:(NSZone *)zone
{
    
}

引用计数器

在64bit中,引用计数可以直接存储在优化过的isa指针中,存储在位域中的extra_rc,也可能存储在SideTable类中。

image.png

refcnts是一个存放着对象引用计数的散列表

weak的实现原理

weak和unsafe_unretain的区别?
两者都不会有强引用,但是weak所指的对象被销毁之后会被自动置为nil,而unsafe_unretain不会清空对象。

image.png

注意: ARC是结合了LLVM + Runtime来实现代码的自动插入

自动释放池原理

1.自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage
2.调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的

其实在自动释放池内部会先调用objc_autoreleasePoolPush来创建池,调用objc_autoreleasePoolPop来销毁池

@autoreleasepool {
//        atautoreleasepoolobj = objc_autoreleasePoolPush();
        
       MJPerson *person = [[[MJPerson alloc] init] autorelease];
        
//        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

AutoreleasePoolPage结构

image.png

1.每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放。
2.所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起

image.png

1.调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址

2.调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY

3.id *next指向了下一个能存放autorelease对象地址的区域

autorelease对象在什么时机会被调用release

对于下面这种情况,就是在大括号结束的时候,page调用objc_autoreleasePoolPop的时候将对象进行释放

@autoreleasepool {

 MJPerson *person = [[[MJPerson alloc] init] autorelease];

    }

如果是下面这种情况,大括号结束的时候并不能销毁对象。

- (void)viewDidLoad
{
    [super viewDidLoad];
    MJPerson *person = [[[MJPerson alloc] init] autorelease];
}

原因在于:

image.png

这个对象的释放是由Runloop来控制的,它可能在Runloop循环中,Runloop休眠的时候调用realease,前提上述代码处在MRC环境下,如果是ARC下,会立马释放,ARC插入了realease