-
一、抛出一个问题:使用
CADisplayLink
、NSTimer
有什么注意点?1.1-1.6的demo-
1.1、分析:
CADisplayLink
、NSTimer
会对target
产生强引用,如果target又对它们产生强引用,那么就会引发循环引用,如下在控制器里面的代码会产生 相互强引用 的问题-
CADisplayLink(在当前控制器按返回按钮,你会发现 dealloc 方法不会走,而linkTest还在一直调用,原因是:self强引用CADisplayLink,而CADisplayLink内部又在强引用self(
displayLinkWithTarget:self
))。@property(nonatomic,strong) CADisplayLink *link; // 保证调用频率和屏幕的刷帧频率一致 60FPS self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)]; [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; -(void)linkTest{ NSLog(@"%s",__func__); } -(void)dealloc{ NSLog(@"%s", __func__); [self.link invalidate]; }
-
NSTimer(在当前控制器按返回按钮,你会发现 dealloc 方法不会走,而timerTest还在一直调用,原因是:self强引用NSTimer,而NSTimer内部又在强引用self(target:self ))。
@property (strong, nonatomic) NSTimer *timer; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES]; - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; }
-
-
1.2、解决上面互相强引用的办法
-
NSTimer 有一个block的方法,我们可以利用block的弱指针来解决
__weak typeof(self) weakSelf = self;
,传weakSelf
进去,如下@property (strong, nonatomic) NSTimer *timer; __weak typeof(self) weakSelf = self; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { [weakSelf timerTest]; }]; - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; }
-
-
1.3、通过中间对象(代理对象)的方式来解决,下面用到了消息转发机制(会先发送消息、再动态解析、最后再消息转发)
下面是创建了一个继承于类 :
JKMiddleProxy : NSObject
#import
NS_ASSUME_NONNULL_BEGIN @interface JKMiddleProxy : NSObject + (instancetype)proxyWithTarget:(id)target; @property (weak, nonatomic) id target; @end NS_ASSUME_NONNULL_END #import "JKMiddleProxy.h" @implementation JKMiddleProxy + (instancetype)proxyWithTarget:(id)target { JKMiddleProxy *proxy = [[JKMiddleProxy alloc] init]; proxy.target = target; return proxy; } // 消息转发机制(会先发送消息、再动态解析、最后再消息转发) - (id)forwardingTargetForSelector:(SEL)aSelector { return self.target; } @end - 使用如下(不管是CADisplayLink还是NSTimer,把self换为中间对象
[JKMiddleProxy proxyWithTarget:self]
就好)
#import "JKMiddleProxy.h" @property (strong, nonatomic) NSTimer *timer; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[JKMiddleProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES]; - (void)timerTest { NSLog(@"%s", __func__); } - (void)dealloc { NSLog(@"%s", __func__); [self.timer invalidate]; }
- 使用如下(不管是CADisplayLink还是NSTimer,把self换为中间对象
-
1.4、效率更加高的中间对象(不需要进行发送消息和再动态解析,直接进行消息转发),利用
NSProxy
可以略过 发送消息和动态解析。-
下面是创建了一个继承于类 :JKProxy : NSProxy
#import
NS_ASSUME_NONNULL_BEGIN @interface JKProxy : NSProxy + (instancetype)proxyWithTarget:(id)target; @property (weak, nonatomic) id target; @end NS_ASSUME_NONNULL_END #import "JKProxy.h" @implementation JKProxy + (instancetype)proxyWithTarget:(id)target { // NSProxy对象不需要调用init,因为它本来就没有init方法 JKProxy *proxy = [JKProxy alloc]; proxy.target = target; return proxy; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [self.target methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { [invocation invokeWithTarget:self.target]; } @end 使用和上面1.3一样,直接(
[JKProxy proxyWithTarget:self]
)self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[JKProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
-
-
1.5、看下面的打印结果
1 和 0
(原因是JKProxy继承于 NSProxy,在调用isKindOfClass的时候直接走的消息转发(- forwardInvocation),会转换成ViewController的调用isKindOfClass,而JKMiddleProxy继承于NSObject,不会进入forwardInvocation进而invokeWithTarget)JKProxy *proxy1 = [JKProxy proxyWithTarget:vc]; JKMiddleProxy *proxy2 = [JKMiddleProxy proxyWithTarget:vc]; NSLog(@"%d %d", [proxy1 isKindOfClass:[ViewController class]], [proxy2 isKindOfClass:[ViewController class]]);
-
-
二、GCD定时器:比较准时,它直接和系统内核挂钩的(NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时)
-
2.1、使用如下:(可以看demo里面的
GCDTimerViewController
有具体的源码)// 定义GCD定时器对象 dispatch_source_t @property(nonatomic,strong) dispatch_source_t gcdTimer; // 创建队列 dispatch_queue_t queue = dispatch_get_main_queue(); // 创建定时器 self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); // 设置时间 /* dispatch_source_t _Nonnull source: 定时器 dispatch_time_t start: 开始的时间,dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),start多长时间后开始,NSEC_PER_SEC(纳秒) uint64_t interval:时间间隔 uint64_t leeway: 误差,写0就好 */ uint64_t start = 2.0; uint64_t interval = 1.0; dispatch_source_set_timer(self.gcdTimer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC,0); // 设置回调 static int count = 0; dispatch_source_set_event_handler(self.gcdTimer, ^{ count ++; NSLog(@"count== %d",count); }); // 启动定时器 dispatch_resume(self.gcdTimer);
-
2.2、如果上面的想在子线程执行的话,我们可以自己创建队列(下面是一个串行队列)
// DISPATCH_QUEUE_SERIAL 串行 // DISPATCH_QUEUE_CONCURRENT 并行 dispatch_queue_t queue = dispatch_queue_create("timer", DISPATCH_QUEUE_SERIAL);
在 2.1 里面回调是用的block,咱们还可以用函数,把
dispatch_source_set_event_handler
换为dispatch_source_set_event_handler_f
dispatch_source_set_event_handler_f(self.gcdTimer, timerFire); void timerFire(void *param) { NSLog(@"定时器打印 - %@", [NSThread currentThread]); }
-
2.3、对上面GCD定时器的一个封装 JKGCDTimer自己下载,下面展示一下使用
-
第1种使用方式(block返回执行的任务)
// 导入,这个是封装的类名 #import "JKGCDTimer.h" @property(nonatomic,strong) NSString *gcdTimerKeyName; // 第1种使用方式(Block里面做task) static int number = 0; /** task 定时器开启后执行的任务 startTime 多长时间后开启任务 intervalTime 时间间隔 repeats 是否重复执行任务 YES: 重复 NO: 执行一次 async 同步还是异步执行任务 YES:async(全局并发队列) NO: sync(主队列) */ self.gcdTimerKeyName = [JKGCDTimer execTask:^{ number ++; NSLog(@"number==%d-------%@",number,[NSThread currentThread]); } startTime:2.0 intervalTime:1.0 repeats:YES async:YES];
-
第2种使用方式(在自己的控制器里面的方法 实现任务)
// 导入,这个是封装的类名 #import "JKGCDTimer.h" @property(nonatomic,strong) NSString *gcdTimerKeyName; /** target 自己VC的 self selector 自己VC里面的 方法 startTime 多长时间后开启任务 intervalTime 时间间隔 repeats 是否重复执行任务 YES: 重复 NO: 执行一次 async 同步还是异步执行任务 YES:async(全局并发队列) NO: sync(主队列) */ self.gcdTimerKeyName = [JKGCDTimer execTaskTarget:self selector:@selector(timerExecTask) startTime:2.0 intervalTime:1.0 repeats:YES async:YES]; #pragma mark 采用自己控制器执行任务的方法 -(void)timerExecTask{ static int number = 0; number ++; NSLog(@"number==%d-------%@",number,[NSThread currentThread]); }
-
-
-
三、iOS 程序的内存布局
-
3.1、先用一个图展示
代码段:编译之后的代码
-
数据段
- 字符串常量:比如NSString *str = @"456"
- 已初始化数据:已初始化的全局变量、静态变量等,比如:
static int c = 20;
- 未初始化数据:未初始化的全局变量、静态变量等,比如:
static int d;
堆:通过alloc、malloc、calloc等动态分配的空间, 分配的内存空间地址越来越大 ,如:
NSObject *obj = [[NSObject alloc] init];
栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小,如:
int e; int f = 20;
-
3.2、Tagged Pointer (推荐博客一 、推荐博客二、推荐博客三),这是一个苹果对内存做的优化技术,将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。
-
(1)、从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
从上面可以看出 str1存的@"123"
是比较小的,内存地址最后一位 是 9,转化为 二进制是:1001
,最后一位是 1,而 str2存的@"fffffffffffff"
比较大,Tagged Pointer不能再存,只能放到堆区
,从上面的打印可以看出其内存地址的最后一位是 0。 (2)、在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
(3)、使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
(4)、当指针不够存储数据时,才会使用动态分配内存的方式来存储数据(
如上面的str2
)(5)、objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销,如下:
-
(6)、那怎么判断一个指针是不是 Tagged Pointer 呢?可以通过 objc 源码看到对应的判断方法如下:
static inline bool _objc_isTaggedPointer(const void *ptr) { return ((intptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK; } #if OBJC_MSB_TAGGED_POINTERS # define _OBJC_TAG_MASK (1ULL<<63) #else # define _OBJC_TAG_MASK 1 #endif #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
iOS平台,最高有效位是1(第64bit)
Mac平台,最低有效位是1
看下面的例子:NSString *str1 = [NSString stringWithFormat:@"%@",@"abc"]; NSString *str2 = [NSString stringWithFormat:@"%@",@"ffffffffffffffffffff"]; NSLog(@"%p %p %@ %@",str1,str2,[str1 class],[str2 class]); 打印结果为: 0xad16dee4304feb33 0x6000005dd470 NSTaggedPointerString __NSCFString
分析: str1 的内存地址是:0xad16dee4304feb33,最左边a在十六进制里面是 10,转化为二进制是 1010,可以看到是最高有效位是: 1;而str2的内存地址是0x6000005dd470 ,结尾是0,就能确定在堆区。
-
-
-
3.3、思考以下2段代码能发生什么事?有什么区别?
-
第 1 段代码
@property (strong, nonatomic) NSString *name; 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"]; }); }
-
第 2 段代码(崩溃,坏内存访问)
@property (strong, nonatomic) NSString *name; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); for (int i = 0; i < 1000; i++) { dispatch_async(queue, ^{ self.name = [NSString stringWithFormat:@"fffffffffffffffffffffffff"]; }); }
答:第 2 段代码会坏内存访问,原因是:第2段代码在给 self.name赋值会走下面的方法,由于 第2段代码是 异步并行的会多个线程调用
- (void)setName:(NSString *)name
, _name释放[_name release]
两次,从而造成坏内存访问;然而第1段代码[NSString stringWithFormat:@"abc"]
就不是一个OC对象,仅仅是一个Tagged Pointer
中存储的数据,把指针变量的值取出来给成员变量self.name
而已。解决第2段代码崩溃的办法在self.name = [NSString stringWithFormat:@"fffffffffffffffffffffffff"]; });
上下加锁和解锁就好了,来防止两次release
。// set方法的本质 - (void)setName:(NSString *)name { if (_name != name) { [_name release]; _name = [name retain]; } } // set方法在ARC下表面的现象 - (void)setName:(NSString *)name { _name =name }
-
-
四、copy 与 mutableCopy
-
4.1、拷贝的目的
- 产生一个副本对象,跟源对象互不影响
- 修改了源对象,不会影响副本对象
- 修改了副本对象,不会影响源对象
-
4.2、iOS 提供了2个拷贝方法
copy
与mutableCopy
- copy,不可变拷贝,产生不可变副本
- mutableCopy,可变拷贝,产生可变副本
-
4.3、深拷贝和浅拷贝
- 深拷贝:内容拷贝,产生新的对象
- 浅拷贝:指针拷贝,没有产生新的对象
-
4.4.以字符串为例举例
-
原字符串是不可变的(三个字符串的内存地址一样)
NSString *str1 = [NSString stringWithFormat:@"123"]; // 浅拷贝:指针拷贝,同一块内存地址 NSString *str2 = [str1 copy]; // 深拷贝,对象拷贝,生成新的内存地址 NSMutableString *str3 = [str1 mutableCopy]; NSLog(@"%p %p %p",str1,str2,str3); 打印结果:0xcd37ac23abfbc18c 0xcd37ac23abfbc18c 0x600000910840
-
原字符串是可变的(三个字符串的内存地址不一样)
NSMutableString *str1 = [NSMutableString stringWithFormat:@"123"]; // 深拷贝,对象拷贝,生成新的内存地址 NSString *str2 = [str1 copy]; // 深拷贝,对象拷贝,生成新的内存地址 NSMutableString *str3 = [str1 mutableCopy]; NSLog(@"%p %p %p",str1,str2,str3); 打印结果:0x6000038c9470 0xdae0103e19bd5106 0x6000038c9140
-
-
4.5、copy和mutableCopy的总结图
-
4.6、自定义一个类的copy方法
-
自定义的类(JKStudent)遵守
协议@property (strong, nonatomic) int number;
-
实现
- (id)copyWithZone:(NSZone *)zone
方法- (id)copyWithZone:(NSZone *)zone { JKStudent *student = [[JKStudent allocWithZone:zone] init]; student.number = self. number; return student; }
-
-
-
五、引用计数的存储在哪里?
-
在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中
- extra_rc : 里面存储的值是引用计数器减1
- has_sidetable_rc: 引用计数器是否过大无法存储在isa中; 如果为1,那么引用计数器会存储在一个叫sideTable的类的属性中,refcnts是一个存放着对象引用计数的散列表
-
-
六、 看两个面试题
6.1、weak指针的实现原理?
答:将那些弱引用存在一个哈希表里面,到时候这个对象要销毁,它就会取出当前对象对应的弱引用表,把若引用表里面存储的若引用都给清除掉。-
6.2、__weak与__unsafe_unretained的区别?
-
定义一个 JKString 类继承于 NSObject,
__strong JKString *string1; __weak JKString *string2; // 不安全的,当JKString对象销毁的时候,string3不会被赋空,会产生野指针的情况 __unsafe_unretained JKString *string3; NSLog(@"begin"); { JKString *string = [[JKString alloc]init]; string3 = string; } NSLog(@"%@",string3);
答: __weak与__unsafe_unretained共同点是:都不会产生强引用,__weak更加安全,当__weak指向的对象销毁的时候,这个指针的值被清空(nil),防止野指针的错误。而__unsafe_unretained指向的对象销毁的时候,这个指针的值不会被清空,会产生野指针的错误
-
七、ARC都帮我们做了什么?
答:ARC是LLVM编译器和Runtime系统相互协作的一个结果,具体是利用编译器给我们生成内存管理相关的代码,然后在程序运行的过程中又帮我们处理弱引用这种操作。-
八、autorelease自动释放池
- 8.1、自动释放池的主要底层数据结构是
__AtAutoreleasePool
、AutoreleasePoolPage
- 8.2、调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
-
8.3、objc4源码:NSObject.mm
- 8.4、AutoreleasePoolPage的结构
- 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
- 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
- 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
- 调用pop方法时传入一个POOL_BOUNDARY(
boundary 美[ˈbaʊndəri, -dri] 分界线
)的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY -
id *next
指向了下一个能存放autorelease对象地址的区域
- 8.5、Runloop和Autorelease
- iOS在主线程的Runloop中注册了2个Observer
- 第1个Observer
- 监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
- 第2个Observer
- 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
- 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
- 第1个Observer
- iOS在主线程的Runloop中注册了2个Observer
- 8.1、自动释放池的主要底层数据结构是
-
九、方法里面有局部变量,出了方法后会立即被释放吗?
- ARC与MRC的切换:
Build Settings
搜索automatic re
答:因下面演示的需要大家可以建一个类JKString类,解释如下:
-
(1)、如果这个局部对象最终是通过autorelease的形式(MRC)来去释放的话,就意味着它不是马上释放,而是等它那次所处的RunLoop休眠之前就会进行相应的release操作;
-
(2)、如果ARC生成的是release代码的话, 确实局部变量是立马就会释放。
- ARC与MRC的切换:
-
十、OC对象的内存管理(下面是结论)
10.1、在iOS中,使用引用计数来管理OC对象的内存
10.2、一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
10.3、调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
-
10.4、内存管理的经验总结
- 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
- 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
-
10.5、可以通过以下私有函数来查看自动释放池的情况(声明一下C的函数,程序会自动寻找该函数,extern),在MRC下测试,多在
@autoreleasepool { }
写对象测试调用下面的函数extern void _objc_autoreleasePoolPrint(void); @autoreleasepool { JKString *string1 = [[[JKString alloc]init] autorelease]; _objc_autoreleasePoolPrint(); @autoreleasepool { JKString *string3 = [[[JKString alloc]init] autorelease]; @autoreleasepool { JKString *string4 = [[[JKString alloc]init] autorelease]; } } }