一.定时器
1.CADisplayLink、NSTimer
CADisplayLink、NSTimer 会对 target 产生强引用,如果 target 又对它们产生强引用,那么就会引发循环引用。
解决方案
方式一:使用 block 的形式触发定时器
方式二:使用继承自 NSObject 的中间对象
@interface MJProxy1 : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation MJProxy1
+ (instancetype)proxyWithTarget:(id)target {
MJProxy1 *proxy = [[MJProxy1 alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
方式三:使用继承自 NSProxy 的中间对象
@interface MJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@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 与 NSObject
NSProxy 做代理,比 NSObject 的优势
- NSProxy 不会去父类搜索方法,会直接走消息转发流程,效率更高。
- NSProxy 没有 - (id)forwardingTargetForSelector:(SEL)aSelector 方法
另外,NSObject 是普通的 OC 对象,而 NSProxy 调用 isKindOfClass 会匹配其代理类型,如下图
查看 NSProxy 的 isKindOfClass 如下:
内部也走的消息转发。
2.GCD Timer
NSTimer 依赖于 RunLoop,如果 RunLoop 的任务过于繁重,可能会导致 NSTimer 不准时。而GCD 的定时器会更加准时。
demo
注:GCD Timer 无需手动释放内存
Timer 封装
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_);
}
二.内存布局
1.内存布局
验证:高地址和低地址
int a = 10;
int b;
int main(int argc, char * argv[]) {
@autoreleasepool {
static int c = 20;
static int d;
int e;
int f = 20;
NSString *str = @"123";
NSObject *obj = [[NSObject alloc] init];
NSLog(@"\n&a=%p\n&b=%p\n&c=%p\n&d=%p\n&e=%p\n&f=%p\nstr=%p\nobj=%p\n",
&a, &b, &c, &d, &e, &f, str, obj);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
/*
字符串常量
str=0x10dfa0068
已初始化的全局变量、静态变量
&a =0x10dfa0db8
&c =0x10dfa0dbc
未初始化的全局变量、静态变量
&d =0x10dfa0e80
&b =0x10dfa0e84
堆
obj=0x608000012210
栈
&f =0x7ffee1c60fe0
&e =0x7ffee1c60fe4
*/
三.OC 对象的内存管理
1. 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
判断您是否为 Tagged Pointer
一个问题
打印两个字符串的内存地址,发现第一个长字符串在堆空间。第二个短字符串,直接将字符串的内容存在指针中,触发了 Tagged Pointer
打印类型也可以发现是 NSTaggedPointerString 类型
第一个直接奔溃,多线程异步对 name 赋值,会同时进入到 setter 方法,这时有可能多次调用 release 操作,造成坏内存访问。
第二个字符串"abc" 属于 Tagged Pointer,所以它不是一个 OC 对象,不会调用 setter 方法,会直接提取内存中的字符串值进行赋值。
小技巧:判断一个对象是否是 OC 对象,看二进制的最后一位,如果不是0,则不是 OC 对象。(堆空间地址内存对齐,最后一位一定是0)
2. MRC
在iOS中,使用引用计数来管理OC对象的内存
一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理的经验总结
- 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
- 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
可以通过以下私有函数来查看自动释放池的情况
- extern void _objc_autoreleasePoolPrint(void);
setter 方法的内存管理
- 对象属性的 setter 方法中,要进行一次 retain 或 copy
- 属性重新赋值时,要对之前的对象进行 release,在对新对象进行 retain
- 属性重新赋值时,要判断赋值的对象和之前的对象是否一样,不一样时才会 release
- 基本数据类型不需要进行内存管理
retain setter
- (MJDog *)dog
{
return _dog;
}
- (void)setDog:(MJDog *)dog
{
if (_dog != dog) {
[_dog release];
_dog = [dog retain];
}
}
基本数据类型不进行内存管理(assign)
- (void)setAge:(int)age
{
_age = age;
}
- (int)age
{
return _age;
}
copy setter
- (void)setData:(NSArray *)data
{
if (_data != data) {
[_data release];
_data = [data copy];
}
}
dealloc 内存管理
- dealloc 中对对象属性进行一次 release
- dealloc 方法中要调用 super
- MRC 下设置 @property 会自动生成 setter 和 getter,但 dealloc 中仍然需要进行内存管理
- 保持计数器的平衡,有+1就要有-1
- (void)dealloc
{
[_dog release];
_dog = nil;
// 父类的dealloc放到最后
[super dealloc];
}
或者
- (void)dealloc
{
self.dog = nil;
// 父类的dealloc放到最后
[super dealloc];
}
使用 autorelease
- alloc 一个对象就需要进行一次 release,所以使用 autorelease 可以简化代码,让编译器自动去 release
- 很多对象的类方法可以实现自动调用 autorelease
下面几种方式都等价
NSMutableArray *data = [[NSMutableArray alloc] init];
self.data = data;
[data release];
self.data = [[NSMutableArray alloc] init];
[self.data release]; self.data = [NSMutableArray array];
self.data = [[[NSMutableArray alloc] init] autorelease];
self.data = [NSMutableArray array];
3. copy 和 mutableCopy
拷贝的目的:产生一个副本对象,跟源对象互不影响
- 修改了源对象,不会影响副本对象
- 修改了副本对象,不会影响源对象
iOS提供了2个拷贝方法
- copy,不可变拷贝,产生不可变副本
- mutableCopy,可变拷贝,产生可变副本
深拷贝和浅拷贝
- 深拷贝:内容拷贝,产生新的对象
- 浅拷贝:指针拷贝,没有产生新的对象
NSString copy
void test2()
{
NSString *str1 = [[NSString alloc] initWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 浅拷贝,指针拷贝,没有产生新对象,返回的是NSString
NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝,内容拷贝,有产生新对象,返回的是NSMutableString
NSLog(@"%@ %@ %@", str1, str2, str3);
NSLog(@"%p %p %p", str1, str2, str3);
}
NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 深拷贝
NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝
copy 和 mutablecopy 的内存管理
tagged pointer NSString 的内存管理
tagged pointer 的 NSString 不使用引用计数管理,retainCount 的值为 -1.
4. 引用计数的存储
在64bit中,引用计数可以直接存储在优化过的isa指针中
extra_rc
- 里面存储的值是引用计数器减1
has_sidetable_rc
- 引用计数器是否过大无法存储在isa中
- 如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
也可能存储在SideTable类中.
refcnts是一个存放着对象引用计数的散列表
retain 源码查看
SideTable 源码
5. dealloc 源码(weak 的原理)
当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是
- dealloc
- _objc_rootDealloc
- rootDealloc
- object_dispose
- objc_destructInstance、free
ARC是LLVM编译器和Runtime系统相互协作的一个结果,在编译器插入内存管理相关的代码,在运行时处理 weak 弱引用。
四.autorelease pool
1.介绍
调用 autorelease 方法的对象,会在 @autorelease {} 大括号结束时,自动调用 release 操作。
@autorelease {} 允许嵌套。
自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage
调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
2. __AtAutoreleasePool 结构
clang重写@autoreleasepool,重写以下代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
return 0;
}
得到 c++ 代码
{
__AtAutoreleasePool __autoreleasepool;
MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
}
__AtAutoreleasePool 是一个结构体,结构如下
struct __AtAutoreleasePool {
__AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
最终的代码相当于
// __AtAutoreleasePool 构造
atautoreleasepoolobj = objc_autoreleasePoolPush();
MJPerson *person = [[[MJPerson alloc] init] autorelease];
// __AtAutoreleasePool 析构
objc_autoreleasePoolPop(atautoreleasepoolobj);
objc_autoreleasePoolPush 方法会将一个 POOL_BOUNDARY 入栈,并且返回其存放的内存地址,对象调用 autorelease 方法,会将改对象的地址入栈,当调用 objc_autoreleasePoolPop 时,会将所有对象进行一次 release,直到 POOL_BOUNDARY 标记处
3.AutoreleasePoolPage的结构
每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址。
所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
id *next指向了下一个能存放autorelease对象地址的区域
另外可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);
PAGE hot 表示当前的 page,cold 表示已经存满对象的 page
4.autorelease 和 runloop
结论
iOS在主线程的Runloop中注册了2个Observer
第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
第2个Observer
监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
第3个监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
由此可知,在每次 waiting 时,进行一次 autorelease 对象的清理。
挖掘过程
查看主线程的 runloop observer,注册了响应的 observer 进行 autorelease
举例说明
viewDidLoad 中创建的局部变量,会在 viewWillAppear 和 viewDidAppear 之间进行释放,也就是当前的那一次 runloop 循环的休眠之前至今 release
问题:方法中的局部变量会方法结束立即释放吗
如果是 autorelease 对象,不会立即释放,否则会立即释放
[[NSObject] alloc] init] // 立即释放
[NSObject objectWithCCC] // 不立即释放(类方法一般生成 autorelease 对象)