Block 在 ARC 下的拷貝

来源:http://www.cnblogs.com/v2m_/archive/2017/07/25/7234999.html
-Advertisement-
Play Games

前言 現在有一種說法,是開啟arc選項時,已經沒有棧上的block了,所以所有的block都不需要copy來拷貝到堆上了。那麼這個說法正確與否呢? 結論是這個說法必須是錯誤的,首先的一點就是arc只是編譯器幫助我們給對象自動增加retain,release方法,我們不需要手動的去管理這些成對出現的內 ...


前言

現在有一種說法,是開啟arc選項時,已經沒有棧上的block了,所以所有的block都不需要copy來拷貝到堆上了。那麼這個說法正確與否呢?
結論是這個說法必須是錯誤的,首先的一點就是arc只是編譯器幫助我們給對象自動增加retain,release方法,我們不需要手動的去管理這些成對出現的記憶體計數方法,其本質上與mrc是一脈相承的,所以arc下必然還是有stack block的。
再次可以簡單的寫一個例子(就舉參考1的要點3):

    1   -(id) getBlockArray{  
    2       int val =10;  
    3       return [NSArray arrayWithObjects:  
    4           ^{NSLog(@"blk0:%d",val);},  
    5           ^{NSLog(@"blk1:%d",val);},nil];  
    6   }  
    7   // Other Method  
    8   id obj = getBlockArray();  
    9   typedef void (^blk_t)(void);  
    10  blk_t blk = (blk_t){obj objectAtIndex:0};  
    11  blk();  

這段代碼會異常,但是作者解釋的不正確。首先我們可以列印一下這個Array,會發現第一個是NSMallocBlock,第二個是NSStackBlock。所以這段代碼說明瞭arc下也是有stack block的。其次這段代碼異常是因為array釋放的時候,第二個block是棧上面的,對其釋放必然會引發異常。

為什麼會這樣呢,我們接著往下看。

一、NSArray 的生成

1. + (instancetype)arrayWithObjects:(ObjectType)firstObj, ...
- (void)show
{
    id obj = [self getBlockArray];
    typedef void (^blk_t)(void);
    blk_t blk = (blk_t)[obj objectAtIndex:0];
    blk();
    blk = (blk_t)[obj objectAtIndex:1];
    blk();
}

-(id) getBlockArray{
    int val =10;

    NSArray* a = [NSArray arrayWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}, nil];
    
    return a;
}

通過 clang rewrite 看看得到的c++代碼,關鍵地方如下:

static id _I_BlockTest_getBlockArray(BlockTest * self, SEL _cmd) {
    int val =10;
    NSArray* a = ((NSArray *(*)(id, SEL, ObjectType, ...))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("arrayWithObjects:"), (id)((void (*)())&__BlockTest__getBlockArray_block_impl_0((void *)__BlockTest__getBlockArray_block_func_0, &__BlockTest__getBlockArray_block_desc_0_DATA, val)), ((void (*)())&__BlockTest__getBlockArray_block_impl_1((void *)__BlockTest__getBlockArray_block_func_1, &__BlockTest__getBlockArray_block_desc_1_DATA, val)), __null);
    return a;
}

第一個 block 強轉成 id 類型,第二個 block 沒有。首先 block 在賦值給 id 類型或者 block 類型的成員變數時,block 會拷貝到堆上,所以第一個 block 變成了堆上的 block,但是第二個還是棧上的記憶體。
整個 getBlockArray 方法在 show 方法中調用,所以用的是同一個 runloop 中帶過來的 autoreleasepool,getBlockArray 中的 NSArray* a = [NSArray arrayWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}, nil]; 會往這個 autoreleasepool 中添加兩個 NSArray (賦值給 a 是添加一次,show 中得到返回值時添加一次),這兩個NSArray 不會立即釋放,而會在 這個 runloop 結束的時候釋放,這個時機會在show的結束設置更外層的調用。而這個時機已經超過了 getBlockArray 的區域,超過這個區域去訪問棧記憶體,所以會crash。

2.一些其他的 NSArray 生成方法說明

+ (instancetype)arrayWithArray:(NSArray<ObjectType> *)array;
效果同上

- (instancetype)initWithArray:(NSArray<ObjectType> *)array;
效果同上

+ (instancetype)arrayWithObject:(ObjectType)anObject;
rewrite後代碼參考上面,這個方法的block會轉成 id,所以生成 NSArray 中的 block 是堆上的 block。

- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;
不管flag 是 true 還是 false,都會crash

另外 - (void)addObject:(ObjectType)anObject; 也是因為會轉成 id,所以添加的都是堆上的 block。

二、AutoReleasePool

現在給 getBlockArray 加上一個autoreleasepool,看看會發生什麼


     -(id) getBlockArray{
         int val =10;

         NSArray* a = nil;
         @autoreleasepool {
             a = [NSArray arrayWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}, nil];
         }

         return [[NSArray alloc] initWithArray:a copyItems:YES];
     }
     

此時並不會crash,正確的運行了,clang rewrite 一下看看

     static id _I_BlockTest_getBlockArray(BlockTest * self, SEL _cmd) {
         int val =10;
         NSArray* a = __null;
         /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
             a = ((NSArray *(*)(id, SEL, ObjectType, ...))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("arrayWithObjects:"), (id)((void (*)())&__BlockTest__getBlockArray_block_impl_0((void *)__BlockTest__getBlockArray_block_func_0, &__BlockTest__getBlockArray_block_desc_0_DATA, val)), ((void (*)())&__BlockTest__getBlockArray_block_impl_1((void *)__BlockTest__getBlockArray_block_func_1, &__BlockTest__getBlockArray_block_desc_1_DATA, val)), __null);
         }
         return ((NSArray *(*)(id, SEL, NSArray<ObjectType> *, BOOL))(void *)objc_msgSend)((id)((NSArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("alloc")), sel_registerName("initWithArray:copyItems:"), (NSArray *)a, ((bool)1));

     }

其實並沒有多出來什麼特殊的處理,我們可以分析分析。

getBlockArray 給 a 賦值的數組,加入到了我們添加的autoreleasepool中,在這個作用域外面,他已經被釋放了,其中的棧上的 block 在自己的作用域內釋放,沒有任何問題。而 a 預設是一個 strong 修飾的變數,在不在使用的時候,編譯器會幫我們添加上 release 而釋放,所以在函數結束的時候,他也釋放了。
另外 copyItem 是 YES,他會往 withArray 的每個 item 都發送 copy 消息,所以函數返回的 NSArray 中每個 item 都是堆上的 block,所以這一次並不會 crash。但是如果 copyItem 是 NO,那麼就會出現超過作用域訪問棧上 block 的問題,就會 crash 了。

三、字面量

還有一種生成 NSArray 的方式是:

      NSArray* a = @[^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);}];

看看對應的 rewrite 代碼是什麼樣的:

      static id _I_BlockTest_getBlockArray(BlockTest * self, SEL _cmd) {
          int val =10;
          NSArray* a = __null;
    
              a = ((NSArray *(*)(Class, SEL, const ObjectType *, NSUInteger))(void *)objc_msgSend)(objc_getClass("NSArray"), sel_registerName("arrayWithObjects:count:"), (const id *)__NSContainer_literal(2U, ((void (*)())&__BlockTest__getBlockArray_block_impl_0((void *)__BlockTest__getBlockArray_block_func_0, &__BlockTest__getBlockArray_block_desc_0_DATA, val)), ((void (*)())&__BlockTest__getBlockArray_block_impl_1((void *)__BlockTest__getBlockArray_block_func_1, &__BlockTest__getBlockArray_block_desc_1_DATA, val))).arr, 2U);
          return a;
}

這裡我們看到有 __NSContainer_literal 這個函數,他做了些什麼呢?看下他的定義:

struct __NSContainer_literal {
  void * *arr;
  __NSContainer_literal (unsigned int count, ...) {
    va_list marker;
    va_start(marker, count);
    arr = new void *[count];
    for (unsigned i = 0; i < count; i++)
      arr[i] = va_arg(marker, void *);
    va_end( marker );
  };
  ~__NSContainer_literal() {
    delete[] arr;
  }
};

多個棧上的 block 作為參數傳進去,然後在內部賦值給數組,當 block 作為返回值返回的時候,會被拷貝到堆上,所以,這個數組裡面的每個元素都是堆上的 block,所以不會crash。

四、NSDictionary

由於 NSDictionary 會 copy key 但是不 copy object,所以也會出現上面類似的crash情況。

參考1.http://blog.csdn.net/hherima/article/details/38620175
參考2.https://stackoverflow.com/questions/4010578/nsdictionary-dont-copy-values


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • /* 此文不斷更新中 */ 使用 .col-md-offset-* 類可以將列向右側偏移 ...
  • 倒計時主要用到的知識點:1、設置時間間隔的setInterval可以被clearInterval取消 2、毫秒轉換為時分格式 這個是效果圖 下麵是js中的函數 第二個是html資源,為了方便我css直接寫在html中了 需要代碼的小伙伴可以自行下載 鏈接:http://pan.baidu.com/s ...
  • 【來源】由於自己非電腦出身,所以對於底層的一些常識的認識不夠;近期開始自修《網易雲課堂》的大學四年電腦,碰到了一個通過三角函數計算角度的問題;為了讓自己重溫三角函數知識,引出了之後一些列的實踐和思考,而且最後我用的非三角函數知識; 【思考】對於時鐘這種插件,《慕課網》上有很多講解,也看了一些,怎 ...
  • 一說到數組排序,最直觀的想法就是用sort啊! 請問不用使用sort方法還可以使用什麼方法進行數組排序? 比如 : 快速排序法、合併排序法、冒泡排序法、選擇排序法、插入排序法、布爾排序法、交互排序、選擇排序、二分法排序..... 等等一下,在我們瞭解這些排序方法之前,為了更好的理解,先讓我們探索一下 ...
  • 互動朋友圈模型設計 根據該模型生出資料庫後的表結構如下: 如有問題,請留言! ...
  • s中判斷對象是否存在,寫法有很多種: 出自:http://www.ruanyifeng.com/blog/2011/05/how_to_judge_the_existence_of_a_global_object_in_javascript.html ...
  • 查找電腦埠的占用情況 例如查埠號5037(adb的埠)的占用情況 一、使用 netstat -ano命令 找到埠號為5037的,並且找到對應進程的PID 然後我們區windows任務管理器裡面找PID是5736的進程是什麼 找到為eclipse,說明找對了,因為eclipse會調用adb ...
  • 引言 根據拷貝內容的不同,分為深淺拷貝 深拷貝:指針賦值,且內容拷貝 淺拷貝:只是簡單的指針賦值 蘋果為什麼這麼設計呢?總結起來很簡單:即安全又省記憶體。但是要理解或者避免踩一些坑,還需要看下麵的介紹 記憶體 不得不先說到記憶體,又不得不說記憶體分區: "程式底層——程式如何在RAM ROM運行,記憶體分配與 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...