iOS 内存管理

面试题

iOS 内存分布

stack:栈区 方法调用都是在这里

heap:堆区 alloc 分配的对象

bss:未初始化的全局变量

data:已初始化的全局变量等

text:代码段 程序代码

1.使用CADisplayLink NSTimer 有什么注意点

一般我们在使用NSTimer 或者 CADisplayLink 的时候,对象都会持有定时器。那么我们在这样使用的时候

self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

就会造成对象持有定时器 定时器通过target持有对象 造成循环引用 导致对象和定时器都不能释放。那么我们应该怎样解决这个问题呢?我们可以定制一个中间对象 使NSTimer 持有这个中间对象 中间对象弱引用着使用NSTimer的对象。然后利用消息转发机制把持有NSTimer的对象 设置为timer事件的执行者。具体代码如下:

1.定义中间对象

#import <Foundation/Foundation.h>

@interface LFProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation LFProxy

+ (instancetype)proxyWithTarget:(id)target
{
    LFProxy *proxy = [[LFProxy alloc] init];
    proxy.target = target;
    return proxy;
}
//中间对象没有实现timer调用的方法 RunTime的消息发送机制 1.消息查找 2动态解析 3消息转发
//我们可以利用第三步的消息转发 把Timer调用的事件指向持有Timer的对象
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}

@end

使用:

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LFProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest
{
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    [self.timer invalidate];
}

@end

上面的代码 我们可以看到LFProxy 是继承于NSObject的。这样虽然也能解决问题。但其实有苹果给我们提供了一个更好的类来解决这类问题NSProxy。这是和NSObject平级的一个类。

这个类的好处就在于 如果方法没实现会直接消息转发。不会像NSObject一样 先经过消息查找 动态解析 再进入消息转发阶段。

 @interface NSProxy <NSObject> {
     __ptrauth_objc_isa_pointer Class    isa;
 }

 + (id)alloc;
 + (id)allocWithZone:(nullable NSZone *)zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
 + (Class)class;
//直接在类中声明的消息转发方法
 - (void)forwardInvocation:(NSInvocation *)invocation;
 - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
 @end

那么我们的中间类可以直接继承于NSProxy

@interface LFProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation LFProxy

+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy对象不需要调用init,因为它本来就没有init方法
    LFProxy *proxy = [LFProxy alloc];
    proxy.target = target;
    return proxy;
}
//由于是和NSObject平级的类 所以没有这个方法 - (id)forwardingTargetForSelector:(SEL)aSelector
//而是直接通过下面两个方法 进行消息转发的。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}
@end

使用方法和上面相同 但是效率更高。

由于NSTimer 和 CADisplayLink 是基于RunLoop实现的 所以如果RunLoop中有某些任务比较耗时的时候,可能会导致RunLoop此次循环较长 调用Timer事件受阻 导致定时器不是很准确 。

如果我们对定时器的要求比较高我们可以使用GCD的定时器 这个是基于内核而不是RunLoop的。不受RunLoop的影响 也可以在子线程中执行。

基本使用:

 dispatch_queue_t queue = dispatch_queue_create("timer", 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秒执行
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                              interval * NSEC_PER_SEC, 0);
    
    // 设置回调
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"1111");
    });
    // 启动定时器
    dispatch_resume(timer);
    self.timer = timer;

但是我们也可以看到使用起来比较麻烦 我们可以封装一下 这样使用起来比较简单。

@interface LFTimer : 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

@implementation LFTimer

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

这样用起来就很方便了。

2.介绍下内存的几大区域

从低地址到高地址

保留内存空间  代码段(_TEXT) 数据段(_DATA 字符串常量 已初始化的数据 未初始化的数据) 堆(heap) 栈(stack) 内核区

我们用到的就是 代码段 数据段 堆区 栈区空间

代码段:放置编译之后的代码

数据段:

字符串常量(放在常量区 两个内容相同的字符串 内存地址是一样的 比如 str1 = @"123",str2 = @"123")

已初始化的数据: 已初始化全局变量 静态变量 

未初始化的数据: 未初始化的全局变量 静态变量等

堆:

通过alloc malloc calloc等动态分配的空间 分配地址由低到高

栈:

函数调用开销 比如函数中的局部变量 分配地址由高到低

内存管理理解:

https://www.jianshu.com/p/c3344193ce02

内存管理方案:

https://www.jianshu.com/p/4a9fb33870a5

TaggedPointer 方案 管理小对象

NONPOINTER_ISA 方案 就是64位下isa中位域技术 其中的19位存储的引用计数 如果不够存储的话 再使用散列表方案

散列表 方案

tag用来标记类型

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"];
    });
}

这段代码会运行会发生崩溃现象。坏内存访问。

我们知道self.name = @"xxx" 其实调用的是name属性的setter方法。在底层setter是长这个样子的。

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

那么上面的代码就有可能同时执行[_name release] 可能会导致释放一个不存在的对象。导致坏内存访问。我们如果写成atomic属性或者赋值代码前后加锁解锁的话就可以解决。

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"];
    });
}

这段代码 就不会有问题 因为字符串比较简单 使用的是tagged Pointer技术。不是对象了 赋值不再使用setter方法了 而是直接存储在指针中。所以不会产生坏内存访问。

copy mutableCopy

拷贝的目的:产生一个副本对象 跟原对象互不影响

修改了原对象 不会影响到副本对象 修改了副本对象 不会影响原对象

copy 产生不可变副本

mutableCopy 产生可变副本

不可变字符串: copy 产生不可变字符串 且指向的地址为同一块(节省了空间 达到了拷贝的目的) mutableCopy 产生一个可变的字符串 且指向新地址(只有这样才能达到目的 修改互不影响)

深拷贝:内容拷贝 产生新的对象

浅拷贝: 指针拷贝 不产生新的对象

不可变对象 copy 浅拷贝 mutableCopy 深拷贝

可变对象 copy mutableCopy 都是深拷贝

在iOS中我们习惯用copy修饰字符串 就是因为如果用strong 就有可能会是这种情况 :外面传来了一个可变字符串给一个对象的属性然后显示到UI上 理论上外面的可变字符串修改 不能影响UI的显示。但是如果用strong 就会对象的属性和可变字符串指向同一个地址 外面变 里面也变 导致UI显示错误。

我们都知道 iOS的内存管理是通过引用计数来实现的。那么一个对象的引用计数存放在哪里呢? 其实从64bit开始 饮用技术就直接存储在优化过的isa指针中。也有可能存放在sideTable中。

在RunTime中 我们曾讲过isa指针中的位域技术 其中有19位叫做extra_rc存放的就是引用计数减1的数值 如果这19位不够存储 isa中的has_sidetable_rc位就会变为1 那么引用计数就会存储在一个叫做 sidetable的类的属性里。

sideTable被包含在一个SideTables里面 sideTables 是苹果为了管理所有对象的引用计数和weak指针而维护的一张全局的哈希表

sideTables(哈希表)里面包含了很多Sidetable这种结构体 我们可以根据对象的指针地址通过一定的算法 找到对应的SideTable取出引用计数

struct SideTable {
  //锁 自旋锁 spinlock_t slock;
  // 强引用相关 引用计数 RefcountMap refcnts;
  //弱引用 weak_table_t weak_table;
};

spinlock_t 自旋锁在等待解锁的过程 线程不会休眠 效率比互斥锁快的多。适用于线程保持锁时间比较短的情况。这个锁的作用就是在操作引用计数的时候 对sideTable进行线程同步的。

refcountMap 对象具体的饮用计数 数量是存储在这里的。

//查看引用计数的源码
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;
    //如果是OC对象 加锁
    sidetable_lock();
    //拿到isa指针的信息
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    //如果是优化过的isa指针 说明信息存储在isa指针中
    if (bits.nonpointer) {
        //extra_rc 引用计数减1
        uintptr_t rc = 1 + bits.extra_rc;
        //如果extra_rc 不够存储 就存储在sideTable里
        if (bits.has_sidetable_rc) {
            //取出sideTable中存储的引用计数
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
} 

释放过程:

是否优化过isa 是否有weak指针 是否有关联对象 是否有C++内容  是否使用了散列表维护引用计数 如果都是否 直接释放

否则调用object_dispose()对象清除函数函数 

object_dispose()函数的实现

objc_destructInstance()函数实现 

判断是否有相关的C++变量 如果有释放C++变量 再判断是否有关联对象 如果有 释放关联对象 如果没有调用clearDeallocating()函数

clearDeallocating()函数的实现

weak指针的实现原理

简单的概括 RunTime维护了一个weak表 用于存储指向某个对象的所有weak指针。weak表其实就是一个哈希表 key是所指对象的地址 value是weak指针的地址数组

传递了两个参数 一个是对象的地址 一个是被修饰的对象

storeWeak()函数 先根据对象地址找到所对应的sideTable 然后调用weak_register_no_lock 并把sideTable中的弱引用表传进去 并且设置该对像有弱引用的标志位

weak_register_no_lock 通过对象地址找到 查找到它所对应的弱引用表的数组(也是hash算法) 然后把弱引用指针添加到这个数组里面

根据对象的地址 找到它所对应的sideTable 然后在找到与它相对应的弱引用表 然后在通过对象的地址 找到弱引用表所对应的数组 并把weak指针地址保存到这个数组里面 一旦这个对象被释放 也会找到该数组列表 把所有的weak指针置为nil

实现过程:

1.初始化时:RunTime会调用objc_initWeak函数 初始化一个新的weak指针 指向对象的地址

2.添加引用时:objc_initWeak函数会调用objc_storeWeak函数 这个函数的作用是更新指针的指向 创建对应的弱引用表

3.释放时 调用clearDeallocating函数 这个函数首先根据对象的地址获取所有weak指针地址的数组 然后遍历这个数组把其中的数据设为nil 最后把这个entry(对象)从weak表中删除 最后清理对象的记录。

 

4.autorelease在什么时机会被释放

首先我们要明确一下autoreleasePool的底层实现  自动释放池是以栈为节点 通过双向链表的形式组合而成 和线程一一对应

 struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
 
    ~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
 
    void * atautoreleasepoolobj;
 };

所以像下面这种代码本质其实是这样的

int main(int argc, const char * argv[]) {
    @autoreleasepool {//括号开头吊调用autoreleasePool的构造函数
//        atautoreleasepoolobj = objc_autoreleasePoolPush();
//        LFPerson *person = [[[LFPerson alloc] init] autorelease];
//        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }//括号结尾调用autoreleasePool的析构函数(释放内容)
    return 0;
}
本质上就是这样的
atautoreleasepoolobj = objc_autoreleasePoolPush();
 
LFPerson *person = [[[LFPerson alloc] init] autorelease];
 
objc_autoreleasePoolPop(atautoreleasepoolobj);

autoreleasePool的作用域结束后 person被释放。那么我们就要搞清楚objc_autoreleasePoolPush和objc_autoreleasePoolPop干了什么。

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

可以看到autoreleasePool的底层实现和AutoReleasePoolPage这个结构体有关

每个autoreleasePoolPage都占用4096个字节内存 除了用来存放它内部的成员变量 剩下的空间用来存放autorelease对象的地址。

所有的autoreleasePoolPage对象 是通过双向链表(表中的任何一个不是头部或者尾部的数据 都能通过特定的方法找到前面或后面的对象)的形式连接在一起

如果一个autoreleasePoolPage不够存储所有的autorelease对象 就会创建另一个 所以每个autoreleasepool之间必定是有联系的(通过双向链表联系)

autoReleasePoolPage内部有两个函数begin()和end()  begin()函数返回一个autoreleasepoolPage从哪里开始存放autorelease对象地址的地址。end()函数返回

一个autoreleasePoolPage的内存结束地址。

autoReleasePoolPage内部的child指针指向下一个autoReleasePoolPage对象(如果是最后一个为nil)

autoReleasePoolPage内部的parent指针指向上一个autoReleasePoolPage对象(如果是第一个为nil)

调用push方法 会将一个POOL_BOUNDARY(就是个nil)入栈,并且返回其存放的内存地址(就是autoreleasePoolPage可以盛放autorelease对象的开始地址) autorelease对象紧邻着改地址

顺序存储 如果不够 会再创建一个autoreleasePoolPage对象 继续存储

pop函数执行的时候 会传入当初push压入POOL_BOUNDARY的地址值(边界地址),也就是你当初开始存储autorelease对象的开始的地址值。然后会从我们存储的最后一个autorelease对象的地址开始向前寻找 直到边界地址 依次调用他们的release方法 进行释放。

autoreleasePoolPage中的next指针指向下一个可以存放autorelease对象的地址。

RunLoop和Autorelease

iOS 在主线程的RunLoop中注册了两个Observer 用于监听RunLoop的状态 一旦监听到某个状态就会调用_wrapRunLoopWithAutoreleasePoolhandler()方法 处理autorelease对象

第一个observer 监听的是kCFRunLoopEntry 进入的状态 进入后调用 objc_autoreleasePoolPush()函数

第二个observer 监听的是 kCFRunLoopBeforeWaiting | kCFRunLoopExit (休眠之前 | 退出) 休眠之前会调用objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush()函数

kCFRunLoopExit 退出会调用 objc_autoreleasePoolPop()函数

5.方法里面的局部对象 出了方法后会立即被释放吗

我们知道在ARC的情况下 LLVM编译器会自动帮我们生成 reatain release autorelease代码 如果插入的代码是release 那么会在方法结束后释放 如果插入的代码是autorelease 那么只能在这段代码所在的RunLoop状态在休眠之前再释放。不一定是方法结束后立马释放。

6.ARC 都帮我们做了什么

LLVM+RunTime 互相协调 达到ARC的效果。LLVM编译器会自动帮我们生成 reatain release autorelease代码 弱引用这样的存在是RunTime维护的一张weak表实现的

原文地址:https://www.cnblogs.com/huanying2000/p/14359697.html