• 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表实现的

  • 相关阅读:
    ABP 异常
    Vmware中安装的Ubuntu不能全屏问题解决
    centos7.4 文件权限
    webpack 入门(1)
    webpack(2) 概念
    centos7.4 rpm命令
    centos7.4 which、whereis、locate的使用
    centos7.4 find命令
    centos7.4 lsof用法
    centos7.4 用户和组的管理
  • 原文地址:https://www.cnblogs.com/huanying2000/p/14359697.html
Copyright © 2020-2023  润新知