Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> block的學習(block和timer的循環引用問題)

block的學習(block和timer的循環引用問題)

編輯:關於android開發

block的學習(block和timer的循環引用問題)


一、什麼是回調函數?

回調函數,本質上也是個函數(擱置函數和方法的爭議,就當這二者是一回事)。由“聲明”、“實現”、“調用”三部分組成。

在上面的例子中,我可以看出,函數amount(其實是Block),的聲明和調用在A類中,而實現部分在B類中。也就是說,B類實現了amount函數,但並沒有權限調用,最終還是 由A類觸發調用。我們稱這樣的機制為“回調”。意思是“雖然函數的實現寫在B類中,但是真正的調用還是得由A類來完成。”正常函數“函數聲明、實現均在一個類中完成。”

一句大白話理解“回調”的概念:“函數的實現部分雖然不在老家(A類),但是最終的調用還是由老家人完成”,這樣的函數就叫做回調函數。“老家人調用你,就叫回調,因為你本來就屬於老家。

用《無間道》理解“回調函數”概念:

香港警務處(類):

招聘了一名警察張三(聲明函數),並培養、訓練他(實現函數)。

招聘了一名警察陳仁貴(聲明函數),但並沒有培養他,而是被送進了三合會。但有任務的時候,警務處會調用陳仁貴(回調函數)。

廉政總署(類):使用警務處的張三(普通調用)。

三合會(類):培養、訓練陳仁貴(實現函數)。

第二個問題:什麼情況下使用回調函數?

假設有A、B兩個類。

(1)A類有多種形態,要在B類中實現回調函數。如假設A類是網絡請求開源類ASIHttpRequest,它可能請求成功,也可能請求失敗。這個時候,B類就要針對以上兩個情況,作不同的處理。

(2)A類的形態由B類決定時,要在B類中實現回調函數。如UITableView類就會提供很多回調函數(iOS專業術語稱“委托”方法)

(3)A類需要向B類傳遞數據時,可以在B類中實現回調函數(A類一般是數據層比較耗時的操作類)。如舉的那個發工資的例子。在實際編程中,這樣的機制有個好處就是可以提升用戶的操作體驗。比如用戶從X頁面跳轉到Y頁面,需要向網絡請求數據,而且比較耗時,那我們怎麼辦?有三種方案:第一種就是在X頁面展示一個旋轉指示器,當收到網絡傳回的數據時,在展現Y頁面。第二種就是使用回調函數。用戶從X頁面直接跳轉到Y頁面,Y頁面需要到數據讓數據層去執行,當收到數據時,再在Y頁面展現。第三種就是在Y頁面中開啟多線程。讓一個子線程專門到後台去取數據。綜合來說,第二種更加簡介易懂,而且代碼緊湊。

第三個問題:使用回調函數有什麼好處?

(1)可以讓實現方,根據回調方的多種形態進行不同的處理和操作。(ASIHttpRequest)

(2)可以讓實現方,根據自己的需要定制回調方的不同形態。(UITableView)

(3)可以將耗時的操作隱藏在回調方,不影響實現方其它信息的展示。

(4)讓代碼的邏輯更加集中,更加易讀。

什麼是回調函數?——就是由聲明函數的類來調用的函數叫做回調函數。普通函數可以讓任何類調用。

“回調”的主語是誰?——聲明“回調函數”的那個類。

Block、委托、通知、回調函數,它們雖然名字不一樣,但是原理都一樣,都是“回調機制”的思想的具體實現!

二、block 注意事項
1,block 在實現時就會對它引用到的它所在方法中定義的棧變量進行一次只讀拷貝,然後在 block 塊內使用該只讀拷貝。

如下代碼:

- (void)testAccessVariable
{
    NSInteger outsideVariable = 10;
    //__block NSInteger outsideVariable = 10;
    NSMutableArray * outsideArray = [[NSMutableArray alloc] init];

    void (^blockObject)(void) = ^(void){
        NSInteger insideVariable = 20;
        KSLog(@"  > member variable = %d", self.memberVariable);
        KSLog(@"  > outside variable = %d", outsideVariable);
        KSLog(@"  > inside variable = %d", insideVariable);

        [outsideArray addObject:@"AddedInsideBlock"];
    };

    outsideVariable = 30;
    self.memberVariable = 30;

    blockObject();

    KSLog(@"  > %d items in outsideArray", [outsideArray count]);
}

輸出結果為:

member variable = 30
outside variable = 10
inside variable = 20
1 items in outsideArray

注意到沒?outside 變量的輸出值為10,雖然outside變量在定義 block 之後在定義 block 所在的方法 testAccessVariable 中被修改為 20 了。這裡的規則就是:blockObject 在實現時會對 outside 變量進行只讀拷貝,在 block 塊內使用該只讀拷貝。因此這裡輸出的是拷貝時的變量值 10。如果,我們想要讓 blockObject 修改或同步使用 outside 變量就需要用 __block 來修飾 outside 變量。
說法二:(此處不能修改的原因是在編譯期間確定的,編譯器編譯的時候把a的值復制到block作為一個新變量(假設是a‘ = 10),此時a’和a是沒有關系的。這個地方就是函數中的值傳遞。如果要修改就要加關鍵字:__block或者static)

__block NSInteger outsideVariable = 10;

注意:

a),在上面的 block 中,我們往 outsideArray 數組中添加了值,但並未修改 outsideArray 自身,這是允許的,因為拷貝的是 outsideArray 自身。

b),對於 static 變量,全局變量,在 block 中是有讀寫權限的,因為在 block 的內部實現中,拷貝的是指向這些變量的指針。

c), __block 變量的內部實現要復雜許多,__block 變量其實是一個結構體對象,拷貝的是指向該結構體對象的指針。

2,非內聯(inline) block 不能直接訪問 self,只能通過將 self 當作參數傳遞到 block 中才能使用,並且此時的 self 只能通過 setter 或 getter 方法訪問其屬性,不能使用句點式方法。但內聯 block 不受此限制。

typedef NSString* (^IntToStringConverter)(id self, NSInteger paramInteger);
- (NSString *) convertIntToString:(NSInteger)paramInteger
                 usingBlockObject:(IntToStringConverter)paramBlockObject
{
    return paramBlockObject(self, paramInteger);
}

typedef NSString* (^IntToStringInlineConverter)(NSInteger paramInteger);
- (NSString *) convertIntToStringInline:(NSInteger)paramInteger
                 usingBlockObject:(IntToStringInlineConverter)paramBlockObject
{
    return paramBlockObject(paramInteger);
}

IntToStringConverter independentBlockObject = ^(id self, NSInteger paramInteger) {
    KSLog(@" >> self %@, memberVariable %d", self, [self memberVariable]);

    NSString *result = [NSString stringWithFormat:@"%d", paramInteger];
    KSLog(@" >> independentBlockObject %@", result);
    return result;
};

- (void)testAccessSelf
{
    // Independent
    //
    [self convertIntToString:20 usingBlockObject:independentBlockObject];

    // Inline
    //
    IntToStringInlineConverter inlineBlockObject = ^(NSInteger paramInteger) {
        KSLog(@" >> self %@, memberVariable %d", self, self.memberVariable);

        NSString *result = [NSString stringWithFormat:@"%d", paramInteger];
        KSLog(@" >> inlineBlockObject %@", result);
        return result;
    };
    [self convertIntToStringInline:20 usingBlockObject:inlineBlockObject];
}

3,使用 weak–strong dance 技術來避免循環引用

在第二條中,我提到內聯 block 可以直接引用 self,但是要非常小心地在 block 中引用 self。因為在一些內聯 block 引用 self,可能會導致循環引用。如下例所示:

@interface KSViewController ()
{
    id _observer;
}

@end

@implementation KSViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    KSTester * tester = [[KSTester alloc] init];
    [tester run];

    _observer = [[NSNotificationCenter defaultCenter]
                 addObserverForName:@"TestNotificationKey"
                 object:nil queue:nil usingBlock:^(NSNotification *n) {
                     NSLog(@"%@", self);
                 }];
}

- (void)dealloc
{
    if (_observer) {
        [[NSNotificationCenter defaultCenter] removeObserver:_observer];
    }
}

在上面代碼中,我們添加向通知中心注冊了一個觀察者,然後在 dealloc 時解除該注冊,一切看起來正常。但這裡有兩個問題:

a) 在消息通知 block 中引用到了 self,在這裡 self 對象被 block retain,而 _observer 又 retain 該 block的一份拷貝,通知中心又持有 _observer。因此只要 _observer 對象還沒有被解除注冊,block 就會一直被通知中心持有,從而 self 就不會被釋放,其 dealloc 就不會被調用。而我們卻又期望在 dealloc 中通過 removeObserver 來解除注冊以消除通知中心對 _observer/block 的 retain。

b) 同時,_observer 是在 self 所在類中定義賦值,因此是被 self retain 的,這樣就形成了循環引用。

上面的過程 a) 值得深入分析一下:

蘋果官方文檔中對 addObserverForName:object:queue:usingBlock: 中的 block 變量說明如下:

The block is copied by the notification center and (the copy) held until the observer registration is removed.

因此,通知中心會拷貝 block 並持有該拷貝直到解除 _observer 的注冊。在 ARC 中,在被拷貝的 block 中無論是直接引用 self 還是通過引用 self 的成員變量間接引用 self,該 block 都會 retain self。

這兩個問題,可以用 weak–strong dance 技術來解決。該技術在 WWDC 中介紹過:2011 WWDC Session #322 (Objective-C Advancements in Depth)


    __weak KSViewController * wself = self;
    _observer = [[NSNotificationCenter defaultCenter]
                 addObserverForName:@"TestNotificationKey"
                 object:nil queue:nil usingBlock:^(NSNotification *n) {
                     KSViewController * sself = wself;
                     if (sself) {
                         NSLog(@"%@", sself);
                     }
                     else {
                         NSLog(@" dealloc before we could run this code.");
                     }
                 }];

下面來分析為什麼該手法能夠起作用。
首先,在 block 之前定義對 self 的一個弱引用 wself,因為是弱引用,所以當 self 被釋放時 wself 會變為 nil;然後在 block 中引用該弱應用,考慮到多線程情況,通過使用強引用 sself 來引用該弱引用,這時如果 self 不為 nil 就會 retain self,以防止在後面的使用過程中 self 被釋放;然後在之後的 block 塊中使用該強引用 sself,注意在使用前要對 sself 進行了 nil 檢測,因為多線程環境下在用弱引用 wself 對強引用 sself 賦值時,弱引用 wself 可能已經為 nil 了。

通過這種手法,block 就不會持有 self 的引用,從而打破了循環引用。

擴展:其他還需要注意避免循環引用的地方

與此類似的情況還有 NSTimer。蘋果官方文檔中提到”Note in particular that run loops retain their timers, so you can release a timer after you have added it to a run loop.”,同時在對接口

(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

的 target 說明文檔中提到:

The object to which to send the message specified by aSelector when the timer fires. The target object is retained by the timer and released when the timer is invalidated.

結合這兩處文檔說明,我們就知道只要重復性 timer 還沒有被 invalidated,target 對象就會被一直持有而不會被釋放。因此當你使用 self 當作 target 時,你就不能期望在 dealloc 中 invalidate timer,因為在 timer 沒有被invalidate 之前,dealloc 絕不會被調用。因此,需要找個合適的時機和地方來 invalidate timer,但絕不是在 dealloc 中。

4,block 內存管理分析

block 其實也是一個 NSObject 對象,並且在大多數情況下,block 是分配在棧上面的,只有當 block 被定義為全局變量或 block 塊中沒有引用任何 automatic 變量時,block 才分配在全局數據段上。 __block 變量也是分配在棧上面的。

在 ARC 下,編譯器會自動檢測為我們處理了 block 的大部分內存管理,但當將 block 當作方法參數時候,編譯器不會自動檢測,需要我們手動拷貝該 block 對象。幸運的是,Cocoa 庫中的大部分名稱中包含”usingBlock“的接口以及 GCD 接口在其接口內部已經進行了拷貝操作,不需要我們再手動處理了。但除此之外的情況,就需要我們手動干預了。

- (id) getBlockArray
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{ KSLog(@"  > block 0:%d", val); },    // block on the stack
            ^{ KSLog(@"  > block 1:%d", val); },    // block on the stack
            nil];

//    return [[NSArray alloc] initWithObjects:
//            [^{ KSLog(@"  > block 0:%d", val); } copy],    // block copy to heap
//            [^{ KSLog(@"  > block 1:%d", val); } copy],    // block copy to heap
//            nil];
}

- (void)testManageBlockMemory
{
    id obj = [self getBlockArray];
    typedef void (^BlockType)(void);
    BlockType blockObject = (BlockType)[obj objectAtIndex:0];
    blockObject();
}

執行上面的代碼中,在調用 testManageBlockMemory 時,程序會 crash 掉。因為從 getBlockArray 返回的 block 是分配在 stack 上的,但超出了定義 block 所在的作用域,block 就不在了。正確的做法(被屏蔽的那段代碼)是在將 block 添加到 NSArray 中時先 copy 到 heap 上,這樣就可以在之後的使用中正常訪問。

在 ARC 下,對 block 變量進行 copy 始終是安全的,無論它是在棧上,還是全局數據段,還是已經拷貝到堆上。對棧上的 block 進行 copy 是將它拷貝到堆上;對全局數據段中的 block 進行 copy 不會有任何作用;對堆上的 block 進行 copy 只是增加它的引用記數。

如果棧上的 block 中引用了__block 類型的變量,在將該 block 拷貝到堆上時也會將 __block 變量拷貝到堆上如果該 __block 變量在堆上還沒有對應的拷貝的話,否則就增加堆上對應的拷貝的引用記數。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved