iOS知识小集 第7期(2016.08.31)

好久没写这个系列了,一看都快一年了,当时说好的呢?嗯,说来总是有各种借口,所以还是不说,直接开始新的一期。之前在微博上发了不少知识小集,到现在应该有50多条了,都是平时开发遇到的一些问题,或者看书,看文档,看博客,看WWDC的一些笔记,分享出来。所以在这偷个懒,做一个合集,每期把微博上的知识小集集中一下,可别吐槽。

本期主要收集以下几个小问题:

  1. UIImageView显示gif图片有两种方式
  2. Objective-C中的BOOL类型
  3. dispatch_once死锁
  4. GNU 复合语句
  5. URL转义

UIImageView显示gif图片有两种方式

UIImageView显示gif图片有两种方式。当然前提都是先将gif中的每一帧取出来放到一个个UIImage对象中,将这些对象放到一个数组中,如下代码所示。

1
2
3
4
5
6
7
8
9
10
11
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
size_t count = CGImageSourceGetCount(source);
NSMutableArray *images = [NSMutableArray array];
for (size_t i = 0; i < count; i++) {
CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
[images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen] orientation:UIImageOrientationUp]];
CGImageRelease(image);
}
CFRelease(source)

一种方式是将这些UIImage对象通过UIImage的类方法+animatedImageWithImages:duration:组合成一个UIImage对象,然后赋值给UIImageView对象的image属性。

第二种方式是将UIImage对象的数组赋值给UIImageView对象的animationImages属性,然后调用UIImageView对象的startAnimating方法来启动动画。

当然,两种方式都需要计算duration

Objective-C中的BOOL类型

Objective-C中的BOOL类型在Watch和64位iOS上的原始类型为bool,而在其它情况下是signed char。我们用@encode去看看BOOL的类型串:

1
2
@encode(BOOL) // 64位iOS系统:"B"
@encode(BOOL) // 32位iOS系统,32/64位OS X:"c"

所有这边有一个问题,下面这段代码中变量b的值在不同环境下,其结果可能是不一样的:

1
2
BOOL a = 100 & 20;
BOOL b = (a == YES);

当BOOL为bool时,b的值为1;而当BOOL为signed char时,b的值为0。所以,如果我们判断一个BOOL值是否为真时,不应该通过if(a == YES)这种方式来判断,要么直接就if (a),要么就if (a != NO)

dispatch_once死锁

在iOS开发中,我们经常会使用到单例,现在Objective-C中写单例的标配是使用dispatch_once。相信这个函数的意义大家都非常清楚了,就是希望dispatch_once参数中的block在全局只执行一次。这个基本上没什么问题。

不过,今天在工程中看到类似于下面这样的代码。在主线程中调用test()方法,会有什么结果呢?

1
2
3
4
5
6
7
8
9
void test() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
test();
});
printf("This is a test");
}

死锁。是的,死锁,线程直接卡住了。为什么呢?

我们暂停程序,可以看到程序的调用栈,如下图所示:

image

发现程序是卡在dispatch_once_f中。研究一下dispatch_once_f的实现吧,如下代码所示,会发现一些有意思的东西。

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
void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
struct _dispatch_once_waiter_s * volatile *vval =
(struct _dispatch_once_waiter_s**)val;
struct _dispatch_once_waiter_s dow = { NULL, 0 };
struct _dispatch_once_waiter_s *tail, *tmp;
_dispatch_thread_semaphore_t sema;
if (dispatch_atomic_cmpxchg(vval, NULL, &dow)) {
dispatch_atomic_acquire_barrier();
_dispatch_client_callout(ctxt, func);
dispatch_atomic_maximally_synchronizing_barrier();
//dispatch_atomic_release_barrier(); // assumed contained in above
tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);
tail = &dow;
while (tail != tmp) {
while (!tmp->dow_next) {
_dispatch_hardware_pause();
}
sema = tmp->dow_sema;
tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
_dispatch_thread_semaphore_signal(sema);
}
} else {
dow.dow_sema = _dispatch_get_thread_semaphore();
for (;;) {
tmp = *vval;
if (tmp == DISPATCH_ONCE_DONE) {
break;
}
dispatch_atomic_store_barrier();
if (dispatch_atomic_cmpxchg(vval, tmp, &dow)) {
dow.dow_next = tmp;
_dispatch_thread_semaphore_wait(dow.dow_sema);
}
}
_dispatch_put_thread_semaphore(dow.dow_sema);
}
}

简单描述一下吧。onceToken在第一次执行block之前,其值将由NULL变为指向第一个调用者的指针(&dow)。如果在block完成之前,有其它的调用者进来,则会把这些调用者放到一个waiter链表中(走else分支),直到block执行完成。waiter链中的每个调用者都会等待一个信号量(dow.dow_sema)。在block执行完成后,除了将onceToken置为DISPATCH_ONCE_DONE外,还会去遍历waiter链中的所有waiter,抛出相应的信号量,以告知waiter们调用结束。

因此上面的死锁问题就好理解了。递归调用test()时,第二次调用作为一个waiter,在等待block完成,而block的完成依赖于test()的执行完成,这就成了一个死锁。

所以应该避免在dispatch_once做递归调用,不管是直接的还是间接的。

再说回单例,个人看法是单例是个好东西,但应该在适当的场景使用。不能因为简便就滥用。抛开内存问题不说,使用不当的话,单例类迟早会变成一个垃圾场。

参考

  1. Why am I getting deadlock with dispatch_once?
  2. 滥用单例之dispatch_once死锁
  3. libdispatch

GNU 复合语句

我们在看一些第三方的代码时,可能会看到类似于下面的代码。

1
2
3
4
5
6
7
[self.view addSubview:({
UIView *view = [[UIView alloc] initWithFrame:(CGRect){CGPointZero, 100.0f, 100.0f}];
view.backgroundColor = [UIColor blueColor];
view.layer.masksToBounds = YES;
view.layer.cornerRadius = 4.0f;
view;
})];

addSubview的参数放在一个”({})”代码块中,而view的创建及属性设置都是在”({})”完成,代码块最后一句即我们要添加的子view。

这种写法沿用了GNU C的一个特性,即复合语句(compound statement)。即在”({})”代码块中,我们可以放置多个语句,这些语句可以是循环、分支、变量声明、函数调用等。而复合语句的最后一句是一个表达式,其作为整个复合语句的最终值。

在写Objective-C代码时,使用复合语句能让我们的代码变得更优雅,特别是创建并添加一堆子view时,能让我们的代码看上去更整洁。建议经常使用。

参考

  1. Statements and Declarations in Expressions

URL转义

在使用+URLWithString:-initWithString:来创建一个URL对象时,提供的参数字符串必须符合RFC 2396标准Uniform Resource Identifiers (URI): Generic Syntax。而这两个方法又是根据RFC 1738 Uniform Resource Locators (URL)和1808 Relative Uniform Resource Locators两个标准来解析字符串的。故弄玄虚一下。当然我们不需要去了解所有的细节,简单了解一下就行,可以参考一下阮大侠的这篇关于URL编码

这里要说明的就是:对于我们而言,如果用带有中文的字符串(如”https://www.baidu.com?q=北京“)去创建一个URL对象的话,返回的是一个nil。

image

我们所需要做的就是对不符合标准的字符串进行转义操作。NSString类提供了两个方法来做这种转义操作,一个是-stringByAddingPercentEscapesUsingEncoding:,不过这个方法在iOS 9.0已被废弃;现在更提倡的是用-stringByAddingPercentEncodingWithAllowedCharacters:方法,这个方法是iOS 7.0后添加的。

小结

知识是一点一点积累的,每天一两点,一段时间后,收获也会很大。知识小集的初衷就是这样。

当然,另一方面也需要系统性地去学习整理一些知识,才能把零零碎碎的东西串起来。


欢迎关注我的微信公众号:iOS知识小集,扫扫左边站点概览里的二维码就OK了。对了,还有微博:@南峰子_老驴