好久没写这个系列了,一看都快一年了,当时说好的呢?嗯,说来总是有各种借口,所以还是不说,直接开始新的一期。之前在微博上发了不少知识小集,到现在应该有50多条了,都是平时开发遇到的一些问题,或者看书,看文档,看博客,看WWDC的一些笔记,分享出来。所以在这偷个懒,做一个合集,每期把微博上的知识小集集中一下,可别吐槽。
本期主要收集以下几个小问题:
- UIImageView显示gif图片有两种方式
- Objective-C中的BOOL类型
- dispatch_once死锁
- GNU 复合语句
- URL转义
UIImageView显示gif图片有两种方式
UIImageView显示gif图片有两种方式。当然前提都是先将gif中的每一帧取出来放到一个个UIImage对象中,将这些对象放到一个数组中,如下代码所示。
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的类型串:
@encode(BOOL) // 64位iOS系统:"B"
@encode(BOOL) // 32位iOS系统,32/64位OS X:"c"
所有这边有一个问题,下面这段代码中变量b的值在不同环境下,其结果可能是不一样的:
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()
方法,会有什么结果呢?
void test() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
test();
});
printf("This is a test");
}
死锁。是的,死锁,线程直接卡住了。为什么呢?
我们暂停程序,可以看到程序的调用栈,如下图所示:
发现程序是卡在dispatch_once_f
中。研究一下dispatch_once_f
的实现吧,如下代码所示,会发现一些有意思的东西。
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
做递归调用,不管是直接的还是间接的。
再说回单例,个人看法是单例是个好东西,但应该在适当的场景使用。不能因为简便就滥用。抛开内存问题不说,使用不当的话,单例类迟早会变成一个垃圾场。
参考
- Why am I getting deadlock with dispatch_once?
- 滥用单例之dispatch_once死锁
- libdispatch
GNU 复合语句
我们在看一些第三方的代码时,可能会看到类似于下面的代码。
[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时,能让我们的代码看上去更整洁。建议经常使用。
参考
- 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。
我们所需要做的就是对不符合标准的字符串进行转义操作。NSString
类提供了两个方法来做这种转义操作,一个是-stringByAddingPercentEscapesUsingEncoding:
,不过这个方法在iOS 9.0已被废弃;现在更提倡的是用-stringByAddingPercentEncodingWithAllowedCharacters:
方法,这个方法是iOS 7.0后添加的。
小结
知识是一点一点积累的,每天一两点,一段时间后,收获也会很大。知识小集的初衷就是这样。
当然,另一方面也需要系统性地去学习整理一些知识,才能把零零碎碎的东西串起来。
南峰子技术博客