iOS底层原理之内存管理
文章目录
- 定时器
- CADisplayLink、NSTimer
- GCD定时器
- 内存管理
- iOS程序的内存布局
- Tagged Pointer
- OC对象的内存管理
- 拷贝
- 引用计数的存储
- dealloc
- 自动释放池
- 面试题
定时器
CADisplayLink、NSTimer
CADisplayLink
、NSTimer
会对target
产生强引用,如果target
又对它们产生强引用,那么就会引发循环引用。- 示例:
#import "ViewController.h"
@interface ViewController ()
@property(strong, nonatomic)CADisplayLink * link;
@property(strong, nonatomic)NSTimer * timer;
@end
@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];[self linkTest];[self timerTest];
}
//CADisplayLink
- (void)linkTest
{// __weak typeof(self)weakSelf = self;//这会产生循环引用,由于是内部造成的循环引用(不是block),就是有__weak修饰也是不起效果的.// CADisplayLink保证调用频率和屏幕的刷帧频率一致,大概为60FPSself.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(test)];[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)timerTest
{//如果是在block中执行任务,是可以通过__weak来解决循环引用的.
// __weak typeof(self)weakSelf = self;
// self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {// [weakSelf test];
// }];//下面NSTimer内部会对target造成循环引用self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
}- (void)test
{static int a = 0;a++;NSLog(@"%d",a);
}
-(void)dealloc
{ NSLog(@"%s",__func__);//取消定时器[self.link invalidate];[self.timer invalidate];
}
@end
结果及分析
:上面控制器对其属性link
和timer
是强应用,link
和timer
内部又对其target
也就是控制器强引用,这就造成了循环引用(并不是block,所以用__weak修饰self是没用的),当退出控制器时,定时器任然工作,并且控制器也无法销毁。
循环引用示意图:
- 解决办法:
- 如果是
NSTimer
可以用block方法实现:
__weak typeof(self)weakSelf = self;self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {[weakSelf test];}];
- 但是
CADisplayLink
是没有block
方法实现的,这是我们可以用一个中间的代理对象,定时器强引用代理,代理弱引用控制器,控制器强引用定时器;当控制器退出时就没有了强引用就销毁了,从而解除了循环应用。
代理模式示意图:
- 那么如何设计这个代理呢?虽然定时器可以定时的让代理器调用器自己的方法,但这种模式使用不方法,每次不同的方法调用都要添加新方法;所以可以用消息转发机制,调用谁的方法就把消息转发给谁。
设计代理
#import <Foundation/Foundation.h>
@interface HJobjProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end#import "HJobjProxy.h"
@implementation HJobjProxy
+(instancetype)proxyWithTarget:(id)target{HJobjProxy * proxy = [[HJobjProxy alloc]init];proxy.target = target;return proxy;
}
//消息转发(返回self.target 是说明aSelector方法由self.target去调用)
- (id)forwardingTargetForSelector:(SEL)aSelector{return self.target;
}
@end
代理使用
//部分关键daim
- (void)linkTest
{HJobjProxy * proxy = [HJobjProxy proxyWithTarget:self];self.link = [CADisplayLink displayLinkWithTarget:proxy selector:@selector(test)];[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)timerTest
{HJobjProxy * proxy = [HJobjProxy proxyWithTarget:self];self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:proxy selector:@selector(test) userInfo:nil repeats:YES];}
这样就可以完美的解决定时器循环引用的问题了。
- 其实系统有个
NSProxy
是专门做这种代理用的,可以继承NSProxy
来分装一个代理。
#import <Foundation/Foundation.h>
@interface HJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end#import "HJProxy.h"
@implementation HJProxy
+(instancetype)proxyWithTarget:(id)target{// NSProxy对象不需要调用init,因为它本来就没有init方法,它是一个基类,根NSObject一样没有父类,都遵守NSObject协议HJProxy * proxy = [HJProxy alloc];proxy.target = target;return proxy;
}
//NSProxy 并没有forwardingTargetForSelector这个方法
//方法签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel{return [self.target methodSignatureForSelector:sel];
}
//转发调用
- (void)forwardInvocation:(NSInvocation *)invocation{[invocation invokeWithTarget:self.target];
}
@end
这里需要注意的是:NSProxy
并没有init
方法,也没有forwardingTargetForSelector
方法;这里面要完成消息转发需要调用两个方法,比上面更麻烦一点,但为啥还要这么做呢?这是因为第一种代理方法是需要执行消息发送
,动态方法解析
,消息转发
三大流程才能完成消息转发,而下面这种方法,直接完成了消息转发
,效率更高。
GCD定时器
NSTimer
除了可能产生循环引用的情况,还可能产生定时不准的情况,因为NSTimer
依赖于RunLoop
,如果RunLoop
的任务过于繁重,可能会导致NSTimer不准时。而GCD的定时器会更加准时,因为GCD定时器不依赖于RunLoop
,系统自动触发,系统级别的源。- GCD定时器的使用:
//创建定时器static dispatch_source_t timer;timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());//设置时间,开始时间和时间间隔都是以纳秒为单位uint64_t start = 2.0; // 2秒后开始执行uint64_t interval = 1.0; // 每隔1秒执行dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, start * NSEC_PER_SEC, interval * NSEC_PER_SEC);//设置回调dispatch_source_set_event_handler(timer, ^{static int a = 0;a++;NSLog(@"%d秒",a);});//启动定时器dispatch_resume(timer);//取消定时器任务dispatch_source_cancel(timer);
注意
:
- timer必须是
全局变量或者静态变量
,否则回调不执行 - dispatch_source_set_timer 中第二个参数,当我们使用 dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用 dispatch_walltime 可以让计时器按照真实时间间隔进行计时
- dispatch_source_set_timer 的第四个参数 leeway 指的是一个期望的容忍时间,将它设置为 1 秒,意味着系统有可能在定时器时间到达的前 1 秒或者后 1 秒才真正触发定时器。在调用时推荐设置一个合理的 leeway 值。需要注意,就算指定 leeway 值为 0,系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。
- 分装GCD定时工具类:
#import <Foundation/Foundation.h>
@interface HJTimer : NSObject
/*返回定时器唯一标识,标识和定时器一一对应,存放在全局字典中,标识是key,定时器是valuetask:任务回调start:开始时间interval:定时间隔repeats:是否重复async:是否同步*/
+ (NSString *)IdentifierWithTimerStart:(NSTimeInterval)startinterval:(NSTimeInterval)intervalrepeats:(BOOL)repeatsasync:(BOOL)asyncexecTask:(void(^)(void))task;+ (NSString *)execTask:(id)targetselector:(SEL)selectorstart:(NSTimeInterval)startinterval:(NSTimeInterval)intervalrepeats:(BOOL)repeatsasync:(BOOL)async;
//根据定时器唯一标识取消任务
+ (void)cancelTask:(NSString *)identifier;
@end/*********实现文件*********/#import "HJTimer.h"
@implementation HJTimer
static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;
+ (void)initialize
{static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{//初始化一次timers_ = [NSMutableDictionary dictionary];//设置信号量为1保证线程安全semaphore_ = dispatch_semaphore_create(1);});
}+ (NSString *)IdentifierWithTimerStart:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async execTask:(void(^)(void))task
{ //校验避免一些输入错误,如果设置不重复的话这里的时间间隔就是无效的可以鼠标输入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];NSLog(@"name%",name);// 存放到字典中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 IdentifierWithTimerStart:(NSTimeInterval)start interval:interval repeats:repeats async:async execTask:^{if ([target respondsToSelector:selector]) {//去掉警告⚠️
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"[target performSelector:selector];
#pragma clang diagnostic pop}}];
}
//取消任务
+ (void)cancelTask:(NSString *)identifier
{if (identifier.length == 0) return;dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);dispatch_source_t timer = timers_[identifier];if (timer) {//取消任务dispatch_source_cancel(timer);//移除定时器[timers_ removeObjectForKey:identifier];}dispatch_semaphore_signal(semaphore_);
}
@end
调用
:
- (void)viewDidLoad {[super viewDidLoad];
// [self linkTest];
// [self timerTest];
// [GCDTimer test];self.timer1 = [HJTimer IdentifierWithTimerStart:1 interval:1 repeats:YES async:YES execTask:^{static int a = 0;a++;NSLog(@"A:%d--%@",a,[NSThread currentThread]);}];self.timer2 = [HJTimer execTask:self selector:@selector(test) start:1 interval:1 repeats:YES async:NO];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{[HJTimer cancelTask:self.timer1];[HJTimer cancelTask:self.timer2];
}
封装后的GCD定时器使用非常简单方便,不会引起循环引用问题,并且定时也非常准确。
内存管理
iOS程序的内存布局
- iOS程序的内存布局,地址由低到高:
代码段
:编译之后的代码
数据段
字符串常量:比如NSString *str = @“123”
已初始化数据:已初始化的全局变量、静态变量等
未初始化数据:未初始化的全局变量、静态变量等
堆
:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大
栈
:函数调用开销,比如局部变量。分配的内存空间地址越来越小
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。
示例:
NSNumber *number1 = @4;NSNumber *number2 = @5;NSNumber *number3 = @10;NSNumber *number4 = @(0xFFFFFFFFFFFFFFF);NSLog(@"num:\n%p\n%p\n%p\n%p\n", number1, number2, number3,number4);NSString * str1 = [NSString stringWithFormat:@"1"];NSString * str2 = [NSString stringWithFormat:@"2"];NSString * str3 = [NSString stringWithFormat:@"8"];NSString * str4 = [NSString stringWithFormat:@"sadasdasdasdsasdasdsa"];NSLog(@"str:\n%p\n%p\n%p\n%p\n", str1, str2, str3,str4);
结果可以看出:number1、number2、number3
地址很小8字节,是将值放在指针中的,number4
地址很大16字节,因为值比较大指针里面放不下,所以放在了堆中,字符串也是一样,字符串是字符对应的阿斯克码。
- 面试题
OC对象的内存管理
- 在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);
- 对象类型的局部变量,通过
alloc
等方法创建后retainCount
为1,所以在使用完后必须调用release
,或者在创建的时候就调用autorelease
方法。
NSString * str = [[NSString alloc] initWithString:@"abcdefghijklmnaaaaaaaa"];[str release];NSString * str2 = [[[NSString alloc] initWithString:@"abcdefghijklmnaaaaaaaa"] autorelease];
- MRC下属性或者全局变量使用需注意,这里举例说明:
自定义HJPerson
并且有个自定义HJDog
类型的dog
属性。
#import <Foundation/Foundation.h>
#import "HJDog.h"
@interface HJPerson : NSObject{HJDog * _dog;
}
- (void)setDog:(HJDog *)dog;
- (HJDog *)dog;
@end
主要研究其实现:
实现方式1:
//属性的实现
-(void)setDog:(HJDog *)dog{_dog = dog;NSLog(@"dog1:%p",dog);
}
-(HJDog *)dog{return _dog;
}//使用HJDog * dog = [[HJDog alloc]init];HJPerson *person1 = [[HJPerson alloc] init];[person1 setDog:dog];[dog release];//坏内存访问[[person1 dog] run];[person1 release];
可以看出person1
没有销毁并且也给其属性dog
设了值,但由于dog
已经释放,这时候调用person1
的dog
属性run
就会报坏内存访问的错误。如果person1
是个全局变量,属性这样设置,release就没了,这样的属性就没有意义了。所以设置属性时应该将其retainConut
加1。
实现方式2:
//主要改了setter方法,getter方法不变
-(void)setDog:(HJDog *)dog{_dog = [dog retain];
}
-(void)dealloc{[_dog release];NSLog(@"%s",__func__);[super dealloc];
}
//调用HJDog * dog = [[HJDog alloc]init];//1HJDog * dog2 = [[HJDog alloc]init];//1HJPerson *person1 = [[HJPerson alloc] init];[person1 setDog:dog];//2[person1 setDog:dog2];//2[dog release];//1[dog2 release];//1[person1.dog run];//内存泄漏:该释放的对象没有释放[person1 setDog:dog];[person1 release];//这时候只有dog2的引用计数为0,dog的引用计数还是1,
在setter
方法中调用retain
,在HJPerson
的方法中调用release
,这样就能保证创建之后HJPerson
销毁时才销毁。但是多次设置属性时就会出现问题 ,person1
销毁是_dog
调用release
只能把最后一次赋值给属性的变量的引用计数值减1变为0,前面多次调用setter
方法传进来的变量都没有减1,前面这些变量都没法销毁,就造成了内存泄漏。
实现方式3:
-(void)setDog:(HJDog *)dog{if (_dog != dog) {[_dog release];_dog = [dog retain];}
}
-(void)dealloc{// [_dog release];
// _dog = nil;self.dog = nil;NSLog(@"%s",__func__);[super dealloc];
}
判断每次传进来的对象是不是同一个,是就不做操作,不是就先把以前的对象release
释放掉,然后再retain
引用着,最后在dealloc
要将最后一次赋值的属性release
释放掉,为了避免僵尸对象,将属性对应的成员变量的指针指向nil
,调用setter
方法并将nil
传进去也达到了同样的目的。
注意
:不管是arc
还是mrc
环境,定义属性只需要用@property
定义即可,不需要再重写setter、getter
方法,系统已经自动生成了,但要在dealloc
方法中将属性置为nil
。
注意2
:只要不是alloc、new、copy、mutableCopy
创建的对象都不需要调用release
或autorelease
,方法内部已经调用了,例如:
NSString * str = [NSString stringWithFormat:@"asdasdasdasdaadsadas"];//报坏内存访问
// [str release];
NSString * str2 = [NSString string];NSArray * arr = [NSArray array];
拷贝
- 拷贝的目的:产生一个副本对象,跟源对象互不影响; iOS提供了2个拷贝方法,1.copy,不可变拷贝,产生不可变副本,2.mutableCopy,可变拷贝,产生可变副本。
- 深拷贝:内容拷贝,产生新的对象
- 浅拷贝:指针拷贝,没有产生新的对象
- 只有对不可以变对象进行了copy操作才是浅拷贝,其他都是深拷贝。
NSString * str1 = [[NSString alloc] initWithFormat:@"asdasdasdsadsa"];NSString * str2 = [str1 copy];
// NSString * str2 = [str1 retain];NSMutableString * str3 = [str1 mutableCopy];[str3 appendString:@"1111"];NSLog(@"\n%p--%@--%ld\n%p--%@--%ld\n%p--%@--%ld",str1,str1,[str1 retainCount],str2,str2,[str2 retainCount],str3,str3,[str3 retainCount]);[str1 release];[str2 release];[str3 release];
可以看出浅拷贝就相当于retain
操作。
copy
属性实现类似retain
属性实现:
- (void)setData:(NSArray *)data
{if (_data != data) {[_data release];_data = [data copy];}
}
- (void)dealloc
{self.data = nil;[super dealloc];
}
关于拷贝详解可参看我的另一篇文章:iOS底层原理之OC语法
(深拷贝和浅拷贝)
引用计数的存储
前面runtime
中已经讲过在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable
类中:
refcnts
是一个存放着对象引用计数的散列表
dealloc
当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是
dealloc
_objc_rootDealloc
ootDealloc
object_dispose
objc_destructInstance、free
weak
关键字
// ARC是LLVM编译器和Runtime系统相互协作的一个结果__strong HJPerson *person1;__weak HJPerson *person2;__unsafe_unretained HJPerson *person3;NSLog(@"begin");{HJPerson *person = [[HJPerson alloc] init];// person1 = person;
// person2 = person;person3 = person;}NSLog(@"end - %@", person3);}
可以看出如果是__strong
,person
会在end
之后释放;如果是__weak
,person
会在begin
之后end
之前释放,然后person2
会指向nil;如果是__unsafe_unretained
,person
也会在begin
之后end
之前释放,但是person3
依旧指向被释放了的对象的内存,打开僵尸监听可以看见报坏内存访问,及产生了僵尸指针。
weak
和assign的区别就在于此,
weak安全的,指向的对象释放后,指针指向
nil,
assign`依然指向被释放的内存,可能产生僵尸对象。weak
的原理就是在调用dealloc
运行时,isa
中会判断有没有弱引用,弱引用是存储在一个哈希表中,如果有就将弱引用就会根据当前对象的地址在去哈希表中去找到对应的弱引用并清除弱引用。
自动释放池
- 自动释放池的主要底层数据结构是:
__AtAutoreleasePool、AutoreleasePoolPage
;调用了autorelease
的对象最终都是通过AutoreleasePoolPage
对象来管理的。
源码分析:
clang重写@autoreleasepool
objc4源码:NSObject.mm
struct __AtAutoreleasePool {__AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用atautoreleasepoolobj = objc_autoreleasePoolPush();}~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用objc_autoreleasePoolPop(atautoreleasepoolobj);}void * atautoreleasepoolobj;};
- AutoreleasePoolPage的结构
每个AutoreleasePoolPage
对象占用4096
字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease
对象的地址;所有的AutoreleasePoolPage
对象通过双向链表的形式连接在一起。
调用push
方法会将一个POOL_BOUNDARY
入栈,并且返回其存放的内存地址;
调用pop
方法时传入一个POOL_BOUNDARY
的内存地址,会从最后一个入栈的对象开始发送release
消息,直到遇到这个POOL_BOUNDARY
;
id *next
指向了下一个能存放autorelease
对象地址的区域。
#import "ViewController.h"
#import "HJPerson.h"
@interface ViewController ()
@end
//系统方法,用来打印autoreleasepool
extern void _objc_autoreleasePoolPrint(void);
@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];@autoreleasepool {HJPerson *p1 = [[[HJPerson alloc] init] autorelease];HJPerson *p2 = [[[HJPerson alloc] init] autorelease];@autoreleasepool {HJPerson *p3 = [[[HJPerson alloc] init] autorelease];@autoreleasepool {HJPerson *p4 = [[[HJPerson alloc] init] autorelease];}@autoreleasepool{HJPerson *p5 = [[[HJPerson alloc] init] autorelease];_objc_autoreleasePoolPrint();}}}
}
因为p4
地址已经移除了所以只打印出四个Person
对象地址,其中###
是代码POOL_BOUNDARY
参数。如果对象太多不够存储就会再新增一个AutoreleasePoolPage
。对象清空后AutoreleasePoolPage
也就销毁了。
- Runloop和Autorelease
iOS在主线程的Runloop
中注册了2个Observer
:
第1个Observer
监听了kCFRunLoopEntry
事件(进入runloop),会调用objc_autoreleasePoolPush()
,即将对象地址保存到AutoreleasePoolPage当中;
第2个Observer
监听了kCFRunLoopBeforeWaiting
事件(runloop休眠),会调用objc_autoreleasePoolPop()
、这时候就根据对象的地址调用release、objc_autoreleasePoolPush()
;
监听了kCFRunLoopBeforeExit
事件(runloop退出),会调用objc_autoreleasePoolPop()
。
- (void)viewDidLoad {[super viewDidLoad];// 这个Person什么时候调用release,是由RunLoop来控制的// 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了releaseHJPerson *person = [[[HJPerson alloc] init] autorelease];//HJPerson *person = [[HJPerson alloc] init];NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{[super viewWillAppear:animated];NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{[super viewDidAppear:animated];NSLog(@"%s", __func__);
}
可以看出person
对象并不是在viewDidLoad
中释放的,二而是在viewWillAppear
之后viewDidAppear
之前释放的,这是因为viewDidLoad
和viewWillAppear
是在RunLoop
休眠之前加入的事件如source1
,所以这时候并没有调用release
。
面试题
- 思考以下2段代码能发生什么事?怎么造成的?如何解决?
#import "ViewController.h"
@interface ViewController ()
@property(strong,nonatomic)NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];//思考以下2段代码能发生什么事?有什么区别?//循环1for (int a = 0; a < 1000; a++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{self.name = [NSString stringWithFormat:@"abcdefghijk"];});}//循环2for (int a = 0; a < 1000; a++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{self.name = [NSString stringWithFormat:@"abcd"];});}
}
结果
:循环1会报坏内存访问的错误,循环2不会
;
原因
:因为字符串“abcdefghijk”值比较大,里面是通过alloc
创建的,在name
的setter
方法中每次判断不是同一个变量时会调用[_name release]
方法,但是由于是是异步并发执行的,可能同时release
多次,就造成了坏内存访问(当第一次release
时,_name指向的对象已经销毁,现在就是一个野指针,第二再给这个野指针发生release
的时候就会报坏内存访问);而字符串"abcd"的值比较小,是通过Tagged Pointer
的方式存储的,在name
的setter
方法中会判断如果是这种方式就不会进行release
操作,故不会发生坏内存访问的问题。
解决方案
:方案1用atomic
修饰属性,方案2在设置name
时加锁设置完后解锁。
- 使用CADisplayLink、NSTimer有什么注意点?
可能会产生对target
的循环引用,造成内存泄露。 - 介绍下内存的几大区域
由低到高依次为:代码区、数据区、堆区、栈区、内核区。 - 讲一下你对 iOS 内存管理的理解
iOS是通过引用计数来管理内存的,当用alloc、copy、new
创建对象时,对象的引用计数为1,当调用retain
时,引用计数加1,当调用release
时,引用计数减1,当引用计数为0时,对象销毁,释放对象内存。 - ARC 都帮我们做了什么?
ARC是编译器和运行(LLVM + Runtime)时协同的结果,编译器会自动帮助我们插入管理内存的代码,运行时会检测是否有weak
引用,如果有会在调用dealloc方法时清空weak
引用,将weak
引用的对象引用计数减1,并将weak
指向nil
避免参数僵尸对象。 - weak指针的实现原理?
weak
的原理就是在调用dealloc
运行时,isa
中会判断有没有弱引用,弱引用是存储在一个哈希表中,如果有就将弱引用就会根据当前对象的地址在去哈希表中去找到对应的弱引用并清除弱引用。 - autorelease对象在什么时机会被调用release
如果是手动创建的自动释放池(@autoreleasepool),会在大括号结束时调用release
,如果是在Runloop
中,则是由是由RunLoop来控制的,RunLoop休眠之前调用了release。 - 方法里有局部对象, 出了方法后会立即释放吗?(arc中)
如果这个对象在方法中调用的是release则会立即释放,如果是调用的autorelease
则不一定会立即释放,RunLoop休眠之前调用了release,然后释放对象,看编译器插入的是什么内存管理代码。
iOS底层原理之内存管理相关推荐
- 视频教程-iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化-iOS
iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化 小码哥教育CEO,曾开发了2个iOS的流行开源框架(MJRefresh.MJExtension),目前在国内的使用率非常高. 李 ...
- iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化-李明杰-专题视频课程...
iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化-236人已学习 课程介绍 得遇名师,突飞猛进!iOS培训王者MJ(李明杰)老师精心研发,iOS进阶课程,实用技术 ...
- iOS底层原理 - 常驻线程
iOS底层原理 - 常驻线程 在 AFN 2.0 时代,会经常看到 AFN 创建一个常驻线程的方式: 0️⃣ AFN 2.0 时代的常驻线程 + (NSThread *)networkRequestT ...
- 理解 iOS 和 macOS 的内存管理
在 iOS 和 macOS 应用的开发中,无论是使用 Objective-C 还是使用 swift 都是通过引用计数策略来进行内存管理的,但是在日常开发中80%(这里,我瞎说的,8020 原则嘛?)以 ...
- iOS底层原理之架构设计
文章目录 何为架构? MVC - Apple版 MVC – 变种 MVP MVVM 设计模式 面试题 何为架构? 架构(Architecture):软件开发中的设计方案,类与类之间的关系.模块与模块之 ...
- iOS底层原理探究 第一探. 事件传递和响应者链
一. 声明: 本文意在探讨, 也参考了几位大神的文章, 在最后我会把链接发出来, 如果有理解错误的地方, 请大神们指正哈! 二. 前言: 最近自己做项目的时候, 用到了UITabbarContro ...
- 关于IOS的多任务以及内存管理
看了很多FY为自己的可用内存是350MB还是380MB纠结.为了多优化出一点可用内存费脑筋. IOS的任务管理和内存管理,跟windows是有很大差别的.很多FY习惯于用 windows的思维去看待 ...
- iOS之深入解析内存管理retain与release的底层原理
一.内存管理 ① 内存管理原理 iOS 的每个对象内部都保存了一个与之相关联的整数,称为引用计数器(auto reference count): 每当使用 alloc.new 或者 copy 创建一个 ...
- iOS之深入解析内存管理Tagged Pointer的底层原理
一.前言 ① Tagged Pointer 概念 iOS 开发者对"引用计数"这个名词肯定不陌生,引用计数是苹果为了方便开发者管理内存而引入的一个概念.当引用计数为 0 时,对象就 ...
最新文章
- 使用DRS的维护模式实现单个VM的测试
- AI开发者大会:2020年7月3日09:50--10:10唐杰《人工智能的下一个十年》
- 全球及中国彩超市场销售渠道与投资竞争力研究报告2022版
- python3多线程实例_python3多线程糗事百科案例
- AI 的下一个重大挑战:理解语言的细微差别
- STM32工作笔记0091---ADC模数转换实验-M3
- 阶段3 1.Mybatis_09.Mybatis的多表操作_5 完成user的一对多查询操作
- Android小白关于Activity,Fragment,Adapter之间传值的一些记录
- QAM调制解调的仿真实现
- 零一块学计算机二级题库,2017年计算机二级office题库及答案
- markdowm快捷键学习
- 阿里云服务器无法ping通,ping不同阿里云服务器
- 别人的域名到期后可以抢注吗
- Ubuntu 下查看图片
- 2017前端开发手册三-前端职位描述
- Oracle中根据日期范围进行查询,查询大于某一天的数据,查询小于某一天的数据
- 【医学信息学】研究和统计——队列研究和数据分析
- 基于惯性动作捕捉技术进行快速动画制作教程
- Proteus 抢答器设计经验 边沿触发的单稳态触发器实现 74LS148的一些BUG 74LS05反相器
- 基于云开发创建(小程序云商城,基本页面的创建及动态代码的编写)
热门文章
- Psins代码解析之静基座仿真(test_SINS_static.m)傅科、修拉周期水平通道误差传播(test_SINS_static_verify.m)
- spring项目如何升级mysql包_SpringBoot项目版本升级:从1.5.3升级到2.1.8版本
- 一个投资人必须具备的心理素质 心理素质如何培养
- 天润云通过港交所聆讯:依赖教育行业客户,预计下半年业绩将下滑
- 还不知道如何写文章上热榜吗?听1_bit大佬给你讲讲
- 餐饮业的前景与发展趋势
- android dat 乱码,微信dat文件打开乱码
- 我理解的嵌入式几个发展方向
- 计算机外文参考文献2018,2018年英文参考文献格式-推荐word版 (5页)
- 使用CDS下载ERA5数据(保姆级教程)