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;
}

问答拓展

思考以下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   

以上代码可以正常编译运行,但是离开这个页面时,因为它们之间互相强引用,导致内存无法正常释放(内存泄漏)。以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__);
}

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


ZMProxy.h:

#import <Foundation/Foundation.h>

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

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

ZMProxy1.h:

#import <Foundation/Foundation.h>

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

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

- (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__);
    [self.link invalidate];
    [self.timer invalidate];
}

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

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, 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

实现:

#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

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

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

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

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

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

相关文章

在有效期内的苹果开发者账号(类型为个人或者公司账号)。还...
Appuploader官网--IOS ipa上传发布工具,证书制作工具跨平台...
苹果在9月13号凌晨(北京时间)发布 iOS 16,该系统的设备可...
计算机图形学--OpenGL递归实现光线追踪
Xcode 14打出来的包在低版本系统运行时会崩溃,报错信息是Li...