一:关于定时器
CADisplayLink、NSTimer,GCD都可以实现定时器的功能,但是需要注意循环引用的问题,需要注意:CADisplayLink、NSTimer都是需要依赖于runLoop来实现,正因为如此,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时
CADisplayLink
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
解决循环引用
使用中间类,运用消息转发机制来进行处理
@interface MJProxy1 : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#import "MJProxy1.h"
@implementation MJProxy1
+ (instancetype)proxyWithTarget:(id)target
{
MJProxy1 *proxy = [[MJProxy1 alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
在控制器找那个这样调用
self.link = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self] selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
NSTimer
下面的方法NSTimer会对target进行一个强引用,如果控制器也声明了一个strong的timer就会造成循环引用的
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:YES];
如果我们声明一个weakSelf的self代入呢?依然无法解决循环引用问题,这种方法只会应用在block内部,这里既是使用weakSelf,只是一个参数,其实找到的依然是self,timer内部可能会有个参数对target进行了强引用。
__weak typeof(self) weakSelf = self;
解决循环引用方案1
其实我们可以用下面👇这个API来创建定时器,在其中使用weakSelf就可以解决循环引用问题
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"定时器,需要执行的内容");
}];
解决循环引用方案2
使用中间类,运用消息转发机制来进行处理
@interface MJProxy1 : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#import "MJProxy1.h"
@implementation MJProxy1
+ (instancetype)proxyWithTarget:(id)target
{
MJProxy1 *proxy = [[MJProxy1 alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
在控制器找那个这样调用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
解决循环引用方案3
使用NSProxy,专门用来做消息转发
@interface MJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
#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
控制器中调用(注意:CADisplayLink也可以使用)
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
GCD定时器
注意:
- GCD不需要我们去进行销毁
- 可以在子线程调用
GCD定时器封装
#import <Foundation/Foundation.h>
@interface WYTimer : 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;
#import "WYTimer.h"
@implementation WYTimer
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
控制器中调用
// 接口设计
self.task = [MJTimer execTask:self
selector:@selector(doTask)
start:2.0
interval:1.0
repeats:YES
async:NO];
iOS内存布局
Tagged Pointer
-
从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
-
在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
-
使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
-
当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
-
objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销
如何判断一个指针是否为Tagged Pointer?
iOS平台,最高有效位是1(第64bit)
Mac平台,最低有效位是1
面试题
下面的两段代码运行结果?
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"];
});
}
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"];
});
}
第一段崩溃,坏内存访问
第二段没有问题
原因:本质上self.name = [NSString stringWithFormat:@"abc"]这段代码会调用setName的方法,而此时会有多条线程执行这段代码,在调用release的时候,重复释放,导致坏内存访问。
- (void)setName:(NSString *)name
{
if (_name != name) {
[_name release];
_name = [name retain];
}
}
解决方案:可以在self.name = [NSString stringWithFormat:@"abc"]进行加锁解锁的操作
第二段没有问题的原因:[NSString stringWithFormat:@"abc"]使用的是Tagged Pointer,将abc直接存储在指针当中,所以就不会调用setName的方法,自然不会出现问题。
OC对象的内存管理
1.在iOS中,使用引用计数来管理OC对象的内存
2.一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
3.调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理的经验总结
当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void)
copy和mutableCopy
关于自定义对象的Copy
自定义对象要想实现copy的拷贝功能,需要遵循NSCopying协议,并实现copyWithZone方法
-(id)copyWithZone:(NSZone *)zone
{
}
引用计数器
在64bit中,引用计数可以直接存储在优化过的isa指针中,存储在位域中的extra_rc,也可能存储在SideTable类中。
refcnts是一个存放着对象引用计数的散列表
weak的实现原理
weak和unsafe_unretain的区别?
两者都不会有强引用,但是weak所指的对象被销毁之后会被自动置为nil,而unsafe_unretain不会清空对象。
注意: ARC是结合了LLVM + Runtime来实现代码的自动插入
自动释放池原理
1.自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage
2.调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
其实在自动释放池内部会先调用objc_autoreleasePoolPush来创建池,调用objc_autoreleasePoolPop来销毁池
@autoreleasepool {
// atautoreleasepoolobj = objc_autoreleasePoolPush();
MJPerson *person = [[[MJPerson alloc] init] autorelease];
// objc_autoreleasePoolPop(atautoreleasepoolobj);
}
AutoreleasePoolPage结构
1.每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放。
2.所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
1.调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
2.调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
3.id *next指向了下一个能存放autorelease对象地址的区域
autorelease对象在什么时机会被调用release
对于下面这种情况,就是在大括号结束的时候,page调用objc_autoreleasePoolPop的时候将对象进行释放
@autoreleasepool {
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
如果是下面这种情况,大括号结束的时候并不能销毁对象。
- (void)viewDidLoad
{
[super viewDidLoad];
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
原因在于:
这个对象的释放是由Runloop来控制的,它可能在Runloop循环中,Runloop休眠的时候调用realease,前提上述代码处在MRC环境下,如果是ARC下,会立马释放,ARC插入了realease