• 小码哥学习笔记:APP内存管理


    1、iOS程序的内存布局

    在这里插入图片描述

    1、代码段:编译之后的代码
    2、数据段

    2.1、字符串常量:比如NSString *str = @“123”
    2.2、已初始化数据:已初始化的全局变量、静态变量等
    2.3、未初始化数据:未初始化的全局变量、静态变量等

    3、:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大
    4、:函数调用开销,比如局部变量。分配的内存空间地址越来越小

    2、Tagged Pointer 标记指针

    1、从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储.
    2、在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值。使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。(Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。)
    3、当指针不够存储数据时,才会使用动态分配内存的方式来存储数据。
    4、objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销。
    5、在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。

    优点:
    1、减少了 64 位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。

    弊病:
    1、因为并不是真正的对象,而是一个伪对象,是没有isa指针的。
    2、因为不是真正的对象,所以如果你直接访问Tagged Pointer的isa成员的话,在编译时将会得到警告。这时我们只要避免在代码中直接访问对象的 isa 变量,即可避免这个问题。

    判断是否为Tagged Pointer:

    1、iOS平台,最高有效位是1(第64bit)
    2、Mac平台,最低有效位是1

    #if TARGET_OS_OSX & __x86_64__
        //64-bit Mac tag bit is LSB
    #   define OBJC_MSB_TAGGED_POINTERS 0
    #else
        //Everything else tag bit is MSB
    #   define OBJC_MSB_TAGGED_POINTERS 1
    #endif
    
    
    #if OBJC_MSB_TAGGED_POINTERS
    #   define _OBJC_TAG_MASK (1UL<<63)
    #else
    #   define _OBJC_TAG_MASK 1UL
    #endif
    
    static inline bool
    _objc_isTaggedPointer(const void *_Nullable ptr) {
    	return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    问答拓展

    思考以下2段代码能发生什么事?有什么区别?
    在这里插入图片描述
    1、第一段代码会导致闪退,因为第一段代码每次赋值都是给地址赋值,每次setter之前都会执行release操作。因为是异步操作,所以可能会导致多次release,导致引用计数已经小于等于0,对象已经没销毁。最终抛出坏内存异常。
    2、第二段代码属于指针赋值,直接赋值,没有引用计数操作,所以没有问题。

    3、定时器

    已知iOS中常用的定时器有三种:NSTimer、CADisplayLink、GCD。
    它们有各自的特性和应用场景:
    1、NSTimer

    1.1、存在延迟:不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。
    1.2、必须加入Runloop:注意与uiscrollview的使用,要切换RunLoopMode状态。

    2、CADisplayLink

    1、保证调用频率和屏幕的刷帧频率一致,60FPS。
    2、如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会,跳过次数取决CPU的忙碌程度。
    3、CADisplayLink适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。

    3、GCD

    1、时间准确
    2、可以使用子线程,解决定时间跑在主线程上卡UI问题。

    3.1、NSTimer、CADisplayLink的使用注意点

    查看以下代码能否正常编译运行?会出现什么问题?需要怎么实现?

    @interface ViewController ()
    @property (strong, nonatomic) CADisplayLink *link;
    @property (strong, nonatomic) NSTimer *timer;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
        [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
    }
     
     - (void)timerTest {
        NSLog(@"%s", __func__);
    }
    
    - (void)linkTest {
        NSLog(@"%s", __func__);
    }
    
    - (void)dealloc {
        NSLog(@"%s", __func__);
        [self.link invalidate];
        [self.timer invalidate];
    }
     @end   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    以上代码可以正常编译运行,但是离开这个页面时,因为它们之间互相强引用,导致内存无法正常释放(内存泄漏)。以timer为例,见下图:
    在这里插入图片描述

    解决方案,可以在两者之间添加一个弱引用,见下图:
    在这里插入图片描述
    这样当ViewController销毁时,发现没有其他对象强引用它,那么整个相关链条就可以正常销毁。

    代码实现:

    方法一、使用block
    timer可以使用block弱引用实现

    - (void)viewDidLoad {
        [super viewDidLoad];
        __weak typeof(self) weakSelf = self;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            [weakSelf timerTest];
        }];
    }
    
    - (void)timerTest {
        NSLog(@"%s", __func__);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    方法二、使用代理对象(NSProxy)
    1、NSProxy是专为代理而生,当调用没有实现的方法时,直接触发消息转发。
    2、如果继承的事NSObject时,会先触发消息发送机制,如果没有时才会进入消息转发阶段,更耗时耗性能。


    ZMProxy.h:

    #import <Foundation/Foundation.h>
    
    @interface ZMProxy : NSProxy
    + (instancetype)proxyWithTarget:(id)target;
    @property (weak, nonatomic) id target;
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ZMProxy.m:

    #import "ZMProxy.h"
    
    @implementation ZMProxy
    
    + (instancetype)proxyWithTarget:(id)target {
        ZMProxy *proxy = [[ZMProxy alloc] init];
        proxy.target = target;
        return proxy;
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
        return [self.target methodSignatureForSelector:sel];
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation {
        [invocation invokeWithTarget:self.target];
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ZMProxy1.h:

    #import <Foundation/Foundation.h>
    
    @interface ZMProxy1 : NSObject
    + (instancetype)proxyWithTarget:(id)target;
    @property (weak, nonatomic) id target;
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ZMProxy1.m:

    #import "ZMProxy1.h"
    
    @implementation ZMProxy1
    
    + (instancetype)proxyWithTarget:(id)target {
        ZMProxy1 *proxy = [[ZMProxy1 alloc] init];
        proxy.target = target;
        return proxy;
    }
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        return self.target;
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.link = [CADisplayLink displayLinkWithTarget:[ZMProxy proxyWithTarget:self] selector:@selector(linkTest)];
        [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[ZMProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
    
    }
    
    - (void)timerTest {
        NSLog(@"%s", __func__);
    }
    
    - (void)linkTest {
        NSLog(@"%s", __func__);
    }
    
    - (void)dealloc {
        NSLog(@"%s", __func__);
        [self.link invalidate];
        [self.timer invalidate];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    3.2、GCD定时器封装

    ZMTimer.h:

    #import <Foundation/Foundation.h>
    
    @interface ZMTimer : NSObject
    
    
    /// 开启定时器【代码块回调】
    /// @param task 任务
    /// @param start 开始时间
    /// @param interval 时间间隔
    /// @param repeats 是否重复
    /// @param async 是否异步
    /// @return 返回定时器标记
    + (NSString *)execTask:(void(^)(void))task
                     start:(NSTimeInterval)start
                  interval:(NSTimeInterval)interval
                   repeats:(BOOL)repeats
                     async:(BOOL)async;
    
    /// 开启定时器【SEL实现】
    /// @param target 添加对象
    /// @param selector 方法实现
    /// @param start 开始时间
    /// @param interval 时间间隔
    /// @param repeats 是否重复
    /// @param async 是否异步
    /// @return 返回定时器标记
    + (NSString *)execTask:(id)target
                  selector:(SEL)selector
                     start:(NSTimeInterval)start
                  interval:(NSTimeInterval)interval
                   repeats:(BOOL)repeats
                     async:(BOOL)async;
    
    
    /// 取消定时器任务
    /// @param name 定时器标记
    + (void)cancelTask:(NSString *)name;
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    ZMTimer.m:

    #import "ZMTimer.h"
    
    @implementation ZMTimer
    
    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);
        });
    }
    
    /// 开启定时器【代码块回调】
    /// @param task 任务
    /// @param start 开始时间
    /// @param interval 时间间隔
    /// @param repeats 是否重复
    /// @param async 是否异步
    /// @return 返回定时器标记
    + (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;
        
        // 1、创建队列
        dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
        
        // 2、创建定时器
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        
        // 3、设置时间
        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];
        // 4、存放到字典中
        timers_[name] = timer;
        // 解锁
        dispatch_semaphore_signal(semaphore_);
        
        // 5、设置回调
        dispatch_source_set_event_handler(timer, ^{
            task();
            
            if (!repeats) { // 不重复的任务
                [self cancelTask:name];
            }
        });
        
        // 6、启动定时器
        dispatch_resume(timer);
        
        return name;
    }
    
    /// 开启定时器【SEL实现】
    /// @param target 添加对象
    /// @param selector 方法实现
    /// @param start 开始时间
    /// @param interval 时间间隔
    /// @param repeats 是否重复
    /// @param async 是否异步
    /// @return 返回定时器标记
    + (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];
    }
    
    /// 取消定时器任务
    /// @param name 定时器标记
    + (void)cancelTask:(NSString *)name {
        if (name.length == 0) return;
        
        dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
        
        dispatch_source_t timer = timers_[name];
        if (timer) {
        	// 7、取消定时器
            dispatch_source_cancel(timer);
            [timers_ removeObjectForKey:name];
        }
    
        dispatch_semaphore_signal(semaphore_);
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107

    实现:

    #import "ViewController.h"
    #import "ZMTimer.h"
    
    @interface ViewController ()
    @property (copy, nonatomic) NSString *task;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.task = [ZMTimer execTask:self
                             selector:@selector(doTask)
                                start:2.0
                             interval:1.0
                              repeats:YES
                                async:NO];
        
    //    self.task = [MJTimer execTask:^{
    //        NSLog(@"111111");
    //    } start:2.0 interval:-10 repeats:NO async:NO];
    
    }
    
    - (void)doTask {
        NSLog(@"222222");
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [ZMTimer cancelTask:self.task];
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    4、OC对象的内存管理

    1、在iOS中,使用引用计数来管理OC对象的内存。
    2、一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。
    2、调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
    3、内存管理的经验总结:

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

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

    4.1、引用计数的存储

    1、在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中。
    在这里插入图片描述
    2、refcnts是一个存放着对象引用计数的散列表。

    4.2、dealloc

    当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是:

    dealloc
    _objc_rootDealloc
    rootDealloc
    object_dispose
    objc_destructInstance、free

    在这里插入图片描述

    4.3、自动释放池

    4.3.1、autorelease 底层结构

    1、自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage
    2、调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。
    3、源码分析:

    clang重写@autoreleasepool
    objc4源码:NSObject.mm

    在这里插入图片描述

    4.3.2、AutoreleasePoolPage

    1、链表关系

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

    在这里插入图片描述

    2、实现原理

    1、调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址。
    2、调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
    3、id *next指向了下一个能存放autorelease对象地址的区域。

    3、触发逻辑

    iOS在主线程的Runloop中注册了2个Observer

    1、第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
    2、第2个Observer:

    2.1、监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
    2.2、监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

    5、问答拓展

    1、使用CADisplayLink、NSTimer有什么注意点?
    2、介绍下内存的几大区域
    3、讲一下你对 iOS 内存管理的理解
    4、ARC 都帮我们做了什么?

    LLVM(编译器) + Runtime(运行时)
    编译器:如自动释放池的实现逻辑
    运行时:比如弱引用的实现逻辑
    总的来说:ARC是LLVM编译器和Runtime系统相互协作的结果。

    5、weak指针的实现原理

    当一个对象要释放时,会自动调用dealloc。底层实现:

    obj->clearDeallocating();//将指向当前对象的指针置为nil
    
    • 1

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

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

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

    1、如果这个对象是autorelease对象,什么时候调用release是由RunLoop来控制的,它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release。
    2、如果不是autorelease对象,在方法结束之前,底层会调用release方法,释放这个局部对象。

  • 相关阅读:
    Linux和本地Windows如何互传文件(sz和rz指令)
    深度学习中的epoch, batch 和 iteration
    蓝牙运动耳机排行榜,目前排名最好的运动耳机推荐
    Django视图
    复现Multi-Adapter RGBT Tracking(二)——Tracking
    第四章:Python中的字典(下)
    postgrest API CURD数据库
    关于websocket数据过多造成浏览器卡顿问题
    括号匹配问题(C语言)
    HTTP请求头中Referer的作用
  • 原文地址:https://blog.csdn.net/weixin_38633659/article/details/124919304