OC 学习记录随笔 之内存管理

总资料
全是随笔 笔记 与 学习资料。没有规律。

一、 CADisplayLink、NSTimer

  • CADisplayLink、NSTimer 都会对target产生强引用、如果target又对他们强引用,则会循坏引用。

以下是循环引用的解决方法。

1.1 循环引用

1.1.1 解决方法1 - block

//没有循环引用
   self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    
    
        
    }];


//有循环引用, 因为timer 内部包含有target属性,造成循环引用
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(xxx) userInfo:nil repeats:YES];

1.1.2 解决方法- proxy

  • 创建 中间代理对象
  • 代理对象弱引用 target
  • 代理对于将自身的所有实例方法,全部转发到target对象本身
1.1.2.1 继承与NSProxy 的实现(推荐)
  • 直接继承与NSproxy的实现
  • 直接进入消息转发,无中间查找过程,效率高
  • NSProxy 本身就是用于消息转发的类
    头文件
//MYProxy1 头文件
@interface MYProxy1 : NSProxy
@property(nonatomic, weak) id target;

+ (instancetype)proxyWithTarget:(id)target;

@end

实现文件

//MYProxy.m 文件
#import "MYProxy1.h"

@implementation MYProxy1
+ (instancetype)proxyWithTarget:(id)target {
    
    
    //NSProxy 没有init方法
    MYProxy1 *proxy = [MYProxy1 alloc];
    proxy.target = target;
    return  proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    
    
    return [self.target methodSignatureForSelector:sel];
}

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

@end
1.1.2.2 继承与NSObject 的实现 (效率低)
  • 直接继承与NSObject的实现
  • 效率低,里面有消息的查找过程,查找转发的步骤比较多
  • 需要在父类里面查找方法,当所有的搜索完毕都找不到的时候,才会进入消息转发阶段

头文件

//MYProxy 头文件
#import <Foundation/Foundation.h>

@interface MYProxy : NSObject
@property(nonatomic, weak) id target; //弱引用

+ (instancetype)proxyWithTarget:(id)target;
@end

实现文件

//MYProxy.m 文件
#import "MYProxy.h"

@implementation MYProxy

+ (instancetype)proxyWithTarget:(id)target {
    
    
    MYProxy *proxy = [[MYProxy alloc] init];
    proxy.target = target;
    return  proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    
    
    return  self.target; //将原本消息全部转发
}

@end
1.1.2.3 通用代码调用的实现

代码使用位置

//使用位置,target里面使用了代理对象,然后弱引用target
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
                                                  target:[MYProxy proxyWithTarget:self]
                                                selector:@selector(test)
                                                userInfo:nil
                                                 repeats:YES];


- (void)test {
    
    
    NSLog(@"%s", __FUNCTION__);
}
-(void)dealloc {
    
    
    NSLog(@"%s", __FUNCTION__);
    if (self.timer) {
    
    
        [self.timer invalidate];
    }
}

1.2 准确度

  • NSTimer 依赖于RunLoop,而当RunLoop任务繁重的时候,可能导致RunLoop比较耗时,导致timer触发不准确

GCD的计时器更加准时,不依赖与RunLoop

2. GCD 计时器 dispatch_source_t

  • GCD的计时器更加准时,不依赖与RunLoop
//必须强引用,不然GCD不会帮自己管理内存,所以添加为属性
@property(nonatomic, strong) dispatch_source_t timer_source;
---
    //队列 可以是主队列或者异步队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    self.timer_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    //时间
    int64_t start = 2.0; //2秒后开始
    int64_t interval = 1.0; //间隔
    dispatch_source_set_timer(self.timer_source, dispatch_time(DISPATCH_TIME_NOW, start*NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
    
    //回调
    dispatch_source_set_event_handler(self.timer_source, ^{
    
    
    //这里注意循环引用
        NSLog(@"ssss");
    });
    
    //默认是暂停的,所以需要开始
    dispatch_resume(self.timer_source);


-(void)dealloc {
    
    
    if(self.timer_source){
    
    
    //需要停止操作, 这个方法调用后,timer是不能被恢复的。
  		  dispatch_source_cancel(self.timer_source);
  		  self.timer_source = nil;
    }
}

3 内存布局

在这里插入图片描述

4. Tagged Pointer

Tagged Pointer 是64位引进的技术,iPhone5s是首个使用的手机,主要是为了节省内存和提高使用效率。

主要优化:NSNumber、NSDate、NSString 等小对象的存储

内存布局

NSNumber *a = @(1);为例,

  • 如果不适用Tagged Pointer,则需要栈上8字节指针,加上堆栈16字节的对象。还需要进行对象的创建销毁与引用计数的维护等
  • 使用了Tagged Pointer后,实质是一个伪指针,对象的指针中存储的数据变成了 Tag+Data 形式,并不是真实的堆地址(因为并没有创建),只需要一个指针8字节大小。
  • objc_msgSend() 在方法内部会先识别是否是Tagged Pointer, 如果是的话,就不进行方法调用,直接取值,节省了开销。
  • 当存储的值大的导致不能存下的时候,才会进行对象开辟,使用以前的方法。

代码混淆

设置环境变量 OBJC_DISABLE_TAG_OBFUSCATION 为 YES, 为关闭 Tagged Pointer 的数据混淆;
不然地址混淆后,不能看见真实的值。

  • 在 NSNumber 验证中,代码混淆变量有效。
  • 在 NSString 验证中,代码混淆变量有效。

具体见6. NSString 和 NSNumber 的存储位置及方式 章节查看对比。

标志位

//objc_internal.h
static inline bool _objc_isTaggedPointerOrNil(const void * _Nullable ptr)
{
    
    
    // this function is here so that clang can turn this into
    // a comparison with NULL when this is appropriate
    // it turns out it's not able to in many cases without this
    return !ptr || ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

在上面方法 可以看见是 可以通过某一二进制位 来判断是会否是Tagged Pointer 变量的。

  • MacOS下采用 LSB(Least Significant Bit,即最低有效位)为Tagged Pointer标志位。
  • iOS下则采用 MSB(Most Significant Bit,即最高有效位)为Tagged Pointer标志位。

crash 例子

@property(nonatomic, copy) NSString *name;
dispatch_queue_t q = dispatch_queue_create("0000", DISPATCH_QUEUE_CONCURRENT);

for (int i = 0; i <1000; i++) {
    
    
    dispatch_async(q, ^{
    
    
        self.name = [NSString stringWithFormat:@"abcdefghijklm"];
    });
}
  • crash 报错 *** -[CFString release]: message sent to deallocated instance 0x6000020ce340
  • 因为 self.name 的字符串内容 超过 tagged pointer 长度,所以使用的是原始存储方法,用的是堆内存。
  • 在多线程过程中,setter方法的_name 释放了在其他线程已经释放的内存导致crash。(可参考下节的strong的的setter代码)

解决方法:选其一

  1. 使用atomic 修饰,保证set方法线程安全
  2. 不适用长度超长的字符串,如:[NSString stringWithFormat:@"abcdefghij"];
  3. 直接使用 self.name = @“abcdefghijklm” 赋值,因为代码放在的是在常量区,直接取就行了,不用拷贝
  4. dispatch_async 里面加锁

关于 2 和 3 不会cash的结论可以 去 6. NSString 和 NSNumber 的存储位置及方式 章节查看内存位置

5. Strong 和 Copy 修饰NSString

@property(nonatomic, copy) NSString *name;
@property(nonatomic, strong) NSString *name;

两者有什么不一样呢。
从 setter 方法的角度来看, set 本质来说都是转换成MRC的。

//strong 
- (void)setName:(NSString *)name {
    
    
    if (_name != name) {
    
    
        [_name release];
        _name = [name retain];
    }
}

//copy
- (void)setName:(NSString *)name {
    
    
    if (_name != name) {
    
    
        [_name release];
        _name = [name copy];
    }
}
  • 可以看出,其实setter方法里面,最主要的是对 传入的形参的 处理方式不一样,一个调用的是copy。一个是retain 来添加引用计数。

6. NSString 和 NSNumber 的存储位置及方式

NSSTring

可通过地址 和 类对象 进行区别判断

    NSString *s = [NSString stringWithFormat:@"abcdefghijklm"];  //0x600002066740
    NSString *s1 = [NSString stringWithFormat:@"abc"];           //0xa000000006362613
    NSString *s2 = @"abc";                                       //0x10422f0f0
    
    NSLog(@"\n%p\n%p\n%p", s, s1, s2);
    
    NSLog(@"%@ %@ %@", [s class], [s1 class], [s2 class]);
  • s 堆区 __NSCFString

  • s1 tagged pointer NSTaggedPointerString

  • s2 字符常量区 __NSCFConstantString
    在这里插入图片描述

  • 其他:
    s1 的值 代码 正常打印,即混淆 为: 0xc95dbca6c28acc21
    禁用混淆后打印为 0xa00000000636261361a62b63为`c‘、;;;;、

NSNumber

NSNumber *n1 = [NSNumber numberWithInt:4];
NSNumber *n2 = @4;
NSLog(@"%p %p", n1,  n2);
//混淆中   [48479:2297830] 0xada560a21cde7f6c 0x1063593b0
//取消混淆 [48499:2298606] 0xb000000000000042 0x1074b43b0
可见同 NSSTring 一样,使用 @4 创建的对象,是直接存在常量区的,

7. 深拷贝、浅拷贝

  • 深拷贝:拷贝了内容,新分配内存

  • 浅拷贝:只拷贝存储的指针,和以前的公用内存

  • NSString : copy-浅拷贝、mutableCopy-深拷贝

  • NSMutableCopy: copy-深拷贝、mutableCopy-深拷贝

原因:浅拷贝只在针对拷贝前后的两个指针都是const类型,不可修改的情况下才共用内存,如果拷贝后的内容修改可能会影响 以前的内容,则一定会使用深拷贝。
在这里插入图片描述

8. retain count 引用计数存储

isa中,extra_rc 存储的就是引用计数的数量,如果数量超标后,才会存储在has_sidetable_rc的散列表中。

uintptr_t has_sidetable_rc  : 1; 
uintptr_t extra_rc          : 19

这个是散列表的struct

struct SideTable {
    
    
    spinlock_t slock;
    RefcountMap refcnts; //散列表 引用计数的数量量
    weak_table_t weak_table;
}

以下是具体的逻辑代码

//获取 引用计数数量
- (NSUInteger)retainCount {
    
    
    return _objc_rootRetainCount(self);
}

inline uintptr_t  objc_object::rootRetainCount()
{
    
    
    if (isTaggedPointer()) return (uintptr_t)this; //tagged pointer 忽略

    sidetable_lock();
    isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);
    if (bits.nonpointer) {
    
     // 判断如果是优化过后的isa 对象
        uintptr_t rc = bits.extra_rc; //这里可以看出是直接存的值,而不是别人说的 值-1
        if (bits.has_sidetable_rc) {
    
     //has_sidetable_rc 标志位为1 代表散列表有值
            rc += sidetable_getExtraRC_nolock(); //rc 加上散列表存的值
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

size_t  objc_object::sidetable_getExtraRC_nolock()
{
    
    
    ASSERT(isa.nonpointer);
    SideTable& table = SideTables()[this];  //从全局的散列表中取出 对应的table
    RefcountMap::iterator it = table.refcnts.find(this); //在从对应的table取出 遍历器
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT; //取出具体值
}

9. weak

  • weak 被存在全局的 散列表中。

  • weak 被自动销毁,是runtime的功劳

  • weak 指向的指针被释放后会自动 置为 nil的原理, 关键代码为 *referrer = nil;

  • unsafe_unretain也能实现弱引用,但是指向的对象被释放后不能被置为 nil,只会变成野指针

以下是weak 释放的实现

调用流程
- (void)dealloc 
- void _objc_rootDealloc(id obj)
- inline void objc_object::rootDealloc()
- id object_dispose(id obj)
- void *objc_destructInstance(id obj) 
- inline void  objc_object::clearDeallocating()
- NEVER_INLINE void objc_object::clearDeallocating_slow()
- void  weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
- *referrer = nil;

objc 源码分析

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is 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); //据说会释放ivar ,待验证
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true); //释放关联属性
        obj->clearDeallocating(); //在这里会寻找weak 指针进行nil操作
    }

    return obj;
}


inline void 
objc_object::clearDeallocating()
{
    
    
    if (slowpath(!isa.nonpointer)) {
    
    
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
    
    
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow(); // 进行往下释放weak,或者大引用计数
    }

    assert(!sidetable_present());
}


NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    
    
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
    
    
    //释放weak指针
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
    
    
    // 含有较大引用计数
        table.refcnts.erase(this);
    }
    table.unlock();
}

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    
    
    objc_object *referent = (objc_object *)referent_id;
	//在weak 散列表里面查找自己的所有weak 引用
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
    
    
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line()) {
    
    
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
    
    
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
    
    
        objc_object **referrer = referrers[i];
        if (referrer) {
    
    
            if (*referrer == referent) {
    
    
                *referrer = nil; //在这里真正将weak 置为nil
            }
            else if (*referrer) {
    
    
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}



备注:部分笔记包含有MJ老师的学习资料。

猜你喜欢

转载自blog.csdn.net/goldWave01/article/details/122262611