iOS底层day10 - 内存管理

Example1:NSTimer 、CADisplayLink 循环引用问题

先看以下代码:

self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(TimerTest) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

self.timer为强引用,这里Timer与控制器形成了循环引用,如果要解决这个问题,一可以使用block代替即:

[NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        // todo  使用weakSelf
    }]

当类没有block时,亦或者使用中间变量 : proxy,可以让 Timer强引用Proxy,让Proxy弱引用 Target
请看以下代码:

self.timer = [NSTimer timerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(TimerTest) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
@interface MJProxy : NSObject
+ (MJProxy *)proxyWithTarget:(id)target;
@property (nonatomic, weak)id target;
@end
+ (MJProxy *)proxyWithTarget:(id)target {
    MJProxy *proxy = [MJProxy new];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}

这里运用了runtime的消息转发,当Timer的Target为MJProxy对象时,找不到TimerTest方法,即进入动态方法解析,再进入消息转发,会调用forwardingTargetForSelector方法,返回target对象即传进来的Target,调用TargetTimerTest方法
系统还有另外一个类NSProxy,我们集成它,他的原理和刚刚的MJProxy是一样的,只是在找不到方法的时候,他会直接进入:

@interface MJProxy1 : NSProxy
+ (MJProxy1 *)proxyWithTarget:(id)target;
@property (nonatomic, weak)id target;
@end
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

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

省去了objc_msgSend 前面的动态方法解析,以及forwardingTargetForSelector,性能更佳

CADisplayLink也是一个定时器,跟NSTimer的解决方法是一样的

Example2:GCD定时器

NSTimer有可能会出现不准时的情况:因为NSTImer是基于RunLoop实现的,而RunLoop是循环执行的,有可能在繁忙的时候,循环执行出现延迟
解决方案:使用GCD定时器
使用方法:

   // 定时器的执行队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    // 创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 设置开始时间与间隔
    uint64_t start = 2.0f;
    uint64_t interval = 1.0f;
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                              interval * NSEC_PER_SEC,
                              0);
    // 设置回调
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"111222333");
    });
    // 启动定时器
    dispatch_resume(timer);

Example3:内存布局

内存布局

如图所示,内存地址由低往高分别是 代码段数据段,栈的内存分配地址顺序是由高往低,堆的内存分配地址是由低往高

Tagged Point

NSNumber为栗子

    NSNumber *number = [NSNumber numberWithInt:10];
    NSNumber *number1 = [NSNumber numberWithInt:11];
    NSNumber *number2 = @(0xFFFFFFFFFFFFFFF);
    
    NSLog(@"%p ---%p---- %p ",number,number1,number2);

我们看他的输出:

0xb0000000000000a2 ---0xb0000000000000b2---- 0x604000035e60

我们可以看到他们的内存有很大的区别,这就是Tagged Point的作用,在使用Tagged Point 之前,像NSNumberNSStringNSData这些数据都是以对象的形式存储于堆空间,这样无疑是浪费内存空间的,而使用了Tagged Point之后,数据会存储在指针内,以标记+数据的形式存在
如:
0xb0000000000000a2b2即为NSNumber的标记,而a则对应数字10的存储

如何判断一个内存是否为Tagged Point ?
mac 下最低有效位 &11
iOS 下最高有效位 &1<<63 (64位) 为 1<<63

Copy

请看以下代码:

    NSString *str = @"123";
    NSString *str2 = [str copy];
    NSMutableString *str3 = [str mutableCopy];
    NSLog(@"%p --- %p --- %p",str,str2,str3);
    
    NSMutableString *str4 = [NSMutableString stringWithFormat:@"33333"];
    NSString *str5 = [str copy];
    NSMutableString *str6 = [str mutableCopy];
    NSLog(@"%p --- %p --- %p",str4,str5,str6);

打印输出:

0x10b3090a0 --- 0x10b3090a0 --- 0x6000004492d0
0x604000442760 --- 0x10b3090a0 --- 0x604000442940

我们可以看到内存地址的变化,当类进行copy时返回的都是不可变的类,而进行MutableCopy时,返回的都是可变的类
当不可变类进行copy时,只是对指针进行复制,指向同一个对象(不可变对象copy不需要多一块内存地址,可以节省内存空间),而mutableCopy时则拷贝了一块新内存地址
无论可变类进行copy/mutableCopy,都是拷贝一份新的内存地址

copy

不可变内存图
可变内存图

同理 数组字典也是一样的
用一张图总结:

image.png

由于copyFoundation框架的方法,当对象要进行copy时,对象则需要遵守NSCopying协议,并实现copyWithZone方法

内存总结

image.png

weak 、 unsafe_unretain

weakunsafe_unretain都不会对对象进行强引用,但是weak是相对安全的,在对象释放后,指向对象的指针会自动置nil,而unsafe_unretain不会

你可能感兴趣的:(iOS底层day10 - 内存管理)