面试题
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表实现的