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、通過中間對象(代理對象)的方式來解決,下面用到了消息轉發機制(會先發送消息、再動態解析、最后再消息轉發)

      通過中間對象(代理對象)的方式來解決

      下面是創建了一個繼承于類 :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];
             }
          }
      }
      
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容