一. 单元测试覆盖率&调试测试代码
1.1 查看单元测试覆盖率
打开开源项目SYTimer,如下图所示
开源项目SYTimer介绍:
- SYTimer基于RunLoop Timer二次封装
- 我们在不同页面使用不同NSTimer的时候,多个NSTimer的启动时机不同。其实我们在底层使用一个NSTimer就够用了,只需要控制NSTimer在不同启动时机运行相应代码就可实现。SYTimer就是使用一个RunLoop Timer在不同线程里提供一个比NSTimer更好的运行机制。
设计此项目需要考虑的问题?
- 使用一个Timer来运行,需要对不同启动时机进行排序
- 对启动时机进行排序,使用堆排序会更好(堆排序分为 大顶堆 小顶堆)
我们在进行单元测试的时候需要关注,堆排序在进行不同timer排序时的单元测试覆盖率?接下来测试代码在进行timer排序时调用了多少次?该怎么进行测试?
选择Edit Scheme -- Test -- Options,如下图所示
// 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,测试成功
查看文件的单元测试覆盖率
展开SYHeap.mm文件,查看文件内方法的单元测试覆盖率
点击进入SYHeap查看comparator:b: 方法
- 红色代表未使用的代码
- 绿色代表使用了的代码
- 红色注释形状,代表此处代码部分被使用
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,报错如下
直接显示了报错信息,并没有给我们调试的机会,此时我们可以添加如下断点来进行调试
再次执行单元测试方法testSimple,断点断在了报错地方,这时我们就可以进行相关的调试
二. 集成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工程(下图选中的蓝色文件),最终工程目录如下
现在我们想边运行工程边测试,我们在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测试
运行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时设置的,如下图
接下来我们探讨怎么实现边运行边进行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测试该怎么办?
- 我们可以使用KIF框架,KIF代表Keep It Functional,是一款iOS集成测试框架。 通过利用操作系统为具有视觉障碍的用户提供的辅助功能属性,可以轻松实现iOS应用程序的自动化。
- KIF使用标准的XCTest测试目标来构建和执行测试。
- 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),是敏捷开发中的一项核心实践和技术,也是一种设计方法论,测试驱动开发的步骤如下图
- 写一个失败的测试用例
- 再让这个测试用例通过
- 再去进行重构
按照上面步骤我们来做这样一件事,在线程中存储数据?比如断点续传 下载等
这里我们举一个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的路径,如下图所示
现在请思考一个问题,我们能否编译自己的带调试符号的debugserver?
- 只要带调试符号,我们就可以通过可执行文件找到源码,debugserver源码可以在llvm中找到,找到之后打开工程如下图
- 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 文件,这个文件就是对一些权限的请求
编译debugserver工程,并把生成的可执行文件放入SBAPI学习工程根目录,并进行路径配置如下图所示
在main.mm文件的main(int argc, const char * argv[])方法下打断点,并运行SBAPI学习 Debug -- Attach to Process 顶部会显示可能关联的target,运行完成之后,断点进入debugserver源码
此时就可以进行SBAPI学习 与 debugserver源码进行联调
六. block相关
Block的类型
- A. GlobalBlock
- 位于全局区
- 在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秒钟会发现打印