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

作者: IIronMan | 来源:发表于2018-11-06 00:39 被阅读50次
    • 一、抛出一个问题:使用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、通过中间对象(代理对象)的方式来解决,下面用到了消息转发机制(会先发送消息、再动态解析、最后再消息转发)

        通过中间对象(代理对象)的方式来解决

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

        #import <Foundation/Foundation.h>
        
        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 <Foundation/Foundation.h>
          
          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、先用一个图展示


        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等小对象的存储

          以字符串为例
          从上面可以看出 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
          
          原字符串是不可变的
        • 原字符串是可变的(三个字符串的内存地址不一样)

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


        copy和mutableCopy的总结图
      • 4.6、自定义一个类的copy方法

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

          @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类中


        引用计数的存储
        • 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


        AutoreleasePoolPage
      • 8.4、AutoreleasePoolPage的结构
        • 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
        • 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
          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
        ARC与MRC的切换

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

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


        局部对象最终是通过autorelease的形式来去释放的
      • (2)、如果ARC生成的是release代码的话, 确实局部变量是立马就会释放。


        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底层知识(十二) : 内存管理

      本文链接:https://www.haomeiwen.com/subject/dzshtqtx.html