0%

GCD介绍

  • 1、GCD简介
  • 2、GCD任务和队列
  • 3、GCD 的基本使用
  • 4、GCD 线程间的通信
  • 5、GCD 的其他方法(栅栏方法:dispatch_barrier_async、延时执行方法:dispatch_after、一次性代码(只执行一次):dispatch_once、快速迭代方法:dispatch_apply、队列组:dispatch_group、信号量:dispatch_semaphore)

1、GCD简介

Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。

GCD 的好处具体如下:

  • GCD 可用于多核的并行运算
  • GCD 会自动利用更多的 CPU 内核(比如双核、四核)
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

2、GCD任务和队列

GCD 中两个核心概念:任务队列

任务

任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。

  • 同步执行(sync)

    • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
    • 只能在当前线程中执行任务,不具备开启新线程的能力。
  • 异步执行(async)

    • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
    • 可以在新的线程中执行任务,具备开启新线程的能力。
1
2
3
4
5
6
7
8
9
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});

// 异步执行任务创建方法
dispatch_async(queue, ^{
// 这里放异步执行任务代码
});

队列

队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。

  • 串行队列(Serial Dispatch Queue)

    • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
  • 并发队列(Concurrent Dispatch Queue)

    • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

注意:并发队列的并发功能只有在异步(dispatch_async)函数下才有效。

队列的创建方法/获取方法

可以使用dispatch_queue_create来创建队列,需要传入两个参数,第一个参数表示队列的唯一标识符,用于 DEBUG,可为空,Dispatch Queue 的名称推荐使用应用程序 ID 这种逆序全程域名;第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并发队列。

1
2
3
4
5
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);

对于串行队列,GCD 提供了的一种特殊的串行队列:主队列(Main Dispatch Queue)

  • 所有放在主队列中的任务,都会放到主线程中执行。
  • 可使用dispatch_get_main_queue()获得主队列。
1
2
// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();

对于并发队列,GCD 默认提供了全局并发队列(Global Dispatch Queue)

可以使用dispatch_get_global_queue来获取。需要传入两个参数。第一个参数表示队列优先级,一般用DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用0即可。

1
2
// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

1.使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)。
2.并发功能只有在异步函数才会生效。

3、GCD 的基本使用

同步串行队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// 追加任务1
for (int i = 0; i < 2; i++) {
NSLog(@"1---%@",[NSThread currentThread]);
}
});

dispatch_sync(queue, ^{
// 追加任务2
for (int i = 0; i < 2; i++) {
NSLog(@"2---%@",[NSThread currentThread]);
}
});

运行结果:

1
2
3
4
2020-07-04 10:05:33.862875+0800 GCD[2180:85864] 1---<NSThread: 0x600003205e40>{number = 1, name = main}
2020-07-04 10:05:33.863044+0800 GCD[2180:85864] 1---<NSThread: 0x600003205e40>{number = 1, name = main}
2020-07-04 10:05:33.863190+0800 GCD[2180:85864] 2---<NSThread: 0x600003205e40>{number = 1, name = main}
2020-07-04 10:05:33.863331+0800 GCD[2180:85864] 2---<NSThread: 0x600003205e40>{number = 1, name = main}

根据打印结果可知,同步串行队列即没有开启新的线程,也没有异步执行

同步并行队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
// 追加任务1
for (int i = 0; i < 2; i++) {
NSLog(@"1---%@",[NSThread currentThread]);
}
});

dispatch_sync(queue, ^{
// 追加任务2
for (int i = 0; i < 2; i++) {
NSLog(@"2---%@",[NSThread currentThread]);
}
});

运行结果:

1
2
3
4
2020-07-04 10:11:28.625131+0800 GCD[2267:92158] 1---<NSThread: 0x6000010c6d40>{number = 1, name = main}
2020-07-04 10:11:28.625290+0800 GCD[2267:92158] 1---<NSThread: 0x6000010c6d40>{number = 1, name = main}
2020-07-04 10:11:28.625433+0800 GCD[2267:92158] 2---<NSThread: 0x6000010c6d40>{number = 1, name = main}
2020-07-04 10:11:28.625566+0800 GCD[2267:92158] 2---<NSThread: 0x6000010c6d40>{number = 1, name = main}

根据两种打印我们发现:同步函数既不会开启新的线程,也不会执行并发任务。

异步串行队列

1
2
3
4
5
6
7
8
9
10
11
12
NSLog(@"主线程:%@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
for (int i = 0; i < 2; i++) {
NSLog(@"1====%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 2; i++) {
NSLog(@"2====%@",[NSThread currentThread]); // 打印当前线程
}
});

运行结果:

1
2
3
4
5
2020-07-04 10:19:00.528690+0800 GCD[2414:99573] 主线程:<NSThread: 0x6000002fd0c0>{number = 1, name = main}
2020-07-04 10:19:00.529043+0800 GCD[2414:99785] 1=====<NSThread: 0x6000002a4d40>{number = 3, name = (null)}
2020-07-04 10:19:00.529176+0800 GCD[2414:99785] 1=====<NSThread: 0x6000002a4d40>{number = 3, name = (null)}
2020-07-04 10:19:00.529315+0800 GCD[2414:99785] 2====<NSThread: 0x6000002a4d40>{number = 3, name = (null)}
2020-07-04 10:19:00.529455+0800 GCD[2414:99785] 2====<NSThread: 0x6000002a4d40>{number = 3, name = (null)}

结果:有开启新的线程,串行执行任务

异步并行队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSLog(@"主线程:%@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"1====%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 2; i++) {
[NSThread sleepForTimeInterval:2];
NSLog(@"2====%@",[NSThread currentThread]); // 打印当前线程
}
});

运行结果:

1
2
3
4
5
2020-07-04 10:21:16.460023+0800 GCD[2450:101648] 主线程:<NSThread: 0x6000025e6d40>{number = 1, name = main}
2020-07-04 10:21:16.460411+0800 GCD[2450:101857] 2====<NSThread: 0x60000258d080>{number = 5, name = (null)}
2020-07-04 10:21:16.460411+0800 GCD[2450:101859] 1=====<NSThread: 0x6000025b4f80>{number = 3, name = (null)}
2020-07-04 10:21:16.460562+0800 GCD[2450:101857] 2====<NSThread: 0x60000258d080>{number = 5, name = (null)}
2020-07-04 10:21:16.460589+0800 GCD[2450:101859] 1=====<NSThread: 0x6000025b4f80>{number = 3, name = (null)}

结果:有开启新的线程,并发执行任务。想要出现明显的并发执行效果,可以sleep一下。

sync函数造成的线程死锁

首先你要理解同步和异步执行的概念,同步和异步目的不是为了是否创建一个新的线程,同步会阻塞当前函数的返回,异步函数会立即返回执行下面的代码;队列是一种数据结构,队列有FIFO,LIFO等,控制任务的执行顺序,至于是否开辟一个新的线程,因为同步函数会等待函数的返回,所以在当前线程执行就行了,没必要浪费资源再开辟新的线程,如果是异步函数,当前线程需要立即函数返回,然后往下执行,所以函数里面的任务必须要开辟一个新的线程去执行这个任务。

队列上是放任务的,而线程是去执行队列上的任务的。

【问题1】:以下代码是在主线程执行的,会不会产生死锁?会!

1
2
3
4
5
6
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");

dispatch_sync立马在当前线程同步执行任务

分析:

  • 1、主线程中任务执行:任务1sync任务3
  • 2、主队列:viewDidLoad任务2

其中在主队列viewDidLoad里面的任务3执行结束才会执行任务2;而主线程中是执行完sync才会执行任务3。也就是任务2等待任务3执行,任务3再也等待任务2执行,造成死锁。

【问题2】:以下代码是在主线程执行的,会不会产生死锁?不会!

1
2
3
4
5
6
7
8
9
- (void)interview02 {
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
// dispatch_async不要求立马在当前线程同步执行任务
}

因为dispatch_async不要求立马在当前线程同步执行任务,不会造成线程死锁。

【问题3】:以下代码是在主线程执行的,会不会产生死锁?会!

1
2
3
4
5
6
7
8
9
10
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");

其中执行任务3执行任务4之间造成死锁。

【问题4】:以下代码是在主线程执行的,会不会产生死锁?不会!

1
2
3
4
5
6
7
8
9
10
11
12
- (void)interview04 {
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}

4、GCD 线程间的通信

在iOS开发过程中,我们一般在主线程里边进行UI刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)communication {
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 异步追加任务
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
// 回到主线程
dispatch_async(mainQueue, ^{
// 追加在主线程中执行的任务
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
});
}

运行结果:

1
2
3
2020-07-04 10:53:50.617918+0800 GCD[2798:125429] 1---<NSThread: 0x6000025eb800>{number = 5, name = (null)}
2020-07-04 10:53:52.622555+0800 GCD[2798:125429] 1---<NSThread: 0x6000025eb800>{number = 5, name = (null)}
2020-07-04 10:53:54.624007+0800 GCD[2798:125174] 2---<NSThread: 0x6000025e2d40>{number = 1, name = main}

5、GCD 的其他方法

5.1、GCD 栅栏方法:dispatch_barrier_async

就是我们在异步执行一些操作的时候,我们使用dispatch_barrier_async函数把异步操作暂时性的做成同步操作,就像一个栅栏一样分开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)barrierTest {
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
for (int i=0; i<5; i++) {
[self readWithIndex:i];
[self readWithIndex:i];
[self writeWithIndex:i];
[self readWithIndex:i];
}
}

- (void)readWithIndex:(NSInteger)index {
dispatch_async(self.queue, ^{
sleep(1);
NSLog(@"第%ld次循环:read", (long)index);
});
}

- (void)writeWithIndex:(NSInteger)index {
dispatch_barrier_async(self.queue, ^{
sleep(1);
NSLog(@"第%ld次循环:write", (long)index);
});
}

我们观察时间可以看到在执行dispatch_barrier_async写操作的时候是同步执行的,不会出现异步情况。

5.2、GCD 延时执行方法:dispatch_after

我们经常会遇到这样的需求:在指定时间(例如3秒)之后执行某个任务。可以用 GCD 的dispatch_after函数来实现。
需要注意的是:dispatch_after函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after函数是很有效的。

1
2
3
4
5
6
7
8
9
10
11
/**
* 延时执行方法 dispatch_after
*/
- (void)after {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线
NSLog(@"asyncMain---begin");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)),dispatch_get_main_queue(), ^{
// 2.0秒后异步追加任务代码到主队列,并开始执行
NSLog(@"after---%@",[NSThread currentThread]); // 打印当前线程
});
}

运行结果:

1
2
3
2020-07-04 11:24:46.248616+0800 GCD[3252:146325] currentThread---<NSThread: 0x600003589e40>{number = 1, name = main}
2020-07-04 11:24:46.248769+0800 GCD[3252:146325] asyncMain---begin
2020-07-04 11:24:48.248964+0800 GCD[3252:146325] after---<NSThread: 0x600003589e40>{number = 1, name = main}

5.3、GCD 一次性代码(只执行一次):dispatch_once

我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once 函数。

1
2
3
4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});

5.4、GCD 队列组:dispatch_group

有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。

  • 调用队列组的 dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enterdispatch_group_leave 组合 来实现dispatch_group_async
  • 调用队列组的 dispatch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程)。
  • dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1
  • dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1
  • 当 group 中未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞,以及执行追加到dispatch_group_notify中的任务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 队列组 dispatch_group_notify
*/
- (void)groupNotify {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---begin");
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
// 追加任务1
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
});

dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
// 追加任务2
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步任务1、任务2都执行完毕后,回到主线程执行下边任务
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
}
NSLog(@"group---end");
});
}

运行结果:

1
2
3
4
5
6
7
8
9
2020-07-04 11:28:55.585362+0800 GCD[3414:151664] currentThread---<NSThread: 0x6000019bad00>{number = 1, name = main}
2020-07-04 11:28:55.585511+0800 GCD[3414:151664] group---begin
2020-07-04 11:28:57.588445+0800 GCD[3414:151882] 1---<NSThread: 0x6000019dcb40>{number = 3, name = (null)}
2020-07-04 11:28:57.588463+0800 GCD[3414:151886] 2---<NSThread: 0x6000019d89c0>{number = 5, name = (null)}
2020-07-04 11:28:59.593759+0800 GCD[3414:151886] 2---<NSThread: 0x6000019d89c0>{number = 5, name = (null)}
2020-07-04 11:28:59.593718+0800 GCD[3414:151882] 1---<NSThread: 0x6000019dcb40>{number = 3, name = (null)}
2020-07-04 11:29:01.595497+0800 GCD[3414:151664] 3---<NSThread: 0x6000019bad00>{number = 1, name = main}
2020-07-04 11:29:03.597050+0800 GCD[3414:151664] 3---<NSThread: 0x6000019bad00>{number = 1, name = main}
2020-07-04 11:29:03.597351+0800 GCD[3414:151664] group---end

如果使用dispatch_group_async包裹起来的操作是异步的,可以使用dispatch_group_enter(dispatchGroup);dispatch_group_leave(dispatchGroup);替代,注意dispatch_group_enter()dispatch_group_leave()需要配对使用,如果未配对容易出现dispatch_group_notify()不调用或者崩溃。

5.5、GCD 信号量:dispatch_semaphore

Dispatch Semaphore 提供了三个函数。

  • dispatch_semaphore_create:创建一个信号量,具有整形的数值,即为信号的总量。
  • dispatch_semaphore_signal:发送一个信号,让信号总量加1
  • dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。

示例代码,异步线程的并发量设置为3,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)interview05{
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建信号量,并且设置值为3
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 10; i++){
/*
由于是异步执行的,所以每次循环Block里面的dispatch_semaphore_signal根本还没有执行就会执行dispatch_semaphore_wait,从而semaphore-1.当循环10此后,semaphore等于0,则会阻塞线程,直到执行了Block的dispatch_semaphore_signal 才会继续执行
*/
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_group_async(group, queue, ^{
NSLog(@"%i",i);
sleep(2);
// 每次发送信号则semaphore会+1,
dispatch_semaphore_signal(semaphore);
});
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
2020-07-04 11:46:30.490317+0800 GCD[3689:165328] 0
2020-07-04 11:46:30.490327+0800 GCD[3689:165332] 1
2020-07-04 11:46:30.490348+0800 GCD[3689:165329] 2
2020-07-04 11:46:32.490797+0800 GCD[3689:165329] 3
2020-07-04 11:46:32.490809+0800 GCD[3689:165328] 4
2020-07-04 11:46:32.490818+0800 GCD[3689:165332] 5
2020-07-04 11:46:34.493311+0800 GCD[3689:165332] 6
2020-07-04 11:46:34.493326+0800 GCD[3689:165328] 7
2020-07-04 11:46:34.493345+0800 GCD[3689:165329] 8
2020-07-04 11:46:36.495699+0800 GCD[3689:165328] 9

NSThread介绍

NSThread 是苹果官方提供的,使用起来比 pthread 更加面向对象,简单易用,可以直接操作线程对象。不过也需要需要程序员自己管理线程的生命周期(主要是创建),我们在开发的过程中偶尔使用 NSThread。比如我们会经常调用[NSThread currentThread]来显示当前的进程信息。

NSThread的创建与运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 //使用target对象的selector作为线程的任务执行体,该selector方法最多可以接收一个参数,该参数即为argument
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument

//使用block作为线程的任务执行体
- (instancetype)initWithBlock:(void (^)(void))block


/*
类方法,返回值为void
使用一个block作为线程的执行体,并直接启动线程
上面的实例方法返回NSThread对象需要手动调用start方法来启动线程执行任务
*/
+ (void)detachNewThreadWithBlock:(void (^)(void))block


/*
类方法,返回值为void
使用target对象的selector作为线程的任务执行体,该selector方法最多接收一个参数,该参数即为argument
同样的,该方法创建完县城后会自动启动线程不需要手动触发
*/
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument

简单运用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc] initWithTarget:self
selector:@selector(firstThread:) object:@"Hello, World"];
//设置线程的名字,方便查看
[thread setName:@"firstThread"];
//启动线程
[thread start];
}

//线程的任务执行体并接收一个参数arg
- (void)firstThread:(id)arg {
NSLog(@"Task %@ %@", [NSThread currentThread], arg);
NSLog(@"Thread Task Complete");
}

常见API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获得主线程
+ (NSThread *)mainThread;

// 判断是否为主线程(对象方法)
- (BOOL)isMainThread;

// 判断是否为主线程(类方法)
+ (BOOL)isMainThread;

// 获得当前线程
NSThread *current = [NSThread currentThread];

// 线程的名字——setter方法
- (void)setName:(NSString *)n;

// 线程的名字——getter方法
- (NSString *)name;

线程状态控制方法

1
2
3
4
5
6
7
8
9
// 线程进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态
- (void)start;

// 线程进入阻塞状态
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

//强制停止线程 线程进入死亡状态
+ (void)exit;

线程之间的通信

在开发中,我们经常会在子线程进行耗时操作,操作结束后再回到主线程去刷新 UI。这就涉及到了子线程和主线程之间的通信。我们先来了解一下官方关于 NSThread 的线程间通信的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在主线程上执行操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
// equivalent to the first method with kCFRunLoopCommonModes

// 在指定线程上执行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

// 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

线程的状态转换

当我们新建一条线程NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];,在内存中的表现为:

当调用[thread start];后,系统把线程对象放入可调度线程池中,线程对象进入就绪状态,如下图所示。

  • 如果CPU现在调度当前线程对象,则当前线程对象进入运行状态,如果CPU调度其他线程对象,则当前线程对象回到就绪状态。
  • 如果CPU在运行当前线程对象的时候调用了sleep方法\等待同步锁,则当前线程对象就进入了阻塞状态,等到sleep到时\得到同步锁,则回到就绪状态。
  • 如果CPU在运行当前线程对象的时候线程任务执行完毕\异常强制退出,则当前线程对象进入死亡状态。

LLDB

LLDB 是一个有着 REPL 的特性和 C++ ,Python 插件的开源调试器。LLDB 绑定在 Xcode 内部,存在于主窗口底部的控制台中。调试器允许你在程序运行的特定时暂停它,你可以查看变量的值,执行自定的指令,并且按照你所认为合适的步骤来操作程序的进展。

expression

可简写为e,作用为执行一个表达式。

  • 1、查询当前堆栈变量的值
  • 2、动态修改当前线程堆栈变量的值

我们在调试数据的时候,有的时候需要动态修改变量值,使用expression那是十分方便的调试。

po & p

po的作用为打印对象,事实上,我们可以通过help po得知,poexpression -O --的简写,我们可以通过它打印出对象,而不是打印对象的指针。而值得一提的是,在 help expression 返回的帮助信息中,我们可以知道,po命令会尝试调用对象的 description 方法来取得对象信息,因此我们也可以重载某个对象的description方法,使我们调试的时候能获得可读性更强,更全面的信息。

p即是print,也是expression --的缩写,与po不同,它不会打出对象的详细信息,只会打印出一个$符号,数字,再加上一段地址信息。打印对象的时候我们也可以指定特定格式。

  • x :十六进制打印
  • d:十进制打印
  • u:无符号十进制打印
  • o:八进制打印
  • t:二进制形式打印
  • f:浮点数

堆栈

bt即是thread backtrace,作用是打印出当前线程的堆栈信息。

我们在打印断点的时候,我们可以在左侧看到一些堆栈信息,但是看不完全,这个时候使用bt指令可以打印出完整的堆栈信息。

输入frame select指令我们可以任意的去选择一个作用域去查看。

1
(lldb)frame select 2

thread另一个比较常用的用法是 thread return,调试的时候,我们希望在当前执行的程序堆栈直接返回一个自己想要的值,可以执行该命令直接返回。
thread return <expr>
在这个断点中,我们可以执行 thread return NO让该函数调用直接返回NO ,在调试中轻松覆盖任何函数的返回路径。

frame即是帧,其实就是当前的程序堆栈,我们输入bt命令,打印出来的其实是当前线程的frame

  • 展示当前作用域下的参数和局部变量
    1
    2
    3
    (lldb)frame variable
    (lldb)fr v
    frame variable --no-summary-depth
  • 展示当前作用域下的局部变量
    1
    2
    (lldb)frame variable --no-args
    (lldb)fr v -a
  • 展示指定变量var的具体内容
    1
    2
    3
    4
    (lldb)frame variable var
    (lldb)fr v var
    (lldb)p var
    (lldb)po var
  • 展示当前对象的全局变量
    1
    2
    (lldb)target variable
    (lldb)ta v
  • 打印某一方法具体的信息
    1
    frame select num(123.

设置断点

breakpoint

所有调试都是由断点开始的,我们接触的最多,就是以breakpoint命令为基础的断点。
一般我们对breakpoint命令使用得不多,而是在XCode的GUI界面中直接添加断点。除了直接触发程序暂停供调试外,我们可以进行进一步的配置。

  • 添加condition,一般用于多次调用的函数或者循坏的代码中,在作用域内达到某个条件,才会触发程序暂停
  • 忽略次数,这个很容易理解,在忽略触发几次后再触发暂停
  • 添加Action,为这个断点添加子命令、脚本、shell命令、声效(有个毛线用)等Action,我的理解是一个脚本化的功能,我们可以在断点的基础上添加一些方便调试的脚本,提高调试效率。
  • 自动继续,配合上面的添加Action,我们就可以不用一次又一次的暂停程序进行调试来查询某些值(大型程序中断一次还是会有卡顿),直接用Action将需要的信息打印在控制台,一次性查看即可。

watchpoint

有时候我们会关心类的某个属性什么时候被人修改了,最简单的方法当然就是在setter的方法打断点,或者在@property的属性生命行打上断点。这样当对象的setter方法被调用时,就会触发这个断点
当然这么做是有缺点的,对于直接访问内存地址的修改,setter方法的断点并没有办法监控得到,因此我们需要用到watchpoint命令。

1
2
3
watchpoint set variable str
watchpoint list //列出所有watchpoint
watchpoint delete // 删除所有watchpoint

Chisel

Chisel是facebook开源的一个LLDB命令的集合,它里面简化和扩展了LLDB的命令。使用它会更方便的调试我们的程序。在它的GitHub上有详细的安装方式,这里就不赘述了。

常用命令

1. pviews

这个命令可以按层级递归打印指定view的所有subView,相当于 UIView 的私有辅助方法 [view recursiveDescription]。如果不指定view的话就是默认window:

2. pvc

这个命令递归打印出viewController的层级,相当于 UIViewController 的一个私有辅助方法 [UIViewController _printHierarchy] :

3. visualize

它可以使用Mac的预览打开一个 UIImage, CGImageRef, UIView, 或 CALayer。 我们其实可以用这个功能来截图或者查看一个view的具体内容:

1
2
(lldb) visualize 0x7feb5cf18210
(lldb) visualize self.view

4. mask/unmask

mask用来在view或者layer上覆盖一个半透明的矩形, unmask解除。当我们想找一个view的时候可以使用。

1
2
(lldb) mask self.imageView
(lldb) unmask 0x7f8732e037b0

5. border/unborder

border可以给view或者layer添加边框,unborder解除。当我们想找一个view的时候可以使用。

1
2
(lldb) border self.imageView
(lldb) unborder 0x7f8732e037b0

6. show/hide

显示隐藏一个view或者layer。

1
2
(lldb) hide self.imageView
(lldb) show self.imageView

7. caflush

这个命令用来刷新UI,当我们改变了UI的时候,不用重新启动,使用caflush刷新UI就行。

8. presponder

打印响应者链:

9. pclass

打印指定对象的class的继承关系:

10. pjson

打印一个字典或者数组的json样式。

11. slowanim/unslowanim

减慢动画的效果,检查一个动画哪里有问题时可以使用。

12. pdocspath

打印App的Documents路径:

13. fv & fvc

这两个命令是用来搜索当前内存中存在的view和viewController实例的命令,支持正则搜索。

14. vs
这个命令可以移动正在调试的对象,让对象切换到其父视图之前,或切换到其兄弟视图之前。

1
2
3
4
5
6
7
8
(lldb) vs 0x128430c50

Use the following and (q) to quit.
(w) move to superview
(s) move to first subview
(a) move to previous sibling
(d) move to next sibling
(p) print the hierarchy

Chisel安装步骤

官网地址:https://github.com/facebook/chisel ,官网推荐使用Homebrew安装,但是实际测试下来发现Homebrew安装的版本有问题,所以这里推荐使用git clone最新的版本,然后配置路径使用。

下载仓库最新的代码

1
$ git clone https://github.com/facebook/chisel.git

配置~/.lldbinit文件

查找~/.lldbinit文件是否存在,如果不存在,创建一个。如果存在,就修改这个文件。下面创建一个这样的文件:

1
2
3
$ cd ~
$ touch .lldbinit
$ open .lldbinit

打开.lldbinit文件后,在这个文件中配置第1步中克隆下来的代码里fbchisellldb.py文件的路径,我这里提供我自己的.lldbinit文件的内容:

1
2
# ~/.lldbinit
command script import /Users/kris/Documents/git/chisel/fbchisellldb.py

配置完这些操作后,重新启动Xcode,打个断点,使用新的LLDB命令试下

参考

iOS调试-LLDB学习总结

iOS开发调试 - LLDB使用概览

LLDB 常用命令

chisel命令列表

RunLoop

RunLoop 运行循环,在程序运行过程中循环做一些事情。
应用范畴

  • 1、定时器(Timer)、PerformSelector
  • 2、GCD
  • 3、事件响应、手势识别、界面刷新
  • 4、网络请求
  • 5、AutoreleasePool

概念介绍

在我们命令行项目的main函数里面

1
2
3
4
5
6
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}

执行完NSLog(@"Hello, World!");这个代码以后,程序立即退出,但是在我们的正常项目main函数里面

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

如果用一个伪代码来简单的解释一下上面代码的意思,就是

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char * argv[]) {
@autoreleasepool {
int retVal = 0;
do {
//休眠等待消息
int message = sleep_and_wait();
//处理消息
retVal = process_message(message)
} while (0 == retVal);
}
}

程序不会马上退出,而是保持运行状态,RunLoop的基本作用:

  • 1、保持程序持续的运行
  • 2、处理app中的各种事件(比如触摸事件,定时器事件)
  • 3、节省CPU资源,提高程序性能,该做事的时候做事,改休息的时候休息

RunLoop对象
iOS中有2套API来访问和使用RunLoop

  • 1、Fundataion:NSRunLoop
  • 2、Core Fundataion:CFRunLoop

NSRunLoop是基于CFRunLoop的一层OC包装,CFRunLoop是开源的,地址:https://opensource.apple.com/tarballs/CF/

RunLoop与线程

  • 1、每一条线程都有唯一的一个与之对应的RunLoop对象
  • 2、RunLoop保存在一个全局的Dictionary里,线程作为Key,RunLoop作为Value
  • 3、线程刚创建时,并没有RunLoop对象,RunLoop会在第一次获取她时创建
  • 4、RunLoop会在线程结束的时候销毁
  • 5、主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

获取RunLoop对象

  • 1、Fundation
    • 1、获取当前线程的RunLoop对象[NSRunLoop currentRunLoop]
    • 2、获取主线程的RunLoop对象[NSRunLoop mainRunLoop]
  • 2、Core Foundation
    • 1、获取当前线程的RunLoop对象CFRunLoopGetCurrent()
    • 2、获取主线程的RunLoop对象CFRunLoopGetMain()

RunLoop相关类
Core Foundation中关于RunLoop一共有5个类

  • 1、CFRunLoopRef
  • 2、CFRunLoopModeRef
  • 3、CFRunLoopSourceRef
  • 4、CFRunLoopTimerRef
  • 5、CFRunLoopObserverRef

我们下载RunLoop,然后搜索CFRunLoop的组成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

我们打印一下NSLog(@"%@",[NSRunLoop currentRunLoop]);

其中主要的有下面几个:

  • 1、_pthread记录当前线程
  • 2、_commonModes
  • 3、_commonModeItems
  • 4、_currentMode,当前mode类型
  • 5、_modes 存放CFRunLoop里面的所有mode

我们在RunLoop源码中搜索CFRunLoopMode来查看一下CFRunLoopMode都存放了哪些东西:

  • 1、Source0:
    • 处理触摸事件,
    • performSelector:onThread:
  • 2、Source1:
    • 基于Port的线程间通信,
    • 系统事件的捕捉
  • 3、Timer :
    • NSTimer,
    • performSelector:withObject:afterDelay:
  • 4、Observers:
    • 用于监听RunLoop的状态
    • UI刷新
    • Autorelease pool(BeforeWaiting)

我们来简单的证明一下Source0,我们随便写一个touchesBegan触摸事件,然后在里面打一个断点,bt指令就是打印线程执行的所有方法

我们可以在线程执行方法中可以发现,在调用RunLoop相关方法的时候,第一个是调用的__CFRunLoopDoSources0

RunLoop里面会有多个Mode,但是只有一个_currentMode

CFRunLoopModeRef

  • 1、CFRunLoopModeRef代表着RunLoop的运行模式
  • 2、一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
  • 3、RunLoop启动的时候只能选择其中一个Mode作为currentMode
  • 4、如果要切换Mode,只能退出当前Loop,再重新选择一个Mode进入,不同组的Source0/Source1/Timer/Observer互不影响
  • 5、如果Mode里面没有任何Source0/Source1/Timer/Observer,RunLoop会立刻退出

CFRunLoopModeRef常见的Mode

  • 1、KCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行的
  • 2、UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

获取当前Mode

1
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());

CFRunLoopObserverRef

RunLoop的几种状态

1
2
3
4
5
6
7
8
9
10
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入runloop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer
kCFRunLoopBeforeSources = (1UL << 2), //即将处理source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), //即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

我们可以添加一个Observer监听RunLoop的所有状态,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
});
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);

我们运行上面代码,然后查看打印结果:

在没有任何事件处理的情况下,最终RunLoop的活动状态为kCFRunLoopBeforeWaiting即将进入休眠。

既然我们可以监听到了RunLoopMode变化情况,那么我们就可以打印一下KCFRunLoopDefaultModeUITrackingRunLoopMode的切换情况了。

我们在view上随便拉一个UITextView,然后滚动UITextView
监听代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopEntry - %@", mode);
CFRelease(mode);
break;
}
case kCFRunLoopExit: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopExit - %@", mode);
CFRelease(mode);
break;
}
default:
break;
}
});

//添加observer到runloop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);

打印结果为:

  • 1、在刚开始滚动UITextView的时候,先退出kCFRunLoopDefaultMode,所以默认应该就是kCFRunLoopDefaultMode
  • 2、在滚动中,进入UITrackingRunLoopMode
  • 3、在滚动结束,先退出UITrackingRunLoopMode,然后在进入kCFRunLoopDefaultMode

RunLoop的运行逻辑:

每次运行RunLoop,线程的RunLoop会自动处理之前未处理的消息,并通知相关的观察者。具体顺序

  • 1、通知观察者(observers)RunLoop即将启动
  • 2、通知观察者(observers)任何即将要开始的定时器
  • 3、通知观察者(observers)即将处理source0事件
  • 4、处理source0
  • 5、如果有source1,跳到第9步
  • 6、通知观察者(observers)线程即将进入休眠
  • 7、将线程置于休眠知道任一下面的事件发生
    • 1、source0事件触发
    • 2、定时器启动
    • 3、外部手动唤醒
  • 8、通知观察者(observers)线程即将唤醒
  • 9、处理唤醒时收到的时间,之后跳回2
    • 1、如果用户定义的定时器启动,处理定时器事件
    • 2、如果source0启动,传递相应的消息
  • 10、通知观察者RunLoop结束

RunLoop休眠原理

在RunLoop即将休眠的时候,通过mach_msg()方法来让软件和硬件交互

  • 1、即将休眠的时候,程序调用mach_msg()传递给CPU,告诉CPU停止运行
  • 2、即将启动RunLoop的时候,程序调用mach_msg()传递给CPU,告诉CPU开始工作

RunLoop简单应用

滚动视图上面NSTimer不失效

我们写一个简单的定时器,然后视图上面创建一个TextView,然后滚动TextView

1
2
3
4
static int count = 0;
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d",count++);
}];

我们观察可以发现在打印的第二秒和第三秒之间其实相差了14s,因为一个线程只会有一个RunLoop,默认情况下是kCFRunLoopDefaultMode,在滚动UITextView的时候,RunLoop切换到了UITrackingRunLoopMode,这个时候定时器就会停止,在滚动UITextView结束的时候,RunLoop切换到了kCFRunLoopDefaultMode,定时器继续开始启动了。

解决这个问题的方法就是把这个NSTimer添加到两种RunLoop中

  • 1、
    1
    2
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
  • 2、还有一个NSRunLoopCommonModes,我们用NSRunLoopCommonModes标记的时候,就可以实现上面效果
    1
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    NSRunLoopCommonModes并不是一个真的模式,它只是一个标记,timer能在_commonModes数组中存放的模式下工作

线程保活(常驻线程)

开始之前先介绍几个概念

  • 1、线程刚创建时,并没有RunLoop对象,RunLoop会在第一次获取她时创建
    • 1、获取线程:[NSRunLoop currentRunLoop]
    • 2、获取线程:CFRunLoopGetCurrent()
  • 2、启动RunLoop的三种方法
    • 1、- (void)run; ,
      这种方法runloop会一直运行下去,在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;
    • 2、- (void)runUntilDate:(NSDate *)limitDate;
      可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;
    • 3、- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
      runloop会运行一次,超时时间到达或者第一个input source被处理,则runloop就会退出
  • 3、退出RunLoop的方式
    • 1、启动方式的退出方法,如果runloop没有input sources或者附加的timer,runloop就会退出。
    • 2、启动方式runUntilDate,可以通过设置超时时间来退出runloop。
    • 3、启动方式runMode:beforeDate,通过这种方式启动,runloop会运行一次,当超时时间到达或者第一个输入源被处理,runloop就会退出。

如果我们想控制runloop的退出时机,而不是在处理完一个输入源事件之后就退出,那么就要重复调用runMode:beforeDate:,
具体可以参考苹果文档给出的方案,如下:

1
2
3
4
5
NSRunLoop *myLoop  = [NSRunLoop currentRunLoop];
myPort = (NSMachPort *)[NSMachPort port];
[myLoop addPort:_port forMode:NSDefaultRunLoopMode];
BOOL isLoopRunning = YES; // global
while (isLoopRunning && [myLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
1
2
3
4
5
//关闭runloop的地方
- (void)quitLoop {
isLoopRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
}

面试题

1、讲讲RunLoop项目中有用到吗?

  • 1、定时器切换的时候,为了保证定时器的准确性,需要添加runLoop
  • 2、在聊天界面,我们需要持续的把聊天信息存到数据库中,这个时候需要开启一个保活线程,在这个线程中处理

2、RunLoop内部实现逻辑?

  • 1、通知观察者(observers)RunLoop即将启动
  • 2、通知观察者(observers)任何即将要开始的定时器
  • 3、通知观察者(observers)即将处理source0事件
  • 4、处理source0
  • 5、如果有source1,跳到第9步
  • 6、通知观察者(observers)线程即将进入休眠
  • 7、将线程置于休眠知道任一下面的事件发生
  • 1、source0事件触发
  • 2、定时器启动
  • 3、外部手动唤醒
  • 8、通知观察者(observers)线程即将唤醒
  • 9、处理唤醒时收到的时间,之后跳回2
  • 1、如果用户定义的定时器启动,处理定时器事件
  • 2、如果source0启动,传递相应的消息
  • 10、通知观察者RunLoop结束

3、RunLoop和线程的关系?

  • 1、每一条线程都有唯一的一个与之对应的RunLoop对象
  • 2、RunLoop保存在一个全局的Dictionary里,线程作为Key,RunLoop作为Value
  • 3、线程刚创建时,并没有RunLoop对象,RunLoop会在第一次获取她时创建
  • 4、RunLoop会在线程结束的时候销毁
  • 5、主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

4、RunLoop有几种状态

kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop

5、RunLoop的mode的作用
系统注册了5中mode

1
2
3
4
5
kCFRunLoopDefaultMode //App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode //界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
UIInitializationRunLoopMode // 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode // 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes //这是一个占位用的Mode,不是一种真正的Mode

但是我们只能使用两种mode

1
2
kCFRunLoopDefaultMode //App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode //界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

iOS 多线程:『RunLoop』详尽总结

iOS RunLoop入门小结

iOS-Runloop常驻线程/性能优化

可选项是什么?

You use optionals in situations where a value may be absent. An optional represents two possibilities: Either there is a value, and you can unwrap the optional to access that value, or there isn’t a value at all.

可选项,一般也叫可选类型,它代表2种可能,一种可能是有值,一种可能是没值。没值代表值缺失的情况,使用nil来表示,有值的时候,可以通过解包获取真正的值。也可以手动将值设置为nil。Swift中的可选项适用于对象类型、结构体类型、枚举类型和基础类型。而Objective-C中只有对象类型的值可以为nil

如何定义可选项?

在类型名称后面加问号来定义一个可选项。可选项的值默认为nil,非可选项没有默认值,需要初始化给定默认值。

1
2
3
4
5
6
7
8
var name: String? = "张三" // 可选项
print(String(describing: name))
name = nil // 可选项可以设置成nil

var name1: String = "李四" // 非可选项
print(name1)
// 非可选项设置nil,会报错"'nil' cannot be assigned to type 'String'"
// name1 = nil

最终控制台打印结果:

1
2
Optional("张三")
李四

可选项默认值为nil,非可选项没有默认值,并且非可选项在使用前需要赋值。

强制解包

  • 可选项是对其他类型的一层包装,可以将它理解为一个盒子

  • 如果为nil,那么它是一个空盒子

  • 如果不为nil,那么盒子里装的是:被包装类型的数据
    代码示例:

    1
    2
    3
    var age: Int? // 默认就是nil
    age = 10
    age = nil

    图形表示:

  • 如果需要从可选项中取出被包装的数据,需要使用感叹号!进行强制解包

    1
    2
    3
    4
    5
    6
    7
    var age: Int? = 10
    print(String(describing: age))
    let ageValue: Int = age!
    print(ageValue)
    //控制台打印数据
    //Optional(10)
    //10
  • 如果对值为nil的可选项进行强制解包,将会产生运行时错误

    1
    2
    var age: Int?
    print(age!)

    会产生错误:Fatal error: Unexpectedly found nil while unwrapping an Optional value

判断可选项是否包含值

可选值可以和nil直接进行比较,用于判断可选项是否包含值。

1
2
3
4
5
6
7
8
let number = Int("aa123")
if number != nil {
print("字符串转换成功:\(number!)")
} else {
print("字符串转换失败")
}
//控制台打印:
//字符串转换失败

可选项绑定(Optional Binding)

  • 可以使用可选项绑定来判断可选项是否包含值。
  • 如果包含就自动解包,把值赋值给一个临时的常量(let)或者变量(var),并返回true,否则返回false

示例1:

1
2
3
4
5
6
7
8
9
if let number = Int("123") {
print("字符串转换成功:\(number)")
// number是强制解包之后的Int值
// number作用域仅限于这个大括号
} else {
print("字符串转换失败")
}
//控制台打印结果:
//字符串转换成功:123

示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Season : Int {
case spring = 1, summer, autumn, winter
}
if let season = Season(rawValue: 6) {
switch season {
case .spring:
print("season is spring")
default:
print("season is other")
}
} else {
print("no such season")
}
//控制台打印结果:
//no such season

示例3:多条件组合判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 一般写法
if let first = Int("4") {
if let second = Int("42") {
if first < second && second < 100 {
print("\(first) < \(second) < 100")
}
}
}
//控制台打印结果:
//4 < 42 < 100

// 使用组合条件判断的等价写法:
if let first = Int("4"),
let second = Int("42"),
first < second && second < 100 {
print("\(first) < \(second) < 100")
}
//控制台打印结果:
//4 < 42 < 100

示例4:while循环中使用可选项绑定

1
2
3
4
5
6
7
8
9
10
11
12
// 遍历数组,将遇到的正数都加起来,如果遇到负数或非数字,停止遍历
var strs = ["10", "20", "abc", "-20", "30"]

var index = 0;
var sum = 0
while let num = Int(strs[index]), num > 0 {
sum += num
index += 1
}
print(sum)
//控制台打印结果:
//30

空合并运算符 ?? (Nil-Coalescing Operator)

定义如下:
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?

  • a ?? b
  • a可选项
  • b可选项 或者 不是可选项
  • ba的存储类型必须相同
  • 如果a 不为nil,就返回 a
  • 如果anil,就返回 b
  • 如果b 不是可选项,返回a 时会自动解包
  • 计算结果的类型与 b的类型相同

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 示例1
let a: Int? = 1
let b: Int? = 2
let c = a ?? b
print(String(describing: c)) // c是Int? , Optional(1)

// 示例2
let a: Int? = nil
let b: Int? = 2
let c = a ?? b
print(String(describing: c)) // c是Int? , Optional(2)

// 示例3
let a: Int? = nil
let b: Int? = nil
let c = a ?? b
print(String(describing: c)) // c是Int? , nil

// 示例4
let a: Int? = 1
let b: Int = 2
let c = a ?? b
print(String(describing: c)) // c是Int , 1

// 示例5
let a: Int? = nil
let b: Int = 2
let c = a ?? b
print(String(describing: c)) // c是Int , 2

// 示例6,与示例5等价,不使用??运算符的时候
let a: Int? = nil
let b: Int = 2
let c: Int
// 如果不使用??运算符
if let temp = a {
c = temp
} else {
c = b
}

多个??一起使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 示例1
let a: Int? = 1
let b: Int? = 2
let c = a ?? b ?? 3
print(String(describing: c)) // c是Int,1

// 示例2
let a: Int? = nil
let b: Int? = 2
let c = a ?? b ?? 3
print(String(describing: c)) // c是Int,2

// 示例3
let a: Int? = nil
let b: Int? = nil
let c = a ?? b ?? 3
print(String(describing: c)) // c是Int,3

??if let配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 示例1
let a: Int? = nil
let b: Int? = 2
if let c = a ?? b { // 类似于 if a != nil || b != nil
print(c)
}
//控制台打印结果:
//2

// 示例2
let a: Int? = nil
let b: Int? = 2
if let c = a, let d = b { // 类似于 if a != nil && b != nil
print(c)
print(d)
}
print("123")
//控制台打印结果:
//123

guard语句

1
2
3
4
5
guard 条件 else {
// do something...
退出当前作用域
// return、break、continue、throw error
}
  • guard语句的条件为false时,就会执行大括号里面的代码
  • guard语句的条件为true时,就会跳过guard语句
  • guard语句特别适合用来”提前退出”
  • 当使用guard语句进行可选项绑定时,绑定的常量(let)、变量(var)也能在外层作用域中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
func login(_ info: [String : String]) {
guard let username = info["username"] else {
print("请输入用户名")
return
}
guard let password = info["password"] else {
print("请输入密码")
return
}
// if username ...
// if password ...
print("用户名:\(username)", "密码:\(password)", "登录...")
}

隐式解包(Implicitly Unwrapped Optional)

  • 在某些情况下,可选项一旦被设定值后,就会一直拥有值
  • 在这种情况下,可以去掉检查,也不必每次访问时都进行解包,因为它能确定每次访问的时候都是有值
  • 可以在类型后面加个!定义一个隐式解包的可选项
1
2
3
4
5
6
7
8
9
10
11
12
let num1: Int! = 10
let num2: Int = num1
if num1 != nil {
print(num1 + 6) // 16
}
if let num3 = num1 {
print(num3) // 10
}


let num4: Int! = nil
let num5: Int = num4 // Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

字符串插值中使用可选项

  • 可选项在字符串插值或者直接打印时,编译器会发出警告
    1
    2
    var age: Int? = 10
    print("My age is \(age)") // 此处会出现警告
  • 至少有3种方法消除警告
    1
    2
    3
    4
    var age: Int? = 10
    print("My age is \(age!)")
    print("My age is \(String(describing: age))")
    print("My age is \(age ?? 0)")

多重可选项

1
2
3
4
5
6
var num1: Int? = 10
var num2: Int?? = num1
var num3: Int?? = 10
print(num2 == num3) // true, 值相同
print(num1 == num2) // true, 值相同
print(num1 == num3) // true, 值相同

以上代码可以按照下图理解:

可以使用lldb指令frame variable -R或者fr v -R查看区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(lldb) fr v -R num1
(Swift.Optional<Swift.Int>) num1 = some {
some = {
_value = 10
}
}
(lldb) fr v -R num2
(Swift.Optional<Swift.Optional<Swift.Int>>) num2 = some {
some = some {
some = {
_value = 10
}
}
}
(lldb) fr v -R num3
(Swift.Optional<Swift.Optional<Swift.Int>>) num3 = some {
some = some {
some = {
_value = 10
}
}
}
(lldb)

空值示例:

1
2
3
4
5
6
var num1: Int? = nil
var num2: Int?? = num1
var num3: Int?? = nil
print(num2 == num3) // false, 值不相同
print(num1 == num2) // true, 值相同
print(num1 == num3) // false, 值不相同

以上代码可以按照下图理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(lldb) fr v -R num1
(Swift.Optional<Swift.Int>) num1 = none {
some = {
_value = 0
}
}
(lldb) fr v -R num2
(Swift.Optional<Swift.Optional<Swift.Int>>) num2 = some {
some = none {
some = {
_value = 0
}
}
}
(lldb) fr v -R num3
(Swift.Optional<Swift.Optional<Swift.Int>>) num3 = none {
some = some {
some = {
_value = 0
}
}
}
(lldb)

字符串字面量

字符串字面量可以用于为常量和变量提供初始值:

1
2
let someString = "Some string const value"
var someString2 = "Some string varible value"

多行字符串字面量

如果你需要一个字符串是跨越多行的,那就使用多行字符串字面量 — 由一对三个双引号包裹着的具有固定顺序的文本字符集:

1
2
3
4
5
6
7
let quotation = """
The White Rabbit put on his spectacles. "Where shall I begin,
please your Majesty?" he asked.

"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""

初始化空字符串

1
2
3
var emptyString = ""  // 空字符串字面量
var emptyString2 = String() // 字符串初始化方法
// 两个字符串均为空并等价。

字符串判空

1
2
3
if emptyString.isEmpty {
print("Nothing to see here")
}

字符串可变性

1
2
3
4
5
6
7
var variableString = "Horse"
variableString += " and carriage"
// variableString 现在为 "Horse and carriage"

let constantString = "Highlander"
constantString += " and another Highlander"
// 这报告一个编译错误(compile-time error) - 常量字符串不可以被修改

遍历字符串中的字符

方法1:通过for in遍历字符串中全部的字符

1
2
3
for character in "ABCDE" {
print(character)
}

方法2:使用 indices 属性会创建一个包含全部索引的范围(Range),用来在一个字符串中访问单个字符。

1
2
3
4
5
let greeting = "Helo, World!"
for index in greeting.indices {
print("\(greeting[index]) ", terminator: "")
}
// 控制台输出:H e l o , W o r l d !

通过字符数组初始化字符串

1
2
3
let catCharacters: [Character] = ["C", "h", "a", "r"]
let catString = String(catCharacters)
print(catString)

字符串连接

字符串可以通过加法运算符” + “相加在一起,创建一个新的字符串

1
2
3
4
let string1 = "hello"
let string2 = " there"
var welcom = string1 + string2
print(welcom)

字符串末尾追加字符

1
2
let exclamationMark: Character = "!"
welcom.append(exclamationMark)

字符串插值

1
2
3
let multiplier = 3
let message = "\(multiplier) * 2.5 is \(Double(multiplier) * 2.5)"
print(message)

如果要在使用扩展字符串分隔符的字符串中使用字符串插值,需要在反斜杠后面添加与开头和结尾数量相同的扩展字符串分隔符。

1
print(#"6 * 7 is \#(6 * 7)"#)

如果是字符串可选项,在直接使用字符串插值的时候会报警告,避免警告有三种方式:

1
2
3
4
5
6
7
8
let str1: String? = "123"
print("str1 = \(str1!)") // 强制解包
print("str1 = \(String(describing: str1))") // 使用字符串方法
print("str1 = \(str1 ?? "空值")") // 使用空合并运算符
// 控制台打印如下信息:
//str1 = 123
//str1 = Optional("123")
//str1 = 123

计算字符数量

想要获得一个字符串中Character值的数量,可以使用count属性:

1
2
let unusualStr = "1234\t"
print(unusualStr.count) // 5

通过索引访问字符串的值

通过startIndex访问第一个字符,通过before lastIndex访问最后一个字符,lastIndex无法直接访问。

1
2
3
4
5
let greeting = "Helo, World!"
print(greeting[greeting.startIndex])
print(greeting[greeting.index(before: greeting.endIndex)])
print(greeting[greeting.index(after: greeting.startIndex)])
print(greeting[greeting.index(greeting.startIndex, offsetBy: 3)])

字符串插入操作

调用insert(_:at:)方法可以在一个字符串的指定索引插入一个字符,调用insert(contentsOf:at:)方法可以在一个字符串的指定索引插入一个字符串。

1
2
3
4
5
6
var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)
print(welcome) // 打印 hello!

welcome.insert(contentsOf: " there", at: welcome.index(before: welcome.endIndex))
print(welcome) // 打印 hello there!

字符串移除操作

调用remove(at:)方法可以在一个字符串指定索引删除一个字符,调用removeSubrange(_:)方法可以在一个字符串的指定索引删除一个子字符串。

1
2
3
4
5
6
7
var welcome = "hello there!"
welcome.remove(at: welcome.index(before: welcome.endIndex))
print(welcome) // 打印 hello there

let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex
welcome.removeSubrange(range)
print(welcome) // 打印 hello

子字符串

当你从字符串中获取一个子字符串 —— 例如,使用下标或者 prefix(_:)之类的方法 —— 就可以得到一个 Substring 的实例,而非另外一个 String。Swift 里的 Substring 绝大部分函数都跟 String 一样,意味着你可以使用同样的方式去操作 Substring 和 String。然而,跟 String 不同的是,你只有在短时间内需要操作字符串时,才会使用 Substring。当你需要长时间保存结果时,就把 Substring 转化为 String 的实例:

1
2
3
4
5
6
7
let greeting = "Hello, world!"
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex
let beginning = greeting[..<index]
// beginning 的值为 "Hello"

// 把结果转化为 String 以便长期存储。
let newString = String(beginning)

字符串相等

Swift提供了三种方式来比较文本值:字符串字符相等、前缀相等和后缀相等。

字符串/字符相等

字符串/字符可以用等于操作符(==)和不等于操作符(!=)。

1
2
3
4
5
6
7
8
9
10
11
12
13
var quotation1 = String("this is a test string")
var quotation2 = String("this is a test string")
var ptr1 = withUnsafePointer(to: &quotation1) { $0 }
var ptr2 = withUnsafePointer(to: &quotation2) { $0 }
print("quotation1地址:", ptr1)
print("quotation2地址:", ptr2)
if quotation1 == quotation2 {
print("quotation1 == quotation2")
}
// 控制台打印消息
// quotation1地址: 0x0000000102b1e090
// quotation2地址: 0x0000000102b1e0a0
// quotation1 == quotation2

参考链接

字符串和字符
Swift-06.指针(UnsafePointer)

1.查看NodeJS当前使用的镜像源,使用命令:

1
2
// 查看当前NodeJS使用的镜像源
$ npm config get registry

使用上述命令查看NodeJS使用的默认镜像源:

1
2
3
4
5
6
7
8
9
10
11
12
13
poetmacbook-pro:~ kris$ npm config get registry
https://registry.npmjs.org/


╭────────────────────────────────────────────────────────────────╮
│ │
│ New minor version of npm available! 6.4.1 → 6.14.5 │
│ Changelog: https://github.com/npm/cli/releases/tag/v6.14.5 │
│ Run npm install -g npm to update! │
│ │
╰────────────────────────────────────────────────────────────────╯

poetmacbook-pro:~ kris$

2.将NodeJS的源切换成淘宝的镜像源。
首先打开网址https://developer.aliyun.com/mirror/NPM?from=tnpm,根据网站提示在终端执行命令:

1
$ npm install -g cnpm --registry=https://registry.npm.taobao.org

切记:如果淘宝镜像源不管用了,及时切回默认的镜像源。

参考链接

nodeJs修改镜像源

ES6新语法

1.变量/赋值
var 可以重复定义,不能限制修改,没有块级作用域
let 不能重复定义,变量,块级作用域
const 不能重复定义,常量,块级作用域

解构赋值:
1.左右两边格式相同,右边必须是合法类型
2.必须是定义和赋值同步完成

1
2
let [a,b,c] = [1, 12, 23];
let {a, b, c} = {a: 12, b: 89, c: 55};

2.函数
箭头函数
function (参数,参数) {
函数体
}
(参数,参数) => {
函数体
}
1.如果有且仅有1个参数,()可以省
2.如果函数体只有一句话,而且是return,{}可以省

​ 默认参数
​ (a, b=xx, c=xxx)

​ 参数展示
​ function show(a, b, …参数名)
​ 剩余参数必须在参数列表的最后

3.数组/json

4.字符串

5.面向对象

6.Promise

7.generator

8.模块

标题的使用

1
2
3
4
5
6
# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题

无序列表使用

1
2
- 无序列表1
- 无序列表2
  • 无序列表1
  • 无序列表2

有序列表使用

1
2
1. 有序列表1
2. 有序列表2
  1. 有序列表1
  2. 有序列表2

引用使用

1
> 注意不同的Xcode版本,打的iPA大小不同。

注意不同的Xcode版本,打的iPA大小不同。

表格使用

1
2
3
4
|ID|姓名|年龄|
|---|---|---|
|1|小明|18|
|2|小李|25|
ID 姓名 年龄
1 小明 18
2 小李 25

表格居中对齐

1
2
3
4
|ID|姓名|年龄|
|:---:|:---:|:---:|
|1|小明|18|
|2|小李|25|
ID 姓名 年龄
1 小明 18
2 小李 25

表格右对齐

1
2
3
4
|ID|姓名|年龄|
|---:|---:|---:|
|1|小明|18|
|2|小李|25|
ID 姓名 年龄
1 小明 18
2 小李 25

链接使用

1
2
[百度](https://www.baidu.com)
[谷歌](https://www.google.com)

百度
谷歌

图片使用

1
2
![]()
![]()

HEXO图片引用

1
{% asset_image 1.png 截图注释 %}

在工作中有时会遇到这样的情况,修改完文件之后,因为各种原因,误操作导致文件本提交,但是还没有push到远端,希望撤回之前的修改,并且将改动保存到暂存区,方便再次修改或贮存操作。

问题截图如下:

使用git log查看一下日志,截图如下:

解决方法是,使用命令:

1
$ git reset --soft HEAD^

使用上面的命令,可以将刚刚commit的数据撤销回来,改动的数据不会丢失。命令撤销commit的截图:

关掉SourceTree,重新打开,发现commit的状态已经没有了,数据已经恢复到commit之前的状态。

上述命令在使用时是可以加参数的具体参数如下:

1
2
3
4
5
HEAD^    // 意思是撤销上一个commit提交,也可以写成 HEAD^1
HEAD^~2 // 撤销之前的2次commit提交
--soft // 不删除工作空间改动代码,撤销commit,不撤销git add .
--hard // 删除工作空间改动代码,撤销commit,撤销git add .
--mixed // 不删除工作空间改动代码,撤销commit,并且撤销git add .操作。这个为默认参数,git reset --mixed HEAD^和 git reset HEAD^效果是一样的。

如果commit注释写错了,只是想改一下注释,需要执行以下命令:

1
$ git commit --amend

此时会进入默认vim编辑器,修改注释完毕后保存就好了。

链接

git使用情景2:commit之后,想撤销commit