OC底层学习-内存管理

1. 定时器

1.1 CADisplayLink、NSTimer的问题

  • CADisplayLink:保证调用频率表和屏幕的刷新频率一致
  • 常用属性 :
    • duration 两次屏幕刷新的时候间隔,通过此值可以拿到屏幕的刷新频率,苹果一般是60hz(一秒60次)是个估值,
    • frameInterval 多少次屏幕刷新后才调用一次方法 IOS10以后被废弃,默认刷新一次调用一次
    • timestamp 屏幕显示的上一帧的时间戳,是CoreAnimation使用的时间格式
    • targetTimestamp 屏幕显示的下一帧时间戳
    • paused 是否暂停计时器
    • preferredFramesPerSecond 一秒内执行多次方法 默认60
  //CADisplayLink
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
//
    //需要添加到runloop中
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
  • NSTimer用法我这里就不做介绍了,下面我们来分析下这两个定时器存在的问题。
    • 定时器随控制器的销毁而销毁的问题, 很多人会写下面这段代码:
- (void)dealloc {
    NSLog(@"%s",__func__);
    [self.timer invalidate];
    self.timer = nil;
}

但是实际上测试后我们可以发现,控制器和定时器都没有销毁,那是因为定时器内部对target对象(也就是当前控制器)是有强引用的,而当前控制器又强引用定时器对象,所以形成循环引用,所以根本不会走dealloc方发。

有的人可能会想,在target对象处传入一个弱指针对象,不就解决循环引用的问题了吗?但是事实上并没有解决,因为CADisplayLinkNSTimer两个定时器对target产生强引用,跟外面传入的target对象是强指针和弱指针没有任何关系,他们内部有一个强指针指向这个target对象,不像block跟外面对象的强弱指针有关

  • 定时器有时候不准时的问题
    • 因为这两个定时器都是依赖RunLoop来实现,我们都知道RunLoop是在循环处理事情,不是每时每刻都在关注定时器,而是到某一时间会去查看定时器是否到时间执行,如果当前RunLoop比较繁忙,那么就会导致延迟去执行定时器的任务

1.2 定时器循环引用解决方案

1.2.1 NSTimer使用blcok创建方式

  • 针对NSTimer我们可以使用block的创建方式, 这样就可以使用weakSelf来解决循环引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        //执行任务
    }];

1.2.2 利用中间对象和消息转发

  • 我们可以创建一个中间对象otherProxy,使定时器对otherProxy强引用,而otherProxy对控制器是弱引用,这样就打破了循环引用,在使用消息转发机制来实现方法的调用
@interface GYOtherProxy : NSObject

@property (nonatomic, weak)id target;
+ (instancetype)proxyWithTarget:(id)target;

@end

@implementation GYOtherProxy

+ (instancetype)proxyWithTarget:(id)target {
    GYOtherProxy *otherProxy = [[GYOtherProxy alloc] init];
    otherProxy.target = target;

    return  otherProxy;
}


-(id)forwardingTargetForSelector:(SEL)aSelector {
    //本质:objc_msgSend(self.target, aSelector)
    return  self.target;
}

//测试
 self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[GYOtherProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
  • 经过测试可以发现,现在定时器不会和当前VC形成循环应用了,会随着VC的销毁而销毁

1.2.3 NSProxy

  • NSProxy:基类,它存在的意义用来解决上面的代理行为,转发行为(消息转发),如果你调用NSProxy的某个方法,那么他会直接去调用methodSignatureForSelector方法,直接进入消息转发机制
  • 下面我们使用NSProxy来解决定时器循环引用的问题
@interface GYProxy : NSProxy

@property (nonatomic, weak)id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
@implementation GYProxy

+ (instancetype)proxyWithTarget:(id)target {
    //继承自NSProxy 是没有init方法的,直接alloc方法即可直接分配内存
    GYProxy *proxy = [GYProxy alloc];
    proxy.target = target;
    
    return proxy;
}

//消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
@end

//测试
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[GYProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
  • 利用中间对象和直接使用NSProxy有什么区别吗?
    • 直接使用NSProxy效率比较高,因为NSProxy是专门用来做消息转发的,当没有找到方法,会直接报methodSignatureForSelector错误,而继承自NSObject的首先会进行消息查找、然后在是动态方法解析、最后才是消息转发,但是 继承是NSProxy的是直接进入消息转发阶段,所以继承自NSObject的要比继承自NSProxy的多了两个阶段 所以继承NSProxy的效率较高

1.2.4 NSProxy和NSObject的区别

 ViewController *vc = [[ViewController alloc] init];
        
        GYOtherProxy *otherProxy = [GYOtherProxy proxyWithTarget:vc];
        GYProxy *proxy = [GYProxy proxyWithTarget:vc];
        
        NSLog(@"otherproxy ====%d",[otherProxy isKindOfClass:[UIViewController class]]); //0
        NSLog(@"proxy ====%d",[proxy isKindOfClass:[UIViewController class]]);//1
  • 由于NSProxy中方法几乎都会转发,所以NSProxy调用isKindOfClass方法也会进入消息转发阶段,所以[proxy isKindOfClass:[UIViewController class]]本质上是 vc调用isKindOfClass方法,所以返回1,这也NSProxyNSObject的区别之处

1.3 GCD定时器

  • GCD定时器是不依赖于RunLoop的,是依赖内核的,所以GCD定时器是非常精准的

1.3.1 GCD定时器的基本使用

//创建一个GCD定时器
    dispatch_queue_t queue = dispatch_get_main_queue(); //创建队列
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    /**
     void
     dispatch_source_set_timer(dispatch_source_t source,
         dispatch_time_t start, //开始时间 typedef uint64_t dispatch_time_t;
         uint64_t interval, 间隔时间
         uint64_t leeway); 延迟时间
	
		NSEC_PER_SEC: 纳秒
     */
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"2223333----%@",[NSThread currentThread]);
    });
    
    //启动定时器
    dispatch_resume(timer);
    
    self.gcdTimer = timer;
  • GCD定时器必须要强引用,保证生命周期,不然不会起作用
  • ARC环境下GCD创建的对象都不用我们管理内存,不需要我们去销毁

1.3.2 封装定时器

@interface GYTimer : NSObject

/// 使用该方法不需要时,需要手动停止定时器,不然定时器不会销毁,会一直在内存中执行
/// @param task <#task description#>
/// @param start <#start description#>
/// @param inteval <#inteval description#>
/// @param repeat <#repeat description#>
/// @param async <#async description#>
+ (NSString *)executeTask:(void(^)(void))task
                    start:(NSTimeInterval)start
                 interval:(NSTimeInterval)inteval
                   repeat:(BOOL)repeat
                    async:(BOOL)async;

/// 使用该方法不需要时,需要手动停止定时器,不然定时器不会销毁。 并且该定时器对target对象是强引用,如果不手动关闭定时器,那么会导致target对象不会销毁,但是不会和target对象形成循环引用
/// @param target <#target description#>
/// @param action <#action description#>
/// @param start <#start description#>
/// @param inteval <#inteval description#>
/// @param repeat <#repeat description#>
/// @param async <#async description#>
+ (NSString *)executeTaskWithTarget:(id)target
                             action:(SEL)action
                              start:(NSTimeInterval)start
                           interval:(NSTimeInterval)inteval
                             repeat:(BOOL)repeat
                              async:(BOOL)async;

//根据标记取消定时器
+ (void)cancelTiemr:(NSString *)identifier;
@end

@implementation GYTimer
/**
 使用静态变量会造成以下问题,使用block执行任务时,即使退出,定时器依不会销毁,因为定时间cachesTiemr所拥有,除非手动停止定时器,不然会一直执行定时器
 
 使用self selector等方式时,定时器会对target强引用,造成target对象不能释放,除非手动销毁定时器
 */
//缓存定时器
static NSMutableDictionary *cachesTiemr;
dispatch_semaphore_t semaphore;

+ (void)initialize {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        cachesTiemr = [NSMutableDictionary dictionary];
        semaphore = dispatch_semaphore_create(1);
    });
}

+ (NSString *)executeTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)inteval repeat:(BOOL)repeat async:(BOOL)async {
    if (!task || start < 0 || (inteval <= 0 && repeat)) return nil;
    
    //创建一个GCD定时器
    dispatch_queue_t queue = async ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 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), inteval * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    

    //为了保证多线程安全 同时操作字典,我们需要加锁
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    //创建每个定时器对应的标记
    NSString *identifier = [NSString stringWithFormat:@"%ld",cachesTiemr.count];
    //定时器添加到字典,背字典引用, 字典被当前类强引用
    cachesTiemr[identifier] = timer;
    
    dispatch_semaphore_signal(semaphore);
    
    dispatch_source_set_event_handler(timer, ^{
        task();
        
        if (!repeat) {
            //如果不是重复,那么直接取消该定时器
            [self cancelTiemr:identifier];
        }
    });
    
    //启动定时器
    dispatch_resume(timer);
    
    
    return  identifier;
}

+ (NSString *)executeTaskWithTarget:(id)target action:(SEL)action start:(NSTimeInterval)start interval:(NSTimeInterval)inteval repeat:(BOOL)repeat async:(BOOL)async {
    if (!target || !action) return nil;
    
    return [self executeTask:^{
        //去除警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
    //写在这个中间的代码,都不会被编译器提示-Wdeprecated-declarations类型的警告
        if ([target respondsToSelector:action]) {
            [target performSelector:action];
        }
#pragma clang diagnostic pop
        
        
    } start:start interval:inteval repeat:repeat async:async];
}


+ (void)cancelTiemr:(NSString *)identifier {
    if (!identifier || identifier.length == 0) {
        return;
    }
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    dispatch_source_t timer = cachesTiemr[identifier];
    if (timer) {
        dispatch_source_cancel(timer);
        [cachesTiemr removeObjectForKey:identifier];
    }
    
    dispatch_semaphore_signal(semaphore);
}
@end

2. 内存布局

在这里插入图片描述

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
 */

3. Target Pointer技术

  • 从64bit开始,IOS引入了Target Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的储存
  • 在没有使用Target Pointer之前,NSNumber等对象需要动态分配内存 、维护引用计数器等,NSNumber指针存储的是堆中的NSNumber对象的地址值
  • 使用Target Pointer之后,NSNumber指针里面就存储的数据变成了: Tag + Data,也就是将数据直接存储了指针中
  • 当对象指针最低有效位是1,则该指针为Target Pointer
  • 当指针不够存储数据时,才会使用动态分配内存的方式来储存数据
  • objc_msgSend()能识别Target Pointer ,比如NSNumberintValue方法,直接从指针提取数据,节省了以前的调用开销
  • 如何判断一个指针是否为Target Pointer
    • IOS平台,最高有效位为1(第64bit)
    • Mac OS 平台,最低有效位为1
	 NSNumber *number1 = @2;
    NSNumber *number2 = @3;
    NSNumber *number3 = @4;
    NSNumber *number4 = @5;
    NSNumber *number5 = @(0xFFFFFFFFFFFFFFF);
    
    
    //NSLog(@"%d %d %d", isTaggedPointer(number1), isTaggedPointer(number2), isTaggedPointer(number3));
    NSLog(@"%p %p %p %p %p", number1, number2, number3,number4,number5);
    //0xc9aa33901e86fdcb 0xc9aa33901e86fddb 0xc9aa33901e86fdab 0xc9aa33901e86fdbb 0x600003fbdc60
  • 观察地址,number5的地址值明显要大很多,属于堆空间的地址
  • 查看源码是怎么判断是不是属于Target Pointer对象,查看objc源码中objc-internal.h
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr) 
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

#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

#if OBJC_MSB_TAGGED_POINTERS
	//如果是IOS平台 (指针最高有效位是1 ,就是Target Pointer)
#   define _OBJC_TAG_MASK (1UL<<63)
#else
	//如果是MAC平台 (指针最低有效位是1 ,就是Target Pointer)
#   define _OBJC_TAG_MASK 1UL
#endif

//所以我们完全可以根据源码写一个判断是否是Target Pointer 对象
BOOL isTaggedPointer(id pointer)
{
    return (long)(__bridge void *)pointer & 1;
}
//在根据源码改造
BOOL isTargetPointer(id pointer) {
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;;
}
  • 思考下面两段代码能发生什么事? 有什么区别?
 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"];
        });
    }
  • 正常运行
  • 那么这两段代码有什么区别,为什么前者会崩溃,后者征程运行?
    • 首先我们要知道属性的setter方法底层实现, 底层实质是转成MRC执行,首先是把原先储存的变量释放,在赋值新的值
- (void)setName:(NSString *)name
{
    if (_name != name) {
    	//崩溃的原因就是多条线程可能同时访问到[_name release];这一行代码,同样的一个变量多次释放,所以发生坏内存访问(释放一个已经不存在的对象)
        [_name release];
        _name = [name retain];
    }
}
  • 第二段代码为什么不会崩溃,因为第二代码,给name赋值是直接缓存在地址中(也就是Target Pointer)的而不是动态分配内存的储存模式,下面我们可以看看这两个字符串的类型、地址
 	NSString *str1 = [NSString stringWithFormat:@"abcdefghijk"];
    NSString *str2 = [NSString stringWithFormat:@"123abc"];
    
    NSLog(@"%@ %@", [str1 class], [str2 class]); //
    NSLog(@"%p %p", str1,str2);
//输出结果
//  __NSCFString NSTaggedPointerString
// 0x600003b88de0 0xb413d58ae9046c7c

我们可以看出str1是__NSCFString类型,地址是比较大的0x600003b88de0, 而str2的类型NSTaggedPointerString,地址是0xb413d58ae9046c7c,由此我们可以看出,地址可以储存的对象,是直接使用Target Pointer方式 来储存,而地址无发储存的对象采用动态分配内存的方式来储存的,而Target Pointer方式储存的对象不会像OC对象一样去调用set方法 就不会release 而是直接把值赋值到指针中(相当于不是一个oc对象,直接给指针赋值) 所以不会崩溃

4. MRC内存管理

4.1 概述

  • 在IOS中,使用计时器来管理OC对象的内存
  • 一个新创建的OC对象计数器默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
  • 所谓的内存泄漏:该释放的东西没有释放,所以我们需要保证引用计数的平衡
  • 内存管理总结经验:
    • 当调用alloc、new、copy、mutableCopy方法返回一个对象,在不需要这个对象时,要调用release或则autorelease来释放它
    • 想拥有某个对象,就让它的引用计数+1;不想在拥有某个对象,就让它引用计数-1

4.2 成员变量式内存管理

  • 简单对象的内存管理
//alloc  创建对象;引用计数默认为1
        GYPerson *person = [[GYPerson alloc] init];
        //GYPerson *person = [GYPerson new]; 这两句代码等价 
        //如果你不释放,就会形成内存泄漏,
        [person release]; // 引用计数器减1  -> 销毁
  • 对象中包含对象的内存管理,如果成员变量是OC对象那么必须进行内存管理;如果成员变量是基本数据类型,那就不需要内存管理
@interface GYPerson : NSObject
{
    GYDog *_dog;
    
    //如果是基本数据类型,我们就不必进行内存管理了 setter方法内部直接赋值就可以了
    int _age;
}

- (void)setDog:(GYDog *)dog;
- (GYDog *)dog;

- (void)setAge:(int)age;
- (int)age;
@end

@implementation GYPerson
//ARC中对象setter方法转换成MRC模式下底层就是这样实现的
- (void)setDog:(GYDog *)dog {
    //每次赋值的时候,需要判断赋值对象是否相同,如果没有这个判断,那么每次赋值同样的对象时候执行[_dog release];,就可能把当前赋值的对象给释放掉,然后在访问_dog时,_dog对象早就被释放了,会发生坏内存访问
    if (_dog != dog) {
        //每次赋值的时候,我们需要先释放上一个对象,在赋值新的对象,如果不释放掉以前的对象,那么就会造成内存泄漏
        [_dog release];
        //首先我们往_dog在当前对象存在的时候,就存在不会死,所以赋值让属性引用计数器+1
        _dog = [dog retain];
    }
}

- (GYDog *)dog {
    return _dog;
}

- (void)setAge:(int)age {
    _age = age;
}

- (int)age {
    return _age;
}

- (void)dealloc {
    //最后,当我们的set方法中把成员变量对象引用计数+1操作,我们这里需要做建议-1操作,达到引用计数器平衡
    [_dog release];
    _dog = nil; //最好是吧指针也星空
    
    //上面两句代码相当于下面这句代码
    //self.dog = nil; self.dog相当于调用set方法,会把nil传递进去[nil retain] = nil
    
    NSLog(@"%s",__func__);
    //一般父类的dealloc放到最好,也就是父类的东西放到最后进行销毁
    [super dealloc];
}
@end
GYPerson *person = [[GYPerson alloc] init];
        GYDog *dog = [[GYDog alloc] init];
        
        [person setDog:dog];
        //重复调用也不会发生问题
        [person setDog:dog];
        [person setDog:dog];
        
        GYDog *dog1 = [[GYDog alloc] init];
        //中途更换对象也不会报错
        [person setDog:dog1]
        
        [dog release];
        [[person dog] run];
    
        [person release];

4.3 属性式内存管理

  • 首先我们不必在声明一个个成员变量,setter、getter方法了,直接声明一个属性即可
    • 声明属性如果使用assign 生成的setter方法就是直接赋值,如果是retain修饰生成的setter方法就会把原来的值执行release操作,然后新传进来的值进行一次retain操作在赋值
    • 属性: 声明一个属性编译器帮我们自动生成属性了settergetter方法的声明,无需我们自己写声明
    • @synthesize:帮我们生成了一个和属性关联的成员变量,还自动帮我们生成了setter和getter方法的实现(例子:@synthesize age = _age 帮age属性生成一个对应的成员变量_age和setter、getter方法)
    • retain使用retain修饰的OC对象,系统会自动帮我们生成setter和getter方法实现,就和上面我们自己写的setter和getter方法的实现是一样的,还会帮我们自动生成对应的成员变量
@interface GYPerson : NSObject
//声明一个OC对象属性时,需要使用retain来修饰,现在使用strong也可以
@property (nonatomic, retain) GYCar *car;
@end

@implementation GYPerson
//关联属性和成员变量,现在声明属性之后,就不需要这一步了
//@synthesize car = _car;
- (void)dealloc {
    //虽然是系统自动帮我们生成setter方法,但是这里最后我们还是需要自己释放属性对象
    [_car release];
    _car = nil;
    
    NSLog(@"%s",__func__);
    
    //一般父类的dealloc放到最好,也就是父类的东西放到最后进行销毁
    [super dealloc];
}
@end
  • 只要不是alloc、new、copy开头创建对象的,一般我是不需要进行release操作的,底层内部可能调用了autorelease方法

4.4 MRC开发

@interface ViewController ()
@property (retain, nonatomic) NSMutableArray *data;
@property (retain, nonatomic) UITabBarController *tabBarController;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.tabBarController = [[[UITabBarController alloc] init] autorelease];
    
    self.data = [NSMutableArray array];
    
//    self.data = [[[NSMutableArray alloc] init] autorelease];
    
//    self.data = [[NSMutableArray alloc] init];
//    [self.data release];
    
//    NSMutableArray *data = [[NSMutableArray alloc] init];
//    self.data = data;
//    [data release];
}

- (void)dealloc {
    self.data = nil;
    self.tabBarController = nil;
    
    [super dealloc];
}
@end
  • 如果你不调用autorelease方法,那么你就需要自己在不需要这个对象的时候调用release方法,dealloc方法中对应是set方法中的引用计数器

5. Copy

  • 拷贝: 产生一个副本对象,并切两者互不影响,这也是拷贝的本质所在
    • copy:不可变拷贝,产生不可变副本
    • mutableCopy: 可变拷贝,产生可变副本
    • 只要调用的是copy方法返回就是不可变的对象,调用mutableCopy返回的对象就是可变的,不管拷贝的对象是否是可变

5.1 copy、mutableCopy

NSString *str1 = [[NSString alloc] initWithFormat:@"NSString-copy"];
        NSString *str2 = [str1 copy];
        NSMutableString *str3 = [str1 mutableCopy];
        [str3 appendString:@"1213123"];
        NSLog(@"%@  %@   %@", str1, str2, str3);
        NSLog(@"%p   %p   %p",str1,str2,str3);
        [str3 release];
        [str2 release];
        [str1 release];
//打印结果 :0x10056fe70   0x10056fe70   0x10056fe90
  • 由打印结果,str1和str2地址指向都是同一个对象,而str3明显是一个新的对象
NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"NSString-copy"];
        NSString *str2 = [str1 copy];
        NSMutableString *str3 = [str1 mutableCopy];
        [str1 appendString:@"11111"];
        [str3 appendString:@"1213123"];
        NSLog(@"%@  %@   %@", str1, str2, str3);
        NSLog(@"%p   %p   %p",str1,str2,str3);
        [str3 release];
        [str2 release];
        [str1 release];
 //打印结果 :NSString-copy11111  NSString-copy   NSString-copy1213123
//打印结果 :0x100404120   0x100404150   0x1004068f0
  • 上述打印结果可知,str2、str3都是全新对象,并且修改其中任何一个,不会影响其他的对象

  • 总结:

    • 不管拷贝的对象是否是可变对象,调用copy方法拷贝出来的对象都是不可变的,而调用mutableCpoy方法拷贝出来的对象都是一个可变对象
    • 不可变对象调用copy方法,拷贝出来的对象对象本质上就是它本身,因为他们本身是不可变的,所以也满足拷贝出来的对象和原对象互不影响,因为它们是无法改变的,而两个对象指向同一个对象,也节省了内存
    • copy方法返回都是不可变对象, mutableCopy方法返回的都是可变对象
  • 由此上述结论,我们引申出了,浅拷贝深拷贝的概念

    • 浅拷贝:指针拷贝,没有产生新的对象
    • 深拷贝:内容拷贝,没有产生新的对象
    • 注意: 不要认为copy方法就是浅拷贝,上述案例中如果可变对象调用copy方法,拷贝出来也是一个全新的对象,也是深拷贝
  • 数组和字典和字符串的情况类似,这几类拷贝情况如下图:在这里插入图片描述

5.2 使用copy修饰可变类型属性问题

@interface GYPerson : NSObject

/// MRC下使用copy来修饰可变类型的变量
@property (copy, nonatomic) NSMutableArray *data;
@end

@implementation GYPerson
- (void)dealloc {
    self.data = nil;
    NSLog(@"%s",__func__);
    [super dealloc];
}
@end


//测试代码:
 GYPerson *person = [[GYPerson alloc] init];
        NSMutableArray *data = [[NSMutableArray alloc] initWithObjects:@"tom",@"jack", nil];
        
        person.data = data;
        
        [person.data addObject:@"andrew"];
        
        [data release];
        [person release];

执行结果报了一个非常经典错误:-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x10283bec0NSAarry对象找不到addObject方法,有得人可能很奇怪,明明的我声明的是一个可变数组,为什么报错NSAarry对象找不到addObject方法错误了?

首先我们需要知道在MRC的环境下,使用copy来修饰属性,编译器帮我们自动生成的setter是什么样的

//在MRC环境下,我们使用copy来修饰属性,系统自动生成的set方法的实现如下
- (void)setData:(NSMutableArray *)data {
    if (_data != data) {
        [_data release];
        _data = [data copy];
    }
}

由上述代码可知,copy修饰属性,底层是传递进来的对象调用一次copy,返回一个不可变的对象,然后再复制给属性,所以不管你外面出传递进来的参数是否可变,经过copy方法之后,这个属性最后一定是不可变类型,所以你在调用上面的data,看似是一个可变数组,但是本质是已经是NSArray类型的不可变数组了,所以你调用addObject时会找不到该方法,因为NSArray对象中确实没有该方法(把一个不可变的对象当成一个可变的对象来用 会出现问题)

有的人可能想,能不能用mutableCopy来修饰属性,这个是没有的,属性中只存在copy操作,不存在mutableCopy操作

  • 其实我们发现mutableCopy这种方案是只给Foundation框架自带的一些类做事情的,如:NSArray、NSMuatbleArray、NSDictonary、NSMutableDictionary、NSString、NSMutableString、NSData、NSMutableData、NSSet、NSMutableSet等等

5.3 自定义对象使用copy

@interface GYPerson : NSObject
@property (assign, nonatomic) int age;
@property (assign, nonatomic) int weight;
@end

@implementation GYPerson
- (NSString *)description {
    return [NSString stringWithFormat:@"age====%d  weight====%d",self.age, self.weight];
}
@end

//测试代码:
 GYPerson *person = [[GYPerson alloc] init];
        person.age = 20;
        person.weight = 50;
        
        GYPerson *person2 = [person copy];
        person2.age = 30;
        person2.weight = 60;
        
        NSLog(@"person====%@",person);
        NSLog(@"person2====%@",person2);
        
        [person2 release];
        [person release];

运行之后发现报错-[GYPerson copyWithZone:]: unrecognized selector sent to instance 0x100524ed0,调用copy方法,底层实质上调用copyWithZone方法,所以如果要OC自定义对象,调用copy方法,首先需要遵守NSCopying协议,然后实现copyWithZone方法,调用mutableCopy需要遵守NSMutableCopying协议,然后实现mutableCopyWithZone方法

所以GYPerson需要遵守NSCopying协议,实现copyWithZone方法

- (id)copyWithZone:(NSZone *)zone {
	//首先需要生成一个新的对象,然后把拷贝对象的所有属性都赋值给这个全新的对象
    GYPerson *person = [GYPerson allocWithZone:zone];
    person.age = self.age;
    person.weight = self.weight;
    
    return person;
}

实现上述方法之后,在此运行程序,打印结果如下:在这里插入图片描述

6. 引用计数器、weak指针

6.1 引用计数器

  • 在64bit中,引用计数器可以直接储存在优化过的isa指针中,也可能储存在SideTable类中(关于isa结构,有在Runtime中讲解过)
  • SideTable的结果源码(删除部分源码):
struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts; //refcnts是一个储存着对象引用计数的散列表
    weak_table_t weak_table;//储存着弱指针的表(也是采用散列表储存的)
};
  • 下面我们查看retainCount方法的源码
- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}
inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {//判断是不会优化过的isa指针
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {//判断引用计数不是存在isa中,而是存在sidetable中
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

size_t 
objc_object::sidetable_getExtraRC_nolock()
{
    assert(isa.nonpointer);
    //根据当前类作为key去除表中对应的引用计数,然后进行一些操作后返回
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

6.2 weak指针的实现原理

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __strong GYPerson *person1;
    __weak GYPerson *person2;
    __unsafe_unretained GYPerson *person3;
    
    NSLog(@"111111");
    
    {
        //正常情况下 person对象 出了{} 就会销毁
        GYPerson *person = [[GYPerson alloc] init];
        
        //使用一个强指针来延长person的声明周期,等到强指针销毁时,person对象才销毁
        //person1 = person;
        
        //不会产生一个强引用,当指向对象销毁时,会自动设置值为null
        //person2 = person;  //打印perosn2 为null,
    
        //也不会产生强引用,但是如果对象销毁之后,还继续访问该指针,会发生野指针错误,对象销毁时,不会自动清空指针的值,还保存着原来的地址值,这个时候再去访问,会发生坏内存访问
        person3 = person;
    }
    NSLog(@"22222====%@", person3);
}
  • 下面我们可以通过查看dealloc方法的源码,连接weak指针的原理
    • dealloc方法
    • _objc_rootDealloc
    • objc_object::rootDealloc
    • object_dispose
    • objc_destructInstance
- (void)dealloc {	
	//把当前对象传递进去
    _objc_rootDealloc(self);
}

void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

inline void
objc_object::rootDealloc()
{
	//判断是不是一个Target Pointer对象,如果是则直接返回
    if (isTaggedPointer()) return;  // fixme necessary?

	
    if (fastpath(isa.nonpointer  &&  //判断是否是优化之后的isa指针
                 !isa.weakly_referenced  &&  //判断是否存在弱指针指向
                 !isa.has_assoc  &&  //判断是否有关联对象
                 !isa.has_cxx_dtor  &&  //判断是否有C++析构函数
                 !isa.has_sidetable_rc))//判断引用计数是否存在sidetable中
    {
        assert(!sidetable_present());
       	//如果上述条件都不存在 那么直接释放对象
        free(this);
    } 
    else {
    	//否则条用该方法释放对象
        object_dispose((id)this);
    }
}

id 
object_dispose(id obj)
{
    if (!obj) return nil;

	//调用该方法释放对象
    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        //清除成员变量
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        //将指向当前对象的弱指针设置为nil
        obj->clearDeallocating();
    }
    return obj;
}

7. autorelease

7.1 概述

首先我们观察自动释放池转成的c++代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        GYPerson *person = [[[GYPerson alloc] init] autorelease];
    }
    return 0;
}
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        GYPerson *person = ((GYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((GYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("GYPerson"), sel_registerName("alloc")), sel_registerName("init"));
    }
    return 0;
}

//结构体自动释放池代码
 struct __AtAutoreleasePool {
 
    // 构造函数,在创建结构体的时候调用
   __AtAutoreleasePool() {
    atautoreleasepoolobj = objc_autoreleasePoolPush();
 
    }
 
    //析构函数,在结构体销毁的时候调用
   ~__AtAutoreleasePool() {
    objc_autoreleasePoolPop(atautoreleasepoolobj);
 
    }
   void * atautoreleasepoolobj;
 };

所以上述代码的实质是,自动释放池的开始时调用objc_autoreleasePoolPush()方法,结束时调用objc_autoreleasePoolPop()方法

@autoreleasepool {
	//自动释放池开始时会调用构造方法创建
        //atautoreleasepoolobj = objc_autoreleasePoolPush();
        
        GYPerson *person = [[GYPerson alloc] init];
        
        //objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

7.2 autoreleasePool的结构

  • 自动释放池的主要底层主要结构是:__AtAutoreleasePoolAutoreleasePoolPage
  • 调用了autorelease的对象最终都会通过AutoreleasePoolPage对象来管理的
  • 源码分析:
    • clang重写@autoreleasepool
    • objc4源码:NSObject.mm
struct AutoreleasePoolPageData
{
	magic_t const magic;
	__unsafe_unretained id *next;//永远都指向下一个能存放autorelease对象的地方 
	pthread_t const thread;
	AutoreleasePoolPage * const parent;//指向上一个AutoreleasePoolPage对象
	AutoreleasePoolPage *child;//指向下一个AutoreleasePoolPage对象
	uint32_t const depth;
	uint32_t hiwat;
};

class AutoreleasePoolPage : private AutoreleasePoolPageData
{}
  • 每个AutoreleasePoolPage对象占用4096字节内容,除了用来存放它内不的成员变量,剩下的空间用来存放autorelease对象的地址
  • objc_autoreleasePoolPush()方法会把调用autorelease方法的对象的地址存放在AutoreleasePoolPage这个对象内存中
  • 所有的AutoreleasePoolPage对象是通过双向链表的形式连接在一起的
    在这里插入图片描述
  • 调用push方法会传入一个POOL_BOUNDARY入栈,并且返回其存放的内存地址,然后如果有对象调用autorelease方法,就把这个对象的地址入栈
  • 调用pop方法传入一个POOL_BOUNDARY的内存地址,然后从最后一个入栈对象开始发送release消息(出栈),直到遇到这个POOL_BOUNDARY结束
  • 我们可以利用extern关键字来声明系统内部一个函数,来查看@autoreleasePool的情况–extern void _objc_autoreleasePoolPrint(void)

7.3 Runtime与auotreleasePool

  • IOS在主线程的RunLoop中注册了2个Observer
    • 第1个Observer监听了KCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
    • 第二个Observer:
      • 监听了KCFRunLoopBeforeWating事件,会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
      • 监听了KCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
  • 我们可以通过打印RunLoop对象来查看Observer对象
  • 调用autorelease方法的对象什么时候释放,是由RunLoop来控制,比如可能实在当前所在Runloop循环中,RunLoop进入休眠之前调用release,或是在RunLoop退出之前调用release

8. 面试题

8. 1 使用CADisplayLink、NSTimer有什么注意点?

  • 可能产生循环引用的问题
  • 可能不准时的问题

8.2 介绍下内存的几大区域?

  • 参考 本文内存布局

8.3 讲一下你对IOS的内存管理的理解

  • TaggedPointer(针对类似于 NSNumber 的小对象类型),指针可以储存的就直接使用指针储存数据,不能储存的就使用分配内存的方式来储存
  • isa指针在64bit下采用共用体(union)和位域优化过,没有优化之前储存的是一个普通的地址值,但是优化之后,isa指针中除了储存地址值,还储存了许多其他的信息:
    • 第一位的 0 或 1 代表是纯地址型 isa 指针,还是 NONPOINTER_ISA 指针。
    • 第二位,代表是否有关联对象
    • 第三位代表是否有 C++ 代码。
    • 接下来33位代表指向的内存地址
    • 接下来有 弱引用 的标记
    • 接下来有是否 delloc 的标记…等等

8.4 autorelease对象在什么时候会调用release

  • 首先调用autorelease方法的对象什么时候释放,是由RunLoop来控制
  • 比如可能实在当前所在Runloop循环中,RunLoop进入休眠之前调用release,或者是RunLoop退出的时候调用release

8.5 方法里有局部对象,出了方法后会立即释放吗

  • 立刻释放, ARC相当于在方法的结尾插入了一句代码([obj release]) 直接调用对象的release代码 ,所以出了方法之后立即释放 ,如果是给对象加上autorelease方法的话,释放就是由Runloop来控制

8.6 ARC都帮我们做了什么?

  • ARC是由LLVM+Runtime相互协作的结果
  • 利用LLVM这个编译自动帮我们成retain,release、autorelease代码
  • 在程序运行中利用Runtime帮我们处理弱引用这些操作

8.7 weak指针的实现原理

  • 将弱引用存到一个hash表中,到时候这个对象要销毁时,取出当前对象对应的弱引用,把弱应用表中存储的弱引用清空
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值