帕帕's Blog

记录点点滴滴

最近正在用 React Native 重构整个项目,我们用了 react-native-navigation 这个库来作为项目的导航控制器。
所以,我们平常会把页面跳转逻辑的时候放在 Screen 里面的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
class FirstScreen extends React.Component {

// 点击事件
_someAction = () => {
this.props.navigator.push({
screen: 'example.SecondScreen',
});
}

render = () => {
...
}
}

一般情况下,上面的写法没有问题。但是直到我们碰到这样一个需求的时候就抓瞎了:点击一个 PDF 文件,如果 PDF 文件没有下载就先去下载,下载完成之后自动跳转到 PDF 阅读器。由于用了 redux 之后,我们就增加一个 finished 的 state 来判断是否已经下载完成。示例代码如下:

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
class ExampleScreen extends React.Component {

componentWillReceiveProps = (nextProps) => {
// 这里判断下载状态是否已完成,完成的话就去跳转
if (nextProps.finished === true) {
// 这里需要重置一下状态,不然其他 state 发生变化会多次触发页面的跳转
this.props.dispatch(resetFinished());
this.props.navigator.push({
screen: 'example.PDFScreen',
});
}
}

// 点击事件
_someAction = () => {
// openPDF() 这个 action 会自动去下载 PDF 文件,然后修改 finished 的状态
this.props.dispatch(openPDF());
}

render = () => {
...
}
}

const mapStateToProps = state => {
return {
finished: state.finished
}
};

export default connect(mapStateToProps)(ExampleScreen);

上面的做法是可以实现我们的需求,但是这种写法很蛋疼。因为当你在调用用 openPDF() 的时候,你以为后面的事不需要你操心,然后这个时候有人告诉你还需要在其他地方增加一个中间状态去补充 openPDF() 的后续逻辑处理。

经过讨论之后,我们决定改成用 callback 的方式来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ExampleScreen extends React.Component {

// 点击事件
_someAction = () => {
// openPDF() 是一个异步 action
this.props.dispatch(openPDF(callback: () => {
this.props.navigator.push({
screen: 'example.PDFScreen',
});
}));
}

render = () => {
...
}
}

使用 callback 的好处就是去掉了一个烦人的中间状态,并且从阅读体验来说很容易让读者明白这个点击事件在干什么。但是在 redux 的 action 方法中增加一个 callback 的调用,看起来也有点不伦不类的。虽然我认为 callback 和其他参数具有相同的法律地位。

其实最好的实现是,这个点击事件应该连页面的跳转逻辑也不需要处理:

1
2
3
4
5
6
7
8
9
10
11
class ExampleScreen extends React.Component {

// 点击事件,这个事件只做一件事就是去 dispatch 一个 openPDF() 的 action
_someAction = () => {
this.props.dispatch(openPDF());
}

render = () => {
...
}
}

像上面这种实现,我们也就只能在 openPDF() 里动手脚了:

1
2
3
4
5
6
7
8
9
// action.js
export const openPDF = await () => {
return dispatch => {
// 异步下载 PDF
async downloadPDF();
// 完成之后通过 router 去实现页面跳转
dispatch(openRouter('PDFScreen'));
};
}

这里就不再详细说 router 的实现细节了,因为网上有很多现成的资料。(PS: 主要是我也还没看到这一块)

从页面(Screen)的角度来说,我认为这样的处理是最合适的。因为 Screen 只需要关注本页面的 state 和 action,至于跳转的逻辑交给后面的 action 来处理是最好的。

最近在用 RAC 的时候发现自己对内存管理还是有些困惑,于是自己写了一些代码来验证自己的一些理解。
在一开始接触 RAC 的时候,我们知道 RAC 对于 block 都是 copy 赋值的。

1
2
3
4
5
6
7
@implementation RACSignal

#pragma mark Lifecycle

+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
return [RACDynamicSignal createSignal:didSubscribe];
}
1
2
3
4
5
6
7
8
9
@implementation RACDynamicSignal

#pragma mark Lifecycle

+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
RACDynamicSignal *signal = [[self alloc] init];
signal->_didSubscribe = [didSubscribe copy];
return [signal setNameWithFormat:@"+createSignal:"];
}

在创建 RACSingal 的时候会调用其子类 RACDynamicSignal 去创建,我们也看到 RACDynamicSignal 对 didSuscribe 这个 block 是进行了 copy。所以大家可能会被要求注意循环引用的问题,于是大家都用 @weakify(target) 和 @strongify(target) 来避免循环引用的问题。那是不是所有用到 RAC 的地方都需要使用这些宏来避免循环引用的问题,不尽然。比如下面这个:

1
2
3
4
// 场景1
[RACObserve(self, title) subscribeNext:^(id x) {
NSLog(@"%@", x);
}];

接下来,我们来对比以下的几种用法:

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
@interface ViewController()
@property (strong, nonatomic) ViewModel * viewModel;
@end

@implementation ViewController

- (void)viewDidiLoad {
[super viewDidLoad];

self.viewModel = [ViewModel new];

// 场景2
dispatch_async(dispatch_get_main_queue(), ^{
self.title = @"你好";
});

// 场景3
[self.viewModel.titleSignal subscribeNext:^(NSString * title) {
self.title = title;
}];

// 场景4
[RACObserve(self.viewModel, title) subscribeNext:^(NSString * title) {
self.title = title;
}];
}

@end

场景2是我们平常都会用到的,而且我们也没有在这种场景下去考虑循环引用的问题,这是因为 dispatch 的 block 不是属于 self 的(至于这个 block 是属于谁的,回头我再查点资料或者请各位指教),所以即使你在 block 使用了 self 也不会有循环应用的问题。

场景3很明显是有循环引用的问题:self->viewModel->titleSignal->block->self,这个时候如果我们不做处理的话,那么 self 就永远不会被释放。正确的做法应该是使用 @weakify(self) 和 @strongify(self):

1
2
3
4
5
6
// 场景3
@weakify(self);
[self.viewModel.titleSignal subscribeNext:^(NSString * title) {
@strongify(self);
self.title = title;
}];

场景4在我们看来是没有问题的,因为这里看起来只有 singal->block->self 的引用,它们之间并没有造成循环引用的问题。我们先来看看 RACObserve 的实现:

1
2
3
4
5
6
7
8
9
10
#define RACObserve(TARGET, KEYPATH) \
({ \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \
__weak id target_ = (TARGET); \
[target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \
_Pragma("clang diagnostic pop") \
})

- (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath observer:(__weak NSObject *)observer;

其实,看到这里你会认为这里只是调用了一个方法创建了一个 Signal,而且这个 Signal 也并不属于任何对象。我们再来看看具体的实现是怎么样的?

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
- (RACSignal *)rac_valuesAndChangesForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options observer:(__weak NSObject *)weakObserver {
NSObject *strongObserver = weakObserver;
keyPath = [keyPath copy];

NSRecursiveLock *objectLock = [[NSRecursiveLock alloc] init];
objectLock.name = @"org.reactivecocoa.ReactiveCocoa.NSObjectRACPropertySubscribing";

__weak NSObject *weakSelf = self;

RACSignal *deallocSignal = [[RACSignal zip:@[
self.rac_willDeallocSignal,
strongObserver.rac_willDeallocSignal ?: [RACSignal never]
]] doCompleted:^{
// Forces deallocation to wait if the object variables are currently
// being read on another thread.
[objectLock lock];
@onExit {
[objectLock unlock];
};
}];

return [[[RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
// Hold onto the lock the whole time we're setting up the KVO
// observation, because any resurrection that might be caused by our
// retaining below must be balanced out by the time -dealloc returns
// (if another thread is waiting on the lock above).
[objectLock lock];
@onExit {
[objectLock unlock];
};

__strong NSObject *observer __attribute__((objc_precise_lifetime)) = weakObserver;
__strong NSObject *self __attribute__((objc_precise_lifetime)) = weakSelf;

if (self == nil) {
[subscriber sendCompleted];
return nil;
}

return [self rac_observeKeyPath:keyPath options:options observer:observer block:^(id value, NSDictionary *change, BOOL causedByDealloc, BOOL affectedOnlyLastComponent) {
[subscriber sendNext:RACTuplePack(value, change)];
}];
}] takeUntil:deallocSignal] setNameWithFormat:@"%@ -rac_valueAndChangesForKeyPath: %@ options: %lu observer: %@", self.rac_description, keyPath, (unsigned long)options, strongObserver.rac_description];
}

重点观察 deallocSignal 和 **[signal takeUntile:deallocSignal]**,我们把 deallocSignal 单独拿出来看看:

1
2
3
4
5
6
7
8
9
10
11
RACSignal *deallocSignal = [[RACSignal zip:@[
self.rac_willDeallocSignal,
strongObserver.rac_willDeallocSignal ?: [RACSignal never]
]] doCompleted:^{
// Forces deallocation to wait if the object variables are currently
// being read on another thread.
[objectLock lock];
@onExit {
[objectLock unlock];
};
}];

这里的 deallocSignal 是只有在 self 和 strongObserve 都将要发生 dealloc 的时候才会触发的。即用 RACObserve 创建的信号只有在其 target 和 observe 都发生 dealloc 的时候才会被 disposable (这个好像是 RAC 用来销毁自己资源的东西)。不明白的童鞋,我们回头来分析一下场景4的代码:

1
2
3
4
// 场景4
[RACObserve(self.viewModel, title) subscribeNext:^(NSString * title) {
self.title = title;
}];

用 RACObserve 创建的信号看起来只要出了函数体其资源应该就会被回收,但是这个信号其实是只有在 self.viewModel.rac_willDeallocSignal 和 self.rac_willDeallocSignal 都发生的情况下才会被释放。所以场景4的引用关系看起来只有 signal->block->self,但是这个 signal 只有在 self.rac_willDeallocSignal 的时候才会被释放。所以这里如果不打断这种关系的话就会造成循环引用的问题,正确做法应该是:

1
2
3
4
5
6
// 场景4
@weakify(self);
[RACObserve(self.viewModel, title) subscribeNext:^(NSString * title) {
@strongify(self);
self.title = title;
}];

最后,在说一个特别需要注意的,就是 UITableViewCell 和 UICollectionViewCell 复用和 RAC 的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1000;
}

- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"TableViewCell"];

@weakify(self);
[RACObserve(cell.textLabel, text) subscribeNext:^(id x) {
@strongify(self);
NSLog(@"%@", self);
}];

return cell;
}

我们看到这里的 RACObserve 创建的 Signal 和 self 之间已经去掉了循环引用的问题,所以应该是没有什么问题的。但是结合之前我们对 RACObserve 的理解再仔细分析一下,这里的 Signal 只要 self 没有被 dealloc 的话就不会被释放。虽然每次 UITableViewCell 都会被重用,但是每次重用过程中创建的信号确实无法被 disposable。那我们该怎么做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1000;
}

- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"TableViewCell"];

@weakify(self);
[[RACObserve(cell.textLabel, text) takeUntil:cell.rac_prepareForReuseSignal] subscribeNext:^(id x) {
@strongify(self);
NSLog(@"%@", self);
}];

return cell;
}

注意,我们在cell里面创建的信号加上 takeUntil:cell.rac_prepareForReuseSignal,这个是让 cell 在每次重用的时候都去 disposable 创建的信号。

以上所说的关于内存的东西我都用 Instrument 的 Allocations 验证过了,但是依旧建议大家自己也去试试。

记录日常中用到的一些 Bash 脚本,经常更新

Tip 1 : 修改文件里面的内容

早上产品有一个小需求就是把工程中的所有网页的标题修改为黑米流量通,可以使用以下命令来实现

1
$ find . -name '*.html' -print0 | xargs -0 sed -i '' -e 's/<title>.*<\/title>/<title>黑米流量通<\/title>/g'
  • find 查找命令,可以用 man find 查看更多的信息
  • . 代表当前目录
  • -name find 命令的参数,表示要查找的文件名
  • -print0 是一种不换行的输出格式,以 ASCII NUL 字符(也就是\0)作为分隔符。上面的例子可能是 a.html\0b.html\0c.html
  • | 这是一个管道符,表示把前面命令的输出作为后面命令的输入
  • xargs 是用来构造输入参数,并且循环执行每一个参数
  • -0 表示让 xargs 使用 ASCII NUL 来分隔参数。上面的例子将被分隔成 a.html b.html c.html 三个参数依次执行
  • sed 这是一个流编辑器,如果传的是文件名会把文件内容读入内存,如果只是普通字符串就会把字符串读入内存
  • -i 表示要把原来的文件内容做一次备份,后面的 '' 是表示要备份的文件名字,如果没有文件名字就表示不需要备份
  • -e 表示后面的字符串是一个命令,需要被执行
  • s/old/new/g 这个是用来替换字符串的命令

Tip 2 : 查找文件的内容

把匹配的文件内容的相关文件列出来

1
$ find . -name '*.html' -print0 | xargs -0 grep 'PATTERN'

Tip 3 : 解决 Homebrew 的权限问题

查看 Homebrew 的所有权

1
$ ls -al `which brew`

把 Homebrew 的用户和分组修改为 root 和 wheel

1
$ sudo chown root:wheel `which brew`

最后还原 Homebrew 的权限(安全)

1
$ sudo chown : `chown brew`

Tip 4 : 利用 Shell 生成生成 ICON

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
#!/bin/sh
#此脚本是用来生成 iPhone 和 iPad 所需 icon 的不同尺寸的,最好是准备一张 1024x1024 的 Icon 图片


filename="icon.png"

dirname="icon"

name_array=("Icon-20.png" "Icon-20@2x.png" "Icon-20@3x.png"
"Icon-29.png" "Icon-29@2x.png" "Icon-29@3x.png"
"Icon-40.png" "Icon-40@2x.png" "Icon-40@3x.png"
"Icon-60@2x.png" "Icon-60@3x.png"
"Icon-76.png" "Icon-76@2x.png"
"Icon-83.5@2x.png")
size_array=("20" "40" "60"
"29" "58" "87"
"40" "80" "120"
"120" "180"
"76" "152"
"167")

mkdir $dirname

for ((i=0;i<${#name_array[@]};++i)); do
m_dir=$dirname/${name_array[i]}
cp $filename $m_dir
sips -Z ${size_array[i]} $m_dir
# 如果图片是 sRGB 的话,使用下面的命令
# sips --matchTo '/System/Library/ColorSync/Profiles/sRGB Profile.icc' -Z ${size_array[i]} $m_dir
done

Tip5 : 使用 Python 共享当前目录

利用下面的命令可以暂时开启一个端口号为 8000 的 HTTP 服务,其他人只需要在浏览器输入 http://ip-address:8000 即可浏览共享目录下的文件

1
$ python -m SimpleHTTPServer

Tip6 : 加密和解密文件

  • 加密
1
$ tar czf - {SRC_DIR} | openssl des3 -salt -k "{KEY}" -out {DIST_PACKAGE}.tar.gz

示例:

目录名 paris_code,秘钥 meta#com,输出包 paris_code_20161008.tar.gz

1
$ tar czf - paris_code | openssl des3 -salt -k "meta#com" -out paris_code_20161008.tar.gz
  • 解密

第一步:获取代码压缩文件包

下载地址 http://XXXX.com/paris_code_20161008.tar.gz

第二步:解密文件(OS X / Linux only)

在 Terminal 进入压缩文件包同级目录,输入以下命令:

1
$ openssl des3 -d -k "meta#com" -salt -in paris_code_20161008.tar.gz | tar xzf -

Tip7: iOS 打包命令

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
echo "----------------"
echo "Begin Build!"
PROJECT_NAME="orbit"
BUILD_DATE="$(date +'%Y%m%d')"
BUNDLE_ID="com.meta.paris"
cd ${WORKSPACE}

#/usr/local/bin/npm install

if [ -d "${WORKSPACE}/build" ]; then
if ls ${WORKSPACE}/build/**/*.ipa 1> /dev/null 2>&1; then
rm -rf ${WORKSPACE}/build/**/*.ipa;
fi;
if ls ${WORKSPACE}/build/**/*.xcarchive 1> /dev/null 2>&1; then
rm -rf ${WORKSPACE}/build/**/*.xcarchive;
fi;
else
mkdir ${WORKSPACE}/build;
fi;

echo "计算今天的 Build Version"
if [ -d "${WORKSPACE}/build/${BUILD_DATE}" ]; then
#如果不加上面的 if, Jenkins 无法直接执行下面的命令❓
BUILD_DATE_COUNT=$(ls ${WORKSPACE}/build | grep "^${BUILD_DATE}" -c)
if [ ${BUILD_DATE_COUNT} -lt 10 ]; then
BUILD_DATE_COUNT="0${BUILD_DATE_COUNT}"
fi;
BUILD_VERSION="${BUILD_DATE}${BUILD_DATE_COUNT}"
else
BUILD_VERSION=${BUILD_DATE}
fi;
echo "今天的 Build Version 是 ${BUILD_VERSION}"

if [ -d "${WORKSPACE}/build/${BUILD_VERSION}" ]; then
rm -rf ${WORKSPACE}/build/${BUILD_VERSION};
fi;
mkdir ${WORKSPACE}/build/${BUILD_VERSION};

if [ -d "${WORKSPACE}/Enterprise.plist" ]; then
rm ${WORKSPACE}/Enterprise.plist;
fi;

#http://www.matrixprojects.net/p/xcodebuild-export-options-plist/
Enterprise='<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>teamID</key>
<string></string>
<key>method</key>
<string>app-store</string>
<key>uploadSymbols</key>
<true/>
<key>uploadBitcode</key>
<false/>
</dict>
</plist>'
echo ${Enterprise} > ${WORKSPACE}/Enterprise.plist

sed -i '' 's/ProvisioningStyle = Automatic;/ProvisioningStyle = Manual;/g' \
${WORKSPACE}/${PROJECT_NAME}.xcodeproj/project.pbxproj

sed -i '' 's/DEVELOPMENT_TEAM = .*;/DEVELOPMENT_TEAM = "";/g' \
${WORKSPACE}/${PROJECT_NAME}.xcodeproj/project.pbxproj

#动态生成 Build Version
sed -i '' "/<key>CFBundleVersion<\/key>/{N;s/<string>.*<\/string>/<string>${BUILD_VERSION}<\/string>/g;}" \
${WORKSPACE}/${PROJECT_NAME}/${PROJECT_NAME}-Info.plist

xcodebuild -workspace ${WORKSPACE}/${PROJECT_NAME}.xcworkspace \
-scheme ${PROJECT_NAME} -sdk iphoneos \
build CODE_SIGN_IDENTITY="iPhone Distribution: Beijing PS Technology Co., Ltd." \
PROVISIONING_PROFILE="" \
-configuration Release clean archive \
-archivePath ${WORKSPACE}/build/${BUILD_VERSION}/${PROJECT_NAME}.xcarchive

xcodebuild -exportArchive -exportOptionsPlist ${WORKSPACE}/Enterprise.plist \
-archivePath ${WORKSPACE}/build/${BUILD_VERSION}/${PROJECT_NAME}.xcarchive \
-exportPath ${WORKSPACE}/build/${BUILD_VERSION}/

echo "----------------"
echo "Build successfully!"


echo "Begin Upload to itunes..."
#Use [shenzhen](https://github.com/nomad/shenzhen) to upload the ipa file to itunes connect.
/usr/local/bin/ipa distribute:itunesconnect -f ${WORKSPACE}/build/${BUILD_VERSION}/${PROJECT_NAME}.ipa -a YourAppleID -p YourPassword -i ${BUNDLE_ID} --upload
echo "Upload successfully!"

Tip8: 重置 iOS 模拟器

相信各位在做 iOS 开发的同学都会碰到模拟器上各种神奇的现象,通过重置 iOS 模拟器基本上可以解决大部分问题:

1
2
3
4
5
6
// 退出当前的所有模拟器
$ osascript -e 'tell application "iOS Simulator" to quit'
$ osascript -e 'tell application "Simulator" to quit'

// 清掉之前使用模拟器产生的所有内容
$ xcrun simctl erase all

Tip9: 模拟器截图

下面的命令会默认截取第一个启动的模拟器:

1
xcrun simctl io booted screenshot screenshot.png

当你同时启动了多个模拟器的情况下,需要先查看当前启动的模拟器 ID,然后指定 ID 截图:

1
2
xcrun simctl list
xcrun simctl io B5EEDDC0-CDA3-46A9-A2B6-FA940D693DFC screenshot screenshot.png

Dynamic dispatch means that program has to determine at run time which method or property is being referred to and then perform an indirect call or indirect access.

我们都知道 Swift 的 class 是可以被继承,function 和 property 是可以被重写的,而这就意味着 Swift 需要 dynamic dispatch 这种机制来完成这些功能。Swift 的 dynamic dispatch 首先会再 method table 查找方法,然后间接调用。很明显这种方式要比直接调用的效率慢,并且用间接调用的方式还会阻止编译器的一些优化无法实现。

那么应该怎么优化呢?

当我们明确的知道 class、function、property 是不需要 overridden,我们可以通过使用 final 和 private(fileprivate) 这些关键字减少动态派发的发生,从而有效的提高效率。

在 Swift 中,如果被 final 或 private(fileprivate) 修饰的 class、function、property 是不能 overridden,并且调用这些 class、function、property 的时候不再通过 dynamic dispatch 去间接调用,而是直接调用。

所以,通过在必要的代码中使用 final 或 private(fileprivate) 这些关键字进行优化的话,将可以有效提高的效率。

Whole Module Optimization

Swift 的 class、function、property 的默认权限都是 internal ,除非我们明确的加上 public 或 private(fileprivate) 关键字才能改变它们的默认权限。

编译器在编译 Module 的时候都是对里面的源文件进行单独编译,这样的话编译器就无法确切的知道被 internal 修饰的 class、function、property 究竟有没有被 overridden。一旦我们开启 Whole Module Optimization 的优化选项,编译器就会同时对整个 Module 的所有源文件进行编译,这个时候编译器就可以知道哪些被 internal 修饰的 class、function、property 没有被 overridden,从而把它们的权限从 internal 修改为 final。这样的话,就可以减少 dynamic dispatch 的发生从而提高效率。

开启编译优化选项的步骤如下:Xcode -> Build Settings -> Swift Compiler -> Optimization Level。


参考文献

  1. https://www.reddit.com/r/iOSProgramming/comments/3atu5w/does_swift_use_dynamic_method_dispatch_or_a/

  2. https://developer.apple.com/swift/blog/?id=27

  3. https://github.com/apple/swift/blob/3ef6c79e3c591cf31b8a853b1357e1b8c5771252/docs/OptimizationTips.rst#whole-module-optimizations

Array 是随机存储的(random-access)集合类型。

ContiguousArray 是连续存储(contiguously stored)的数组,并且不允许和 NSArray 进行桥接的。

当我们的数组元素是 Class 或 @objc protocol 类型的话,并且我们不需要在 Objective-C 中使用该数组的话,那么我们最好使用 ContiguousArray。这是因为 Array 需要额外的资源来处理跟 NSArray 的桥接功能,但是 ContiguousArray 则不需要,所以 ContiguousArray 比 Array 的效率要高。

1
2
3
4
5
6
7
class A {

}

// 不要用Array: let array = Array<A>()

let contiguousArray = ContiguousArray<A>()

另外需要注意的是官方文档说如果数组元素不是 Class 和 @objc protocol 类型的话,Array 和 ContiguousArray 的效率是一样的。(我猜测是因为如果 Array 的元素都是 Struct 类型的话,它就不需要消耗资源来处理桥接的问题了。)

Efficiency is equivalent to that of Array, unless Element is a class or @objc protocol type, in which case using ContiguousArray may be more efficient.

但是 @Paul Hudson 在他的《Pro Swift》中说他发现即使数组元素是 Struct 类型的话,ContiguousArray 也要比 Array 更快。我们来看看他给出的例子:

1
2
3
4
5
6
7
8
9
10
11
12
let array2 = Array<Int>(1...1000000)
let array3 = ContiguousArray<Int>(1...1000000)

var start = CFAbsoluteTimeGetCurrent()
array2.reduce(0, combine: +)
var end = CFAbsoluteTimeGetCurrent() - start
print("Took \(end) seconds")

start = CFAbsoluteTimeGetCurrent()
array3.reduce(0, combine: +)
end = CFAbsoluteTimeGetCurrent() - start
print("Took \(end) seconds")

经过我的测试,上面的代码中 ContiguousArray 只用了0.19秒而 Array 用了0.38秒,所以 ContiguousArray 确实要比 Array 快。

如果大家想在性能上有所提升的话,建议大家可以用 ContiguousArray 试一试。

1、替换第n1行到第n2行的内容

1
:n1,n2/origin/replace/g

2、替换整个文件的内容

1
:%s/origin/replace/g

3、移动n1-n2行(包括n1,n2)到n3行之下

1
n1,n2 m n3     

4、复制n1-n2行(包括n1,n2)到n3行之下

1
:n1,n2 co n3

5、删除文件的空行

1
:g/^$/d

6、在文本中插入一个1到100的序列(来自池老师《说,谁才是最帅的编程工具?》

1
:r!seq 100

7、在当前的每一行文字前面增加“序号. ”(来自池老师《说,谁才是最帅的编程工具?》

1
:let i=1 | g /^/ s//\=i.". "/ | let i+=1

8、当前目录下(包括子文件夹)所有后缀为 java 的文件中的 apache 替换成 eclipse,那么在当前目录下依次执行如下命令:(来自池老师《说,谁才是最帅的编程工具?》

1
2
3
vim
:n **/*.java
:argdo %s/apache/eclipse/ge | update

今天我们来了解下面这几种包含文件的方式有什么特点和区别:

1
2
3
4
5
#include "fiel"
#include <file>
#import "file"
#import <file>
@import Module

一、#include

学过 C 语言的人都知道,#include 其实是一个预处理命令。它会在预处理的时候简单的把被 #include 包含的文件内容进行复制粘贴。我们来看看下面的代码:

1
2
3
4
5
// A.h
void sampleA() {
// A code
}

1
2
3
4
5
6
// B.h
#include "A.h"

void sampleB() {
// B code
}

我们使用 gcc -E B.h 命令来看看经过预处理后的文件内容大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1 "B.h"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 329 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "B.h" 2
# 1 "./A.h" 1
void sampleA() {

}
# 2 "B.h" 2

void sampleB() {

}

我们可以看到经过预处理之后,A.h 文件中的内容被直接复制并粘贴到 B.h 文件中来。如果我们在 B.h 文件中多次包含了 A.h 文件,会出现什么情况?比如:

1
2
3
4
// A.h
void sampleA() {
// A code
}
1
2
3
4
5
6
7
// B.h
#include "A.h"
#include "A.h"

void sampleB() {
// B code
}

经过预处理之后的内容大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1 "B.h"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 329 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "B.h" 2
# 1 "./A.h" 1
void sampleA() {

}
# 2 "B.h" 2
# 1 "./A.h" 1
void sampleA() {

}
# 3 "B.h" 2

void sampleB() {

}

A.h 文件中的 sampleA() 函数出现了两次,所以我们需要利用其他的一些预处理命令来规避这种情况,看看下面的代码:

1
2
3
4
5
6
7
8
// A.h
#ifndef FILE_A
#define FILE_A

void sampleA() {
// A code
}
#endif
1
2
3
4
5
6
7
// B.h
#include "A.h"
#include "A.h"

void sampleB() {
// B code
}

我们再来看看增加了这些预处理命令之后的预处理文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1 "B.h"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 329 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "B.h" 2
# 1 "./A.h" 1



void sampleA() {

}
# 2 "B.h" 2


void sampleB() {

}

OK,这就正常了。如果我们在 A.h 中包含 B.h,然后又在 B.h 中包含 A.h,具体代码如下:

1
2
3
4
5
6
7
// A.h
#include "B.h"

void sampleA() {
// A code
}
#endif
1
2
3
4
5
6
// B.h
#include "A.h"

void sampleB() {
// B code
}

我们再来看看经过 gcc -E B.h 处理之后的文件内容:

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
55
56
57
58
59
60
61
62
63
64
# 1 "B.h"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 329 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "B.h" 2
# 1 "./A.h" 1
# 1 "./B.h" 1
# 1 "./A.h" 1
# 1 "./B.h" 1
...
...
# 1 "./A.h" 1
# 1 "./B.h" 1
In file included from ./B.h:1:
In file included from ./A.h:1:
In file included from ./B.h:1:
In file included from ./A.h:1:
...
...
In file included from ./B.h:1:
In file included from ./A.h:1:
./A.h:1:10: error: #include nested too deeply
#include "B.h"
^


void sampleA() {

}
# 2 "./B.h" 2

void sampleB() {

}
# 2 "./A.h" 2

void sampleA() {

}
# 2 "./B.h" 2

void sampleB() {

}
# 2 "./A.h" 2

void sampleA() {

}
...
...
# 2 "./A.h" 2

void sampleA() {

}
# 2 "./B.h" 2

void sampleB() {

}
1 error generated.

我们发现 A.h 和 B.h 重复出现,这是因为这个时候 A.h 和 B.h 文件互相引用导致的。从理论上来讲,这个时候会无限循环下去,直至世界终结。在这里最后会出现一句 *1 error generated.*的提示是 gcc 强行中断了这个预处理的过程,所以我们才能看到这样的结果。那我们可以怎么做?当然是利用前面说的预处理命令来避免循环引用的问题。看下面的代码:

1
2
3
4
5
6
7
8
9
10
// A.h
#ifndef FILE_A
#define FILE_A

#include "B.h"

void sampleA() {
// A code
}
#endif
1
2
3
4
5
6
7
8
9
10
// B.h
#ifndef FILE_B
#define FILE_B

#include "A.h"

void sampleB() {
// B code
}
#endif

这个时候使用 gcc -E B.h 就可以正常的进行预处理,最后的结果如下:

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
# 1 "B.h"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 329 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "B.h" 2



# 1 "./A.h" 1



# 1 "./B.h" 1
# 5 "./A.h" 2

void sampleA() {

}
# 5 "./B.h" 2

void sampleB() {

}

所以C程序员总是需要通过各种手段(比如:#pragma once)来防范此类事件的发生。

二、#import

我们在文件中通过#import来导入 iAd Framework:

编译报错:

需要重新导入和链接 Framework:

编译成功:

从上面的过程中我们就知道在 Objective-C 项目中使用 #import 需要注意导入和链接 Framework,否则是会报错的。

预处理器在碰到 #import 命令的时候,它会采用递归的方式把被所有头文件的内容复制并粘贴到当前文件中,如果文件依赖层次比较深就会造成预处理后的文件内容体积大幅度变大。

比如导入 UIKit 的时候只需要一行代码:

1
#import <UIKit/UIKit.h>

预处理之后会变成200多行(UIKit.h 文件有200多行代码):

1
2
3
4
5
6
7
#import <UIKit/UIKitDefines.h>

#if __has_include(<UIKit/UIAccelerometer.h>)
#import <UIKit/UIAccelerometer.h>
.....
#import <UIKit/UIRegion.h>
#endif

接下来还需要递归的把每个头文件的内容展开,最后的结果就是一行代码变成超过11000行代码。如果有多个文件都包含来 UIKit 的头文件,这样就会让每个文件的体积都会变得很大,编译过程也会变得越来越慢。这种递归的方式会让项目的编译时间变成:M source files + N headers => M x N compile time

所以这个时候有一个优化方法就是把项目中频繁被引用的文件放到 PCH(Pre-Compile Header)文件中。PCH 会被编译一次并且会被缓存,这就可以缩短编译时间,我们也不需要在不同的文件里面添加import语法。

当然,PCH 也有自己的缺点:

  • 维护负担:随着项目变得越来越复杂,我们就会不停的往PCH文件加入内容,内容一旦变多就会变得不好维护。(这也是我们平常在项目中要避免在 ViewController 做太多事情的,要研究 MVVM的缘故。)

  • 命名空间污染

最后,给大家提供一个例子看看 #import 编译出来之后的文件内容:

1
2
3
4
5
6
7
// A.h
#import "B.h"

void sampleA() {
// A code
}
#endif
1
2
3
4
5
6
7
// B.h
#import "A.h"
#import "A.h"

void sampleB() {
// B code
}

使用 gcc -E B.h 进行预处理之后的内容如下:

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

# 1 "B.h"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 329 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "B.h" 2
# 1 "./A.h" 1


void sampleA() {

}
# 2 "./B.h" 2

void sampleB() {

}

我们在B.h中有两个 #import “A.h”,但是这些内容跟我们之前在 A.h 和 B.h 文件中使用 #include 和其他预处理命令之后的处理结果很相似,所以我们就明白了 #import 大概做了什么事。

三、@import

在2012年的 LLVM 大会上,苹果的 Doug Gregor 首次提出了 Objective-C 中的 Module。使用 @import 方式导入有几个好处:

  • 不需要像 #import 一样得手动去链接 Framework,@import会自动去链接

  • @import 工作方式和 PCH 很像,但是 @import 要比 PCH 的效率高出许多

  • @import 导入 Modul 优化文件体积变大、编译速度变慢的问题

  • 可以部分导入(@import Framework.A)或全部导入(@import Framework)

所以,建议大家尽量使用 @import 来导入文件。如果你以前的项目用的是 #import,那么你也不需要担心,我们只通过 Build Settings 开启 Modules 选项(看下图),#import 和 #include 会自动被映射成 @import,所以你不需要更改原来的代码也能享受 @import带来的好处。

详细内容可以看看苹果2013年的 Advances in Objective-C,里面就详细介绍了 Module。

四、文件路径

接下来我们来了解一下 #include 和 *#include “file”*:

  • #include <file>: 表示编译器会直接到系统设定的目录下寻找指定的文件。

  • #include “file”: 表示编译器会到当前的目录下寻找指定的文件,如果找不到,则会去系统设定的目录下寻找指定的文件。


参考文献:

  1. https://gcc.gnu.org/onlinedocs/cpp/Include-Syntax.html

  2. http://stackoverflow.com/questions/18947516/import-vs-import-ios-7

  3. https://www.raywenderlich.com/49850/whats-new-in-objective-c-and-foundation-in-ios-7

GCD 简介

GCD(Grand Central Dispatch) 是苹果提供的一套多线程编程技术。想象一下,如果让你编写一个可以高效的跑在不同计算机、不同内核的应用程序,你会怎么做呢?你要看看硬件是什么,看看有有多少个内核,想想用什么算法,想想在什么时候去切换线程…总之,你要做的东西多了去了。而 GCD 帮我们屏蔽了这些技术细节,但是如果要用好 GCD 的话,还是要多了解一些知识点。

Dispatch 对象和内存管理

在 Objective-C 里面,所有的 dispatch 对象都是 Objective-C 对象,所以他们同样适用引用技术的内存管理。如果你是使用 ARC 的话,dispatch 对象会向普通的 Objective-C 对象一样自动进行 retain 和 release 操作;如果你是使用 MRC,要记住使用 dispatch_retain 和 dispatch_release 来进行管理。

常用 API

dispatch_queue_t(调度队列)

1
public func dispatch_queue_create(label: UnsafePointer<Int8>, _ attr: dispatch_queue_attr_t!) -> dispatch_queue_t!

在 GCD 中只能通过上面的 API 来创建调度队列,我们可以通过创建各种各样的 Block 形式的任务并由该调度队列来决定如何去执行这些 Block 任务。上面创建调度队列的函数需要两个参数:

  • label: 这个参数是用来给你创建的调度队列进行命名的,特别是在调试的时候你可以通过该参数来判断是哪个调度队列的任务在执行。
  • attr: 这个参数只有 DISPATCH_QUEUE_SERIAL 和 DISPATCH_QUEUE_CONCURRENT 两种值(在 Objective-C 中这个参数可以为 NULL,这个时候默认是 DISPATCH_QUEUE_SERIAL)。DISPATCH_QUEUE_SERIAL 是告诉调度队列以串行的方式去执行任务,DISPATCH_QUEUE_CONCURRENT 是告诉调度队列以并发的方式去执行任务。

当然我们还可以通过下面的方法来获取系统已经创建好的调度队列:

1
2
// 获取全局队列
public func dispatch_get_global_queue(identifier: Int, _ flags: UInt) -> dispatch_queue_t!
1
2
// 获取主线程的com.apple.main-thread (serial)队列
public func dispatch_get_main_queue() -> dispatch_queue_t!

注意,所有 pending 状态的 Block 任务都会持有该调度队列的引用,所以我们不需要显示的去持有调度队列,而调度队列会在所有的 Block 任务都从 pending 变为 finished 之后才会被释放。

总之,现在大家要知道的是我们可以把不同的 Block任务提交到调度队列,具体的细节和实现看看后面内容。

dispatch_sync 和 dispatch_async(同步和异步)

1
2
3
4
5
6
7
8
9
10
11
12
let queue = dispatch_queue_create("com.PS.Queue", DISPATCH_QUEUE_SERIAL)  // 创建调度队列
print("Begin Sync")
// 同步调用
dispatch_sync(queue) {
// Block任务
print("Execute Block Task1")
}
dispatch_sync(queue) {
// Block任务
print("Execute Block Task2")
}
print("After Sync")

这段代码的输出结果如下:

1
2
3
4
Begin Sync
Execute Block Task1
Execute Block Task2
After Sync

上面的例子就是我们平常对 dispatch_sync 的用法,并且我们可以看到第一个 Block 任务执行之后才会执行第二个 Block 任务。dispatch_sync 需要等待 Block的任务执行完成之后,才能继续往后执行。但是使用 dispatch_sync 的时候,有几点是需要注意的:

  1. 当调用 dispatch_sync 方法的时候,系统默认情况下会在当前线程去执行调度队列里的任务,只有在一些特殊情况下才会把调度队列的任务分配到其他线程去执行。所以我们就知道,线程和调度队列并不是一对一的关系。至于为什么默认情况下会在当前线程去执行调度队列里的任务,我的猜测是为了性能。大家想一想,dispatch_sync 会同步执行 Block任务, Block任务没有结束的情况下,后面的代码是无法执行的。基于这样一个同步的机制,GCD 还有必要先把当前线程挂起,然后去创建新线程,然后切换到新的线程去执行调度队列里的任务,然后再把线程切换到当前线程,然后再让当前线程恢复么?结论是没有必要。

  2. 你不能够在当前的串行调度队列的任务里面去添加新的任务到当前的调度队列里面,否则会造成死锁。这句话怎么理解呢,我们来来看看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 例1
let queue = dispatch_queue_create("com.PS.Queue", DISPATCH_QUEUE_SERIAL) // 创建串行的调度队列
// 同步调用
dispatch_sync(queue) {
// Block1
print("Begin Execute Block Task1")
dispatch_sync(queue) {
// Block2
print("Execute Block Task2")
}
print("End Execute Block Task1")
}

// 例1的结果
Begin Execute Block Task1

为什么 Block1 后面的 print 和 Block2 的 print 都不执行了呢?首先我们要知道被 DISPATCH_QUEUE_SERIAL 声明的调度队列是串行调度队列,串行调度队列里的任务是同时只能有一个任务在执行,并且当前任务没有执行完成,下一个任务也无法执行。上面的例子中会先输出 Block1 中的 Begin Execute Block Task1,然后这个时候再把 Block2 添加到同一个串行调度队列中去。这个时候的 Block1 还没有执行完成,它需要等 dispatch_sync 的 Block2 执行完成之后才能继续执行,而 Block2 又必须等待 Block1 执行完成之后才能执行,所以这个时候就造成 Block1 等着 Block2,Block2 等着 Block1 的死锁。

我们再把调度队列属性改为 DISPAT_QUEUE_CONCURRENT,然后再看看执行结果是什么:

1
2
3
4
5
6
7
8
9
10
11
12
// 例2
let queue = dispatch_queue_create("com.PS.Queue", DISPATCH_QUEUE_SERIAL) // 创建串行的调度队列
// 同步调用
dispatch_sync(queue) {
// Block1
print("Begin Execute Block Task1")
dispatch_sync(queue) {
// Block2
print("Execute Block Task2")
}
print("End Execute Block Task1")
}
1
2
3
4
// 例2的结果
Begin Execute Block Task1
Execute Block Task2
End Execute Block Task1

被 DISPATCH_QUEUE_CONCURRENT 声明的并发调度队列就没有这种死锁的问题。并发调度队列里的任务是不会霸占资源不放的,每一个任务执行一个时间片段之后会把资源交出来给别的任务去执行。所以例2中的 Block1 虽然需要等待 Block2 执行完成之后才能继续执行,但是当 Block1 在等待的过程中,是可以把资源释放出来交给 Block2 去执行,Block2 执行完成之后 Block1 就可以继续执行了。所以,这个时候就不会造成死锁来。

再来看看下面的例子会不会造成死锁:

1
2
3
4
5
override func viewDidLoad() {
dispatch_sync(dispatch_get_main_queue()) {
print("Excute Block Task")
}
}

答案是会的。给大家一点提示,主线程的默认调度队列是串行(DISPATCH_QUEUE_SERIAL)的,viewDidLoad() 是在主线程的调度队列 com.apple.main-thread (serial) 执行的。

上面的例子主要是希望大家理解串行和并发的概念,同时要明白造成死锁的原因。而要解决死锁一般可以用 DISPATCH_QUEUE_CONCURRENT 或接下来我们要讲的 dispatch_async 来解决。

通过对 dispatch_sync 的了解,我们可以利用 dispatch_async 很快的写出异步代码:

1
2
3
4
5
6
7
8
9
10
11
12
let queue = dispatch_queue_create("com.PS.Queue", DISPATCH_QUEUE_SERIAL)  // 创建调度队列
print("Begin Async")
// 异步调用
dispatch_async(queue) {
// Block1
print("Execute Block Task1")
}
dispatch_async(queue) {
// Block2
print("Execute Block Task2")
}
print("After Async")

这个例子的结果有好几种:

1
2
3
4
5
// 结果1
Begin Async
After Async
Execute Block Task1
ExEcute Block Task2
1
2
3
4
5
// 结果2
Begin Async
Execute Block Task1
ExEcute Block Task2
After Async

上面只是列出来两种可能,但实际上还有其他的可能。当我们调用 dispatch_async 的时候,它总是会在 Block 任务被提交之后马上返回,而不会傻傻的等待 Block 任务执行完成。由于上面创建的是串行调度队列,所以我们可以保证 Block1 要比 Block2 优先执行,但是 After Async 就无法确定是在 Block1 的前后还是 Block2 的前后。

如果我们把上面的 DISPATCH_QUEUE_SERIAL 改成 DISPATCH_QUEUE_CONCURRENT,那我们就无法确定 After Async、Block1 和 Block2 这三者的执行顺序了。

我们刚才说到用 dispatch_async 可以解决死锁的问题,那它是怎么解决的呢?

1
2
3
4
5
6
7
8
9
10
11
let queue = dispatch_queue_create("com.PS.Queue", DISPATCH_QUEUE_SERIAL)  // 创建串行的调度队列
// 异步调用
dispatch_async(queue) {
// Block1
print("Begin Execute Block Task1")
dispatch_async(queue) {
// Block2
print("Execute Block Task2")
}
print("End Execute Block Task1")
}

上面的例子会优先输出 Block1 的 Begin Execute Block Task1 之后,通过 dispatch_async 把 Block2 提交到串行队列里面,然后又马上返回到 Block1 去输出 End Execute Block Task1,这个时候的 Block1 就结束了,接下来就开始执行 Block2。所以上面的代码是不会造成死锁的,虽然上面的例子也是创建了一个串行调度队列,但是该调度队列只是保证了 Block1 要比 Block2 优先执行。

dispatch_once

写过 Objective-C 的人都知道,dispatch_once 一般会被用来创建单例对象:

1
2
3
4
5
6
7
8
9
10
@implementation Single
+ (Single *)sharedInstance {
static Single * _single = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_single = [[Single alloc] init];
});
return _single;
}
@end

这是由于 dispatch_once 是线程安全且只会执行一次,所以才会被用来作为单例的实现。这里需要注意的是 dispatch_once_t 必须是静态的或全局的才能保证 dispatch_once 的 Block 只会被执行一次,所以上面的代码用了 static 来修饰 dispatch_once_t。

dispatch_apply

1
public func dispatch_apply(iterations: Int, _ queue: dispatch_queue_t!, _ block: (Int) -> Void)

其中的 interations 是表明要执行多少次 block,block 中的 Int 是该 Block 被执行的序号。调用这个方法的时候要注意该方法跟 dispatch_sync 一样会阻塞当前线程,所以我们需要注意在主线程中调用该方法。

dispatch_after

1
public func dispatch_after(when: dispatch_time_t, _ queue: dispatch_queue_t, _ block: dispatch_block_t)

调用这个方法的时候需要注意的是 when 这个参数,你需要通过 dispatch_time 或 dispatch_walltime 来创建。并且该方法是异步执行的,并不会阻塞当前线程。

一般的写法如下:

1
2
3
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(5 * NSEC_PER_SEC)), queue) {
print("5s \(NSThread.currentThread())")
}

dispatch_group_t

dispatch_group_t 是用来做聚合同步的,它可以用来跟踪你提交的所有任务(即使是在不同的调度队列也可以)的完成状态。

接下来我们来看看 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
// 创建 dispatch_group_t 对象
let group = dispatch_group_create()

// 创建串行队列
let serialQueue = dispatch_queue_create("Serial Queue", DISPATCH_QUEUE_SERIAL)

// 提交两个 Block 任务到 serialQueue,同时关联 serialQueue 和 group 的关系
dispatch_group_async(group, serialQueue) {
print("Execute Block1 within Serial Queue")
}
dispatch_group_async(group, serialQueue) {
print("Execute Block2 within Serial Queue")
}

// 创建并发队列,并提交 Block 任务,同时关联该并发队列和 group 的关系
dispatch_group_async(group, dispatch_queue_create("Concurrent Queue", DISPATCH_QUEUE_CONCURRENT)) {
print("Execute Block within Concurrent Queue")
}

// 下面的代码只有当前面被关联到 group 的所有任务完成之后才会被触发
dispatch_group_notify(group, dispatch_queue_create("Finished")) {
print("Finished")
}

注意,关联到 group 的方法只有 dispatch_group_async 而没有 dispatch_group_sync。

但是还有另外一种方法可以让我们关联一个普通的任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建 dispatch_group_t 对象
let group = dispatch_group_create()

// 使用 dispatch_group_enter 和 dispatch_group_leave 的话,我们不需要调用
// dispatch_group_async 也能关联一个任务到 group 上
dispatch_group_enter(group)
self.executeTask {
// 执行代码

dispatch_group_leave(group)
}

// 下面的代码只有当前面被关联到 group 的所有任务完成之后才会被触发
dispatch_group_notify(group, dispatch_queue_create("Finished")) {
print("Finished")
}

使用 dispatch_group_enter 和 dispatch_group_leave 的时候,它们必须成双成对出现,否则 dispatch_group_notify 是不会被调用的。

接下来我们还要了解一下 dispatch_group_wait:

1
public func dispatch_group_wait(group: dispatch_group_t, _ timeout: dispatch_time_t) -> Int

dispatch_group_wait 可以指定一个 timeout 的参数,当 group 的任务没有在规定的时间内完成,它会返回一个非零的值,当 group 的任务能够在规定的时间内完成就返回0。同时,大家要注意这个方法会挂起当前线程,所以在主线程的时候要慎重使用该方法。

dispatch_barrier_t

我们先来试想一个场景,假如现在有多个线程要去读取一份文件的内容,同时又有其他线程想要去更新该文件的内容,那么就有可能会发生你读错文件内容的现象。这个时候我们可以把所有读写操作都放到我们之前学习的串行队列去执行,但是我们都知道同时有多个线程去读取一份文件内容是没有问题的。

使用 dispatch barrier 可以解决上面的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建操作文件的并发队列
let queue = dispatch_queue_create("File", DISPATCH_QUEUE_CONCURRENT)
dispatch_async(queue) {
// Read1
}
dispatch_async(queue) {
// Read2
}
dispatch_barrier_async(queue) {
// Write
}
dispatch_async(queue) {
// Read3
}

通过 dispatch_barrier_async 或 dispatch_barrier_sync 提交的任务会等待当前队列里正在执行的任务执行完毕才会执行,并且其他还没有执行的任务都必须等待提交到 dispatch barrier 的任务执行完毕之后才会开始执行。所以上面的代码中,当 Write 任务被提交的时候,如果当前队列中只有 Read1 在执行,那么 Write 会等待 Read1 执行完成之后才会执行,Read2 和 Read3 都必须等待 Write 执行完之后才会执行。另外,上面的代码中创建的是并发队列,因为如果是串行队列的话就没有必要用 dispatch barrier 了。

dispatch_semaphore_t

dispatch semaphore 是一个效率非常高的传统计数信号量,所以我们一般可以用这个来控制最大的并发数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建初始值为2的信号量,最大并发数量为2
let semaphore = dispatch_semaphore_create(2)
// 创建并发队列
let queue = dispatch_queue_create("Semaphore", DISPATCH_QUEUE_CONCURRENT)
// 创建100个并发任务
for index in 1...100 {
// 这个方法会进行信号量减1的操作,并且如果信号量减1之后的结果小于0的话,该方法会造成线程的挂起直
// 到该信号量进行加1操作才会恢复,所以在主线程要注意该方法的使用。
// 注意:这个方法要放在 dispatch_async 外面,否则系统依旧会创建超过2个线程同时来处理该调度队列
// 的任务
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
dispatch_async(queue) {

// 释放资源,信号量增加1
dispatch_semaphore_signal(semaphore)
}
}

其他

GCD 在 Swift3 的语法跟现在的语法不太一样了,有兴趣的可以自行去了解。在未来可能会考虑把本文章的代码都用 Swift3 的语法来重新写一下。

0%