28、iOS底层分析 - 内存管理

内存管理

 大纲:

 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
发布了104 篇原创文章 · 获赞 13 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/shengdaVolleyball/article/details/105202199