底层原理-32-内存管理之SideTables/retaincount/强引用问题

509 阅读9分钟

上一篇我们了解了内存管理的几种方式其中SideTables主要介绍了引用计数表。我们继续探讨下SideTables

1.SideTables

SideTables可以理解为一个全局的hash数组,里面存储了SideTable类型的数据,其长度为64。 但是SideTablse并不是一个被定义的数据类型,他是一个全局静态函数,返回值是StripedMap类型,所以SideTables就是StripedMap类型的,我们查看源码:

static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

static StripedMap<SideTable>& SideTables() {

    return SideTablesMap.get();

}

继续看下

image.png 它是一个范型,在真机情况下散列表的个数为8,模拟器为64个,一些方法获取对应的sideTable 为什么这么设计?

  1. 如果散列表只有一张表,意味着全局所有的对象都会存储在一张表中,都会进行开锁解锁(锁是锁整个表的读写)。当开锁时,由于所有数据都在一张表,则意味着数据不安全
  2. 如果每个对象都开一个表,会耗费性能,所以也不能有无数个表.
  3. 对于T类型还是有所要求的(就是能够进行锁操作)。而在SideTables中,T即为SideTable类型。
  • SideTable

image.png SideTable的定义有三个成员:

  • spinlock_t slock : 自旋锁,用于上锁/解锁 SideTable
  • RefcountMap refcnts :以DisguisedPtr<objc_object>为key的哈希表,用来存储OC对象的引用计数(仅在未开启isa优化 或 在isa优化情况下isa_t的引用计数溢出时才会用到)。
  • weak_table_t weak_table : 存储对象弱引用指针的哈希表。是OC weak功能实现的核心数据结构。

2. retainCount

我们上一篇分析了isa中extra_rc和散列表中引用计数的retainrelease操作。那么我们看下它retainCount的流程。

NSObject *objc = [NSObject alloc];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

我们知道打印的结果是1,我们看下它的实现

  • 进入retainCount -> _objc_rootRetainCount -> rootRetainCount源码,前面都是跳转我们看下rootRetainCount其实现如下
inline uintptr_t 

objc_object::rootRetainCount()

{

    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();

    isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);

    if (bits.nonpointer) {

        uintptr_t rc = bits.extra_rc;//获取isa中extra_rc的个数

        if (bits.has_sidetable_rc) {

            rc += sidetable_getExtraRC_nolock();

        }

        sidetable_unlock();

        return rc;

    }

    sidetable_unlock();

    return sidetable_retainCount();

}

我们打断点这里表示初始化的时候已经是1了

image.png 我们在之前的版本源码是在获取的时候引用计数+1

image.png 最新版本是对象初始化的时候绑定isa时候定义extra_rc1.

image.png

我们使用__weak修饰对象,打印它的引用计数,打印结果为1,1,2.

NSObject *objc = [NSObject alloc];

        NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

        __weak typeof(NSObject) *weakObjc = objc;

        NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

        NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)weakObjc));

image.png

  • 表明在时候__weak修饰的时候本身的引用计数不会发生改变,所以objc的引用计数还是1。__weak 修饰的对象进行了指针拷贝。

  • 我们在使用对象比如nslog的时候弱引用对象的isa会进行retain操作,在通过__weak指针寻找对象的时候 objc_loadWeak->objc_loadWeakRetained->rootTryRetain->rootRetain->newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry)因此我们打印weakObjc的引用计数为2.

  • 我们当使用他的时候就会使返回的retainCount + 1(注意这里并非retainCount本身)

  • 持不持有一个对象,是看它是否导致对象的retainCount + 1;而不是看他是否指向那个地址.

image.png

  • 我们使用的时候会导致引用计数+1,但是是在objc_autorelease的作用域,当我们出了作用域后,引用计数还是之前的。

image.png

  • 我们知道散列表中包含一个弱引用表,专门用来存储弱引用对象的。

3. weak_table_t

  • weak_table是一个哈希表的结构, 根据weak指针指向的对象的地址计算哈希值, 哈希值相同的对象按照下标 +1 的形式向后查找可用位置, 是典型的闭散列算法
struct weak_table_t {
    weak_entry_t *weak_entries;//连续地址空间的头指针,数组
    size_t    num_entries;//数组中已经占用位置的个数
    uintptr_t mask;//数组下标最大值(即数组大小-1)
    uintptr_t max_hash_displacement;//最大哈希偏移值
};

weak_entry_t

image.png

  • 我们看下弱引用修饰对象的流程 1.我们Clang下main文件,如果.m文件中使用了weak关键字,在重写时会报cannot create _ weak reference because the current deployment target does not support weak references 。
    我们这样修饰下其中macosx-11.2.3 为你本机的sdk版本。 clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-11.2.3 main.m

image.png 我们搜索objc_ownership,在源码中没有搜索到。这里和我们开始的时候alloc一样,llvm中把方法进行了转换成了objc_initWeak image.png 2. storeWeak

image.png

  • 第一次__weakweak属性修饰的对象,没有haveOldhaveNewTrue,取出散列表中关于这个对象的散列表
  • 对象没有实现的话,初始化操作
  • 如果有新的值,则分配新的值。weak_register_no_lock
  • 不是小对象或不为nil setWeaklyReferenced_nolock 设置引用计数。
  • 解锁后回调callSetWeaklyReferenced
  1. weak_register_no_lock

image.png

  • 首先判断是否是taggedPointer,是的话不作处理
  • 确保引用的对象是可行的,判断当前对象是否在销毁,标记状态
  • 根据对象初始化weak_entry_t,通过weak_entry_for_referent方法
  • 存储到弱引用表以weak_entry_t的类型。
  1. weak_entry_for_referent

image.png 通过哈希算法找到弱引用表中的weak_entries中的weak_entry_t。 5.append_referrer image.png

  • 是否超过内联边界,没有的话尝试插入,每个表最多4个,找到空的插入
    • 没有的话就开辟新的一个一行,大小为4个weak_referrer_t,进行插入
  • 超过内联边界2的话,以占用大小超过表的3/4扩容插入
  • 否则正常通过哈希算法正常插入

image.png

  • 1:首先我们知道有一个非常牛逼的家伙-sideTable
  • 2:得到sideTable的weakTable 弱引用表 
  • 3:创建一个weak_entry_t
  • 4:把referent加入到weak_entry_t的数组inline_referrers
  • 5:把weak_table扩容一下 
  • 6:把new_entry加入到weak_table中

未命名文件-13.jpg weak修饰的对象整个流程,我们开发中不涉及循环引用的话,减少使用弱引用,流程还是比较耗时,耗费性能的。

4.强引用

4.1 强引用问题

我们日常开发中使用runloop添加定时器

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];

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

我们知道通常因为强持有,无法释放,当我们离开页面时候定时器还在执行,我们通常要手动销毁定时器。
那么为什么是无法释放呢?,官方文档说定时器对持有者进行了强引用,直到它(计时器)失效。

image.png 他们的持有关系如下:
[NSRunLoop currentRunLoop]/self->timer->self造成了循环引用无法释放。那么我们使用weak修饰self呢?

 __weak typeof(self) weakSelf = self;

self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];

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

并没有打破循环,他们的关系如下: [NSRunLoop currentRunLoop]/self->timer->weakSelf->selfRunloop的生命周期比当前页面ViewController更长,因此持有timertimer持有self,所以self无法释放

4.2 解决方法

4.2.1 手动销毁

我们平时开发中,会手动管理定时器 比如我们在离开页面的时候手动调用定时器销毁

- (void)viewWillDisappear:(BOOL)animated{

    [super viewWillDisappear:animated];

    // push 到下一层返回就不走了!!!

    [self.timer invalidate];

    self.timer = nil;

    NSLog(@"timer 走了");

}

把 核心timer 销毁 那么 强持有 - 循环引用就不存在

4.2.2 block类型的Timer

__weak typeof(self) weakSelf = self;

    self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {

        [weakSelf fireHome];

    }];

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

image.png 这里通过block进行初始化,我们之前block篇章中知道block内部参数不会造成强引用,因此timer不会强持有self,我们只要避免block循环引用即可,__weak修饰self,和一般的block注意一样。

4.2.3 中介者模式

我们因为timer会强持有引用者,从而造成循环引用,我们可以换一个持有者,从而打破循环,我们只要告诉timer执行我们想要的方法即可。

    self.target = [[NSObject alloc] init];

    class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");

    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];

    void fireHomeObjc(id obj){

    NSLog(@"%s -- %@",__func__,obj);

}

image.png 创建一个计时器,并以默认模式在当前运行runloop上调度它。 运行

image.png 当前页面没有被强引用,可以被销毁,只是targetTimer强引用,继续执行定时器。我们在dealloc销毁定时器即可

image.png

4.2.4 自定义timer

上个方法中我们提出了中间层,那么我们根据中间层自定义一个timer


- (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{

    if (self == [super init]) {

        self.target     = aTarget; // vc

        self.aSelector  = aSelector; // 方法 -- vc 释放

        

        if ([self.target respondsToSelector:self.aSelector]) { 

            Method method    = class_getInstanceMethod([self.target class], aSelector);

            const char *type = method_getTypeEncoding(method);

            class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);//方法转换,把target的

            

            self.timer      = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];

        }

    }

    return self;

}


// 一直跑 runloop

void fireHomeWapper(LGTimerWapper *warpper){

    

    if (warpper.target) { // vc - dealloc

        void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;

         lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);

    }else{ // warpper.target

        [warpper.timer invalidate];

        warpper.timer = nil;

    }

}


- (void)lg_invalidate{

    [self.timer invalidate];

    self.timer = nil;

}

  • 我们给当前类添加方法,相当于把我们tagert的Sel转换当前类的方法
  • 当前类的方法实现有target的话就向target发消息,没有的话说明target销毁了,我们关闭定时器。 总结:我们把target的sel转换为当前类的sel,在当前类sel向target发送消息

image.png

4.2.5 自定义NSProxy

  • OC是只能单继承的语言,但是它是基于运行时的机制,所以可以通过NSProxy来实现 伪多继承,填补了多继承的空白

  • NSProxy 和 NSObject是同级的一个类,也可以说是一个虚拟类,只是实现了NSObject的协议

  • NSProxy 其实是一个消息重定向封装的一个抽象类,类似一个代理人,中间件,可以通过继承它,并重写下面两个方法来实现消息转发到另一个实例

@interface LGProxy()

@property (nonatomic, weak) id object;

@end


@implementation LGProxy

+ (instancetype)proxyWithTransformObject:(id)object{

    LGProxy *proxy = [LGProxy alloc];

    proxy.object = object;

    return proxy;

}


// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。

// 转移

// 强引用 -> 消息转发


//-(id)forwardingTargetForSelector:(SEL)aSelector {

//    return self.object;

//}


//// sel - imp -

//// 消息转发 self.object

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{

    if (self.object) {

    }else{

        NSLog(@"麻烦收集 stack111");

    }

    return [self.object methodSignatureForSelector:sel];

}


- (void)forwardInvocation:(NSInvocation *)invocation{


    if (self.object) {

        [invocation invokeWithTarget:self.object];

    }else{

        NSLog(@"麻烦收集 stack");

    }

}
*********vc*******


- (void)dealloc{

    [self.timer invalidate];

    self.timer = nil;

    NSLog(@"%s",__func__);

}

我们在当前控制器dealloc销毁定时器。timer持有了自定义proxy,而proxy持有self是weak修饰的,不会增加引用计数。因此vc可以释放,vc持有的timer 和proxy也可以释放,self->timer->proxy->weakself

image.png

5. 总结

对于weak属性__weak修饰的对象有了更深刻的理解,通过探索弱引用表,添加弱引用对象过程了解了retainCount的变化,知道了作用域的作用。通过解决timer强引用问题,结合之前block循环引用的问题,感受到了对象内存的释放和管理