單元測試(三)

一. 單元測試覆蓋率&調(diào)試測試代碼

1.1 查看單元測試覆蓋率
打開開源項(xiàng)目SYTimer橄唬,如下圖所示

image.png

開源項(xiàng)目SYTimer介紹:

  • SYTimer基于RunLoop Timer二次封裝
  • 我們在不同頁面使用不同NSTimer的時(shí)候,多個(gè)NSTimer的啟動(dòng)時(shí)機(jī)不同榜揖。其實(shí)我們在底層使用一個(gè)NSTimer就夠用了,只需要控制NSTimer在不同啟動(dòng)時(shí)機(jī)運(yùn)行相應(yīng)代碼就可實(shí)現(xiàn)抗蠢。SYTimer就是使用一個(gè)RunLoop Timer在不同線程里提供一個(gè)比NSTimer更好的運(yùn)行機(jī)制举哟。

設(shè)計(jì)此項(xiàng)目需要考慮的問題?

  1. 使用一個(gè)Timer來運(yùn)行迅矛,需要對不同啟動(dòng)時(shí)機(jī)進(jìn)行排序
  2. 對啟動(dòng)時(shí)機(jī)進(jìn)行排序妨猩,使用堆排序會(huì)更好(堆排序分為 大頂堆 小頂堆)

我們在進(jìn)行單元測試的時(shí)候需要關(guān)注,堆排序在進(jìn)行不同timer排序時(shí)的單元測試覆蓋率秽褒?接下來測試代碼在進(jìn)行timer排序時(shí)調(diào)用了多少次壶硅?該怎么進(jìn)行測試?
選擇Edit Scheme -- Test -- Options销斟,如下圖所示

image.png
// SYHeapTest.m文件內(nèi)單元測試方法
- (void)testSimple {
    // 堆排序的初始化庐椒,指定SYMaxHeap
    SYHeap<NSNumber *>* h = [[SYHeap alloc] initWithHeapType:SYMaxHeap usingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        // 指定堆里面元素的排序規(guī)則
        return [obj1 compare:obj2];
    }];
    [h addObject:@(1)];
    [h addObject:@(3)];
    [h addObject:@(2)];

    XCTAssertEqual(@(3), [h removeRootObject]);
    XCTAssertTrue([h checkHeapProperty]);
}

執(zhí)行單元測試方法testSimple,測試成功
查看文件的單元測試覆蓋率

image.png

展開SYHeap.mm文件蚂踊,查看文件內(nèi)方法的單元測試覆蓋率

image.png

點(diǎn)擊進(jìn)入SYHeap查看comparator:b: 方法

image.png
  • 紅色代表未使用的代碼
  • 綠色代表使用了的代碼
  • 紅色注釋形狀约谈,代表此處代碼部分被使用

1.2 調(diào)試測試代碼
如果我們不了解堆排序的初始化方式,testSimple方法寫成如下形式

- (void)testSimple {
    // SYMaxHeap
    SYHeap<NSNumber *>* h = [[SYHeap alloc] init];
    [h addObject:@(1)];
    [h addObject:@(3)];
    [h addObject:@(2)];

    XCTAssertEqual(@(3), [h removeRootObject]);
    XCTAssertTrue([h checkHeapProperty]);
}

執(zhí)行單元測試方法testSimple,報(bào)錯(cuò)如下

image.png

直接顯示了報(bào)錯(cuò)信息窗宇,并沒有給我們調(diào)試的機(jī)會(huì)措伐,此時(shí)我們可以添加如下斷點(diǎn)來進(jìn)行調(diào)試

image.png

再次執(zhí)行單元測試方法testSimple,斷點(diǎn)斷在了報(bào)錯(cuò)地方军俊,這時(shí)我們就可以進(jìn)行相關(guān)的調(diào)試

image.png

二. 集成XCTest與Unit測試

打開LoginApp工程侥加,引入SYCSSColor SYTimer兩個(gè)庫

// podfile文件配置
pod 'SYCSSColor'
pod 'SYTimer'

pod install之后會(huì)報(bào)警告,提示CocoaPods did not set the base configuration of your project already has a custom config set.
解決辦法:

// UnitTest.debug.xcconfig文件內(nèi)導(dǎo)入CocoaPods中的config文件
#include "Pods/Target Support Files/Pods-LoginApp/Pods-LoginApp.debug.xcconfig"

接下來再把SYTimer工程中的單元測試文件導(dǎo)入LoginApp工程(下圖選中的藍(lán)色文件)粪躬,最終工程目錄如下

image.png

現(xiàn)在我們想邊運(yùn)行工程邊測試担败,我們在ViewController.m文件中的兩個(gè)按鈕點(diǎn)擊事件中寫單元測試如下

// ViewController.m文件按鈕點(diǎn)擊事件
- (IBAction)testCSSColorTap:(id)sender {
    LGXCTestCenter *center = [LGXCTestCenter testSuiteForTestCaseClassString:@"SYCSSColorTests"];
    for (XCTest *test in center.tests) {
        [test runTest];
    }
}

- (IBAction)testTimerTap:(id)sender {
    LGXCTestCenter *center = [LGXCTestCenter testSuiteForTestCaseClassString:@"SYHeapTest"];
    for (XCTest *test in center.tests) {
        [test runTest];
    }
}

// LGXCTestCenter文件方法
#import "LGXCTestCenter.h"

@implementation LGXCTestCenter

+ (instancetype)testSuiteForTestCaseClassString:(NSString *)cls {
    Class cl = NSClassFromString(cls);
    if (cl) {
        return [self testSuiteForTestCaseClass:cl];
    }
    return nil;
}
@end

// Run工程
// 點(diǎn)擊觸發(fā)testCSSColorTap方法,測試成功打印如下
Test Case '-[SYCSSColorTests testExample]' started.
2021-03-28 16:24:21.842481+0800 LoginApp[3808:11039066] rgb(26, 115, 0)
Test Case '-[SYCSSColorTests testExample]' passed (0.014 seconds).
// 點(diǎn)擊觸發(fā)testTimerTap方法镰官,測試成功打印如下
Test Case '-[SYHeapTest testAddAndRemoveRandomNumbers]' started.
Test Case '-[SYHeapTest testAddAndRemoveRandomNumbers]' passed (0.002 seconds).
Test Case '-[SYHeapTest testRemoveElement]' started.
Test Case '-[SYHeapTest testRemoveElement]' passed (0.000 seconds).
Test Case '-[SYHeapTest testSimple]' started.
Test Case '-[SYHeapTest testSimple]' passed (0.000 seconds).
Test Case '-[SYHeapTest testSortedDesc]' started.
Test Case '-[SYHeapTest testSortedDesc]' passed (0.001 seconds).

上面單元測試方法在SYCSSColorTests SYHeapTest兩個(gè)類中已經(jīng)寫好了提前,現(xiàn)在猜想能不能邊運(yùn)行工程,邊寫一些測試用例泳唠?
方向: 可以使用runtime來實(shí)現(xiàn)這一猜想

上面導(dǎo)入主工程的三個(gè)類SYCSSColorTests SYHeapTest SYTimerTests代碼參與了編譯狈网,意味著增加了代碼體積,現(xiàn)在想通過不同環(huán)境來規(guī)避掉已經(jīng)測試過的方法笨腥,以減小代碼體積拓哺,該怎么解決?
方案一:創(chuàng)建新的target脖母,不同target工程 Build Phases -- Compile Sources 中配置文件士鸥,把單元測試文件刪掉
方案二:使用宏判斷工程中有沒有導(dǎo)入XCTest來進(jìn)行判斷
XConfig官方文檔

三. XCTest與UI測試

打開LoginApp工程,我們來進(jìn)行UI測試

image.png

運(yùn)行testExample方法可以恢復(fù)我們之前點(diǎn)擊頁面進(jìn)行的UI測試
接下來我們分析生成的UI測試代碼

- (void)testExample {
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app launch];
    // 從app中取出指定控件
    XCUIElement *nameinputTextField = app.textFields[@"nameInput"];
    XCUIElement *passwordInputTextField = app.secureTextFields[@"passwordInput"];
    // 對控件進(jìn)行模擬點(diǎn)擊與賦值
    [nameinputTextField tap];
    [nameinputTextField typeText:@"Cat\n"];
    [passwordInputTextField doubleTap];
    [passwordInputTextField typeText:@"123"];
    [app.buttons.staticTexts[@"登錄"] tap];  
}

上面根據(jù)nameInput取出輸入框谆级,其中nameInput是我們Xib創(chuàng)建UITextField時(shí)設(shè)置的烤礁,如下圖

image.png

接下來我們探討怎么實(shí)現(xiàn)邊運(yùn)行邊進(jìn)行UI測試?

  • 我們發(fā)現(xiàn)在進(jìn)行UI測試的時(shí)候肥照,會(huì)先創(chuàng)建一個(gè)LoginAppUITest的app脚仔,再由這個(gè)app調(diào)起我們的主工程LoginApp,一共創(chuàng)建了兩個(gè)app
  • 上面的UI測試能否在主工程使用舆绎?我們修改testCSSColorTap點(diǎn)擊方法內(nèi)容如下
- (IBAction)testCSSColorTap:(id)sender {
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app launch];
    XCUIElement *nameinputTextField = app.textFields[@"nameInput"];
    XCUIElement *passwordInputTextField = app.secureTextFields[@"passwordInput"];
    [nameinputTextField tap];
    [nameinputTextField typeText:@"Cat\n"];
    [passwordInputTextField doubleTap];
    [passwordInputTextField typeText:@"123"];
    [app.buttons.staticTexts[@"登錄"] tap];
}
// 運(yùn)行LoginApp玻侥,點(diǎn)擊按鈕,程序閃退
2021-03-28 20:23:11.033373+0800 LoginApp[4973:11129917] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'No target application path specified via test configuration: (null)'
// 原因是XCUIApplication只在UI測試target下才能有效
  • 我們想讓UI測試在主工程起作用亿蒸,實(shí)現(xiàn)邊運(yùn)行邊進(jìn)行UI測試該怎么辦凑兰?
  1. 我們可以使用KIF框架,KIF代表Keep It Functional边锁,是一款iOS集成測試框架姑食。 通過利用操作系統(tǒng)為具有視覺障礙的用戶提供的輔助功能屬性,可以輕松實(shí)現(xiàn)iOS應(yīng)用程序的自動(dòng)化茅坛。
  2. KIF使用標(biāo)準(zhǔn)的XCTest測試目標(biāo)來構(gòu)建和執(zhí)行測試音半。
  3. KIF中UI測試與XCTest實(shí)現(xiàn)是有不同的

接下來我們來學(xué)習(xí)使用KIF框架

// podfile文件中配置则拷,導(dǎo)入KIF框架
pod 'SYCSSColor'
pod 'SYTimer'
pod 'KIF', '3.7.13', :configurations => ['Debug']

創(chuàng)建LGKIFTests.m文件,內(nèi)容如下

#import <XCTest/XCTest.h>
#import <KIF/KIF.h>
@interface LGKIFTests : KIFTestCase

@end

@implementation LGKIFTests

// 對應(yīng)通用的UI測試文件中的setUp方法
// 測試之前做一些初始化操作
- (void)beforeEach {
}

// 對應(yīng)tearDownWithError方法
// 測試之后做一些收尾工作
- (void)afterEach {
}

// 測試用例
- (void)testSuccessfulLogin {
    // test 測試的標(biāo)準(zhǔn)
    [tester enterText:@"Cat1237@example.com" intoViewWithAccessibilityLabel:@"nameInput"];
    [tester enterText:@"Cat1237" intoViewWithAccessibilityLabel:@"passwordInput"];
    [tester tapViewWithAccessibilityLabel:@"loginButton"];    
}
@end

接下來我們修改testCSSColorTap內(nèi)容如下

- (IBAction)testCSSColorTap:(id)sender {
    // 這里修改類名為LGKIFTests
    LGXCTestCenter *suite = [LGXCTestCenter testSuiteForTestCaseClassString:@"LGKIFTests"];
    for (XCTest *test in suite.tests) {
        [test runTest];
    }
}
// Run工程
// 點(diǎn)擊觸發(fā)testCSSColorTap方法曹鸠,測試成功打印如下
est Case '-[LGKIFTests testSuccessfulLogin]' started.
2021-03-28 20:54:11.362113+0800 LoginApp[5112:11151519] [TraitCollection] Class CKBrowserSwitcherViewController overrides the -traitCollection getter, which is not supported. If you're trying to override traits, you must use the appropriate API.
2021-03-28 20:54:12.213945+0800 LoginApp[5112:11151519] WARN: Main thread was blocked for more than 0.500000s after animations completed!
Test Case '-[LGKIFTests testSuccessfulLogin]' passed (20.319 seconds).

最后我們的單元測試與UI測試都實(shí)現(xiàn)了 邊運(yùn)行邊測試的目標(biāo)
探討方向: 使用OC運(yùn)行時(shí)來實(shí)現(xiàn)邊運(yùn)行煌茬,邊寫測試用例?

四. TDD與線程存儲(chǔ)數(shù)據(jù)

TDD是測試驅(qū)動(dòng)開發(fā)(Test-Driven Development)彻桃,是敏捷開發(fā)中的一項(xiàng)核心實(shí)踐和技術(shù)坛善,也是一種設(shè)計(jì)方法論,測試驅(qū)動(dòng)開發(fā)的步驟如下圖

image.png
  • 寫一個(gè)失敗的測試用例
  • 再讓這個(gè)測試用例通過
  • 再去進(jìn)行重構(gòu)

按照上面步驟我們來做這樣一件事邻眷,在線程中存儲(chǔ)數(shù)據(jù)眠屎?比如斷點(diǎn)續(xù)傳 下載等
這里我們舉一個(gè)NSRunLoop的例子

// 我們從別的線程切換回來之后都可以使用以下兩句獲取當(dāng)前線程的RunLoop
[NSRunLoop currentRunLoop];
[NSRunLoop mainRunLoop];
// 從別的線程切換回來,RunLoop并沒有改變肆饶,線程對應(yīng)的RunLoop并不是重復(fù)創(chuàng)建改衩,其本質(zhì)就是在當(dāng)前線程中存儲(chǔ)RunLoop實(shí)例結(jié)構(gòu)體

接下來就讓我們一起來實(shí)現(xiàn)在線程中存儲(chǔ)數(shù)據(jù)(可以參考SYThreadSpecificVariable類)
前提: CFRunLoop與NSRunLoop本質(zhì)對面向?qū)ο蟮姆庋b并不友好,如果我們能基于CFRunLoop對SYTimer進(jìn)行面向?qū)ο蟮姆庋b驯镊,后面我們就可以按照面向?qū)ο蟮乃枷雭硎褂肦unLoop葫督。我們要實(shí)現(xiàn)類似NSRunLoop currentRunLoop的功能,就要在線程中存儲(chǔ)數(shù)據(jù)板惑。
打開LGTimer工程候衍,Cmd + N 創(chuàng)建類LGThreadSpecificVariable

// LGThreadSpecificVariable.h 文件內(nèi)容
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGThreadSpecificVariable : NSObject

- (instancetype)initWithValue:(id)value;

@property (nonatomic, strong, readonly) id currentValue;

@end
NS_ASSUME_NONNULL_END

// LGThreadSpecificVariable.m 文件內(nèi)容
#import "LGThreadSpecificVariable.h"
#import <pthread.h>

@interface LGThreadSpecificVariable() {
    pthread_key_t _key;
    id _value;
}
@end
@implementation LGThreadSpecificVariable

- (instancetype)initWithValue:(id)value
{
    self = [super init];
    if (self) {
        int error = pthread_key_create(&_key, nil);
        _value = value;
        if (error != 0) {
            NSAssert(error == 0, @"pthread_key_delete failed, error %d", error);
        }
        // 把數(shù)據(jù)存入線程
        pthread_setspecific(_key, (__bridge_retained const void * _Nullable)(_value));
    }
    return self;
}

- (id)currentValue {
    id data = (__bridge id)(pthread_getspecific(_key));
    if (data) {
        return data;
    }
    return nil;
}
@end

// 單元測試文件LGTimerTests.m中添加方法
// 需要導(dǎo)入頭文件#import "LGThreadSpecificVariable.h"
- (void)test_ThreadSpecificVariable {
    LGThreadSpecificVariable *vc = [[LGThreadSpecificVariable alloc] initWithValue: self];
    XCTestExpectation *expectation = [self expectationWithDescription:@"Test ThreadSpecificVariable"];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        XCTAssertNil(vc.currentValue);
        [expectation fulfill];
    });
    XCTAssertNotNil(vc.currentValue);
    [self waitForExpectationsWithTimeout:10 handler:nil];
}
// 測試test_ThreadSpecificVariable方法,Test Success

現(xiàn)在有一個(gè)問題洒放,當(dāng)創(chuàng)建的vc銷毀時(shí),線程中的數(shù)據(jù)依然存在滨砍,我們可以進(jìn)行相應(yīng)驗(yàn)證

// 創(chuàng)建LGThreadSpecificVariable+Private.h(匿名分類)文件往湿,內(nèi)容如下
#import "LGThreadSpecificVariable.h"
#import <pthread.h>
@interface LGThreadSpecificVariable ()
- (pthread_key_t)getKey;
@end

// LGThreadSpecificVariable.m文件添加getKey方法實(shí)現(xiàn)
- (pthread_key_t)getKey {
    return _key;
}

// 我們在單元測試文件LGTimerTests.m中再次添加方法
// 需要導(dǎo)入頭文件#import "LGThreadSpecificVariable+Private.h"
- (void)test_ThreadSpecificVariable_delloc {
    LGThreadSpecificVariable *vc = [[LGThreadSpecificVariable alloc] initWithValue: self];
    pthread_key_t key = [vc getKey];
    vc = nil;
    id data = (__bridge id)(pthread_getspecific(key));
    XCTAssertNil(data);
    [self waitForExpectationsWithTimeout:10 handler:nil];
}
// 測試test_ThreadSpecificVariable_delloc方法,發(fā)現(xiàn)單元測試失敗惋戏,data有值

從架構(gòu)層面分析领追,線程中存數(shù)據(jù),數(shù)據(jù)應(yīng)與線程的生命周期綁定在一起

// nil參數(shù)响逢,應(yīng)該傳入函數(shù)指針绒窑,指針用于當(dāng)線程被銷毀時(shí),調(diào)用這個(gè)函數(shù)做一些操作
int error = pthread_key_create(&_key, nil);
- (instancetype)initWithValue:(id)value 方法內(nèi)
修改int error = pthread_key_create(&_key, nil);
為int error = pthread_key_create(&_key, destroy);
// LGThreadSpecificVariable.m文件添加銷毀方法
static inline void destroy(void* ptr) {
    CFRelease(ptr);
}

// 單元測試文件LGTimerTests.m中添加方法
- (void)test_ThreadSpecificVariable_destroy {
    LGThreadSpecificVariable *vc = [[LGThreadSpecificVariable alloc] initWithValue: self];
    // 線程銷毀方法
    pthread_exit(0);
    pthread_key_t key = [vc getKey];
    vc = nil;
    id data = (__bridge id)(pthread_getspecific(key));
    XCTAssertNil(data);
    [self waitForExpectationsWithTimeout:10 handler:nil];
}
// 測試test_ThreadSpecificVariable_destroy方法舔亭,發(fā)現(xiàn)會(huì)調(diào)用LGThreadSpecificVariable.m文件的destroy方法
// 也可以在LGThreadSpecificVariable銷毀的方法中調(diào)用destroy((__bridge void *)(_value))
- (void)dealloc {
    destroy((__bridge void *)(_value));
}

推薦看 AsyncDisplayKit庫源碼對RunLoop異步的使用

五. 調(diào)試debugserver

接下來我們來學(xué)習(xí)如何調(diào)試debugserver些膨?
打開SBAPI學(xué)習(xí)工程,我們之前在調(diào)試lldb的時(shí)候通過宏LLDB_DEBUGSERVER_PATH指定debugserver的路徑钦铺,如下圖所示

image.png

現(xiàn)在請思考一個(gè)問題订雾,我們能否編譯自己的帶調(diào)試符號(hào)的debugserver?

  • 只要帶調(diào)試符號(hào)矛洞,我們就可以通過可執(zhí)行文件找到源碼洼哎,debugserver源碼可以在llvm中找到,找到之后打開工程如下圖
image.png
  • debugserver中最難的部分為codesign簽名,因?yàn)楫?dāng)前Mac OS要求使用的debugserver必須要有l(wèi)ldb_codesign證書簽名
  • 系統(tǒng)證書里面默認(rèn)帶有l(wèi)ldb_codesign證書噩峦,但是我們自己編譯的debugserver不能使用lldb_codesign證書進(jìn)行簽名锭沟,所以需要我們自己創(chuàng)建lldb_codesign證書
// debugserver工程中腳本
if [ "${CONFIGURATION}" != BuildAndIntegration ]
then
    if [ -n "${DEBUGSERVER_USE_FROM_SYSTEM}" ]
    then
        ditto "${DEVELOPER_DIR}/../SharedFrameworks/LLDB.framework/Resources/debugserver" "${TARGET_BUILD_DIR}/${TARGET_NAME}"
    elif [ "${DEBUGSERVER_DISABLE_CODESIGN}" == "" ]
    then
        // 通過lldb_codesign證書往里面?zhèn)魅胍恍┳兞坑脕碚埱髾?quán)限
        codesign -f -s lldb_codesign --entitlements ${SRCROOT}/../../resources/debugserver-macosx-entitlements.plist "${TARGET_BUILD_DIR}/${TARGET_NAME}"
    fi
fi

根據(jù)上面腳本路徑找到debugserver-macosx-entitlements.plist 文件,這個(gè)文件就是對一些權(quán)限的請求

截屏2021-03-29 下午11.07.38.png
截屏2021-03-29 下午11.08.16.png

編譯debugserver工程识补,并把生成的可執(zhí)行文件放入SBAPI學(xué)習(xí)工程根目錄族淮,并進(jìn)行路徑配置如下圖所示

image.png
image.png

在main.mm文件的main(int argc, const char * argv[])方法下打斷點(diǎn),并運(yùn)行SBAPI學(xué)習(xí) Debug -- Attach to Process 頂部會(huì)顯示可能關(guān)聯(lián)的target李请,運(yùn)行完成之后瞧筛,斷點(diǎn)進(jìn)入debugserver源碼

image.png

此時(shí)就可以進(jìn)行SBAPI學(xué)習(xí) 與 debugserver源碼進(jìn)行聯(lián)調(diào)

六. block相關(guān)

Block的類型

  • A. GlobalBlock
  1. 位于全局區(qū)
  2. 在Block內(nèi)部不使用外部變量,或者只使用靜態(tài)變量和全局變量
  • B. MallocBlock
    位于堆區(qū)导盅。
    在Block內(nèi)部使用局部變量或者OC屬性较幌,并且賦值給強(qiáng)引用或者Copy修飾的變量
  • C. StackBlock
    位于棧區(qū)
    與 MallocBlock一樣,可以在內(nèi)部使用局部變量或者OC屬性白翻。但是不能賦值給強(qiáng)引用或者 Copy修飾的變量

判斷以下代碼的正確乍炉,需要掌握

  • 明確全局block 堆區(qū)block 棧區(qū)block 的區(qū)別
  • 明確堆上 棧上 變量的區(qū)別
  • 掌握block底層源碼

block代碼塊一

// 下面代碼在執(zhí)行過程中會(huì)不會(huì)報(bào)錯(cuò)?
- (void)blockStack_Stack {
    int a;
    void(^__weak weakBlock)(void) = nil;
    {
        int b = 2;
        // 這里是一個(gè)stack block
        void(^ __weak weakBlock1)(void) = ^{
            NSLog(@"-----%d", b);
        };
        a = b;
        // block是一個(gè)結(jié)構(gòu)體滤馍,結(jié)構(gòu)體 = 結(jié)構(gòu)體
        weakBlock = weakBlock1;
    }
    // 只要weakBlock1在作用域內(nèi)沒有被銷毀岛琼,weakBlock就可以調(diào)用
    weakBlock();
}

// 運(yùn)行工程,正常執(zhí)行

block代碼塊二

- (void)blockStack_malloc {
    int a = 0;
    void(^__weak weakBlock)(void) = nil;
    {
         //這是堆上的block
         void(^__strong strongBlock)(void) = ^{
             NSLog(@"---%d", a);
         };
         //這里跟上面block代碼塊一并無區(qū)別巢株,仍然是 結(jié)構(gòu)體 = 結(jié)構(gòu)體
         weakBlock = strongBlock;
        // 出了作用域后槐瑞,strongBlock會(huì)銷毀
         // block_relase
        // free
    }
    // 對 block 做 release 操作。
    // block 在堆上阁苞,才需要 release困檩,在全局區(qū)和棧區(qū)都不需要 release.
    // 先將引用計(jì)數(shù)減 1,如果引用計(jì)數(shù)減到了 0那槽,就將 block 銷毀
    // void _Block_release(const void *arg) 
    // 堆上的變量已經(jīng)釋放
    // free
    weakBlock();
}

// 運(yùn)行工程悼沿,會(huì)報(bào)錯(cuò)Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)

block代碼塊三

- (void)blockHeap {
    int a = 0;
    // stack block
    void(^ __weak block)(void) = ^{
        NSLog(@"---%d", a);
    };
    dispatch_block_t dispatch_block = ^{
        // 調(diào)用棧上block
        block();
    };
    // 延遲3秒調(diào)用dispatch_block
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
}

// 運(yùn)行工程,會(huì)報(bào)錯(cuò)Thread 1: EXC_BAD_ACCESS (code=1, address=0x22)

// 修改代碼如下骚灸,就可以運(yùn)行成功
- (void)blockHeap {
    int a = 0;
    // stack block
    void(^ __weak block)(void) = ^{
        NSLog(@"---%d", a);
    };
    dispatch_block_t dispatch_block = ^{
        // 調(diào)用棧上block
        block();
    };
    // 延遲3秒調(diào)用dispatch_block
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
    // 設(shè)置RunLoop過期時(shí)間
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}

block代碼塊四

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    int a = 0;
    void(^ __weak weakBlock)(void) = ^{
        NSLog(@"-----%d", a);
    };
    // block轉(zhuǎn)換成結(jié)構(gòu)體形式
    struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
    // strongBlock?  結(jié)構(gòu)體 = 結(jié)構(gòu)體  strongBlock為棧區(qū)block
    void(^ __strong strongBlock)(void) = weakBlock;
    blc->invoke = nil;
    strongBlock();
}

// _LGBlock介紹
struct _LGBlock {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    // 函數(shù)指針糟趾,上面weakBlock代碼塊中代碼就保存在函數(shù)指針內(nèi)
    LGBlockInvokeFunction invoke;
    struct _LGBlockDescriptor1 *descriptor;
};

// 運(yùn)行工程,會(huì)報(bào)錯(cuò)Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)

// 現(xiàn)在修改代碼如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    int a = 0;
    void(^ __weak weakBlock)(void) = ^{
        NSLog(@"-----%d", a);
    };
    struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
    // strongBlock為堆區(qū)block甚牲,與weakBlock是兩個(gè)不同的block
    void(^ __strong strongBlock)(void) = [weakBlock copy];
    // 棧區(qū)block置為nil义郑,malloc block并不影響
    blc->invoke = nil;
    // 不報(bào)錯(cuò) strongBlock malloc
    strongBlock();
}

// 運(yùn)行工程,正常執(zhí)行

block代碼塊五

// 下面方法都在ViewController.m中
static ViewController *staticSelf_;
- (void)blockWeak_static {
    // 把我們的self放入弱引用表里
    __weak typeof(self) weakSelf = self;
    // 再從弱引用表里取出self
    staticSelf_ = self;
}

// 這里產(chǎn)生了循環(huán)引用

block代碼塊六

// 請求下面網(wǎng)址非常慢丈钙,請問self會(huì)不會(huì)立馬釋放魔慷?
// malloc block -> 捕獲變量
// 如果是__weak修飾的變量,捕獲之后self引用計(jì)數(shù)不會(huì)加1
// __strong修飾的變量著恩,捕獲之后self引用計(jì)數(shù)才會(huì)加1
- (void)block_weak_strong {
    [[[NSURLSession sharedSession] dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.raywenderlich.com"]] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 這里的self相當(dāng)于__strong來修飾院尔,只有當(dāng)block執(zhí)行完成之后蜻展,引用計(jì)數(shù)才會(huì)減1
        NSLog(@"%@", self);
    }] resume];
}

// 運(yùn)行工程,會(huì)發(fā)現(xiàn)ViewController延遲銷毀

// 現(xiàn)在修改代碼如下
- (void)block_weak_strong {
    __weak typeof(self) weakSelf = self;
    [[[NSURLSession sharedSession] dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.raywenderlich.com"]] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 請問執(zhí)行到dispatch_after邀摆,能否正常打印strongSelf纵顾?
        __strong typeof(self) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", strongSelf);
        });
    }] resume];
}

// 運(yùn)行工程,會(huì)發(fā)現(xiàn)不會(huì)打印栋盹。原因是執(zhí)行到dispatch_after self已經(jīng)被釋放了

// 繼續(xù)修改代碼如下施逾,這樣會(huì)導(dǎo)致強(qiáng)引用
- (void)block_weak_strong {
    // 導(dǎo)致強(qiáng)引用,blcok捕獲變量是通過傳遞的方式捕獲
    self.doWork = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", self);
        });
    };
    self.doWork();
}

//  接下來我們繼續(xù)修改如下例获,會(huì)不會(huì)正常打雍憾睢?
- (void)block_weak_strong {
    __weak typeof(self) weakSelf = self;
    self.doWork = ^{
        // 強(qiáng)制持有self榨汤,使用weakSelf意味著從弱引用表里取出self
        __strong typeof(self) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", strongSelf);
        });
    };
    self.doWork();
}

// 運(yùn)行工程蠕搜,等待5秒鐘會(huì)發(fā)現(xiàn)打印
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市收壕,隨后出現(xiàn)的幾起案子妓灌,更是在濱河造成了極大的恐慌,老刑警劉巖蜜宪,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虫埂,死亡現(xiàn)場離奇詭異,居然都是意外死亡圃验,警方通過查閱死者的電腦和手機(jī)掉伏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澳窑,“玉大人斧散,你說我怎么就攤上這事扎即。” “怎么了凰棉?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵谓着,是天一觀的道長。 經(jīng)常有香客問我期升,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任悲立,我火速辦了婚禮,結(jié)果婚禮上新博,老公的妹妹穿的比我還像新娘薪夕。我一直安慰自己,他們只是感情好赫悄,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布原献。 她就那樣靜靜地躺著馏慨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪姑隅。 梳的紋絲不亂的頭發(fā)上写隶,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機(jī)與錄音讲仰,去河邊找鬼慕趴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鄙陡,可吹牛的內(nèi)容都是我干的冕房。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼趁矾,長吁一口氣:“原來是場噩夢啊……” “哼耙册!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起愈魏,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤觅玻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后培漏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溪厘,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年牌柄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了畸悬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡珊佣,死狀恐怖蹋宦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情咒锻,我是刑警寧澤冷冗,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站惑艇,受9級特大地震影響蒿辙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜滨巴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一思灌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧恭取,春花似錦泰偿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽裕照。三九已至,卻和暖如春课兄,著一層夾襖步出監(jiān)牢的瞬間牍氛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工烟阐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留搬俊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓蜒茄,卻偏偏與公主長得像唉擂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子檀葛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容