使用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的结构.pngAutoreleasePoolPage对象占用4096字节内存 除了用来存放他的成员变量 剩下的空间用来存放autorelease对象的地址
所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
程序运行过程中 存在着多个AutoreleasePoolPage对象
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()
网友评论