iOS-Blocks學習

weixin_34148340發表於2018-12-19

大部分內容來自

Block:帶有自動變數的匿名函式.是C語言的擴充功能.Block擁有捕獲外部變數的功能.在Block中訪問一個外部的區域性變數,Block會持有它的臨時狀態,自動捕獲變數值,外部區域性變數的變化不會影響它的狀態.

Block實質

在通過clange -rewrite-objec檢視Block的原始碼可以看到,Block是作為引數進行了傳遞

//Block結構體
struct __main_block_impl_0 {
    struct __block_impl;
    struct __main_block_desc_0* Desc;

    __main_block_impl_0 (void *fp,struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Floags = flags;
        impl.funcPtr = fp;
        Desc = desc;
    }
}
複製程式碼

其中isa = &_NSConcreteStackBlock,將Block指標賦給Block的結構體成員變數isa.為了理解它,首先要理解Objective-c類和物件的實質.其實,Block就是Objective-c物件.

id這一變數型別用於儲存Objective-c物件.id為objc_object結構體的指標型別

typedef struct objc_object {
    Class isa;
} *id;
複製程式碼

Class為objc_class結構體的指標型別

typedef struct objc_class *Class

struct objc_class {
    Class isa;
};

//這與objc_object結構體相同.然而,objc_object結構體和objc_class結構體歸根結底是在各個物件和類的實現中使用的最基本的結構體
複製程式碼

這裡需要理解一下Objective-c的類與物件的實質,各類的結構體就是基於objc_class結構體的class_t結構體.class_t結構體在objc4執行時庫中的宣告

struct class_t {
    struct class_t *isa;
    struct class_t *superclass;
    Cache cache;
    IMP *vtable;
    uintptr_t data_NEVER_USE;
};
複製程式碼

class_t結構體例項持有宣告的成員變數,方法名稱,方法的實現(函式指標),屬性以及父類的指標,並被Objective-c執行時庫所使用.

在__main_block_impl_0結構體相當於基於objc_object結構體的objective-c類物件的結構體. 即_NSConcreteStackBlock相當於class_t結構體例項.在將Block作為Objective-c的物件處理時,關於該類的資訊放置於_NSConcreteStackBlock中.

截獲自動變數值

所謂的"截獲自動變數值"意味著在執行Block語法時,Block語法表示式所使用的自動變數值被儲存到Block的結構體例項(即Block自身)中. Block中使用自動變數後,在Block的結構體例項中重寫該自動變數也不會改變原先截獲的自動變數.這樣一來就無法在Block中儲存值了,解決這個問題有兩種方法:

  • C語言有一個變數,允許Block改寫值:靜態變數

靜態變數的指標傳遞給__main_block_impl_0結構體的建構函式並儲存.

  • 使用__block說明符

C語言有一下儲存域類說明符: * typedef * extern * static :表示作為靜態變數儲存在資料區中 * auto :表示作為自動變數儲存在棧中 * register

__block說明符類似於這些,用它來指定Block中想變更值的自動變數. 使用__block修飾一個變數,在Block的原始碼中會變成一個結構體例項.


__block int val = 10;

//轉換原始碼
__Block_byref_val_0 val = {
    0,
    &val,
    0,
    sizeof(__Block_byref_val_0),
    10//該變數初始化為10,且這個值也出現在結構體例項的初始化中,這意味著該結構體持有相當於原自動變數的成員變數.
};

//該結構體宣告
struct __Block_byref_val_0 {
    void *__isa;
    __Block_byref_val_0 *forwarding;
    int __flags:
    int __size;
    int val;
}
複製程式碼

在給__block變數賦值時,__Block_byref_val_0結構體例項的成員變數__forwarding持有指向該例項自身的指標.通過成員變數__forwarding訪問成員變數val.通過訪問val的指標,就可以從多個Block中使用同一個__block變數.

Block儲存域

Block的儲存,Block在棧堆上的問題

Block也是objective-c物件,具有這幾個類

設定物件的儲存域
_NSConcreteStackBlock
_NSConcreteGlobalBlock 程式的資料區域(.data區)
_NSConcreteMallocBlock

__block變數用結構體成員變數__forwarding可以實現無論__block變數配置在棧上還是堆上是都能夠正確地訪問__block變數.

當ARC有效時,大多數情形下編譯器會恰當的進行判斷,自動生成將Block從棧上覆制到堆上的程式碼.

Block的copy總結

Block的類 副本源的配置儲存域 複製效果
_NSConcreteStackBlock 從棧複製到堆
_NSConcreteGlobalBlock 程式的資料區域(.data區) 什麼也不做
_NSConcreteMallocBlock 引用結束增加

Block引起的迴圈引用

我們在設定Block之後,不希望再回撥Block時Block已經被釋放了,所以我們會對block進行copy,copy到堆中.

//這裡定義一個陣列,裡面新增兩個block,因為這兩個block沒有copy到堆上,所以當呼叫它時可能已經被釋放了
- (void)viewDidLoad {
    [super viewDidLoad];
    
    //建立陣列
    id obj = [self getBlockArray];
    
    //定義
    typedef void (^blk_t)(void);
    
    //賦值-獲取到obj陣列中的第一個元素
    blk_t blk = (blk_t)[obj objectAtIndex:0];
    
    //呼叫
    blk();
}

-(id)getBlockArray {
    int val = 10;
    
    return [[NSArray alloc]initWithObjects:^{
        NSLog(@"blk0:%d",val);
        
    },^{
        NSLog(@"blk0:%d",val);
        
    }, nil];
}

複製程式碼

但在copy到堆上的時候,會retain其引用的外部變數,如果Block中引用了他的宿主物件,那就可能引起迴圈引用的問題. ARC下,有兩種方法可以解決迴圈引用問題:

  • 事前避免(__weak__strong)

在使用block時,__weak修飾他的宿主物件,物件的引用計數不會+1.還需要在Block內部對弱引用物件進行一次強引用,這是因為僅用__weak修飾的物件,如果被釋放,那麼這個物件在Block執行的過程中就會變成nil,這就可能會帶來一些問題.使用__strong修飾物件,直到Block被執行完畢,這個物件都不會被釋放.

  • 事後補救

在確定block回撥執行完成後,將其中一方強制置為空 xx==nil.

補充關於weak

weak修飾屬性,這個問題就好像iOS程式設計師之間打招呼的方式一樣. weak表示一個由runtime維護的hash表,key是所指物件的地址,value是weak指標的地址陣列. weak是弱引用,所引用物件的計數器不會加1,並在引用物件被釋放的時候自動被設定為nil.這是因為:

  • runtime會對註冊的類進行佈局,當此物件的引用次數為0時釋放物件,在weak表中以物件地址為鍵搜尋,將搜尋到的weak物件置為nil

這也是weak會置為nil,而assign有可能會野指標的原因,所以在修飾Objective-c物件的時候多用weak,不用assign.


如果有不對的地方還請指正,謝謝

相關文章