iOS底层原理之内存管理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Bolted_snail/article/details/83508030

定时器

CADisplayLink、NSTimer

  • CADisplayLinkNSTimer会对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保证调用频率和屏幕的刷帧频率一致,大概为60FPS
     self.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

结果及分析:上面控制器对其属性linktimer是强应用,linktimer内部又对其target也就是控制器强引用,这就造成了循环引用(并不是block,所以用__weak修饰self是没用的),当退出控制器时,定时器任然工作,并且控制器也无法销毁。
循环引用示意图:
循环引用示意图

  • 解决办法:
  1. 如果是NSTimer可以用block方法实现:
    __weak typeof(self)weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf test];
    }];
  1. 但是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);
  • 注意
  1. timer必须是全局变量或者静态变量,否则回调不执行
  2. dispatch_source_set_timer 中第二个参数,当我们使用 dispatch_time 或者 DISPATCH_TIME_NOW 时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用 dispatch_walltime 可以让计时器按照真实时间间隔进行计时
  3. dispatch_source_set_timer 的第四个参数 leeway 指的是一个期望的容忍时间,将它设置为 1 秒,意味着系统有可能在定时器时间到达的前 1 秒或者后 1 秒才真正触发定时器。在调用时推荐设置一个合理的 leeway 值。需要注意,就算指定 leeway 值为 0,系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。
  • 分装GCD定时工具类:
#import <Foundation/Foundation.h>
@interface HJTimer : NSObject
/*
 返回定时器唯一标识,标识和定时器一一对应,存放在全局字典中,标识是key,定时器是value
 task:任务回调
 start:开始时间
 interval:定时间隔
 repeats:是否重复
 async:是否同步
 */
+ (NSString *)IdentifierWithTimerStart:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
               repeats:(BOOL)repeats
                 async:(BOOL)async
                execTask:(void(^)(void))task;

+ (NSString *)execTask:(id)target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
               repeats:(BOOL)repeats
                 async:(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程序的内存布局,地址由低到高:
    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

  • 如何判断一个指针是否为Tagged Pointer
    盘对是否是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已经释放,这时候调用person1dog属性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];//1
    HJDog * dog2 = [[HJDog alloc]init];//1
    HJPerson *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创建的对象都不需要调用releaseautorelease,方法内部已经调用了,例如:

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中会判断有没有弱引用,弱引用是存储在一个哈希表中,如果有就将弱引用就会根据当前对象的地址在去哈希表中去找到对应的弱引用并清除弱引用。

  • dealloc

自动释放池

  • 自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage;调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。
    源码分析:
    clang重写@autoreleasepool
    objc4源码:NSObject.mm
 struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
    ~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    void * atautoreleasepoolobj;
 };

AutoreleasePoolPage

  • AutoreleasePoolPage的结构
    每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址;所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。
    调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址;
    调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY;
    id *next指向了下一个能存放autorelease对象地址的区域。
    AutoreleasePoolPage的结构
#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();
            }
        }
    }
}

在AutoreleasePoolPage存储情况
在这里插入图片描述
因为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休眠之前调用了release
    HJPerson *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__);
}

Runloop和Autorelease
可以看出person对象并不是在viewDidLoad中释放的,二而是在viewWillAppear之后viewDidAppear之前释放的,这是因为viewDidLoadviewWillAppear是在RunLoop休眠之前加入的事件如source1,所以这时候并没有调用release

面试题

  • 思考以下2段代码能发生什么事?怎么造成的?如何解决?
#import "ViewController.h"
@interface ViewController ()
@property(strong,nonatomic)NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //思考以下2段代码能发生什么事?有什么区别?
    //循环1
    for (int a = 0; a < 1000; a++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.name = [NSString stringWithFormat:@"abcdefghijk"];
        });
    }
   //循环2
    for (int a = 0; a < 1000; a++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.name = [NSString stringWithFormat:@"abcd"];
        });
    }
}

结果循环1会报坏内存访问的错误,循环2不会
原因 :因为字符串“abcdefghijk”值比较大,里面是通过alloc创建的,在namesetter方法中每次判断不是同一个变量时会调用[_name release]方法,但是由于是是异步并发执行的,可能同时release多次,就造成了坏内存访问(当第一次release时,_name指向的对象已经销毁,现在就是一个野指针,第二再给这个野指针发生release的时候就会报坏内存访问);而字符串"abcd"的值比较小,是通过Tagged Pointer的方式存储的,在namesetter方法中会判断如果是这种方式就不会进行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,然后释放对象,看编译器插入的是什么内存管理代码。

猜你喜欢

转载自blog.csdn.net/Bolted_snail/article/details/83508030