iOS内存管理注意点

1. 定时器使用CADisplayLinkNSTimer

CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的 CADisplayLink 对象,把它添加到一个RunLoop中,并给它提供一个 targetselector 在屏幕刷新的时候调用。

使用方式:


@property (strong, nonatomic) CADisplayLink *link;
// 保证调用频率和屏幕的刷帧频率一致,60FPS
 self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

每秒钟调用60次linkTest方法。此时的self.linktarget(self) 产生强引用,而控制器本身对link已经有引用,所以此处会产生循环引用。 调用 [self.link invalidate]方法定时器无法取消, 自身self也无法得到释放。同样的 NSTimer也存在此方法

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

也同样会产生循环引用。
解决方案:

(1) 更换定时器方法:使用Block来代替具体的方法实现。
//    __weak typeof(self) weakSelf = self;
//    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
//        [weakSelf timerTest];
//    }];

(2)使用中间对象弱引用Target,打破原来的强引用循环。
iOS内存管理注意点_第1张图片
屏幕快照 2019-05-29 下午12.02.49.png

ViewController 引用着timertimer引用着objectobject引用targettarget弱引用ViewController,打破循环引用圈。具体的做法为:

#import 

@interface MJProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

.m
#import "MJProxy.h"

@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

然后调用中间对象

 self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MJProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];

// link 的解决方案:
self.link = [CADisplayLink displayLinkWithTarget:[MJProxy proxyWithTarget:self] selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

类:NSproxy 也是基类 跟 NSobject 相同级别的,让你的 中间类继承自 NSproxy ,作为中间类,NSproxy 没有 init 方法。NSproxy 存在的意义 就是用于中间消息转发的。继承自NSporxy 的类 调用方法 回先搜索自身的消息,没有的话则 直接进入到 消息转发阶段 调用methodSignatureForSelector: 转发消息,注意跟继承自NSobject类的区别。 此时消息转发会更加方便。

2. NSTimer的缺点以及GCD定时器封装。

NSTimer是依赖于RunLoop的,而有的时候RunLoop需要处理繁重的任务,会导致定时器不准时。RunLoop 循环一圈会计算时间,如果单圈比较耗时则有可能导致超时,所有NSTimer并不准时。

解决方法:使用GCD创建定时器,它不依赖于RunLoop
  • 添加定时器时器属性
@property (strong, nonatomic) dispatch_source_t timer;
  • 创建定时器
//最后一个参数 决定该定时器是在哪个线程调用
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());

  • 设置 source_set_timer
NSTimeInterval start = 2.0f;
    NSTimeInterval invaler = 1.0f;
    //设置时间,第一个参数:几秒后开始,第二个参数:间隔,第三个参数:误差
    dispatch_source_set_timer(timer,dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC) , invaler * NSEC_PER_SEC, 0);
  • 添加回调
 // 设置回调
    dispatch_source_set_event_handler(timer, ^{
        
        NSLog(@"调用定时器");
    });
  • 最后启动定时器
// 启动定时器
    dispatch_resume(timer);
//取消定时器方法:dispatch_source_cancel(timer)

注意 这里的`dispatch_source_create` 并不需要释放

最后进行封装,我把疯转的GCD定时器放在这里了,使用超级简单:

/*
start:开始时间,interval:间隔,repeats: 是否重复,async : 同步还是异步
返回一个 定时器标识
*/
+ (NSString *)execTask:(void(^)(void))task
           start:(NSTimeInterval)start
        interval:(NSTimeInterval)interval
         repeats:(BOOL)repeats
           async:(BOOL)async;
//取消定时器,参数是:执行方法返回的标识
+ (void)cancelTask:(NSString *)name;

3. 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
    面试题: 执行下面代码会发生什么?
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 queue1 = dispatch_get_global_queue(0, 0);

    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue1, ^{
            self.name = [NSString stringWithFormat:@"abc"];
        });
    }

解答:1. 第一个 会发生 crash ,因为同时有多条线程 同时访问 [ _name release]; 方法,会形成坏内存访问。把属性修饰符改成 atomic 线程安全。或者加线程锁。

@property (assign, nonatomic) os_unfair_lock moneyLock;
// 初始化锁
self.ticketLock = OS_UNFAIR_LOCK_INIT;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
         os_unfair_lock_lock(&_ticketLock);
            // 加锁
            self.name = [NSString stringWithFormat:@"abcdefghijk"];
            // 解锁
       os_unfair_lock_unlock(&_ticketLock);
        });
    }

第二个2 : 程序不会崩溃,原因: Tagged Pointer 的存在,str 使用Tagged Pointer的形式存储。意味着 self.name 不会调用 OC对象,不会调用OC对象的 setter 方法,也不会调用release 方法所以不会crash。

可以通过这种方法来 看它的类型
NSString *str1 = [NSString stringWithFormat:@"abcdefghijk"];
NSString *str2 = [NSString stringWithFormat:@"123abc"];
NSLog(@"%@ %@", [str1 class], [str2 class]);

4. MRC 技术

ios 采用引用计数的方式来管理OC对象的内存。

  • 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
  • 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
内存管理的经验总结
  • 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
  • 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1。
MRC下的setter与getter方法
  • 一个属性@property (retain, nonatomic)MJDog *dog;
    对应的Setter与Getter方法为:
- (void)setDog:(MJDog *)dog
{
    if (_dog != dog) {
        [_dog release];
        _dog = [dog retain];
    }
}

- (MJDog *)dog
{
    return _dog;
}

ARC 下 属性修饰符为assign生成的getter 跟 setter 方法为:

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

这个需要知道。

5. Copy 操作

  • 拷贝的目的: 产生一个副本对象,跟原对象互不影响。即 修改了副本对象 不会影响原对象。

Copy : 不可变 拷贝,产生不可变副本
MutableCopy: 产生可变副本。注意: 不管你拷贝的是可变的还是不可变的对象,只要是调用copy那么返回的就是不可变的对象,只要是multableCopy 返回的 就是 不可变的对象。

深拷贝与浅拷贝 总结

深拷贝 :内容拷贝
浅拷贝:指针拷贝

  • 拷贝的 原则: 拷贝的出的对象 要互不影响。不论是原对象是 可变的还是不可变的,只要是调用 copy 就会产生不可变对象,调用multableCopy 就会产生 可变对象
  • 可变对象 : 调用copy、multableCopy 都会生成新的对象,复制内容。都是深拷贝。
  • 不可变对象: 调用 copy :不产生新的对象,用原来的地址,复制内容,是浅拷贝。
  • 调用 multableCopy :产生可变的新的对象,复制内容,是深拷贝。
iOS内存管理注意点_第2张图片
深浅拷贝.png
属性修饰符 Copy

MRC 下的实现是:

- (void)setDog:(MJDog *)dog
{
    if (_dog != dog) {
        [_dog release];
        _dog = [dog copy];//这里是copy 操作
    }
}

- (MJDog *)dog
{
    return _dog;
}

注意:@property (copy,nonatomic)NSMutableArray *dataArr;// 不推荐这样写,因为,对 [dataArr copy] 以后会生成不可变的数组,无法在修改dataArr内容。所有这里要使用 strong 修饰!字符串 都一般 copy 修饰:保证字符串是不可变的,不论外面传入是 可变 还是 不可变的,都保证自己的是 不可变的。

自定义 Copy 协议

OC 自定义类 实现 copy 操作:
(1)遵守 协议 协议
(2) 实现 -(id) copyWithZone: 方法

- (id)copyWithZone:(NSZone *)zone
{
    MyPerson *person = [[MyPerson allocWithZone:zone] init];
    person.age = self.age;//添加原有属性
    person.weight = self.weight;
    return person;
}

那么 这个自定义的类就可以 使用copy 操作了
Myperson *per = [Myperson alloc] init];
Myperson *per2 = [per copy];

6. weak底层的实现原理

https://www.jianshu.com/p/f331bd5ce8f8

7.AutoreleasePool的原理和实现

https://www.jianshu.com/p/1b66c4d47cd7

你可能感兴趣的:(iOS内存管理注意点)