单元测试(三)

一. 单元测试覆盖率&调试测试代码

1.1 查看单元测试覆盖率
打开开源项目SYTimer,如下图所示

image.png

开源项目SYTimer介绍:

  • SYTimer基于RunLoop Timer二次封装
  • 我们在不同页面使用不同NSTimer的时候,多个NSTimer的启动时机不同。其实我们在底层使用一个NSTimer就够用了,只需要控制NSTimer在不同启动时机运行相应代码就可实现。SYTimer就是使用一个RunLoop Timer在不同线程里提供一个比NSTimer更好的运行机制。

设计此项目需要考虑的问题?

  1. 使用一个Timer来运行,需要对不同启动时机进行排序
  2. 对启动时机进行排序,使用堆排序会更好(堆排序分为 大顶堆 小顶堆)

我们在进行单元测试的时候需要关注,堆排序在进行不同timer排序时的单元测试覆盖率?接下来测试代码在进行timer排序时调用了多少次?该怎么进行测试?
选择Edit Scheme -- Test -- Options,如下图所示

image.png
// SYHeapTest.m文件内单元测试方法
- (void)testSimple {
    // 堆排序的初始化,指定SYMaxHeap
    SYHeap<NSNumber *>* h = [[SYHeap alloc] initWithHeapType:SYMaxHeap usingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        // 指定堆里面元素的排序规则
        return [obj1 compare:obj2];
    }];
    [h addObject:@(1)];
    [h addObject:@(3)];
    [h addObject:@(2)];

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

执行单元测试方法testSimple,测试成功
查看文件的单元测试覆盖率

image.png

展开SYHeap.mm文件,查看文件内方法的单元测试覆盖率

image.png

点击进入SYHeap查看comparator:b: 方法

image.png
  • 红色代表未使用的代码
  • 绿色代表使用了的代码
  • 红色注释形状,代表此处代码部分被使用

1.2 调试测试代码
如果我们不了解堆排序的初始化方式,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]);
}

执行单元测试方法testSimple,报错如下

image.png

直接显示了报错信息,并没有给我们调试的机会,此时我们可以添加如下断点来进行调试

image.png

再次执行单元测试方法testSimple,断点断在了报错地方,这时我们就可以进行相关的调试

image.png

二. 集成XCTest与Unit测试

打开LoginApp工程,引入SYCSSColor SYTimer两个库

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

pod install之后会报警告,提示CocoaPods did not set the base configuration of your project already has a custom config set.
解决办法:

// UnitTest.debug.xcconfig文件内导入CocoaPods中的config文件
#include "Pods/Target Support Files/Pods-LoginApp/Pods-LoginApp.debug.xcconfig"

接下来再把SYTimer工程中的单元测试文件导入LoginApp工程(下图选中的蓝色文件),最终工程目录如下

image.png

现在我们想边运行工程边测试,我们在ViewController.m文件中的两个按钮点击事件中写单元测试如下

// ViewController.m文件按钮点击事件
- (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工程
// 点击触发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).
// 点击触发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两个类中已经写好了,现在猜想能不能边运行工程,边写一些测试用例?
方向: 可以使用runtime来实现这一猜想

上面导入主工程的三个类SYCSSColorTests SYHeapTest SYTimerTests代码参与了编译,意味着增加了代码体积,现在想通过不同环境来规避掉已经测试过的方法,以减小代码体积,该怎么解决?
方案一:创建新的target,不同target工程 Build Phases -- Compile Sources 中配置文件,把单元测试文件删掉
方案二:使用宏判断工程中有没有导入XCTest来进行判断
XConfig官方文档

三. XCTest与UI测试

打开LoginApp工程,我们来进行UI测试

image.png

运行testExample方法可以恢复我们之前点击页面进行的UI测试
接下来我们分析生成的UI测试代码

- (void)testExample {
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app launch];
    // 从app中取出指定控件
    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];  
}

上面根据nameInput取出输入框,其中nameInput是我们Xib创建UITextField时设置的,如下图

image.png

接下来我们探讨怎么实现边运行边进行UI测试?

  • 我们发现在进行UI测试的时候,会先创建一个LoginAppUITest的app,再由这个app调起我们的主工程LoginApp,一共创建了两个app
  • 上面的UI测试能否在主工程使用?我们修改testCSSColorTap点击方法内容如下
- (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];
}
// 运行LoginApp,点击按钮,程序闪退
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测试在主工程起作用,实现边运行边进行UI测试该怎么办?
  1. 我们可以使用KIF框架,KIF代表Keep It Functional,是一款iOS集成测试框架。 通过利用操作系统为具有视觉障碍的用户提供的辅助功能属性,可以轻松实现iOS应用程序的自动化。
  2. KIF使用标准的XCTest测试目标来构建和执行测试。
  3. KIF中UI测试与XCTest实现是有不同的

接下来我们来学习使用KIF框架

// podfile文件中配置,导入KIF框架
pod 'SYCSSColor'
pod 'SYTimer'
pod 'KIF', '3.7.13', :configurations => ['Debug']

创建LGKIFTests.m文件,内容如下

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

@end

@implementation LGKIFTests

// 对应通用的UI测试文件中的setUp方法
// 测试之前做一些初始化操作
- (void)beforeEach {
}

// 对应tearDownWithError方法
// 测试之后做一些收尾工作
- (void)afterEach {
}

// 测试用例
- (void)testSuccessfulLogin {
    // test 测试的标准
    [tester enterText:@"Cat1237@example.com" intoViewWithAccessibilityLabel:@"nameInput"];
    [tester enterText:@"Cat1237" intoViewWithAccessibilityLabel:@"passwordInput"];
    [tester tapViewWithAccessibilityLabel:@"loginButton"];    
}
@end

接下来我们修改testCSSColorTap内容如下

- (IBAction)testCSSColorTap:(id)sender {
    // 这里修改类名为LGKIFTests
    LGXCTestCenter *suite = [LGXCTestCenter testSuiteForTestCaseClassString:@"LGKIFTests"];
    for (XCTest *test in suite.tests) {
        [test runTest];
    }
}
// Run工程
// 点击触发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测试都实现了 边运行边测试的目标
探讨方向: 使用OC运行时来实现边运行,边写测试用例?

四. TDD与线程存储数据

TDD是测试驱动开发(Test-Driven Development),是敏捷开发中的一项核心实践和技术,也是一种设计方法论,测试驱动开发的步骤如下图

image.png
  • 写一个失败的测试用例
  • 再让这个测试用例通过
  • 再去进行重构

按照上面步骤我们来做这样一件事,在线程中存储数据?比如断点续传 下载等
这里我们举一个NSRunLoop的例子

// 我们从别的线程切换回来之后都可以使用以下两句获取当前线程的RunLoop
[NSRunLoop currentRunLoop];
[NSRunLoop mainRunLoop];
// 从别的线程切换回来,RunLoop并没有改变,线程对应的RunLoop并不是重复创建,其本质就是在当前线程中存储RunLoop实例结构体

接下来就让我们一起来实现在线程中存储数据(可以参考SYThreadSpecificVariable类)
前提: CFRunLoop与NSRunLoop本质对面向对象的封装并不友好,如果我们能基于CFRunLoop对SYTimer进行面向对象的封装,后面我们就可以按照面向对象的思想来使用RunLoop。我们要实现类似NSRunLoop currentRunLoop的功能,就要在线程中存储数据。
打开LGTimer工程,Cmd + N 创建类LGThreadSpecificVariable

// LGThreadSpecificVariable.h 文件内容
#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 文件内容
#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);
        }
        // 把数据存入线程
        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中添加方法
// 需要导入头文件#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

现在有一个问题,当创建的vc销毁时,线程中的数据依然存在,我们可以进行相应验证

// 创建LGThreadSpecificVariable+Private.h(匿名分类)文件,内容如下
#import "LGThreadSpecificVariable.h"
#import <pthread.h>
@interface LGThreadSpecificVariable ()
- (pthread_key_t)getKey;
@end

// LGThreadSpecificVariable.m文件添加getKey方法实现
- (pthread_key_t)getKey {
    return _key;
}

// 我们在单元测试文件LGTimerTests.m中再次添加方法
// 需要导入头文件#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方法,发现单元测试失败,data有值

从架构层面分析,线程中存数据,数据应与线程的生命周期绑定在一起

// nil参数,应该传入函数指针,指针用于当线程被销毁时,调用这个函数做一些操作
int error = pthread_key_create(&_key, nil);
- (instancetype)initWithValue:(id)value 方法内
修改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方法,发现会调用LGThreadSpecificVariable.m文件的destroy方法
// 也可以在LGThreadSpecificVariable销毁的方法中调用destroy((__bridge void *)(_value))
- (void)dealloc {
    destroy((__bridge void *)(_value));
}

推荐看 AsyncDisplayKit库源码对RunLoop异步的使用

五. 调试debugserver

接下来我们来学习如何调试debugserver?
打开SBAPI学习工程,我们之前在调试lldb的时候通过宏LLDB_DEBUGSERVER_PATH指定debugserver的路径,如下图所示

image.png

现在请思考一个问题,我们能否编译自己的带调试符号的debugserver?

  • 只要带调试符号,我们就可以通过可执行文件找到源码,debugserver源码可以在llvm中找到,找到之后打开工程如下图
image.png
  • debugserver中最难的部分为codesign签名,因为当前Mac OS要求使用的debugserver必须要有lldb_codesign证书签名
  • 系统证书里面默认带有lldb_codesign证书,但是我们自己编译的debugserver不能使用lldb_codesign证书进行签名,所以需要我们自己创建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证书往里面传入一些变量用来请求权限
        codesign -f -s lldb_codesign --entitlements ${SRCROOT}/../../resources/debugserver-macosx-entitlements.plist "${TARGET_BUILD_DIR}/${TARGET_NAME}"
    fi
fi

根据上面脚本路径找到debugserver-macosx-entitlements.plist 文件,这个文件就是对一些权限的请求

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

编译debugserver工程,并把生成的可执行文件放入SBAPI学习工程根目录,并进行路径配置如下图所示

image.png
image.png

在main.mm文件的main(int argc, const char * argv[])方法下打断点,并运行SBAPI学习 Debug -- Attach to Process 顶部会显示可能关联的target,运行完成之后,断点进入debugserver源码

image.png

此时就可以进行SBAPI学习 与 debugserver源码进行联调

六. block相关

Block的类型

  • A. GlobalBlock
  1. 位于全局区
  2. 在Block内部不使用外部变量,或者只使用静态变量和全局变量
  • B. MallocBlock
    位于堆区。
    在Block内部使用局部变量或者OC属性,并且赋值给强引用或者Copy修饰的变量
  • C. StackBlock
    位于栈区
    与 MallocBlock一样,可以在内部使用局部变量或者OC属性。但是不能赋值给强引用或者 Copy修饰的变量

判断以下代码的正确,需要掌握

  • 明确全局block 堆区block 栈区block 的区别
  • 明确堆上 栈上 变量的区别
  • 掌握block底层源码

block代码块一

// 下面代码在执行过程中会不会报错?
- (void)blockStack_Stack {
    int a;
    void(^__weak weakBlock)(void) = nil;
    {
        int b = 2;
        // 这里是一个stack block
        void(^ __weak weakBlock1)(void) = ^{
            NSLog(@"-----%d", b);
        };
        a = b;
        // block是一个结构体,结构体 = 结构体
        weakBlock = weakBlock1;
    }
    // 只要weakBlock1在作用域内没有被销毁,weakBlock就可以调用
    weakBlock();
}

// 运行工程,正常执行

block代码块二

- (void)blockStack_malloc {
    int a = 0;
    void(^__weak weakBlock)(void) = nil;
    {
         //这是堆上的block
         void(^__strong strongBlock)(void) = ^{
             NSLog(@"---%d", a);
         };
         //这里跟上面block代码块一并无区别,仍然是 结构体 = 结构体
         weakBlock = strongBlock;
        // 出了作用域后,strongBlock会销毁
         // block_relase
        // free
    }
    // 对 block 做 release 操作。
    // block 在堆上,才需要 release,在全局区和栈区都不需要 release.
    // 先将引用计数减 1,如果引用计数减到了 0,就将 block 销毁
    // void _Block_release(const void *arg) 
    // 堆上的变量已经释放
    // free
    weakBlock();
}

// 运行工程,会报错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 = ^{
        // 调用栈上block
        block();
    };
    // 延迟3秒调用dispatch_block
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
}

// 运行工程,会报错Thread 1: EXC_BAD_ACCESS (code=1, address=0x22)

// 修改代码如下,就可以运行成功
- (void)blockHeap {
    int a = 0;
    // stack block
    void(^ __weak block)(void) = ^{
        NSLog(@"---%d", a);
    };
    dispatch_block_t dispatch_block = ^{
        // 调用栈上block
        block();
    };
    // 延迟3秒调用dispatch_block
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), dispatch_block);
    // 设置RunLoop过期时间
    [[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转换成结构体形式
    struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
    // strongBlock?  结构体 = 结构体  strongBlock为栈区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;
    // 函数指针,上面weakBlock代码块中代码就保存在函数指针内
    LGBlockInvokeFunction invoke;
    struct _LGBlockDescriptor1 *descriptor;
};

// 运行工程,会报错Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)

// 现在修改代码如下
- (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为堆区block,与weakBlock是两个不同的block
    void(^ __strong strongBlock)(void) = [weakBlock copy];
    // 栈区block置为nil,malloc block并不影响
    blc->invoke = nil;
    // 不报错 strongBlock malloc
    strongBlock();
}

// 运行工程,正常执行

block代码块五

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

// 这里产生了循环引用

block代码块六

// 请求下面网址非常慢,请问self会不会立马释放?
// malloc block -> 捕获变量
// 如果是__weak修饰的变量,捕获之后self引用计数不会加1
// __strong修饰的变量,捕获之后self引用计数才会加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相当于__strong来修饰,只有当block执行完成之后,引用计数才会减1
        NSLog(@"%@", self);
    }] resume];
}

// 运行工程,会发现ViewController延迟销毁

// 现在修改代码如下
- (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) {
        // 请问执行到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];
}

// 运行工程,会发现不会打印。原因是执行到dispatch_after self已经被释放了

// 继续修改代码如下,这样会导致强引用
- (void)block_weak_strong {
    // 导致强引用,blcok捕获变量是通过传递的方式捕获
    self.doWork = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", self);
        });
    };
    self.doWork();
}

//  接下来我们继续修改如下,会不会正常打印?
- (void)block_weak_strong {
    __weak typeof(self) weakSelf = self;
    self.doWork = ^{
        // 强制持有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();
}

// 运行工程,等待5秒钟会发现打印
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容