内存管理
大纲:
1、内存布局
2、taggedPointer
3、散列表
4、MRC & ARC
5、dealloc
6、强引用
一、内存布局
1、五大分区:
五大分区之外还有内核区(例如我们4GB内存的手机,只有3GB可用,那1GB给了内核区)、保留区。
3GB的由来,内核区的地址从 0xc0000000 开始,转成10进制是3221225472 /(1024*1024*1024) = 3;这就是上班说的3GB从哪来的。
五大区的空间,从 0x00400000 到 0xc0000000。
栈区(stack) 向下增长比较小,但是快
堆区(heap) 向上增长比较大,但是相对慢
全局区 全局/静态变量区 未初始化数据(.bss) 已初始化数据(.data)
常量区
代码区(.text)
堆区慢:原因是需要先通过变量找到指针的地址空间(栈区)然后再通过指针指向的堆区的地址空间去堆区找到对应的内容。
栈区快:是通过寄存器直接访问到栈的内存空间。所以比堆区的访问块,
例:
NSObject * obj = [[NSObject alloc] init];
(lldb) po obj
<NSObject: 0x600002405560> //堆区
(lldb) po &obj
0x00007ffee58eb1a0 //栈区
栈区:
函数(函数指针)、方法(函数地址)
创建链式变量时由编译器自动分配,在不需要的时候自动清除的变量的存储区。通常存的是局部变量、函数参数等。在程序执行期间可以动态的扩展和收缩。
堆区:
通过alloc分配的对象,lblock copy
通过alloc、new 创建的对象所分配的内存块。MRC下需要程序员手动释放。在程序执行期间可以动态的扩展和收缩。
全局区(全局/静态区)
BSS段(未初始化数据):未初始化的全局变量、静态变量
DATA数据段(已初始化数据):已初始化的全局变量、静态变量
常量存储区:
里面存放的是常量,不允许修改。一般值都是存放在这个地方。
代码区:
text(代码段):程序代码,加载到内存中。
栈区内存地址:一般为:0x7开头
读取内存地址:一般为:0x6开头
数据段、BSS内存地址:一般为:0x1开头
2、面试题
1、全局变量 和 局部变量 在内存中是否有区别?如果有,是什么区别;
1)、定义的位置不同个。全局变量定义在相应的全局储存区域(在类的外面);局部变动定义在局部的空间。
2)、访问权限也不一样
例:
@implementation ViewController
static int bssA; //全局变量
static NSString *bssStr2 = @"ljl";//全局变量
- (void)viewDidLoad {
[super viewDidLoad];
int a = 10; //局部变量
}
2、Block 是否可以直接修改全局变量?
可以。
因为全局变量作用域非常大,在这个类的Block中也是其全局变量的作用域空间内,所以可以直接访问。
block访问过程中要进行一些copy,主要因为一些访问不到,跨域访问难等问题。
[UIView animateWithDuration:1 animations:^{
bssStr2 = @"test";
}];
3、static(静态修饰)
static 修改的变量是可以修改的。只针对文件有效,在不同的地方地址不同,也就是在一个文件里是一个,在不同的文件中不是同一个。
//LJLPerson.h
#import <Foundation/Foundation.h>
static int personNum = 100;
NS_ASSUME_NONNULL_BEGIN
@interface LJLPerson : NSObject
-(void)run;
+(void)eat;
@end
//***************************************************
//LJLPerson.m
#import "LJLPerson.h"
@implementation LJLPerson
-(void)run{
personNum++;
NSLog(@"LJLPerson内部:%@-%p--%d",self,&personNum,personNum);
}
+(void)eat{
personNum++;
NSLog(@"LJLPerson内部:%@-%p--%d",self,&personNum,personNum);
}
- (NSString *)description{
return @"";
}
@end
// LJLPerson+LJL.h
#import "LJLPerson.h"
@interface LJLPerson (LJL)
- (void)cate_method;
@end
//**********************************************
// LJLPerson+LJL.m
#import "LJLPerson+LJL.h"
@implementation LJLPerson (LJL)
- (void)cate_method{
NSLog(@"LJLPerson+LJL内部:%@-%p--%d",self,&personNum,personNum);
}
@end
// LJLMemoryManagementVC.m
- (void)viewDidLoad {
NSLog(@"************静态区安全测试************");
// 100 可以修改
// 只针对文件有效 -
NSLog(@"vc:%p--%d",&personNum,personNum); // 100
personNum = 10000;
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[[LJLPerson new] run]; // 100 + 1 = 101
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[LJLPerson eat]; // 102
[[LJLPerson new] run]; //103
NSLog(@"vc:%p--%d",&personNum,personNum); // 10000
[[LJLPerson alloc] cate_method];
}
2020-03-28 22:02:12.510058+0800 filedome[90875:2464642] ************静态区安全测试************
2020-03-28 22:02:12.510338+0800 filedome[90875:2464642] vc:0x10b77a140--100
2020-03-28 22:02:12.510718+0800 filedome[90875:2464642] vc:0x10b77a140--10000
2020-03-28 22:02:12.511144+0800 filedome[90875:2464642] LJLPerson内部:-0x10b77a2e0--101
2020-03-28 22:02:12.511267+0800 filedome[90875:2464642] vc:0x10b77a140--10000
2020-03-28 22:02:12.511383+0800 filedome[90875:2464642] LJLPerson内部:LJLPerson-0x10b77a2e0--102
2020-03-28 22:02:12.511531+0800 filedome[90875:2464642] LJLPerson内部:-0x10b77a2e0--103
2020-03-28 22:02:12.511662+0800 filedome[90875:2464642] vc:0x10b77a140--10000
2020-03-28 22:02:12.511824+0800 filedome[90875:2464642] LJLPerson+LJL内部:-0x10b77a2e4--100
由上面的代码测试可以知道。不同的文件里面 personNum 地址不一样,不是同一个东西。
在 ViewController 里面 personNum 的地址是 0x10b77a140
在 LJLPerson 里面 personNum 的地址是 0x10b77a2e0
在 LJLPerson+LJL 里面 personNum 的地址是 0x10b77a2e4
地址是不一样的,也就说明他们不是同一个内容,那么在不同的地方进行修改的话就互不影响。所以
- 在 ViewController 中修改成10000后一直打印的都是10000。
- LJLPerson 里面修改不影响他之外的,但是影响他自身内部的,所以run 和 eat 操作后值会改变。
- LJLPerson+LJL 访问的是 static int personNum = 100; 这行代码所以拿到的还是100。
全局静态变量
优点:
不管对象方法还是类方法都可以访问和修改全局静态变量,并且外部类无法调用静态变量,定义后只会指向固定的指针地址,供所有对象使用,节省空间。
缺点:
存在的生命周期长,从定义直到程序结束。
建议:
从内存优化和程序编译的角度来说,尽量少用全局静态变量,因为存在的声明周期长,一直占用空间。程序运行时会单独加载一次全局静态变量,过多的全局静态变量会造成程序启动慢,当然就目前的手机处理器性能。
局部静态变量
优点:
定义后只会存在一份值,每次调用都是使用的同一个对象内存地址的值,并没有重新创建,节省空间,只能在该局部代码块中使用。
缺点:
存在的生命周期长,从定义直到程序结束,只能在该局部代码块中使用。
建议:
局部和全局静态变量从本根意义上没有什么区别,只是作用域不同而已。如果值仅一个类中的对象和类方法使用并且值可变,可以定义全局静态变量,如果是多个类使用并可变,建议值定义在model作为成员变量使用。如果是不可变值,建议使用宏定义
4、extern
跨域访问的时候经常需要 extern。它的作用是声明外部全局变量。这里需要特别注意extern只能声明,不能用于实现。
引申:
多用类型常量,少用#define预处理指令。(好处这里就不说了)例如:
static const NSTimeInterval kAnimationDuration = 1.0; //推荐
#define ANIMATION_DURATION 1.0 //不推荐
static修饰符则意味着该变量仅在定义此变量的编译单元中可见。但有时需要对外公布某个常量。例如通知的名字,发送通知,需要使用通知名称,注册通知也需要,所以此时这个名字可以声明为一个外界可见的常值变量。
在发送通知的控制器的.h文件中声明XXVCLoginSuccessNotification 登录成功的通知名称
//LJLMemoryManagementVC.h
#import <UIKit/UIKit.h>
extern NSString * _Nullable const XXVCNotification;
@interface LJLMemoryManagementVC : UIViewController
@end
//*******************************************************
//LJLMemoryManagementVC.m
#import "LJLMemoryManagementVC.h"
NSString *const XXVCNotification = @"XXVCNotification";
@interface LJLMemoryManagementVC ()
//点击屏幕的时候发送通知
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[[NSNotificationCenter defaultCenter] postNotificationName:XXVCNotification object:nil];
}
//**************************************************
//ViewController.m
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loginSucess) name:XXVCNotification object:nil];
}
-(void)loginSucess{
NSLog(@"通知接收成功成功");
}
小结:
1、内存布局
核心区
五大分区:
栈区 向下增长比较小,快
堆区 向上增长比较大,慢
全局区 未初始化数据(.bss段) 已初始化数据(.data段)
常量区 里面存放的是常量,不允许修改。一般值都是存放在这个地方。
代码段(.text)
保留区
2、
堆区慢的原因是:需要先通过变量找到指针的地址空间(栈区)然后再通过指针指向的堆区的地址空间去堆区找到对应的内容。
栈区快:是通过寄存器直接访问到栈的内存空间。所以比堆区的访问块,
3、
栈区:函数(函数指针)、方法(函数地址)
堆区:通过alloc分配的对象,block copy
全局区(全局/静态区)
BSS段(未初始化数据):未初始化的全局变量、静态变量
数据段(已初始化数据):已初始化的全局变量、静态变量
常量存储区:
里面存放的是常量,不允许修改。一般值都是存放在这个地方。
代码区(text段):程序代码,加载到内存中。
4、Block 可以直接修改全局变量。
因为全局变量作用域非常大,在这个类的Block中也是其全局变量的作用域空间内,所以可以直接访问。
block访问过程中要进行一些copy,主要因为一些访问不到,跨域访问难等问题。
5、static(静态)修改的变量是可以修改的。只针对文件有效,在不同给的地方地址不同,也就是在一个文件里是一个,在不同的文件中不是同一个。
extern(外部的)跨域访问的时候需要,它的作用是声明外部全局变量。这里需要特别注意extern只能声明,不能用于实现。
例:
extern NSString * _Nullable const XXVCNotification; //.h文件
NSString *const XXVCNotification = @"XXVCNotification"; //.m文件
二、内存管理方案
1、TaggedPointer
TaggedPointer:(标记指针)小对象 - NSNumber,NSDate
NONPOINTER_ISA:非指针行isa(内存优化,isa声明一个指针8个字节,64位存储数据,有其他数据,是否有引用计数、弱引用等)
散列表:引用计数表,弱引用表
//MARK: - taggedPointer 面试题
- (void)taggedPointerDemo {
self.queue = dispatch_queue_create("com.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"cooci"];
NSLog(@"%@",self.nameStr);
});
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//出现崩溃的原因有三种可能
// 1、多线程
// 2、setter getter
// 3、通过汇编看到调用有release,第三种可能是release出错
//在多线程的时候,可能会出现一个线程中还没有释放完,时间片切换到了另个一线程去释放,因为这个时候前面那个线程没有释放完成,这个时候对象标记的就是未释放也即是旧值仍是nameStr ,需要释放。这个线程中赋值新值旧值仍需要释放,就会释放两次。但是于此通知之前的那个线程也会在进行释放。这样就释放了多次。
/**
一个值进来的话
retian newvalue(新值+1)
realase oldvalue(旧值释放)
taggedpointer 影响
*/
NSLog(@"来了");
for (int i = 0; i<10000; i++) {
dispatch_async(self.queue, ^{
self.nameStr = [NSString stringWithFormat:@"一个相对较长的测试字符串"];
NSLog(@"%@",self.nameStr);//这里会崩溃
});
}
}
在崩溃的地方,汇编调试。可以发现先是调用了objc_release,然后崩溃的,可能与release 有关
// NSLog 断点
0x108004218 <+104>: callq *0x15e7a(%rip) ; (void *)0x0000000108347990: objc_release
-> 0x10800421e <+110>: movq -0x18(%rbp), %rax
//奔溃的地方
libobjc.A.dylib`objc_release:
0x108347990 <+0>: testq %rdi, %rdi
0x108347993 <+3>: je 0x108347997 ; <+7>
0x108347995 <+5>: jns 0x108347998 ; <+8>
0x108347997 <+7>: retq
0x108347998 <+8>: movq (%rdi), %rax
-> 0x10834799b <+11>: testb $0x4, 0x20(%rax)
上面代码中的第二个循环,在“一个相对较长的测试字符串”后面的打印会出现崩溃。出现崩溃的原因有三种可能
1、多线程
2、setter getter (在set/get 与多线程同时用的时候容易出现崩溃线程不安全问题,但是这里考察的不是这个所以不多说看3)
3、通过汇编看到调用有release,第三种可能是release出错
在多线程的时候,可能会出现一个线程中还没有释放完,时间片切换到了另个一线程去释放,因为这个时候前面那个线程没有释放完成,这个时候对象标记的就是未释放也即是旧值仍是nameStr ,需要释放。这个线程中赋值新值旧值仍需要释放,就会释放两次。但是于此通知之前的那个线程也会在进行释放。这样就释放了多次。
崩溃也是release 里面,猜测是释放的时候出的问题。
上面的不会崩溃,下面的会崩溃的原因:
因为他们两个的类型不一样。一个NSTaggedPointerSting *,一个是正常的NSCFString *
可以猜测主要就是 taggedPointer 的影响,看一下 objc源码 demo -> 001-内存管理 -> 1-objc源码
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
release 的时候判断一下对象是否存在如果不存在直接返回,如果存在是否是taggedPointer的,是的话直接返回不会release,否者再release。所以上面的例子,第一种不会release 所以不会崩溃。
同理,retain 也是一样的,retain 所以不用release。
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
在分析类的加载的时候,read_images 读取类的镜像文件的时候有调用 initializeTaggedPointerObfuscator();
initializeTaggedPointerObfuscator,在 10.14 之后做了升级。
10.14之前 objc_debug_taggedpointer_obfuscator 是0,之后是一个_OBJC_TAG_MASK(通过随机数得到的,怎么得到不重要)。
objc_debug_taggedpointer_obfuscator 是 taggedPointer 的标
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
全局搜索 objc_debug_taggedpointer_obfuscator 找到如下。传过来了一个地址,然后异或操作。
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
^ (异或,相同返回0,不同返回1) 同一个值,得到原来的值,
a= 1000 0001
^0001 1000
得到 b = 1001 1001
^0001 1000
得到 a 1000 0001
测试一下
#import "LJLMemoryManagementVC.h"
//objc_debug_taggedpointer_obfuscator 是内部变量,需要对外声明引用出来。
extern uintptr_t objc_debug_taggedpointer_obfuscator;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str1 = [NSString stringWithFormat:@"a"];
NSString *str2 = [NSString stringWithFormat:@"b"];
NSLog(@"%p-%@",str1,str1);
NSLog(@"%p-%@",str2,str2);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2));
NSNumber *number1 = @1;
NSNumber *number2 = @1;
NSNumber *number3 = @2.0;
NSNumber *number4 = @3.2;
NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number2));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number3));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number4));
}
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
2020-03-29 16:03:54.168825+0800 filedome[4406:140557] 0xac22cfa72c7fc9f0-a
2020-03-29 16:03:54.168907+0800 filedome[4406:140557] 0xac22cfa72c7fc9c0-b
2020-03-29 16:03:54.169026+0800 filedome[4406:140557] 0xa000000000000621
2020-03-29 16:03:54.169132+0800 filedome[4406:140557] __NSCFNumber-0xbc22cfa72c7fcff3-1 - 0xb000000000000012
2020-03-29 16:03:54.169211+0800 filedome[4406:140557] 0xb000000000000012
2020-03-29 16:03:54.169281+0800 filedome[4406:140557] 0xb000000000000025
2020-03-29 16:03:54.169347+0800 filedome[4406:140557] 0xc22afa72ffc6a81
解码得到的地址是:地址+值
0xb000000000000012 最后的 2 这个用于标记是int(float则为4,Int为2,long是3, double为5),而最高4位的“b”表示是NSNumber类型;其余56位则用来存储数值本身内容。当存储用的数值超过56位存储上限的时候,那么NSNumber才会用真正的64位内存地址存储数值,然后用指针指向该内存地址。(如果数值长度超过64位,那么就crash)。
因为Tagged Pointed不是一个真正的对象,所以其没有isa。不过只要避免在代码中直接访问对象的isa变量,就没问题。具体如Tagged Pointer 怎么访问类方法列表,之后再详细看下,也许是根据最够为的类型标记,然后调用对应的class方法列表。
最开始打印的a 、b的地址是 0xac22cfa72c7fc9f0 这样的的根本看不出来是什么,通过刚才源码分析,通过_objc_decodeTaggedPointer_(id ptr) 去解一下。
objc_debug_taggedpointer_obfuscator 是内部变量,需要对外声明引用出来。 extern uintptr_t objc_debug_taggedpointer_obfuscator;
然后再打印解码后的内容,可以看到 例如0xb000000000000012,这是number1。 这里的 b 代表数值 NSNumber 类型,1 就是我们number1 的值
在看一下 str2 0xa000000000000621 a 代表是 string, 62转成十进制就是98 是ASCLL码 对应 b 。
具体的类型有很多种,在源码中搜一下 objc_tag_ns 就可以看到。
那为什么倒数第二位才是值,为什么不放到最后一位呢?
在源码 _objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value) 中根据传入的类型进行了位移运算,但是什么时候调用的,因为没有开源所以就不知道了。
用途就是可以在控制台输出的时候直接能看到对象的类型和值。
taggedPointer 不支持retain、release。其实retain、telease 也是耗时的,所以对小对象来说没有比较,苹果就做了这样的优化。看别人测试的结果是,taggedPointer 的创建速度是普通的100多倍;访问速度是3倍左右。
一般情况下临界线是 8-10位,如果超过就是普通的,如果没超过就是小对象。
所以将上面测试2循环中的
self.nameStr = [NSString stringWithFormat:@"一个相对较长的测试字符串"];
//改成
self.nameStr = [NSString stringWithFormat:@"1234"];
在去遍历就不会崩溃了,因为这个是的对象是taggedPointer 不需要release
但是如果是 self.nameStr = [NSString stringWithFormat:@"一"];这种中文字符串的,一直是普通字符串
那么taggedPointer类型的对象怎么释放呢?
因为没有引用计数的处理,所以由系统自动回收。
部分参考:
https://www.jianshu.com/p/e354f9137ba8
小结:
1、Tagged Pointer 专门用来存储小的对象,例如 NSNumber 和 NSDate;
2、Tagged Pointer 指针的值不再是地址了,而是真正的值。实际上它不再是一个对象了,它只是一个披着对象皮的变量而已。它的内存并不存储在堆中,也不需要malloc 和 free
3、在内存读取上有着3倍的效率,创建时比普通的块106倍左右。
4、taggedPointer 不需要去retain、release。
2、NONPOINTER_ISA
nonpointer:
表示是否对isa 指针开启指针优化
0:纯isa 指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等。
has_assoc: 关联对象标志位,0没有,1存在
has_cxx_dtor: 该对象是否有C++ 或者Objc 的析构器,如果有析构函数,则需要做析构逻辑,如果没有则可以更快的释放对象
shiftcls:存储类指针的值,开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
magic:用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced:次对象是否被指向或者曾经指向一个ARC 的弱变量,没有弱引用的对象可以更快释放。
deallocating:标志对象是否正在释放内存
has_sidetable_rc: 当对象引用计数大于10 时,则需要借用该变量存储进位。
extra_rc:当表示该对象的引用计数值,实际上是引用计数值减1,例如,如果对象的引用计数为10,那么 extra_rc 为9。如果引用计数大于10,则需要使用下面的 has_sidetable_rc。
3、MRC & ARC
引用计数存在哪?
retain 是如何处理
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
objc_object::retain()
{
ASSERT(!isTaggedPointer());//断言是否是raggedPointer
//快速路径,然后 rootRetain
if (fastpath(!ISA()->hasCustomRR())) {
return rootRetain();
}
//慢速处理 发送 retain 消息
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}
objc_object::rootRetain()
{
return rootRetain(false, false);
}
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
......
// retain 引用计数处理
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);//拿到isa 的 bits 里面会有位的情况
newisa = oldisa;
// 散列表的引用计数表 进行处理 ++
if (slowpath(!newisa.nonpointer)) {//判断是否是nonpointer isa
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
//判断是否正在析构
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
//nonpointer isa 进行 addc 引用计数处理往 newisa.bits 里面 RC_ONE 添加(也就是isa 的 extra_rc++)
//# if __arm64__
//# define RC_ONE (1ULL<<45)
//1左移45位 x86下是56位
//carry 位数里面加满了(超负荷)
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;//如果超负荷,将一半存到 newisa.extra_rc 里面
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);//如果超负荷,将另一半存到 散列表的引用计数 里面
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();//散列表
return (id)this;
}
objc_object::sidetable_unlock()
{
SideTable& table = SideTables()[this];
table.unlock();
}
//散列表
struct SideTable {
spinlock_t slock; //锁
RefcountMap refcnts; //引用计数表
weak_table_t weak_table; //弱引用表
1、判断是否是 nonpointer isa(如果是引用计数存在isa 里面,如果不是存在散列表里)-> 散列表:lock(锁)+ 引用技术表 + 弱引用表
在源码中可以看到很多地方都是 SideTables。
在整个内存中散列表是多张。不同的表做不同的事情,优化加锁解锁的速度。
如果是一张表,每次进行操作的时候所有数据都暴露了出来不利于安全。而且一张表的话会非常大,对象比较多操作表的频次很高,查询能性能就非常低了。
也不可能无限制的开表,因为开表也是需要消耗性能的。网上有说最多有64张表,待查阅验证。
散列表是通过哈希实现的,可以通过下标去查询。是一个哈希结构,通过哈希函数获取下标,然后在通过下标去访问到数组中的元素。
1、retain
rootRetain retain 引用计数处理
1、do whil 循环。拿到isa 的 bits 里面会有位的情况
2、判断如果是非 nonpointer isa 需要对散列表的引用计数表进行处理
3、然后判断是否析构,如果析构的话 return nil
4、如果是 nonpointer isa 进行 addc 引用计数处理往 newisa.bits 里面 RC_ONE 添加(也就是isa 的 extra_rc++)(额外的)
# if __arm64__
# define RC_ONE (1ULL<<45)
1左移45位 x86下是56位
carry isa位数里面加满了(超负荷) 如果超负荷,将一半存到 newisa.extra_rc 里面,将另一半存到 散列表的引用计数表 里面
优先考虑isa,因为isa 比较快。不需要散列表的加锁解锁等。
为什么用哈希不用 链表 或者 数组 结构?
链表:
增加和删除比较快。查询比较慢
因为是 1->2->3 这种形式。例如1和2之间插入4,直接断开1->2 1指向4,4再指向2 即 1->4->2->3 就行了。断开是一样的,但是查询的话需要从1连着练一个一个的去查询。
数组:
增删比较慢。查询快
插入的话需要先 mutableCopy,copy过来之后再往前面后后面插入。
哈希
是结合了链表和数组
class StripedMap {
......
//哈希函数获取下标
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
public:
T& operator[] (const void *p) {
//通过下标获取数组中的元素
return array[indexForPointer(p)].value;
}
const T& operator[] (const void *p) const {
return const_cast<StripedMap<T>>(this)[p];
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
//根据 this地址指针 去SideTables里获取相应的散列表 table。
SideTable& table = SideTables()[this];
//加锁
table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
//#define SIDE_TABLE_RC_ONE (1UL<<2) 左移两位
//为什么要左移两位,因为前面的2位并不是引用计数
//第一个是weak 表示位 第二个是 dealloc 标识位
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
2、release
跟retain 差不多。nonpointer isa 的话需要判断如果isa 里面的减完了,然后去操作散列表,如果没有sidetable 直接返回了。
retainCount
面试题:
alloc 出来的对象 retainCount(引用计数)是多少?
NSObject * obj = [[NSObject alloc] init];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)obj));
2020-03-29 23:37:34.563733+0800 filedome[9510:316069] 1
这时候的引用计数打印的是1,实际上真的是1吗?不是的话那是多少?
实际上这时候的引用计数是 0。那么为什么打印出来的是1 呢?
查看 rootRetainCount。
objc_object::rootRetainCount() // 1
{
if (isTaggedPointer()) return (uintptr_t)this;//判断TaggedPointer
sidetable_lock();//加锁
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
// bits.extra_rc = 0;
// 是 nonpointer isa
uintptr_t rc = 1 + bits.extra_rc; //1 + isa 里面存的引用计数
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock(); //如果有 散列表 再加上散列表里面的引用计数
}
sidetable_unlock();
return rc; // 1
}
sidetable_unlock();
return sidetable_retainCount();//非 nonpointer isa ,直接取散列表里的引用计数
}
根据源码可以知道,NSObject 初始化的是 nonpointer isa ,retainCount 里面返回的结果是 1+isa.bits.extra_rc内的引用计数,打印的是1,说明isa内的引用计数(isa.bits.extra_rc)是0。那么那么alloc 对象的引用计数是 0.
3、autorelease
暂时没有分析,后面分析的话补上。
4、dealloc
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
//进行各种标识位的判断, 然后进行释放。否者就是普通的isa
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);//释放
}
else {
object_dispose((id)this);
}
}
object_dispose(id obj)
{
if (!obj) return nil;
// weak
// cxx
// 关联对象
// ISA 64
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor(); //cxx 函数
bool assoc = obj->hasAssociatedObjects();// 是否有关联对象
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);//删除关联对象
obj->clearDeallocating();//处理散列表
}
return obj;
}
//处理散列表
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();
}
assert(!sidetable_present());
}
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_clear_no_lock(&table.weak_table, (id)this);//清理弱引用表
}
if (isa.has_sidetable_rc) { // 清理引用计数表(不是清理引用计数,是清理引用计数表)
table.refcnts.erase(this);//引用表里面的 erase(清除)
}
table.unlock();
}
流程:
1、进行各种标识符判断不是普通的 isa,然后进行释放。
2、判断是否有cxx函数、是否有关联对象等,有删除关联对象,处理散列表
3、如果不是nonpointer isa 进行散列表清空。如果是 判断是否有弱引用计数表 || 是否有散列表引用计数
4、清空弱引用表,有的话。清理引用计数表(不是清理引用计数,是清理引用计数表)
三、循环应用 + 强引用
#import "LJLMemoryManagementVC.h"
#import <objc/runtime.h>
#import "LJLProxy.h"
static int num = 0;
@interface LJLMemoryManagementVC ()
@property(nonatomic,strong) NSTimer * timer;
@property(nonatomic,strong) LJLProxy * proxy;
@end
@implementation LJLMemoryManagementVC
- (void)viewDidLoad {
[super viewDidLoad];
- (void)fireHome{
num++;
NSLog(@"hello word - %d",num);
}
- (void)dealloc{
// [self.timerWapper lg_invalidate];
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
循环引用 + 强引用
在返回上级页面的时候dealloc 是不会别调用的。
1、
self -> timer -> self 这里是传参进去的,但是在timer 内部进行了强引用。返回不走dealloc
RunLoop -> timer -> self timer是加到RunLoop中,的RunLoop 对Timer强持有,timer强持有self 打不破强持有无法释放
// 需要添加到RunLoop
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
// 无需添加到RunLoop
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
NSLog(@"%@",[NSThread currentThread]);
}
2、使用weakSelf
这里weakSelf 不能打破强引用
self -> timer -> weakSelf -> self 这里是传参进去的,但是在timer 内部进行了强引用。返回不走dealloc
实际上是 RunLoop -> timer -> weakSelf -> self timer是加到RunLoop中,的RunLoop 对Timer强持有,timer强持有weakSelf 打不破强持有无法释放
self -> block -> weakSelf -> self 为什么可以打破循环应用,timer不行呢? weakSelf 存在弱引用表,按理说应该是e可以释放的呀?
__weak typeof(self) weakSelf = self;
这句代码的操作时将self 添加到了弱引用计数表。weakSelf 是一个新的变量与 self 不是同一个对象。他们所指向的地址是一样的。打断点,然后控制台打印一下就知道了。指向的内存地址是一样的,但是取地址他们不是同一个。
(lldb) po self
<LJLMemoryManagementVC: 0x7fa22662d560>
(lldb) po weakSelf
<LJLMemoryManagementVC: 0x7fa22662d560>
(lldb) p &self
(LJLMemoryManagementVC **) $2 = 0x00000001294fbfc8
(lldb) p &weakSelf
(LJLMemoryManagementVC *const *) $3 = 0x00007ffee88561b0
因为weakSelf 和 self 是两个对象,同时指向当前VC,那么会对这两个对象进行引用计数处理。但是self 的引用计数没有变。
timer 强持有weakSelf,间接的直接操作的是slef。block 能通过weakSelf 打破循环是因为
block 里面拿到的是传过去的对象的指针地址,object 强制有的是 weakSelf 的指针地址,而不是self。因为block 持有的是weakSelf 临时变量的指针地址,所以就打破了循环引用。
block 只有的是weakSelf 的指针地址。timer 强持有的是 weakSelf 对象(等同于self 指针指向是一样的),所以一个能打破循环引用一个不能。
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;//指针地址
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object);
*dest = object;
break;
NSLog(@"打印引用计数1 %ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
// 打印引用计数
NSLog(@"打印引用计数2 %ld",CFGetRetainCount((__bridge CFTypeRef)self));
// 2020-03-30 10:55:16.105493+0800 filedome[12713:407952] 打印引用计数1 8
// 2020-03-30 10:55:16.108810+0800 filedome[12713:407952] 打印引用计数2 8
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
3、既然dealloc 不能走可以放到 viewWillDisappear 中处理timer 的释放
但是这样会存在另一个问题,如果当前VC push 跳转下一层,那么当前VC 是还在栈里面的,这个时候也会释放了timer。而timer 应该是在我们VC 退出只有才应该被释放掉的,所以这样做不行
- (void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
// push 到下一层返回就不走了!!!
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
4、放到 didMoveToParentViewController 中处理timer 的释放
移除viewController后,该方法被调用。这样是可行的
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 无论push 进来 还是 pop 出去 正常跑
// 就算继续push 到下一层 pop 回去还是继续
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
5、中间者模式
NSProxy 虚基类 地位等同于NSObject 使用的话需要用一个其子类来使用。VC 传到工具内部使用的话用 weak 修饰,不能强持有。
这样通过Proxy 去持有,VC就不会被强持有,就能正常释放就能走 dealloc 了,timer也就正常释放了。
self.proxy = [LJLProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}
// LJLProxy.h
#import <Foundation/Foundation.h>
@interface LJLProxy : NSProxy
+(instancetype)proxyWithTransformObject:(id)object;
@end
//***********************************
// LJLProxy.m
#import "LJLProxy.h"
@interface LJLProxy ()
@property(nonatomic, weak) id object;//用于接收VC,必须要用weak 不能强引用了
@end
@implementation LJLProxy
+(instancetype)proxyWithTransformObject:(id)object{
LJLProxy * proxy = [LJLProxy alloc];
proxy.object = object;
return proxy;
}
// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
// 转移
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
@end