Hi, this is Alice Home


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 公益404

GCD 全面解析

发表于 2018-06-12 | 分类于 iOS

GCD 概要

Grand Central Dispatch

GCD 是什么?

是异步执行任务的技术之一。通过非常简洁的方式,实现了极为复杂的多线程编程。说白了就是为了使得多线程编程更加简洁

Objective-C

1
2
3
4
5
6
7
8
9
10
11
12
- (void)gcdTest {

dispatch_queue_t queue = dispatch_queue_create("com.hello.world", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
//处理耗时操作
NSLog(@"处理耗时操作 ...");
sleep(3);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"refresh ui ...");
});
});
}

Swift

1
2
3
4
5
6
7
8
9
10
11
12
func gcdTestFunction() {
let queue = DispatchQueue(label: "queue0", qos: .default, attributes: .concurrent, autoreleaseFrequency: .never, target: nil)
queue.async {
/*长时间处理任务*/
print("处理耗时操作")
sleep(3)
DispatchQueue.main.async {
//主线程处理,比如用户界面更新
print("refresh ui ... ")
}
}
}

多线程编程

一个 CPU 无分叉的执行一条条的指令路径,即为线程。
一个 CPU 在各条指令路径之间进行切换,即为多线程,而如果一个 CPU 有多个核,那么就是真正的同时执行多条指令路径,这就是多线程编程技术。

缺点:

  1. 数据不一致(数据共享造成)
  2. 消耗资源(寄存器保存上下文,供 CPU 在各个指令路径之间进行切换)
  3. 死锁(多个线程相互等待)

鉴于上述问题,多线程编程技术就会趋于复杂。

优点:
多线程编程可保证应用程序的响应性能(耗时操作放在子线程中执行,主线程负责 UI 响应)

通过多线程技术,在执行长时间的处理时仍可保证用户界面的响应性能。
GCD 大大简化了偏于复杂的多线程编程源代码。

API

使用 GCD 开发者要做的就是定义要执行的任务然后添加到适当的 Dispatch Queue 中。
GCD 为我们提供了 API 供完成队列的创建和任务的添加。

Dispatch Queue

Dispatch Queue 是执行任务的队列。将任务添加到队列中,任务就会被执行。队列分为两类:

  • 串行队列:等在当前队列中的任务执行完毕之后,在执行下一个任务
  • 并发队列:不会等待当前的任务执行完毕,根据系统情况继续执行后面追加的任务。

Main Dispatch Queue/ Global Dispatch Queue


创建队列的方式,除了通过 dispatch_create() 之外,系统 API 帮我提供了两个可以直接获取到队列的函数,分别是:

  • dispatch_get_main_queue() 获取主队列,向其中添加的任务在主线程的 Runloop 中执行,因此只有一个这种队列,是串行队列
  • dispatch_get_global_queue() 获取全局队列,是并发队列

dispatch_set_target_queue

修改队列的优先级,因为通过 create 函数创建的队列没有优先级,那么可以通过这个函数来设置优先级。

dispatch_after

这个函数是在指定时间后像当前队列中追加任务,至于任务合适执行要看当前的上下文了。

dispatch_group

如果一串任务有序列执行,可以直接通过串行队列实现,但是如果多个任务无序执行结束之后,又有一个确定的任务必须等待前面的无序任务执行结束之后再去执行,那么这就需要用到这个函数了: dispatch_group

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)dispatch_group {

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue, ^{
NSLog(@"one ...");
});

dispatch_group_async(group, queue, ^{
NSLog(@"two ...");
});

dispatch_group_async(group, queue, ^{
NSLog(@"three");
});


dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"done");
});


}

one、two、three 三个任务追加到并发队列中,虽然这个三个任务执行顺序不定,但是 done 一定是在最后执行的。

dispatch_barrier_async

执行读写任务,读任务是可以并发的,但是写任务相互之间不可以并发,与读任务可以控制着进行并发。
下面共有 8 个读取任务,执行完成四个读取任务之后,需要进行一次写任务,这个写入任务必须在前四个读取任务结束之后再执行,那么久可以通过 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
24
25
26
27
28
29
30
31
32
- (void)dispatch_barrier_async {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(queue, ^{
NSLog(@"reading ..... 1");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 2");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 3");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 4");
});

dispatch_barrier_async(queue, ^{
NSLog(@"writing ...");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 5");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 6");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 7");
});
dispatch_async(queue, ^{
NSLog(@"reading ..... 8");
});
}

使用 Concurrent Dispatch Queue 和 dispatch_barrier_async 函数可实现高效率的数据库访问和文件访问

dispatch_sync

同步追加任务到某个队列中。同步追加方式要等到之前的任务完成之后,才会追加进去,所以容易造成死锁:

下面这两种方式就会造成死锁:

1
2
3
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"one ...");
});
1
2
3
4
5
6
7
dispatch_queue_t queue = dispatch_queue_create("one", NULL);
dispatch_async(queue, ^{
NSLog(@"one ...");
dispatch_sync(queue, ^{
NSLog(@"Hello world");
});
});

dispatch_apply

1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t dispatch_global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, dispatch_global_queue, ^(size_t t) {
NSLog(@"Hello world %zu", t);
});
NSLog(@"done --- ");

dispatch_queue_t dispatch_serial_queue = dispatch_queue_create("com.maple.www", NULL);
dispatch_apply(10, dispatch_serial_queue, ^(size_t t) {
NSLog(@"Hello world serial %zu", t);
});
NSLog(@"done");

这个函数像是 dispatch_sync 和 dispatch group 的组合版。该函数按照指定次将指定的 Block 追加都队列中,并且等待所有的任务执行完毕。

可以通过这个函数来访问数组元素:

1
2
3
4
5
NSArray *array = @[@"one",@"two",@"three",@"four",@"five",@"six"];

dispatch_apply([array count], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {
NSLog(@"array[%zu] = %@",index,array[index]);
});

因为 dispatch_apply 函数和dispatch_sync 函数相同,所以最好在 dispatch_async 函数中非同步执行 dispatch_apply 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

NSArray *array = @[@"one",@"two",@"three",@"four",@"five",@"six"];
dispatch_queue_t dispatch_global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(dispatch_global_queue, ^{

dispatch_apply([array count], dispatch_global_queue, ^(size_t index) {
NSLog(@"array[%zu] = %@",index,array[index]);
});

// 到这里已经处理完所有的事情

dispatch_async(dispatch_get_main_queue(), ^{
//更新ui
});
});

dispatch_suspend / dispatch_resume

挂起和恢复队列

dispatch semaphore

信号基数,如果我们直接通过 for 循环启动多个异步线程向数组中存入数据,那么会引发内存分配的错误。而 semaphore 是一种信号机制,当我们通过 create 函数创建一个 count 为 1 的信号时候,表示当前只能执行一次任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)semaphore {

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

NSMutableArray *mArray = [[NSMutableArray alloc] init];

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

for (int i = 0; i < 100000; i ++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[mArray addObject:[NSNumber numberWithInt:i]];
dispatch_semaphore_signal(semaphore);
});
}

NSLog(@"over");
}

dipatch_once

在当前应用程序中如果打算只执行一次任务,那么就可以通过 dispatch_once 来实现。一般用来生成单例对象。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
- (void)dispatch_onceFunc {

static dispatch_once_t onceToken;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_once(&onceToken, ^{
NSLog(@"Hello world");
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_once(&onceToken, ^{
NSLog(@"Hello world");
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_once(&onceToken, ^{
NSLog(@"Hello world");
});
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_once(&onceToken, ^{
NSLog(@"Hello world");
});
});


static int initialized = NO;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (initialized == NO) {
NSLog(@"Hello this is mine.");
initialized = YES;
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (initialized == NO) {
NSLog(@"Hello this is mine.");
initialized = YES;
}
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (initialized == NO) {
NSLog(@"Hello this is mine.");
initialized = YES;
}
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (initialized == NO) {
NSLog(@"Hello this is mine.");
initialized = YES;
}
});

}

output:

1
2
3
4
5
GCDExample[79831:10334594] Hello world
GCDExample[79831:10334575] Hello this is mine.
GCDExample[79831:10334638] Hello this is mine.
GCDExample[79831:10334639] Hello this is mine.
GCDExample[79831:10334640] Hello this is mine.

使用 dispatch_once 保证了线程安全。

Dispatch I/O

当我们读取较大的文件的时候,如果将文件分成合适大小并使用 Global Dispatch Queue 并列读取的话,应该比一般的读取速度会快很多。

实现

GCD 的 Dispatch Queue 非常方便,那么它究竟是怎么实现的 ?

  • 用户管理追加 Block 的 C 语言层实现 FIFO 队列
  • Atomic 函数中实现的用于排他控制的轻量级信号
  • 用于管理线程的 C 语言层实现的一些容器

Mac OS X and iOS Internals

发表于 2018-06-11

Mac OS X and iOS Internals

除了这本书之外,还有一本类似的书叫做《Mac OS X Internals》 ,这本书相对较早了

此书分为两部分:

  • PART 1:FOR POWER USERS
  • PART 2:THE KERNEL

PART 1 FOR POWER USERS

1 Darwinism: The Evolution of OS X

https://pan.baidu.com/s/1nuCMkZn
https://www.jianshu.com/u/360a4cffa818

iOS App 内存专项实践

发表于 2018-06-11

此篇是学习了 QCon 大会记录

iOS 是一个封闭的系统,使用的内存检测工具自然而然较少!

封闭系统下的工具 Instruments

苹果 Xcode 继承了 Instruments 供调试使用,但是这个工具对于测试人员来说其实并不可用,或者基本上是不可用的。开发者使用起来倒是顺手。

封闭系统下的自由


苹果系统自带的开发工具对于测试而言其实并不友好,苹果推崇的是开发、产品和测试集于一身的人员,而实际上这种倾向只适合于小的项目或者模块,模块一旦大了势必不是单打独头可以完成的,尤其是在大公司一个项目动辄编译半个小时,鉴于此就需要有适合于自动化集成的内存检测工具。

获取新建对象申请的内存地址和堆栈

  • 纯 OC 代码: 利用 method_exchangeImplementationse 接管 alloc 和 dealloc
  • C++ 内网版本(非纯 OC 代码): 借用开发工具的权限, malloc_logger 简单直接
  • C++ 外网版本: fishhook hook: malloc\calloc\free

获取指针变量

  • 堆区
  • 全局区
  • 栈区

剩下问题

内存泄漏的结果是:内存耗尽,闪退。除了内存泄漏之外还有没有其他的原因会造成闪退呢?

  • 大内存常驻
  • 内存越界
  • 侵略性内存使用,比如一次申请的内存空间很大

解决思路:

  • 记录申请的内存大小和堆栈
  • 记录堆栈内存的释放
  • 聚合数据

对上面的记录进行判断:

  • 单个申请的内存块大雨 50 MB
  • 申请内存(未释放)的综合大于阈值

针对内存越界,可以通过 address sanitizer 来进行检测:

  • Heap buffer overflow
  • Stack buffer overflow
  • Global buffer overflow
  • dangling pointer
  • Use-after-free
  • Double-free

最后记录:

  • 工具:Xcode、Address Sanitizer、Instruments
  • 界面声明周期
  • Xcode(malloc_logger)、LLDB
  • hook:fishhook、method_exchangeImplementations
  • hook 获取源码能力: fishhook 如何遍历 SECT_DATA 获取全局区
  • Jetsam
    Monkey 做自动化

总结:

这是一名测试工程师的分享,虽然没有实际的实现,但是原理已经讲明包括使用的工具以及有哪些方面,后面会慢慢去实践。

由这次学习有获得了两本书可以看:

  1. 《Mac OS X and iOS Internals 2012》
  2. 《Mac Os X Internals - A Systems Approach》

参考:
iOS App 内存专项实践

LeetCode(四)反转链表

发表于 2018-06-10 | 分类于 LeetCode

预备知识:Data Structure(一) Linked List
代码链接
题目题目链接

反转链表

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

进阶:

你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

思路一: 迭代的方式

通过遍历的方式,核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func reverseList(_ head: ListNode?) -> ListNode? {

var newNode = head
var preNode: ListNode?
while newNode != nil {

var nextNode: ListNode?

if let node = newNode {
nextNode = node.next
node.next = preNode
preNode = node
}
newNode = nextNode
}


return preNode
}

思路一: 递归的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

func reverseList2(_ head: ListNode?) -> ListNode? {

if head?.next == nil {
return head

}else {
let temp = head?.next
head?.next = nil
let node = reverseList(temp)
temp?.next = head

return node
}
}

结果

总结:

看这个问题的时候,对链表还不太熟悉,熟悉了之后再解决这个问题的时候发现,这个反转的过成功根本就看不到链表啊 … 有木有?题目中给出了 Node 的定义,以及 Node 中 value 的类型,其实这就够了,我第一遍做的时候,在 LeetCode 网站上把定义链表的代码也贴上了,然后还加上了 firt、last 以及 description 还有 append() 方法,虽然正确,但是编译时不通过的,因为 LeetCode 内部已经实现了,只需要写反转算法这块代码就好了,这块儿折腾了一大会儿才反应过来。

对于思路一,迭代这种方式,在纸上画个草稿,就差不多能写出来了。
思路二,是通过递归的方式去解决这个问题的,主要是处理好结束条件

LeetCode (三) 两个数组交集

发表于 2018-06-10 | 分类于 LeetCode

题目链接
代码链接

两个数组的交集

给定两个数组,写一个方法来计算它们的交集。

例如:

给定 nums1 = [1, 2, 2, 1], nums2 = [2, 2], 返回 [2, 2].

注意:

输出结果中每个元素出现的次数,应与元素在两个数组中出现的次数一致。
我们可以不考虑输出结果的顺序。

跟进:

  • 如果给定的数组已经排好序呢?你将如何优化你的算法?
  • 如果 nums1 的大小比 nums2 小很多,哪种方法更优?
  • 如果nums2的元素存储在磁盘上,内存是有限的,你不能一次加载所有的元素到内存中,你该怎么办?

思路一:

  1. 首先进行排序
  2. 然后根据数组的下标进行遍历
  3. 遍历过过程中进行对比,小的累加,相等保存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var nums1 = [1,2,2,4,21,2,1,3]
var nums2 = [21,2,3,33,2,21]

var nums = Array<Any>()
nums1 = nums1.sorted(by: <)
nums2 = nums2.sorted(by: <)

var i = 0
var j = 0
while j < nums1.count && i < nums2.count {

if nums1[j] > nums2[i] {
i = i + 1
}else if nums1[j] < nums2[i] {
j = j + 1
}else {
nums.append(nums1[j])
i = i + 1
j = j + 1
}
}

结果:

App Extension

发表于 2018-06-08

Node 搭建本地 http 服务

发表于 2018-06-07

Mac Node JS

发表于 2018-06-07 | 分类于 Node JS

Node JS ?

NodeJS 是 js 的一个解析器,可以让 js 独立于浏览器执行。执行在浏览器中的 js 的解析器就是浏览器。

安装

Node js 下载地址,下载 apk ,双击安装.

终端输入:

1
2
3
4
$ which node
/usr/local/bin/node
$ node
>

表示安装成功。

运行

解析器模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ node
> 1 + 2
3
> console.log("hello node");
hello node
undefined
> function hello() {
... console.log("hello world");
... }
undefined
> hello()
hello world
undefined
>

xxx.js 文件方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# maple @ iMac in ~/workspace/NODEJS [11:12:53]
$ cat hello.js
function hello()
{
console.log("Hello world");

}


hello();

# maple @ iMac in ~/workspace/NODEJS [11:18:28]
$ node hello.js
Hello world

模块

Node JS 可以直接解析 JS code 文件并输出结果,可以将工程通过 JS 文件来进行划分,那么划分出来的各个模块之间的沟通便是一个问题。

例如,当前目录下两个文件 main.js & one.js

exports 导出模块函数

1
2
3
4
5
6
7

function hello() {
console.log("this is hello funciton");
return 1024;
}

exports.hello = hello;

require 读取模块函数

1
2
3
4
5
6

var one = require('./one');

console.log(one.hello());
console.log(one.hello());
console.log(one.hello());

运行

1
2
3
4
5
6
7
$ node main.js
this is hello funciton
1024
this is hello funciton
1024
this is hello funciton
1024

LeetCode (二) 旋转数组

发表于 2018-06-05 | 分类于 LeetCode

旋转数组

题目链接
代码链接

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
尽可能相处更多的解决方案,至少有三种不同的方法可以解决这个问题;
要求使用空间复杂度为 O(1) 的原地算法

分析:

这道题是数组元素移动问题,如果想要达到较好的性能:

  1. 空间复杂度
  2. 时间复杂度(尽量不要触动数组的整体移动)

除此之外,还有就是交换的问题,

思路一:

直接复制一份数组,然后做一下交换

1
2
3
4
5
6
7
8
var numbers = [1,2,3,4,5,6,7]
var k = 3
var numberCopy = numbers
for i in 0 ..< numbers.count {
numberCopy[(k + i) % numbers.count] = numbers[i]
}

numberCopy

思路二:

这种操作,相对来说比较好理解,就是一步一步进行原地置换,但是时间复杂度较高,因为没有个数字的交换都牵扯数组整体数据交换,但是作为理解还是可以实际操作一波的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var  count = numbers.count
var start = 0
k = k % count
for _ in 0 ..< k {

for i in 0 ..< count - 1 {
numbers[count - i - 1] = numbers[count - i-1] ^ numbers[(count - i) % count]
numbers[(count - i) % count] = numbers[(count - i) % count] ^ numbers[count - i-1]
numbers[count - i - 1] = numbers[count - i-1] ^ numbers[(count - i) % count]

}
}

numbers

思路三:

实现数组内数据的交换,只要找到交换规律,就可以实现效率较高的算法。
k = 3 count = 7

1,2,3,4,5,6,7

numbers[0] <-> numbers[4](7 - 3)

5,2,3,4,1,6,7

numbers[1] <-> numbers[5](7-2)

5,6,3,4,1,2,7

numbers[2] <-> numbers[6](7-1)

5,6,7,4,1,2,3

numbers[3] <-> numbers[7-3]

5,6,7,1,4,2,3

numbers[4] <-> numbers[5](7-2)

5,6,7,1,2,4,3

numbers[5] <-> numbers[5](7-1)

5,6,7,1,2,3,4

从上面可以看出, 前一个角标就是迭代此处,后一个是 k 规律性的大小变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

func swapNumbers<T>(_ nums: inout Array<T>, _ p: Int, _ q: Int) {
(nums[p], nums[q]) = (nums[q], nums[p])
}
numbers = [1,2,3,4,5,6,7]

func rotate(_ nums: inout [Int], _ k: Int) {
var count = nums.count
var start = 0
var vk = k
vk = vk % count
while count > 0 && vk > 0{
for i in 0 ..< vk {
// i + start count - vk + i + start
swapNumbers(&nums, i + start, count-vk+i + start)
}
count = count - vk
start = start + vk
vk = vk % count
}
}

思路三 优化:

使用系统自带的数组交换元素函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func rotate(_ nums: inout [Int], _ k: Int) {
var count = nums.count
var start = 0
var vk = k
vk = vk % count
while count > 0 && vk > 0{
for i in 0 ..< vk {
// i + start count - vk + i + start
nums.swapAt(i + start, count-vk+i + start)
}
count = count - vk
start = start + vk
vk = vk % count
}
}

执行结果:

参考:

https://www.cnblogs.com/grandyang/p/4298711.html

iOS 屏幕渲染和事件处理机制

发表于 2018-06-05 | 分类于 iOS

屏幕显示原理

屏幕显示过程

CRT 电子枪一行一行进行扫描,为了与系统的视频控制器同步,会有 HSync 和 VSync 两个信号进行同步,前者是一行,后者是一帧的同步信号。

计算机系统中 CPU 计算好显示的内容,然后提交给 GPU, GPU 渲染完成后将结果放入帧缓冲区,随后视频控制器会按照 VSync 读取缓冲区数据,经过数模转换传递给显示器显示。

垂直同步(V-Sync)

简单的情况下,帧缓冲区只有一个,为了提高效率,显示系统通常引入两个缓冲区, iOS 就是两个缓冲区,安卓是三个。但是,双缓冲区吟哦日帧不同步问题,容易引起图片撕裂现象,也就是当某一个缓冲区的数据还没有完全通过视频控制器显示到屏幕的时候就被 GPU 刷新了,从而引起了上面的问题。而 V-Sync 就是为了解决这个问题引入的垂直同步信号,该信号通知之后 GPU 才会去进行新的一帧数据渲染和缓冲区更新。

卡顿, Why?

垂直同步信号

在 VSync 信号到来后,系统图像服务会通过 CADisplayLink 等机制通知 APP, APP 主线程开始在 CPU 中计算显示内容,然后将结算的结果传递给 GPU,GPU 进行处理之后将渲染结果提交给帧缓冲区,等到下次 VSync 来到之后显示到屏幕上。如上图 1 部分所示,但是如果在一个 VSync 中,如果 CPU 或者 GPU 没有完成内容提交,那么那一帧的数据就会被丢弃等到下一帧再去显示,如上图 2、3 部分两帧的时间实际上显示了一帧数据,这样在 3 帧之前实际上一直显示的是第 1 帧的内容,呈现的效果就是 – 卡顿。

上面显示了 GPU 没有在第二个 Sync 来之前提交完数据,所以对于 GPU 和 CPU 的压力进行评估和优化可以提高屏幕展示效果。

屏幕渲染:


  • On-Screen Rendering
  • Off-Screen Rendering

参考

  1. iOS 保持界面流畅的技巧 - Garan no Dou
1…567…13
Alice

Alice

The Loneliest Whale in the World

121 日志
35 分类
30 标签
© 2021 Alice
由 Hexo 强力驱动
主题 - NexT.Pisces