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__); } - (void)linkTest { NSLog(@"%s", __func__); } - (void)dealloc { 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, 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、自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage。
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方法,释放这个局部对象。