上一篇我们了解了内存管理的几种方式其中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();
}
继续看下
它是一个范型,在真机情况下散列表的个数为
8,模拟器为64个,一些方法获取对应的sideTable 为什么这么设计?
如果散列表只有一张表,意味着全局所有的对象都会存储在一张表中,都会进行开锁解锁(锁是锁整个表的读写)。当开锁时,由于所有数据都在一张表,则意味着数据不安全- 如果
每个对象都开一个表,会耗费性能,所以也不能有无数个表. - 对于T类型还是有所要求的(就是能够进行锁操作)。而在
SideTables中,T即为SideTable类型。
- SideTable
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和散列表中引用计数的retain,release操作。那么我们看下它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了
我们在之前的版本源码是在
获取的时候引用计数+1
最新版本是对象
初始化的时候绑定isa时候定义extra_rc为1.
我们使用__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));
-
表明在时候
__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;而不是看他是否指向那个地址.
- 我们使用的时候会导致引用计数+1,但是是在
objc_autorelease的作用域,当我们出了作用域后,引用计数还是之前的。
- 我们知道散列表中包含一个
弱引用表,专门用来存储弱引用对象的。
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
- 我们看下弱引用修饰对象的流程
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
我们搜索
objc_ownership,在源码中没有搜索到。这里和我们开始的时候alloc一样,llvm中把方法进行了转换成了objc_initWeak
2. storeWeak
- 第一次
__weak或weak属性修饰的对象,没有haveOld,haveNew为True,取出散列表中关于这个对象的散列表 - 对象没有实现的话,
初始化操作 - 如果有新的值,则分配新的值。
weak_register_no_lock - 不是小对象或不为nil
setWeaklyReferenced_nolock设置引用计数。 - 解锁后回调
callSetWeaklyReferenced
- weak_register_no_lock
- 首先判断是否是
taggedPointer,是的话不作处理 - 确保引用的对象是可行的,判断当前对象是否在
销毁,标记状态。 - 根据对象初始化
weak_entry_t,通过weak_entry_for_referent方法 - 存储到弱引用表以
weak_entry_t的类型。
- weak_entry_for_referent
通过哈希算法找到弱引用表中的weak_entries中的weak_entry_t。
5.append_referrer
- 是否超过
内联边界,没有的话尝试插入,每个表最多4个,找到空的插入- 没有的话就开辟新的一个一行,大小为
4个weak_referrer_t,进行插入
- 没有的话就开辟新的一个一行,大小为
- 超过内联边界2的话,以占用大小超过
表的3/4,扩容插入 - 否则正常通过哈希算法
正常插入。
- 1:首先我们知道有一个非常牛逼的家伙-sideTable
- 2:得到sideTable的weakTable 弱引用表
- 3:创建一个weak_entry_t
- 4:把referent加入到weak_entry_t的数组inline_referrers
- 5:把weak_table扩容一下
- 6:把new_entry加入到weak_table中
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];
我们知道通常因为强持有,无法释放,当我们离开页面时候定时器还在执行,我们通常要手动销毁定时器。
那么为什么是无法释放呢?,官方文档说定时器对持有者进行了强引用,直到它(计时器)失效。
他们的持有关系如下:
[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->self。
Runloop的生命周期比当前页面ViewController更长,因此持有timer,timer持有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];
这里通过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);
}
创建一个计时器,并以
默认模式在当前运行runloop上调度它。
运行
当前页面没有被强引用,可以被销毁,只是
target被Timer强引用,继续执行定时器。我们在dealloc销毁定时器即可
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发送消息
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
5. 总结
对于weak属性和__weak修饰的对象有了更深刻的理解,通过探索弱引用表,添加弱引用对象过程了解了retainCount的变化,知道了作用域的作用。通过解决timer的强引用问题,结合之前block循环引用的问题,感受到了对象内存的释放和管理。