美文网首页
内存管理

内存管理

作者: 纳兰沫 | 来源:发表于2019-12-17 15:34 被阅读0次

使用CADisplayLink、NSTimer有什么注意点?

CADisplayLink、NSTimer会对target产生强引用 如果target又对他们产生强引用 那么就会引发循环引用

介绍下内存的几大区域

代码段      - 编译之后的代码
数据段      - 已初始化的数据 (已初始化的全局变量、静态变量等)
           - 为初始化的数据 (未初始化的全局变量、静态变量等)
栈         - 函数调用开销,比如局部变量。分配的内存空间地址越来越小
堆         - 通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大

讲一下你对 iOS 内存管理的理解

ARC 都帮我们做了什么?

LLVM + Runtime 相互协作的结果
- ARC利用LLVM编译器自动帮我们生成release retain autorelease 
- 弱引用利用Runtime 在程序运行时 进行操作

weak指针的实现原理

将弱引用存在哈希表里面,如果对象要销毁 就取出当前对象所对应的弱引用表 把弱引用表里面存储的弱引用都清除

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

autorelease对象什么时候调用release 由RunLoop来控制
它可能是在某次RunLoop循环中 RunLoop休眠之前调用了release

方法里有局部对象, 出了方法后会立即释放吗

会马上释放
使用ARC 如果编译器加入的是autorelease 那么它可能是在某次RunLoop循环中 RunLoop休眠之前调用了release
如果是加入的是relese 那么出了方法后会立即释放

思考以下2段代码能发生什么事?有什么区别?

图1.png
图2.png
图1 崩溃 因为set方法的本质是释放旧值 存储新值 (有可能是多条线程同时释放旧值  报坏内存访问)
解决方案 1.设置name为atomic 2.在self.name设置加锁解锁
图2 name是Tagger Pointer 不是指针

CADisplayLink、NSTimer使用注意

CADisplayLink、NSTimer会对target产生强引用 如果target又对他们产生强引用 那么就会引发循环引用
    //保证调用频率和平面的刷帧频率一致 60FPS
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
解决NSTimer的循环引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
会产生循环引用

把弱引用的对象传入到target也无法解决循环引用的原因是 NSTimer内部对target进行了强引用 无关是传入的是强引用对象还是弱引用对象

解决方案一
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerTest];
    }];
解决方案二
创建一个中间对象 弱引用target
@interface LMProxy : NSObject
+(instancetype) proxyWithTarget:(id)target;
@property (weak,nonatomic) id target;
@end

#import "LMProxy.h"

@implementation LMProxy

+(instancetype) proxyWithTarget:(id)target{
    
    LMProxy *proxy = [[LMProxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self.target;
}

ViewController.m
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LMProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
解决方案三

NSProxy 没有init方法 直接alloc就行
NSProxy 跟 NSObject 都是基类 专门用于消息转发 (如果想直接消息转发 直接使用NSProxy 这样效率更高)
如果继承于NSProxy 大部分的方法就会自动进行消息转发

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

@implementation LMProxy

+(instancetype) proxyWithTarget:(id)target{
    
    LMProxy *proxy = [LMProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
     return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.target];
}
解决CADisplayLink的循环引用

创建中间对象的方式解决

GCD定时器

RunLoop每跑一圈 他的时间是不固定的
NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时

self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//几秒之后开始执行
NSTimeInterval start = 2.0;
//时间间隔
NSTimeInterval interval = 1.0;
    
dispatch_source_set_timer(self.timer , dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC,0);
dispatch_source_set_event_handler(self.timer, ^{
        
});
dispatch_resume(self.timer);
封装GCD定时器
@interface MJTimer : NSObject

+ (NSString *)execTask:(void(^)(void))task
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
              repeates:(BOOL)repeates
                 async:(BOOL)async;

+ (NSString *)execTask:(id)target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
              repeates:(BOOL)repeates
                 async:(BOOL)async;

//取消任务
+ (void)cancelTask:(NSString *)name;
@end

@implementation MJTimer

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
              repeates:(BOOL)repeates
                 async:(BOOL)async{
    
    if (!task || start < 0 || (interval <= 0 && repeates)) 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:@"%lu",(unsigned long)timers_.count];
    //存放到字典中
    timers_[name] = timer;
    dispatch_semaphore_signal(semaphore);
    //设置回调
    dispatch_source_set_event_handler(timer, ^{
        if (!repeates) {
            //如果非重复 取消定时器
            [self cancelTask:name];
        }
    });
    //启动计时器
    dispatch_resume(timer);
    return name;
}

+ (void)cancelTask:(NSString *)name{
    if (name.length == 0) return;
    dispatch_source_t timer = timers_[name];
    if (!timer) return;
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    dispatch_source_cancel(timer);
    [timers_ removeObjectForKey:name];
    dispatch_semaphore_signal(semaphore);
}

+ (NSString *)execTask:(id)target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
              repeates:(BOOL)repeates
                 async:(BOOL)async{
    
    if (!target || start < 0 || (interval <= 0 && repeates)) 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 repeates:repeates async:async];
}

@end

iOS程序的内存布局

内存布局.png
代码段      - 编译之后的代码
数据段      - 已初始化的数据 (已初始化的全局变量、静态变量等)
           - 为初始化的数据 (未初始化的全局变量、静态变量等)
栈         - 函数调用开销,比如局部变量。分配的内存空间地址越来越小
堆         - 通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大

Tagged Pointer

节省内存空间
iOS平台,最高有效位是1(第64bit) 或者 Mac平台,最低有效位是1 就是Tagged Pointer
当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

OC对象的内存管理

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

- 一个新创建的OC对象的引用计数默认是1 当引用计数减为0 OC对象就会销毁,释放其占用的内存空间
- 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
copy 和 mutableCopy
copy 和 mutableCopy.png
拷贝的目的
产生一个副本对象,跟源对象互不影响
修改了源对象,不会影响副本对象
修改了副本对象,不会影响源对象
copy         不可变拷贝,产生不可变副本
mutableCopy  可变拷贝,产生可变副本 
浅拷贝   指针拷贝  没有产生新对象
深拷贝   内容拷贝  有产生新对象
内存管理的经验总结
- 当调用alloc new copy mutbaleCopy 方法返回了一个对象 在不需要这个对象时 要调用release或者autorelease来释放它
- 想拥有某个对象 就让它的引用计数+1 不想再拥有某个对象 就让它的引用计数-1

引用计数的存储

在64bit中 引用计数可以直接存储在优化过的isa指针中 如果不够存储 就会存储在SideTable中 SideTale中有个
RefcountMap的散列表
SideTable结构.png

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

自动释放池

  struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
 
    ~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
 
    void * atautoreleasepoolobj;
 };
自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage
调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
AutoreleasePoolPage的结构
AutoreleasePoolPage的结构.png
AutoreleasePoolPage对象占用4096字节内存 除了用来存放他的成员变量  剩下的空间用来存放autorelease对象的地址

所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
程序运行过程中 存在着多个AutoreleasePoolPage对象

image.png
child 存放着下一个AutoreleasePoolPage对象的地址(如果是最后一个 那么地址为空)
parent  存放着上一个AutoreleasePoolPage对象的地址 (如果是第一个 那么地址为空)
AutoreleasePoolPage的调用
1.调用push会将一个POOL_BOUNDARY入栈 并返回其存放的内存地址
2.调用pop方法时传入一个POOL_BOUNDARY的内存地址 会从最后一个入栈的对象开始发送release消息 直到遇到
POOL_BOUNDARY

id *next指向了下一个能存放autorelease对象地址的区域
可以通过以下私有函数来查看自动释放池的情况 extern void _objc_autoreleasePoolPrint(void);

RunLoop和Autorelease

iOS在主线程的RunLoop注册了2个Observer
- 第一个Observer监听了kCFRunLoopEntry事件 会调用objc_autoreleasePoolPush()
- 第二个Observer监听了
  a.监听了kCFRunLoopBeforeWaiting事件 会调用objc_autoreleasePoolPop() objc_autoreleasePoolPush()
  b.监听了kCFRunLoopBeforeExit事件  会调用objc_autoreleasePoolPop() 

相关文章

  • iOS内存管理详解

    目录 block内存管理 autorelease内存管理 weak对象内存管理 NSString内存管理 new、...

  • 第10章 内存管理和文件操作

    1 内存管理 1.1 内存管理基础 标准内存管理函数堆管理函数虚拟内存管理函数内存映射文件函数 GlobalMem...

  • 操作系统之内存管理

    内存管理 包括内存管理和虚拟内存管理 内存管理包括内存管理概念、交换与覆盖、连续分配管理方式和非连续分配管理方式(...

  • JavaScript —— 内存管理及垃圾回收

    目录 JavaScript内存管理内存为什么需要管理?内存管理概念JavaScript中的内存管理JavaScri...

  • OC - OC的内存管理机制

    导读 一、为什么要进行内存管理 二、内存管理机制 三、内存管理原则 四、MRC手动内存管理 五、ARC自动内存管理...

  • 3. 内存管理

    内存管理 内存管理包含: 物理内存管理; 虚拟内存管理; 两者的映射 除了内存管理模块, 其他都使用虚拟地址(包括...

  • Go语言——内存管理

    Go语言——内存管理 参考: 图解 TCMalloc Golang 内存管理 Go 内存管理 问题 内存碎片:避免...

  • jvm 基础第一节: jvm数据区

    程序内存管理分为手动内存管理和自动内存管理, 而java属于自动内存管理,因此jvm的职能之一就是程序内存管理 j...

  • 内存管理

    内存管理的重要性。 不进行内存管理和错误的内存管理会造成以下问题。 内存泄露 悬挂指针 OC内存模型 内存管理是通...

  • 11-AutoreleasePool实现原理上

    我们都知道iOS的内存管理分为手动内存管理(MRC)和自动内存管理(ARC),但是不管是手动内存管理还是自动内存管...

网友评论

      本文标题:内存管理

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