定时器
CADisplayLink、NSTimer使用注意
强引用问题
- (void)viewDidLoad {
[super viewDidLoad];
// 也会产生强引用
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
// 使用weakSelf还会产生强引用,本质上还是传递指针过去,内部会通过强指针获取,NSTimer内部
// 是保存强指针或者弱指针,跟外部传入内容无关
__weak typeof(self) weakself = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakself selector:@selector(timerTest) userInfo:nil repeats:YES];
// 也会产生循环引用(并没有block方案)
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
解决方案1 : 使用block
__weak typeof(self) weakself = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakself timerTest];
}];
解决方案2 : NSProxy
NSProxy是跟NSObject同一级别,都是作为基类,NSProxy是专门做消息转发的类
NSProxy使用
// ViewController.m
#import "ViewController.h"
#import "MJProxy.h"
#import "MJProxy1.h"
@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest
{
NSLog(@"%s", __func__);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self.timer invalidate];
}
@end
// MJProxy.h
#import <Foundation/Foundation.h>
@interface MJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
// MJProxy.m
#import "MJProxy.h"
@implementation MJProxy
+ (instancetype)proxyWithTarget:(id)target
{
// NSProxy对象不需要调用init,因为它本来就没有init方法
MJProxy *proxy = [MJProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
[invocation invokeWithTarget:self.target];
}
@end
NSProxy 做转发效率更高,首先查看自己的类对象,如果类对象没有,直接会转发(不会去父类查找)
GCD 定时器
使用GCD原因:CADisplayLink、 NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时。 而GCD的定时器会更加准时(GCD 定时器是系统内核级的)
- (void)test
{
// 队列
// dispatch_queue_t queue = dispatch_get_main_queue();
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_source_set_event_handler_f(timer, timerFire);
// 启动定时器
dispatch_resume(timer);
// 控制器内要强引用报活
self.timer = timer;
}
void timerFire(void *param)
{
NSLog(@"2222 - %@", [NSThread currentThread]);
}
封装一个GCD定时器
// .h
#import <Foundation/Foundation.h>
@interface MJTimer : 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
// .m
#import "MJTimer.h"
@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 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
iOS程序的内存布局
TagedPointer
1. 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
2. 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
3. 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
* Tag 用来标记类型(NSNumber、NSString、NSDate)
4. 当指针(8字节)不够存储数据时,才会使用动态分配内存的方式来存储数据
5. objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销
6. 如何判断一个指针是否为Tagged Pointer?
* iOS平台,最高有效位是1(第64bit)
* Mac平台,最低有效位是1
判断是否为TagedPointer
// 如果是iOS平台(指针的最高有效位是1,就是Tagged Pointer)
# define _OBJC_TAG_MASK (1UL<<63)
//// 如果是Mac平台(指针的最低有效位是1,就是Tagged Pointer)
//# define _OBJC_TAG_MASK 1UL
BOOL isTaggedPointer(id pointer)
{
return ((uintptr_t)pointer & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
NSNumber *number1 = @4;
NSNumber *number2 = @5;
NSNumber *number3 = @(0xFFFFFFFFFFFFFFF);
NSLog(@"%d %d %d", isTaggedPointer(number1), isTaggedPointer(number2), isTaggedPointer(number3));
NSLog(@"%p %p %p", number1, number2, number3);
TagedPointer面试题
思考以下2段代码能发生什么事?有什么区别?
长字符串(存储到堆)
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"];
// 解锁
});
}
// 崩溃 , 解决办法是加锁
原因: 因为如果字符串足够长,会存储到堆,使用set方法赋值过程如下
//ARC的代码会转换到MRC执行, 因为不同线程同时进入,可能导致[_name release]多次执行,导致崩溃
- (void)setName:(NSString *)name
{
if (_name != name) {
[_name release];
_name = [name retain];
}
}
短字符串(tagPointer机制)
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"];
});
}
// 正常运行
在字符串较短的情况下使用TagedPointer技术,TagedPointer使用指针直接赋值的形式,不通过set方法,不会崩溃
OC对象的内存管理
1. 在iOS中,使用引用计数来管理OC对象的内存
2. 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
3. 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
4. 内存管理的经验总结
* 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
* 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
可以通过以下私有函数来查看自动释放池的情况
// 这是C语言的一个语法格式,可以只这样生命就可以调用方法,可以在控制台中打印出内存的应用
extern void _objc_autoreleasePoolPrint(void);
copy
copy本质是相互之间不影响
深拷贝: 产生新的对象
浅拷贝: 不产生新的对象
可变对象: copy和mutableCopy都是深拷贝
不可变对象: copy是浅拷贝、 mutableCopy是深拷贝
| copy | mutableCopy | |
|---|---|---|
| 可变 | 深拷贝 | 深拷贝 |
| 不可变 | 浅拷贝 | 深拷贝 |
copy 修饰
- (void)setData:(NSArray*)data {
if (_data != data) {
[_data release];
_data = [data copy];
}
}
- (void)dealloc {
self.data = nil;
[super dealloc];
}
@property (copy, nonatomoc) NSMutableArray *data因为是copy修饰,set方法会执行[data copy]生成一个不可变数组,与定义相悖,在使用中容易出错,不要这样定义
copy修饰字符串(NSString),目的是字符串传入可变字符串之后,字符串对象仍然不可改变
延伸: strong 修饰
- (void)setData:(NSArray*)data {
if (_data != data) {
[_data release];
_data = [data retain];
}
}
- (void)dealloc {
self.data = nil;
[super dealloc];
}
引用计数的存储
在64bit中, 引用计数存储在isa共用体里面,如果比较多(超过19位)就存储到sideTable类里面
inline uintptr_t
objc_object::rootRetainCount()
{
// TaggedPointer 不是OC对象,是一个指针,没有引用计数
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = __c11_atomic_load(&isa.bits, __ATOMIC_RELAXED);
// 优化过的isa指针
if (bits.nonpointer) {
uintptr_t rc = bits.extra_rc;
if (bits.has_sidetable_rc) { // 引用计数不只是存储在isa指针中的,而是存储在SideTable中
// 从sidetable中拿出数据,相加
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
weak指针的实现原理
初始化时候runtime调用 id objc_initWeak(id *location, id newObj) 函数
// 1. locationg: 弱指针地址 newObj 弱指针指向对象
id objc_initWeak(id *location, id newObj)
// 2. 存表(注销旧的表,创建新的表),更新指针的指向到新的表
static id storeWeak(id *location, objc_object *newObj)
清除表的细节
inline void objc_object::clearDeallocating()
{
// 判断是不是普通的isa指针
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
sidetable_clearDeallocating();
}
// 不是普通的isa指针(是共用体,走下面这个函数)
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
void
objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
// 从 SideTable 中拿出 弱引用表
SideTable& table = SideTables()[this];
table.lock();
// 是否有被弱引用指向过, 如果被弱引用指向过,需要清除弱引用的表
if (isa.weakly_referenced) {
// 传入弱引用表和当前对象
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
table.refcnts.erase(this);
}
table.unlock();
}
弱引用表也是采用散列表的形式保存的。存放在sideTable(上图)的weak_table中
将弱引用存放到散列表中,在对象销毁时(dealloc),取出弱引用表,将弱引用清除掉
inline void
objc_object::clearDeallocating()
{
..........
clearDeallocating_slow();
..........
}
void
objc_object::clearDeallocating_slow()
{
..............
// 拿到SideTable 中的弱引用表
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
..............
table.unlock();
}
weak_clear_no_lock :根据对象地址,查找到弱引用表中存储的弱引用对象(__weak 修饰的对象),判断对象引用计数为0,将对象设置为nil
MRC
release / autorelease
自动释放池(autorelease)
为了看清本质,首先进行代码转换
@autoreleasepool {
JXPerson *person = [[[JXPerson alloc] init] autorelease];
}
转化CPP代码如下,
{
__AtAutoreleasePool __autoreleasepool;
/// 这里放上执行代码
JXPerson *person = [[[JXPerson alloc] init] autorelease];
}
可以看出有一个 __AtAutoreleasePool 结构体.拿到相关代码可以看出一个构造函数和一个析构函数
struct __AtAutoreleasePool {
void * atautoreleasepoolobj;
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
};
经转化后分析之前的代码,可以得到一个执行顺序
atautoreleasepoolobj = objc_autoreleasePoolPush();
// 业务代码
JXPerson *person = [[[JXPerson alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);
为了弄明白autoreleasePool的本质,就转变为研究
objc_autoreleasePoolPush 和 objc_autoreleasePoolPop 的底层实现。
我们可以在runtime源码中,找到NSobject.mm文件,里面有对应的实现。 通过查询源码我们能发现自动释放池的管理由一个AutoreleasePoolPage进行管理。
总结:
- 自动释放池的主要底层数据结构是:
__AtAutoreleasePool、AutoreleasePoolPage - 调用了
autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的 - 源码分析
clang重写@autoreleasepool- objc4源码:
NSObject.mm
AutoreleasePoolPage的结构
- 每个
AutoreleasePoolPage对象占用4096(0x1000)即16的三次方字节内存,除了拿出56(0x38)个字节用来存放它内部的成员变量 ,剩下的空间用来存放autorelease对象的地址
1.1 AutoreleasePoolPage 代码内有begin()和end()函数.通过代码可以看出begin是当前对象内存开始 + 当前对象内部成员占用的内存大小 ,也就是存储的autorelease对象 开始的地址,而end值得是当前对象的结束地址
id* begin() {
return this+sizeof(*this));
}
id* end() {
// SIZE 是宏定义的 4096
return this+SIZE;
}
- 所有的
AutoreleasePoolPage对象通过双向链表的形式连接在一起。 通过child找寻自己的下一级链表,通过parent找寻自己的上一级链表
关于双向链表 就是如下:
代码中定义:#define POOL_BOUNDARY nil
- 调用
push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址 - 调用
pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY id *next指向了下一个能存放autorelease对象地址的区域,开始什么都没有存放的时候,就指向begin() 即0x1038函数 的下一个位置,后来POOL_BOUNDARY入栈 就指向POOL_BOUNDARY的下一个位置,当第一个person入栈,就指向第一个person的下一个位置
总结: AutoreleasePoolPage本身是一个栈结构,共4096个字节,其中栈底的56个字节存储自己的一些属性,剩下的4040个字节用来存储需要释放的对象(被autorelease修饰的对象),开始push对象前,会先push一个POOL_BOUNDARY (nil的宏定义)并记录这个位置,类似于一个标记,在出栈时候,会从栈顶开始release,遇到POOL_BOUNDARY停止,当存储不够的时候,会采用双向链表的结构,进行多page存储
autoReleasePool嵌套
@autoreleasepool { // r1 = push()
JXPerson *person0 = [[[JXPerson alloc] init] autorelease];
@autoreleasepool {// r2 = push()
JXPerson *person1 = [[[JXPerson alloc] init] autorelease];
JXPerson *person2 = [[[JXPerson alloc] init] autorelease];
@autoreleasepool {// r3 = push()
JXPerson *person3 = [[[JXPerson alloc] init] autorelease];
}// pop(r3)
}// pop(r2)
}// pop(r1)
在内存中存储结构如下
| AutoreleasePoolPage |
|---|
| magic_t magic |
| id* next |
| pthread_t thread |
| AutoreleasePoolPage *parent |
| AutoreleasePoolPage *child |
| uint32_t depth |
| uint32_t hiwat |
POOL_BOUNDARY |
| person0 |
POOL_BOUNDARY |
| person1 |
| person2 |
POOL_BOUNDARY |
| person3 |
之后释放的时候,先从person3开始释放,依次释放person2、person1、person0
可以通过extern 声明void _objc_autoreleasePoolPrint(void);这个内部函数,查看AutoreleasePoolPage 的释放情况
@autoreleasepool { // r1 = push()
//_objc_autoreleasePoolPrint();
MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
// _objc_autoreleasePoolPrint();
@autoreleasepool { // r2 = push()
for (int i = 0; i < 600; i++) {
MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
}
@autoreleasepool { // r3 = push()
MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint();
} // pop(r3)
} // pop(r2)
} // pop(r1)
RunLoop 和 autoRelease
问题: autoRelease 修饰的对象在什么时机会被释放?
iOS 在主线程注册了两个跟autoRelease相关的observer,用来观察runLoop的状态
activities = 0x1(1) kCFRunLoopEntry
监听到 kCFRunLoopEntry 调用 `objc_autoreleasePoolPush()`
<CFRunLoopObserver 0x60000013e8c0 [0x101987c80]> {
... ...
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x101b35d92)
... ...
}
activities = 0xa0(160) kCFRunLoopExit | kCFRunLoopBeforeWaiting(休眠之前)
监听到 kCFRunLoopBeforeWaiting 调用`objc_autoreleasePoolPop()` 再调用 `objc_autoreleasePoolPush()`
监听到 kCFRunLoopExit 调用`objc_autoreleasePoolPop()`
<CFRunLoopObserver 0x60000013e6e0 [0x101987c80]>{
... ...
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x101b35d92)
... ...
}
#import "ViewController.h"
#import "MJPerson.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 这个Person什么时候调用release,是由RunLoop来控制的
// 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release
// MJPerson *person = [[[MJPerson alloc] init] autorelease];
MJPerson *person = [[MJPerson alloc] init];
NSLog(@"%@",[NSRunLoop mainRunLoop]);
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
打印[NSRunLoop mainRunLoop]可以看出里面有两个observer,看activities属性,可以看出执行的顺序,看出对象的释放时机
/*
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 1
kCFRunLoopBeforeTimers = (1UL << 1), 2
kCFRunLoopBeforeSources = (1UL << 2), 4
kCFRunLoopBeforeWaiting = (1UL << 5), 32
kCFRunLoopAfterWaiting = (1UL << 6), 64
kCFRunLoopExit = (1UL << 7), 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
*/
/*
kCFRunLoopEntry push
<CFRunLoopObserver 0x60000013f220 [0x1031c8c80]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x103376df2), context = <CFArray 0x60000025aa00 [0x1031c8c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fd0bf802048>\n)}}
kCFRunLoopBeforeWaiting | kCFRunLoopExit
kCFRunLoopBeforeWaiting pop、push
kCFRunLoopExit pop
<CFRunLoopObserver 0x60000013f0e0 [0x1031c8c80]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x103376df2), context = <CFArray 0x60000025aa00 [0x1031c8c80]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fd0bf802048>\n)}}
*/
iOS在主线程的Runloop中注册了2个Observer
- 第1个
Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush() - 第2个
Observer- 监听了
kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush() - 监听了
kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
- 监听了