OC底层知识(十二) : 内存管理

  • 一、抛出一个问题:使用CADisplayLinkNSTimer 有什么注意点?1.1-1.6的demo

    • 1.1、分析:CADisplayLinkNSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用,如下在控制器里面的代码会产生 相互强引用 的问题

      • CADisplayLink(在当前控制器按返回按钮,你会发现 dealloc 方法不会走,而linkTest还在一直调用,原因是:self强引用CADisplayLink,而CADisplayLink内部又在强引用self(displayLinkWithTarget:self))。

        @property(nonatomic,strong) CADisplayLink *link;
        
        // 保证调用频率和屏幕的刷帧频率一致 60FPS
        self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
        [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        
        -(void)linkTest{
        
            NSLog(@"%s",__func__);
        }
        
        -(void)dealloc{
        
            NSLog(@"%s", __func__);
            [self.link invalidate];
        }
        
      • NSTimer(在当前控制器按返回按钮,你会发现 dealloc 方法不会走,而timerTest还在一直调用,原因是:self强引用NSTimer,而NSTimer内部又在强引用self(target:self ))。

        @property (strong, nonatomic) NSTimer *timer;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
        
        - (void)timerTest
        {
           NSLog(@"%s", __func__);
        }
        
        - (void)dealloc
        {
           NSLog(@"%s", __func__);
           [self.timer invalidate];
        }
        
    • 1.2、解决上面互相强引用的办法

      • NSTimer 有一个block的方法,我们可以利用block的弱指针来解决__weak typeof(self) weakSelf = self;,传 weakSelf 进去,如下

        @property (strong, nonatomic) NSTimer *timer;
        __weak typeof(self) weakSelf = self;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        
              [weakSelf timerTest];
        }];
        
        - (void)timerTest
        {
           NSLog(@"%s", __func__);
        }
        
        - (void)dealloc
        {
           NSLog(@"%s", __func__);
           [self.timer invalidate];
        }
        
    • 1.3、通过中间对象(代理对象)的方式来解决,下面用到了消息转发机制(会先发送消息、再动态解析、最后再消息转发)

      OC底层知识(十二) : 内存管理_第1张图片
      通过中间对象(代理对象)的方式来解决

      下面是创建了一个继承于类 :JKMiddleProxy : NSObject

      #import 
      
      NS_ASSUME_NONNULL_BEGIN
      
      @interface JKMiddleProxy : NSObject
      
      + (instancetype)proxyWithTarget:(id)target;
      @property (weak, nonatomic) id target;
      
      @end
      
      NS_ASSUME_NONNULL_END
      
      #import "JKMiddleProxy.h"
      
      @implementation JKMiddleProxy
      
      + (instancetype)proxyWithTarget:(id)target
      {
         JKMiddleProxy *proxy = [[JKMiddleProxy alloc] init];
         proxy.target = target;
         return proxy;
      }
      // 消息转发机制(会先发送消息、再动态解析、最后再消息转发)
      - (id)forwardingTargetForSelector:(SEL)aSelector
      {
          return self.target;
      }
      @end
      
      • 使用如下(不管是CADisplayLink还是NSTimer,把self换为中间对象[JKMiddleProxy proxyWithTarget:self]就好)
      #import "JKMiddleProxy.h"
      @property (strong, nonatomic) NSTimer *timer;
      
      self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[JKMiddleProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
      
      - (void)timerTest
      {
         NSLog(@"%s", __func__);
      }
      
      - (void)dealloc
      {
        NSLog(@"%s", __func__);
        [self.timer invalidate];
      }
      
    • 1.4、效率更加高的中间对象(不需要进行发送消息和再动态解析,直接进行消息转发),利用 NSProxy 可以略过 发送消息和动态解析。

      • 下面是创建了一个继承于类 :JKProxy : NSProxy

        #import 
        
        NS_ASSUME_NONNULL_BEGIN
        
        @interface JKProxy : NSProxy
        
        + (instancetype)proxyWithTarget:(id)target;
        @property (weak, nonatomic) id target;
        
        @end
        
        NS_ASSUME_NONNULL_END
        
        #import "JKProxy.h"
        
        @implementation JKProxy
        
        + (instancetype)proxyWithTarget:(id)target
        {
           // NSProxy对象不需要调用init,因为它本来就没有init方法
           JKProxy *proxy = [JKProxy 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.3一样,直接([JKProxy proxyWithTarget:self])

        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[JKProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
        
    • 1.5、看下面的打印结果 1 和 0(原因是JKProxy继承于 NSProxy,在调用isKindOfClass的时候直接走的消息转发(- forwardInvocation),会转换成ViewController的调用isKindOfClass,而JKMiddleProxy继承于NSObject,不会进入forwardInvocation进而invokeWithTarget)

      JKProxy *proxy1 = [JKProxy proxyWithTarget:vc];
      JKMiddleProxy *proxy2 = [JKMiddleProxy proxyWithTarget:vc];
      
      NSLog(@"%d %d", [proxy1 isKindOfClass:[ViewController class]], [proxy2 isKindOfClass:[ViewController class]]);
      
  • 二、GCD定时器:比较准时,它直接和系统内核挂钩的(NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时)

    • 2.1、使用如下:(可以看demo里面的GCDTimerViewController有具体的源码)

      // 定义GCD定时器对象 dispatch_source_t
      @property(nonatomic,strong) dispatch_source_t gcdTimer;
      
      // 创建队列
      dispatch_queue_t queue = dispatch_get_main_queue();
      
      // 创建定时器
      self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
      
      // 设置时间
      /*
         dispatch_source_t  _Nonnull source: 定时器
         dispatch_time_t start: 开始的时间,dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),start多长时间后开始,NSEC_PER_SEC(纳秒)
         uint64_t interval:时间间隔
         uint64_t leeway: 误差,写0就好
       */
      uint64_t start = 2.0;
      uint64_t interval = 1.0;
      dispatch_source_set_timer(self.gcdTimer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),  interval * NSEC_PER_SEC,0);
      
      // 设置回调
      static int count = 0;
      dispatch_source_set_event_handler(self.gcdTimer, ^{
      
            count ++;
            NSLog(@"count== %d",count);
      
      });
      
      // 启动定时器
      dispatch_resume(self.gcdTimer);
      
    • 2.2、如果上面的想在子线程执行的话,我们可以自己创建队列(下面是一个串行队列)

       // DISPATCH_QUEUE_SERIAL 串行
       // DISPATCH_QUEUE_CONCURRENT 并行
      dispatch_queue_t queue = dispatch_queue_create("timer", DISPATCH_QUEUE_SERIAL);
      

      在 2.1 里面回调是用的block,咱们还可以用函数,把dispatch_source_set_event_handler换为dispatch_source_set_event_handler_f

      dispatch_source_set_event_handler_f(self.gcdTimer, timerFire);
      
      void timerFire(void *param)
      {
         NSLog(@"定时器打印 - %@", [NSThread currentThread]);
      }
      
    • 2.3、对上面GCD定时器的一个封装 JKGCDTimer自己下载,下面展示一下使用

      • 第1种使用方式(block返回执行的任务)

        // 导入,这个是封装的类名
        #import "JKGCDTimer.h"
        @property(nonatomic,strong) NSString *gcdTimerKeyName;
        // 第1种使用方式(Block里面做task)
        
        static int number = 0;
        /**
        task 定时器开启后执行的任务
        startTime 多长时间后开启任务
        intervalTime 时间间隔
        repeats 是否重复执行任务  YES: 重复  NO: 执行一次
        async 同步还是异步执行任务  YES:async(全局并发队列)  NO: sync(主队列)
        */
        self.gcdTimerKeyName = [JKGCDTimer execTask:^{
        
              number ++;
              NSLog(@"number==%d-------%@",number,[NSThread currentThread]);
        
         } startTime:2.0 intervalTime:1.0 repeats:YES async:YES];
        
      • 第2种使用方式(在自己的控制器里面的方法 实现任务)

        // 导入,这个是封装的类名
        #import "JKGCDTimer.h"
        @property(nonatomic,strong) NSString *gcdTimerKeyName;
        /**
          target 自己VC的 self
          selector 自己VC里面的 方法
          startTime 多长时间后开启任务
          intervalTime 时间间隔
          repeats 是否重复执行任务  YES: 重复  NO: 执行一次
          async 同步还是异步执行任务  YES:async(全局并发队列)  NO: sync(主队列)
         */
        
        self.gcdTimerKeyName = [JKGCDTimer execTaskTarget:self selector:@selector(timerExecTask) startTime:2.0 intervalTime:1.0 repeats:YES async:YES];
        
        #pragma mark 采用自己控制器执行任务的方法
        -(void)timerExecTask{
        
           static int number = 0;
           number ++;
           NSLog(@"number==%d-------%@",number,[NSThread currentThread]);
        
        }
        
  • 三、iOS 程序的内存布局

    • 3.1、先用一个图展示


      OC底层知识(十二) : 内存管理_第2张图片
      iOS 程序的内存布局
      • 代码段:编译之后的代码

      • 数据段

        • 字符串常量:比如NSString *str = @"456"
        • 已初始化数据:已初始化的全局变量、静态变量等,比如:static int c = 20;
        • 未初始化数据:未初始化的全局变量、静态变量等,比如:static int d;
      • :通过alloc、malloc、calloc等动态分配的空间, 分配的内存空间地址越来越大 ,如:NSObject *obj = [[NSObject alloc] init];

      • :函数调用开销,比如局部变量。分配的内存空间地址越来越小,如:int e; int f = 20;

    • 3.2、Tagged Pointer (推荐博客一 、推荐博客二、推荐博客三),这是一个苹果对内存做的优化技术,将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。

      • (1)、从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储

        OC底层知识(十二) : 内存管理_第3张图片
        以字符串为例

        从上面可以看出 str1存的@"123"是比较小的,内存地址最后一位 是 9,转化为 二进制是:1001,最后一位是 1,而 str2存的@"fffffffffffff"比较大,Tagged Pointer不能再存,只能放到堆区,从上面的打印可以看出其内存地址的最后一位是 0。

      • (2)、在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

      • (3)、使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中

      • (4)、当指针不够存储数据时,才会使用动态分配内存的方式来存储数据(如上面的str2)

      • (5)、objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销,如下:

      • (6)、那怎么判断一个指针是不是 Tagged Pointer 呢?可以通过 objc 源码看到对应的判断方法如下:

        static inline bool 
        _objc_isTaggedPointer(const void *ptr) 
        {
           return ((intptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
        }
        
        #if OBJC_MSB_TAGGED_POINTERS
        #   define _OBJC_TAG_MASK (1ULL<<63)
        #else
        #   define _OBJC_TAG_MASK 1
        #endif
        
        #if TARGET_OS_OSX && __x86_64__
        // 64-bit Mac - tag bit is LSB
        #   define OBJC_MSB_TAGGED_POINTERS 0
        #else
        // Everything else - tag bit is MSB
        #   define OBJC_MSB_TAGGED_POINTERS 1
        #endif
        

        iOS平台,最高有效位是1(第64bit)
        Mac平台,最低有效位是1
        看下面的例子:

        NSString *str1 = [NSString stringWithFormat:@"%@",@"abc"];
        NSString *str2 = [NSString stringWithFormat:@"%@",@"ffffffffffffffffffff"];
        
        NSLog(@"%p %p %@ %@",str1,str2,[str1 class],[str2 class]);
        打印结果为:
        0xad16dee4304feb33 0x6000005dd470 NSTaggedPointerString __NSCFString
        

        分析: str1 的内存地址是:0xad16dee4304feb33,最左边a在十六进制里面是 10,转化为二进制是 1010,可以看到是最高有效位是: 1;而str2的内存地址是0x6000005dd470 ,结尾是0,就能确定在堆区。

  • 3.3、思考以下2段代码能发生什么事?有什么区别?

    • 第 1 段代码

      @property (strong, nonatomic) NSString *name;
      dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
      
      for (int i = 0; i < 1000; i++) {
             dispatch_async(queue, ^{
      
                 self.name = [NSString stringWithFormat:@"abc"];
             });
      }
      
    • 第 2 段代码(崩溃,坏内存访问)

      @property (strong, nonatomic) NSString *name;
      dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
       
      for (int i = 0; i < 1000; i++) {
             dispatch_async(queue, ^{
       
                 self.name = [NSString stringWithFormat:@"fffffffffffffffffffffffff"];
             });
      }
      

    答:第 2 段代码会坏内存访问,原因是:第2段代码在给 self.name赋值会走下面的方法,由于 第2段代码是 异步并行的会多个线程调用- (void)setName:(NSString *)name, _name释放[_name release]两次,从而造成坏内存访问;然而第1段代码[NSString stringWithFormat:@"abc"]就不是一个OC对象,仅仅是一个Tagged Pointer中存储的数据,把指针变量的值取出来给成员变量self.name而已。解决第2段代码崩溃的办法在self.name = [NSString stringWithFormat:@"fffffffffffffffffffffffff"]; });上下加锁和解锁就好了,来防止两次 release

    // set方法的本质
    - (void)setName:(NSString *)name
    {
         if (_name != name) {
              [_name release];
              _name = [name retain];
         }
     }
    
     // set方法在ARC下表面的现象
     - (void)setName:(NSString *)name
     {
         _name =name
     }
    
  • 四、copy 与 mutableCopy

    • 4.1、拷贝的目的

      • 产生一个副本对象,跟源对象互不影响
      • 修改了源对象,不会影响副本对象
      • 修改了副本对象,不会影响源对象
    • 4.2、iOS 提供了2个拷贝方法 copymutableCopy

      • copy,不可变拷贝,产生不可变副本
      • mutableCopy,可变拷贝,产生可变副本
    • 4.3、深拷贝和浅拷贝

      • 深拷贝:内容拷贝,产生新的对象
      • 浅拷贝:指针拷贝,没有产生新的对象
    • 4.4.以字符串为例举例

      • 原字符串是不可变的(三个字符串的内存地址一样)

        NSString *str1 = [NSString stringWithFormat:@"123"];
        // 浅拷贝:指针拷贝,同一块内存地址
        NSString *str2 = [str1 copy]; 
        // 深拷贝,对象拷贝,生成新的内存地址
        NSMutableString *str3 = [str1 mutableCopy]; 
        
        NSLog(@"%p %p %p",str1,str2,str3); 
        打印结果:0xcd37ac23abfbc18c 0xcd37ac23abfbc18c 0x600000910840
        
        OC底层知识(十二) : 内存管理_第4张图片
        原字符串是不可变的
      • 原字符串是可变的(三个字符串的内存地址不一样)

        NSMutableString *str1 = [NSMutableString stringWithFormat:@"123"];
        // 深拷贝,对象拷贝,生成新的内存地址
        NSString *str2 = [str1 copy]; 
        // 深拷贝,对象拷贝,生成新的内存地址
        NSMutableString *str3 = [str1 mutableCopy]; 
        
        NSLog(@"%p %p %p",str1,str2,str3);
        打印结果:0x6000038c9470 0xdae0103e19bd5106 0x6000038c9140
        
        OC底层知识(十二) : 内存管理_第5张图片
        原字符串是可变的
    • 4.5、copy和mutableCopy的总结图


      OC底层知识(十二) : 内存管理_第6张图片
      copy和mutableCopy的总结图
    • 4.6、自定义一个类的copy方法

      • 自定义的类(JKStudent)遵守 协议

        @property (strong, nonatomic) int number;
        
      • 实现 - (id)copyWithZone:(NSZone *)zone方法

        - (id)copyWithZone:(NSZone *)zone
        {
           JKStudent *student = [[JKStudent allocWithZone:zone] init];
           student.number = self. number;
           return student;
        }
        
  • 五、引用计数的存储在哪里?

    • 在64bit中,引用计数可以直接存储在优化过的isa指针中,也可能存储在SideTable类中


      OC底层知识(十二) : 内存管理_第7张图片
      引用计数的存储
      • extra_rc : 里面存储的值是引用计数器减1
      • has_sidetable_rc: 引用计数器是否过大无法存储在isa中; 如果为1,那么引用计数器会存储在一个叫sideTable的类的属性中,refcnts是一个存放着对象引用计数的散列表
  • 六、 看两个面试题

    • 6.1、weak指针的实现原理?
      答:将那些弱引用存在一个哈希表里面,到时候这个对象要销毁,它就会取出当前对象对应的弱引用表,把若引用表里面存储的若引用都给清除掉。

    • 6.2、__weak与__unsafe_unretained的区别?

      • 定义一个 JKString 类继承于 NSObject,

        __strong JKString *string1;
        __weak JKString *string2;
        // 不安全的,当JKString对象销毁的时候,string3不会被赋空,会产生野指针的情况
        __unsafe_unretained JKString *string3;
        
        NSLog(@"begin");
        {
        
          JKString *string = [[JKString alloc]init];
          string3 = string;
        }
        
        NSLog(@"%@",string3);
        

        答: __weak与__unsafe_unretained共同点是:都不会产生强引用,__weak更加安全,当__weak指向的对象销毁的时候,这个指针的值被清空(nil),防止野指针的错误。而__unsafe_unretained指向的对象销毁的时候,这个指针的值不会被清空,会产生野指针的错误

  • 七、ARC都帮我们做了什么?
    答:ARC是LLVM编译器和Runtime系统相互协作的一个结果,具体是利用编译器给我们生成内存管理相关的代码,然后在程序运行的过程中又帮我们处理弱引用这种操作。

  • 八、autorelease自动释放池

    • 8.1、自动释放池的主要底层数据结构是__AtAutoreleasePoolAutoreleasePoolPage
    • 8.2、调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
    • 8.3、objc4源码:NSObject.mm


      OC底层知识(十二) : 内存管理_第8张图片
      AutoreleasePoolPage
    • 8.4、AutoreleasePoolPage的结构
      • 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
      • 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
        OC底层知识(十二) : 内存管理_第9张图片
        AutoreleasePoolPage的结构
      • 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
      • 调用pop方法时传入一个POOL_BOUNDARY(boundary 美[ˈbaʊndəri, -dri] 分界线)的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
      • id *next指向了下一个能存放autorelease对象地址的区域
    • 8.5、Runloop和Autorelease
      • iOS在主线程的Runloop中注册了2个Observer
        • 第1个Observer
          • 监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
        • 第2个Observer
          • 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
          • 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
  • 九、方法里面有局部变量,出了方法后会立即被释放吗?

    • ARC与MRC的切换:Build Settings 搜索automatic re
      OC底层知识(十二) : 内存管理_第10张图片
      ARC与MRC的切换

    答:因下面演示的需要大家可以建一个类JKString类,解释如下:

    • (1)、如果这个局部对象最终是通过autorelease的形式(MRC)来去释放的话,就意味着它不是马上释放,而是等它那次所处的RunLoop休眠之前就会进行相应的release操作;


      OC底层知识(十二) : 内存管理_第11张图片
      局部对象最终是通过autorelease的形式来去释放的
    • (2)、如果ARC生成的是release代码的话, 确实局部变量是立马就会释放。


      OC底层知识(十二) : 内存管理_第12张图片
      ARC生成的是release代码的话
  • 十、OC对象的内存管理(下面是结论)

    • 10.1、在iOS中,使用引用计数来管理OC对象的内存

    • 10.2、一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间

    • 10.3、调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

    • 10.4、内存管理的经验总结

      • 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
      • 想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
    • 10.5、可以通过以下私有函数来查看自动释放池的情况(声明一下C的函数,程序会自动寻找该函数,extern),在MRC下测试,多在@autoreleasepool { }写对象测试调用下面的函数

      extern void _objc_autoreleasePoolPrint(void);
      
      @autoreleasepool {
      
         JKString *string1 = [[[JKString alloc]init] autorelease];
         _objc_autoreleasePoolPrint();
         @autoreleasepool {
          
             JKString *string3 = [[[JKString alloc]init] autorelease];
             @autoreleasepool {
              
                JKString *string4 = [[[JKString alloc]init] autorelease];
             }
          }
      }
      

你可能感兴趣的:(OC底层知识(十二) : 内存管理)