block專遞參數導致野指針引發crash


一、問題引入

  近日開發中引入一個隨機crash,Crash堆棧如下:

  

Exception Type: SIGSEGV
Exception Codes: SEGV_ACCERR at 0x0000000101850148
Crashed Thread: 0

Thread 0 Crashed: 
0  libobjc.A.dylib                0x00000001802601a0 objc_retain + 16
1  CoreFoundation                 0x0000000180f593a0 -[__NSDictionaryM enumerateKeysAndObjectsWithOptions:usingBlock:] +  232
2  LiveAssistant                  0x0000000100213fe8 -[LAPKViewStatusObj notifyViewStateDidChangeForType:fromUser:] (LAPKViewStatusObj.m:313)
3  LiveAssistant                  0x0000000100214958 -[LAPKViewStatusObj changePKStatusTo:changeMatchStatusTo:changeGuestMatchStatusTo:] (LAPKViewStatusObj.m:635)
4  LiveAssistant                  0x0000000100213ed4 -[LAPKViewStatusObj updatePKState] (LAPKViewStatusObj.m:300)
5  CoreFoundation                 0x000000018101cc3c ___CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ +  20
 +  20
6  CoreFoundation                 0x000000018101c1b8 __CFXRegistrationPost +  428
7  CoreFoundation                 0x000000018101bf14 ____CFXNotificationPost_block_invoke +  216
8  CoreFoundation                 0x000000018109984c -[_CFXNotificationRegistrar find:object:observer:enumerator:] +  1408
9  CoreFoundation                 0x0000000180f52f38 _CFXNotificationPost + 376
10 Foundation                     0x00000001819c3bbc -[NSNotificationCenter postNotificationName:object:userInfo:] +  68
11 LiveAssistant                  0x00000001003ae710 __44-[LAPKStatusManager notifyPKStatusDidChange]_block_invoke (LAPKStatusManager.m:107)
12 libdispatch.dylib              0x000000018097caa0 __dispatch_call_block_and_release +  24
13 libdispatch.dylib              0x000000018097ca60 __dispatch_client_callout +  16
14 libdispatch.dylib              0x000000018098965c __dispatch_main_queue_callback_4CF$VARIANT$mp +  1012
15 CoreFoundation                 0x0000000181033070 ___CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ +  12
 +  12
16 CoreFoundation                 0x0000000181030bc8 ___CFRunLoopRun +  2272
17 CoreFoundation                 0x0000000180f50da8 CFRunLoopRunSpecific + 544
18 GraphicsServices               0x0000000182f36020 GSEventRunModal + 100
19 UIKit                          0x000000018af70758 UIApplicationMain + 228
20 LiveAssistant                  0x000000010048b514 main (main.m:14)
21 libdyld.dylib                  0x00000001809e1fc0 _start +  4

  明顯是對一個對象進行retain的時候產生的Crash。仔細回憶卻沒有發現突破點。直到看到自己寫的下列代碼

- (void)xxxxxBlock:(someBlock)block
                      yyyy:(NSString *)zzzzz
{

    
    __block someblock copyUserBlock = [block copy];
    if(block)
    {
        __weak typeof(self) wself = self;
        someblock hook = ^(NSObject *statusObj, NSObject *model, int status, NSString *businessKey) {
            __strong typeof(self) sself = wself;
            block(statusObj, model, status, businessKey);
        };
        return;
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    int x = 2;
    NSString *str = [NSString stringWithFormat:@"fsfsfsdfsfsdf"];
    
    [self addPKAnimationUpdateBlock:^(NSObject *statusObj, NSObject *model, int status, NSString *businessKey) {
        
        NSLog(@"x = %d, str = %@", x, str);
        
    } forBusiness:str];
}

  經過代碼驗證,stackBlock作為參數傳遞的時候,需要確保對其進行copy操作,否則stackBlock在函數返回之后會被釋放,造成野指針。

 

 上面的方法執行完畢之后,傳入的block參數作為 stackBlock 類型依然沒有發生改變;對其進行拷貝之后變成了mallocBlock。

 

二、問題總結分析

  1)block分類

    1、_NSConcreteGlobalBlock全局的靜態 block,不會訪問任何外部變量;
    2、_NSConcreteStackBlock保存在棧中的 block,當函數返回時會被銷毀;
    3、_NSConcreteMallocBlock保存在堆中的 block,當引用計數為0時會被銷毀;

  2)block對外部變量的引用

    一、靜態變量 和 全局變量   在加和不加  __block 都會直接引用變量地址。也就意味着 可以修改變量的值。在沒有加__block 參數的情況下。
      全局block 和 棧block 區別為 是否引用了外部變量,堆block 則是對棧block  copy 得來。對全局block copy 不會有任何作用,返回的依然是全局block。
    二, 常量變量(NSString *a = @"hello";a 為常量變量,@“hello”為常量。)-----不加__block類型 block 會引用常量的地址(淺拷貝)。加__block類型 block會去引用常量變量(如:a變量,a = @"abc".可以任意修改a 指向的內容。)的地址。
    三、對象變量 如(MyClass *class、Block block)。 這里block 也是”類“對象(類似對象,其包含isa指針,clang 反編譯可以查看。因為它不像從NSObject 繼承下來的對象都支持 retain、copy、release)。       Block的copy、retain、release操作不同於NSObjec的copy、retain、release操作:       對Block不管是retain、copy、release都不會改變引用計數retainCount,retainCount始終是1;       NSGlobalBlock:retain、copy、release操作都無效;       NSStackBlock:retain、release操作無效,必須注意的是,NSStackBlock在函數返回后,Block內存將被回收。即使retain也沒用。容易犯的錯誤是[[mutableAarry addObject:stackBlock],在函數出棧后,從mutableAarry中取到的stackBlock已經被回收,變成了野指針。正確的做法是先將stackBlock copy到堆上,然后加入數組:[mutableAarry addObject:[[stackBlock copy] autorelease]]。支持copy,copy之后生成新的NSMallocBlock類型對象。       NSMallocBlock支持retain、release,雖然retainCount始終是1,但內存管理器中仍然會增加、減少計數。copy之后不會生成新的對象,只是增加了一次引用,類似retain;       盡量不要對Block使用retain操作。
  
  3)block作為外部變量的時候,確保其被copy的場景
    在ARC環境,大多數情況下編譯器會適當地進行判斷,會自動生成將Block從棧上復制到堆上的代碼。
    將Block作為函數返回值返回時,編譯器會自動生成復制到堆上的代碼。
    編譯器不能判斷“自動將Block從棧上復制到堆上”的情況:向方法或函數的參數傳遞Block

  比如下面的代碼:

  

-(id)getBlockArray
{
    int val = 10;
    //Block變量類型可以直接調用copy方法。所以說Block其實也是Objective-C對象。
    //不管Block配置在堆、棧或者數據區域,用copy方法復制都不會引起任何問題。
    return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0:%@",@(val));} copy],[^{NSLog(@"blk1:%@",@(val));} copy], nil];
}

- (void)viewDidLoad {
    [super viewDidLoad];

    //正常執行。
    id obj = [self getBlockArray];
    blk_t blk = (blk_t)[obj objectAtIndex:0];
    blk();
}

  4)block什么時候會被copy

1.  調用Block的copy實例方法時;
2.  Block作為函數返回值返回時;
3.  將Block賦值給附有__strong修飾符id類型的類或Block類型成員變量時;
4.  在方法名中含有usingBlock的Cocoa框架方法或Grand Central Dispatch的API中傳遞Block時。

 

三、關於block被嵌套的問題

  看下面的代碼:

  

 

   在運行到56行的時候,str的引用計數是多少呢?

  

 

 

   在運行到78行的時候,str的引用計數是多少呢?

 

   在78行的時候,為什么是4

  首先創建的時候是1,54行str指針引用加1 = 2; 56行第一次執行第一層block引用str加1 ; 第一層中的指針str引用加1 總共等於4

  這也是嵌套block的時候內存管理的方式,嵌套的block執行的時候,運行時只能處理第一層block中引用的外部變量

  此處以后要多加注意。

 

四、引用資料

 1) ARC環境下Block的內存管理 

    作者:sxtra

    鏈接:https://www.jianshu.com/p/0fad960d6795

 
 2)https://www.cnblogs.com/DamonTang/p/4146728.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM