block本質探尋八之迴圈引用

来源:https://www.cnblogs.com/lybSkill/archive/2019/01/18/10283498.html
-Advertisement-
Play Games

說明:閱讀本文,請參照之前的block文章加以理解; 一、迴圈引用的本質 //代碼——ARC環境 //列印 分析:main函數日誌輸出之前,Person實例對象就被銷毀了——因為在test1()方法中,強指針per持有[[Person alloc] init]對象會執行retain操作導致Perso ...


說明:閱讀本文,請參照之前的block文章加以理解;

一、迴圈引用的本質

//代碼——ARC環境

void test1()
{
    Person *per = [[Person alloc] init];
    per.age = 10;
    per.block = ^{
        NSLog(@"-------1");
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test1();
//        test2();
    }
    
    NSLog(@"----");
    return 0;
}
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void(^MyBlock)(void);

@interface Person : NSObject

@property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock block;

@end

NS_ASSUME_NONNULL_END



#import "Person.h"

@implementation Person

- (void)dealloc
{
//    [super dealloc];
    NSLog(@"%s", __func__);
}

@end

//列印

2019-01-17 16:46:28.353740+0800 MJ_TEST[2990:240693] -[Person dealloc]
2019-01-17 16:46:28.354013+0800 MJ_TEST[2990:240693] ----
Program ended with exit code: 0

分析:main函數日誌輸出之前,Person實例對象就被銷毀了——因為在test1()方法中,強指針per持有[[Person alloc] init]對象會執行retain操作導致Person實例對象的retainCount值為2(此前alloc操作,其retainCount值就設置為1),當test1()方法結束時,per被存放在棧區也隨之銷毀,故per不會再持有Person實例對象即執行release操作導致該對象的retainCount指減1;當自動銷毀池autoreleasepool結束時,會自動向池中的所有對象再次發送一條release消息,那麼此時Person實例對象的retainCount值再次減1變成0,對象的引用計數一旦為0,其所占記憶體會被自動回收,因此Person實例對象就會銷毀;

補充:我們知道blcok的記憶體管理模式為copy策略(原因就不分析了),因為在ARC環境下強指針持有block對象,系統會自動將block對象copy到堆區中,所以ARC模式下,系統會自動幫助我們對block進行copy的管理策略,我們寫成strong的策略是沒有任何問題的——但是,MRC模式下必須是copy策略,系統不會幫你管理記憶體,只能手動;這點請註意!

至此,以上Person實例對象銷毀是正常的,那麼什麼情況下是不正常的?往下看:

//代碼

void test2()
{
    Person *per = [[Person alloc] init];
    per.age = 10;
    per.block = ^{
        NSLog(@"-------%d", per.age);
    };
}

//列印

2019-01-17 15:00:31.859710+0800 MJ_TEST[2486:187534] -----
Program ended with exit code: 0

//clang

main.cpp

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong per;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _per, int flags=0) : per(_per) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

Person.cpp

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    MyBlock  _Nonnull _block;
};


struct NSObject_IMPL {
    Class isa;
};


static void(* _I_Person_block(Person * self, SEL _cmd) )(){ return (*(MyBlock  _Nonnull *)((char *)self + OBJC_IVAR_$_Person$_block)); }


static void _I_Person_setBlock_(Person * self, SEL _cmd, MyBlock  _Nonnull block) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _block), (id)block, 0, 1); }

分析:

<1>我們知道,oc對象編譯成C++後的本質就是一個結構體Person_IMPL,該結構體的第一個成員變數就是isa指針,指向類對象本身;同時,@property修飾的屬性,系統會自動生成一個結構體成員變數,還為之生成getter和setter方法——這些之前的文章已經說過,此處不再贅述!

<2>per實例對象結構體Person_IMPL中含有_block變數,通過setter法_I_Person_setBlock_將block對象(等號右邊)賦值給該_block變數,因此_block指向block對象(強引用);

<3>在__main_block_impl_0結構體中,我們看到Person *__strong per,所以,block對象本身對Person實例對象也是強引用;

綜上:block對象結構體__main_block_impl_0通過其內部成員指針變數Person *__strong per持有Person實例對象(強引用),而Person實例對象結構體Person_IMPL通過其內部成員指針變數_block持有block對象(強引用)——因此二者構成迴圈引用,當autoreleasepool大括弧結束時,block對象和Person實例對象所占記憶體依然沒有被系統回收,因為他們的引用計數依然大於0;

//圖解——註:self是一個auto型的局部變數,指向的是[[Person alloc] init]實例對象

 

補充:所以block迴圈引用造成的直接後果是記憶體泄露(即程式結束而記憶體沒有被回收——>根本原因是對象引用計數大於0(retain和release使用次數不對等)——>是因為強指針引用造成的);

引伸:當對象所占記憶體被回收時,指向對象的指針(強指針)應當被賦值於nil或者指向其他的合法記憶體,否則會導致野指針調用(亂指)程式崩潰——但是,用weak做記憶體管理策略(即修飾指針變數)時,為什麼系統會自動將指針變數置為nil?這點後面文章會提到!

 

二、解決方案

思路:

據上分析,打破迴圈引用,只需要將其中一個強引用變成弱引用即可,那麼要改變哪一個弱引用呢?Person實例對象內部擁有block屬性,當該實例對象銷毀時,其block屬性也會隨之銷毀,所以我們只需要將block對象中的Person類型指針變成弱指針即可——通常都是這樣做!

//圖解

 

 

1)ARC環境下

方案一:weak修飾

//代碼

void test3()
{
    Person *per = [[Person alloc] init];
    per.age = 10;
    __weak Person *weakPer = per;
    per.block = ^{
        NSLog(@"-------%d", weakPer.age);
    };
}

//列印

2019-01-18 14:10:17.451718+0800 MJ_TEST[1458:103419] -[Person dealloc]
2019-01-18 14:10:17.452663+0800 MJ_TEST[1458:103419] ----
Program ended with exit code: 0

//clang

struct __test3_block_impl_0 {
  struct __block_impl impl;
  struct __test3_block_desc_0* Desc;
  Person *__weak weakPer;
  __test3_block_impl_0(void *fp, struct __test3_block_desc_0 *desc, Person *__weak _weakPer, int flags=0) : weakPer(_weakPer) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

分析:

<1>block對象中,Person指針變數類型變成了__weak類型,列印前person對象銷毀了;

<2>另外一種寫法:__weak typeof(per) weakPer = per  <=>  __weak Person *weakPer = per;(前者寫法居多)

 

 

方案二:__unsafe_unretained修飾 

 

註:__weak修飾和__unsafe_unretained,二者有一個非常重要的區別:

經上分析,我們知道Person實例對象銷毀後,其內部的block屬性也會銷毀,那麼其也就不再指向block對象了,而此時一旦block對象沒有任何強引用,作用域結束後,其也會被銷毀,其成員變數Person指針也會被銷毀,這點沒問題!————但是,如果block對象還存在呢(被其他指針強引用),此時其內部成員變數Person指針也存在,但是依然會指向Person實例對象銷毀前所占的記憶體區域,但是該記憶體區域已經被系統回收了,Person指針指向的是不合法的記憶體區域——如果是weak修飾,系統會自動將指針置為nil(指向合法的記憶體區域);如果是__unsafe_unretained修飾,什麼也不會做,這樣就會導致野指針調用!

 

方案三:__block修飾

//代碼

void test4()
{
    __block Person *per = [[Person alloc] init];
    per.age = 10;
    per.block = ^{
        NSLog(@"-------%d", per.age);
        per = nil;
    };
    per.block();
}

//列印

2019-01-18 15:20:06.697898+0800 MJ_TEST[1810:136831] -------10
2019-01-18 15:20:06.698250+0800 MJ_TEST[1810:136831] -[Person dealloc]
2019-01-18 15:20:06.698300+0800 MJ_TEST[1810:136831] ----
Program ended with exit code: 0

//clang

struct __Block_byref_per_0 {
  void *__isa;
__Block_byref_per_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 Person *__strong per;
};


struct __test4_block_impl_0 {
  struct __block_impl impl;
  struct __test4_block_desc_0* Desc;
  __Block_byref_per_0 *per; // by ref
  __test4_block_impl_0(void *fp, struct __test4_block_desc_0 *desc, __Block_byref_per_0 *_per, int flags=0) : per(_per->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

分析:

<1>block對象持有__block對象,而__block對象又持有Person實例對象,而Person實例對象又持有block對象——如此,構成一個三角迴圈: 

 

 

 <2>通過block回調,Person實例對象指針被置為nil,而該指針本質是__block對象中的Person *__strong per指針,因此該指針不可能再指向Person實例對象了,所以,第2條持有就斷開了,打破了三角迴圈;

說明:但是該方案看起來比較麻煩,一旦忘記將指針置為nil,就會造成記憶體泄露;

註:以上分析不理解,請參考前述block文章,此處不再贅述!

 

2)MRC環境

說明:該環境下不支持__weak修飾;

方案一:__unsafe_unretained修飾

//代碼

void test5()
{
//    __unsafe_unretained Person *per = [[Person alloc] init];
    __block Person *per = [[Person alloc] init];
    per.age = 10;
    per.block = [^{
        NSLog(@"-------%d", per.age);
    } copy];
    [per release];
    per = nil;
}

//列印

2019-01-18 16:42:49.970441+0800 MJ_TEST[2257:177470] -[Person dealloc]
2019-01-18 16:42:49.971587+0800 MJ_TEST[2257:177470] ----
Program ended with exit code: 0

分析:

<1>根據習慣,MRC環境下,我們通常會將block對象(等號右邊)從棧區copy到堆區,以達到手動控制其記憶體銷毀的目的;

<2>原理:調用alloc創建對象時,系統會自動將該實例對象引用計數置為1,而該對象又會隨著block的copy而一起被copy到堆區,此時該對象的retainCount會加1(變成2),當對該對象發送release消息時,其retainCount自動減1(由2變成1),所以當程式結束時,Person實例對象retainCount為1(>0),其記憶體並不會被系統回收從而導致記憶體泄露;

那麼,__unsafe_unretained修飾後,無論後面有多少次retain或者copy操作,Person實例對象的retainCount始終為1,所以程式結束前release時,其retainCount值變為0,此時記憶體被回收,而不會導致記憶體泄露的問題;

 

方案二:__block修飾 

 

 分析:為什麼block可以?

<1>前述文章我們分析到,MRC環境下,__block修飾對象類型的auto局部變數,系統生成的__block對象並不會根據其記憶體成員變數Person指針變數(其實就是test5()方法中的per指針)是強指針類型而對Person實例對象([[Person alloc] init])進行retain操作(強引用);

<2>所以此時,__block的作用相當於__unsafe_unretained的作用,原理一樣;

 

補充一個問題:在ARC環境下,弱指針不能通過"->"形式來訪問對象的成員變數

原因:就是weakSelf很可能為為空(即有可能提前被釋放了),所以必須使用強指針來訪問

//代碼

Person.m

- (void) test6
{
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"-------%d", strongSelf->_age);
    };
}

main.m

int main(int argc, const char * argv[]) {
    @autoreleasepool {
//        test1();
//        test2();
//        test3();
//        test4();
//        test5();
        Person *per = [[Person alloc] init];
        [per test6];
    }
    
    NSLog(@"----");
    return 0;
}

//列印

2019-01-18 17:43:41.010848+0800 MJ_TEST[2557:208583] -[Person dealloc]
2019-01-18 17:43:41.011346+0800 MJ_TEST[2557:208583] ----
Program ended with exit code: 0

//clang

Person.m

struct __Person__test6_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test6_block_desc_0* Desc;
  Person *const __weak weakSelf;
  __Person__test6_block_impl_0(void *fp, struct __Person__test6_block_desc_0 *desc, Person *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

分析:block對象的內部成員變數weakSelf依然是weak類型,並不受block代碼塊內部的__strong轉化,該轉化只是為了騙取編譯器通過編譯而已;

 

三、結論

1)原因:block對象與OC對象相互持有(強引用)——OC對象有block屬性,block代碼塊中用到了該實例對象;

2)危害:程式結束時,相互強應用(對象的引用計數>0)導致實例對象所占記憶體不能及時被系統回收——即記憶體泄露;

3)解決:

<1>ARC:__weak修飾(常用)、__unsafe_unretained(會引起野指針調用,不推薦)、__block(過於繁瑣,不推薦);

<2>MRC:__unsafe_unretained和__block;

 

 

GitHub


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

-Advertisement-
Play Games
更多相關文章
  • 資料庫建表時,對於一些可填可不填的欄位,我們應該儘量把它設置為 NOT NULL。這種做法即可以提高性能,又可以在很大程度上避免空指針類的問題,好處頗多。 1.節省空間 NULL 列需要更多的存儲空間:需要一個額外位元組作為判斷是否為 NULL 的標誌位。 2.空指針 查詢時,可以在一定程度上減少 N ...
  • 在Mysql管理軟體中, 可以通過sql語句中的dml語言來實現數據的操作, 包括 插入數據INSERT: 更新數據: UPDATE 刪除數據DELETE 查詢數據SELECT (使用頻率最高) 單表查詢: https://www.cnblogs.com/q455674496/p/10289806. ...
  • TFTP服務的作用:提供網路下載服務 tftp伺服器的安裝與配置: tftp主要用於嵌入式交叉開發環境的搭建,傳輸文件。 0、創建tftp的工作目錄,並修改許可權(註意:請在主目錄下創建此工作目錄!) sudo mkdir /tftpboot sudo chmod 777 /tftpboot 1、安裝 ...
  • 問題描述:在使用mybatis對資料庫執行更新操作時,parameterType為某個具體的bean,而bean中傳入的參數為null時,拋出異常如下:org.mybatis.spring.MyBatisSystemException: nested exception is org.apache. ...
  • # 使用的navicat 編輯的存儲過程 CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_student_back`()BEGIN#定義max變數 DECLARE max INT DEFAULT 0; select max(id) into m ...
  • 百度了一下,有一個大佬是這樣說的: 在PL/SQL中查詢資料庫視圖時總是報告“ora-04063:view view_test has errors”的錯誤: Oracle視圖非常強大的功能之一在於其可以創建一個帶有錯誤的視圖。比如說視圖裡的欄位在基表裡不存在,該視圖仍然可以創建成功,但是非法的且無 ...
  • 下載 x64bit https://www.oracle.com/technetwork/cn/database/windows/downloads/index.html 適用於 Windows 的 Oracle Data Access Components (ODAC) 適用於 Windows 的 ...
  • android開發一些有用的網站有很多,可以方便我們開發,記錄一下哈。 1、Android源代碼線上閱讀:https://www.androidos.net.cn/sourcecode 2、線上Json校驗格式化工具:https://www.bejson.com/ 3、開源載入動畫:https:// ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...