记事

一年半前刚搬到这儿的时候,同伴们兴致勃勃地讨论着各种出游计划,大家建了个群,名字就叫“霓虹国出游计划委员会”。然而在接下来的日子里,有的人离职了,有的人刚入职,有的人生病了,渐渐地,这件事仿佛被忘记了,“出游计划委员会”也变成了日常聊天的地方。两个月前,有谁忽然提起了这件事,我才意识到时间已经过去那么久了。大家规划了下时间,又找人咨询了签证事宜,多番努力下,出游计划终于进行了下去。

初到日本第一感觉就是街道非常整洁干净,这种感觉在过去的大连也曾经能体会到一些。灰尘很少,行李箱的轮子滚了半天后,反而变得更干净了些。水分充足,路边的蕨类和苔藓都长得很好。城市的构成和运转看起来很“合理”,某种程度上,这里更能感受到“文明”的发展,至少东京是这样。我一直在提醒自己,我只能以游客的视角去探索这些城市,不懂日语,缺少有效的沟通,很多事情就很难弄明白。这个社会是怎样运作的,我看到的是否只是某种表象,这些问题还是留到之后吧。
(更多…)

不再安全的 OSSpinLock

昨天有位开发者在 Github 上给我提了一个 issue,里面指出 OSSpinLock 在新版 iOS 中已经不能再保证安全了,并提供了几个相关资料的链接。我仔细查了一下相关资料,确认了这个让人不爽的 bug。

OSSpinLock 的问题

2015-12-14 那天,swift-dev 邮件列表里有人在讨论 weak 属性的线程安全问题,其中有几位苹果工程师透露了自旋锁的 bug,对话内容大致如下:

新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: backgroundutilitydefaultuser-initiateduser-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock

具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock

苹果工程师 Greg Parker 提到,对于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但如果系统负载高时,它仍有可能将高优先级的线程阻塞数十秒之久;另一种方案是使用 handoff lock 算法,这也是 libobjc 目前正在使用的。锁的持有者会把线程 ID 保存到锁内部,锁的等待者会临时贡献出它的优先级来避免优先级反转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上目前还一切都好。

libobjc 里用的是 Mach 内核的 thread_switch() 然后传递了一个 mach thread port 来避免优先级反转,另外它还用了一个私有的参数选项,所以开发者无法自己实现这个锁。另一方面,由于二进制兼容问题,OSSpinLock 也不能有改动。

最终的结论就是,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。

OSSpinLock 的替代方案

为了找到一个替代方案,我做了一个简单的性能测试,对比了一下几种能够替代 OSSpinLock 锁的性能。测试是在 iPhone6iOS9 上跑的,代码在这里。我尝试了不同的循环次数,结果并不都一样,我猜这可能是与 CPU Cache 有关,所以这个结果只能当作一个定性分析。

lock_benchmark

可以看到除了 OSSpinLock 外,dispatch_semaphore pthread_mutex 性能是最高的。有消息称,苹果在新系统中已经优化了 pthread_mutex 的性能,所以它看上去和 OSSpinLock 差距并没有那么大了。

开源社区的反应

苹果

查看 CoreFoundation 的源码能够发现,苹果至少在 2014 年就发现了这个问题,并把 CoreFoundation 中的 spinlock 替换成了 pthread_mutex,具体变化可以查看这两个文件:CFInternal.h(855.17)CFInternal.h(1151.16)苹果自己发现问题后,并没有更新 OSSpinLock 的文档,也没有告知开发者,这有些让人失望。

Google

google/protobuf 内部的 spinlock 被全部替换为 dispatch_semaphore,详情可以看这个提交:https://github.com/google/protobuf/pull/1060。用 dispatch_semaphore 而不用 pthread_mutex 应该是出于性能考虑。

其他项目

因为 OSSpinLock 出现这种问题的几率很小,也没有引起很大的重视,我所能找到的也只有 ReactiveCocoa 在讨论这个问题。

相关链接

https://lists.swift.org/pipermail/swift-dev/Week-of-Mon-20151214/000344.html

http://mjtsai.com/blog/2015/12/16/osspinlock-is-unsafe/

http://engineering.postmates.com/Spinlocks-Considered-Harmful-On-iOS/

https://twitter.com/steipete/status/676851647042203648

搬家前

晚上回到家,发现家里停电了。打开电脑连不上网,手机流量又不多,只好写点吐槽了。

又快要到新的一年了呢, 回头看看年初那篇吐槽文章中的年度计划,大半是没实现(摊手)。MIDI App 项目停滞了,数码绘没有搞,电钢没怎么练,空闲时间大都花在写代码上了(笑)。新的一年可能会有些新的计划了,我可能需要更好的时间管理,毕竟精力有限。

来北京后,我和小伙伴们总因为这样那样的原因,每年换一套房子,今年也不例外。如果一切顺利的话,在圣诞节过后的那一周,我们就会搬进更大的新房子了。在这里最舍不得的,可能就是这个书房了吧。不知道新房子还能不能有这么大的书架。

IMG_2604_

最后,整理一下今年买到的纸质书吧:

(更多…)

iOS 保持界面流畅的技巧

这篇文章会非常详细的分析 iOS 界面构建中的各种性能问题以及对应的解决思路,同时给出一个开源的微博列表实现,通过实际的代码展示如何构建流畅的交互。

Index
演示项目
屏幕显示图像的原理
卡顿产生的原因和解决方案
CPU 资源消耗原因和解决方案
GPU 资源消耗原因和解决方案
AsyncDisplayKit
ASDK 的由来
ASDK 的资料
ASDK 的基本原理
ASDK 的图层预合成
ASDK 异步并发操作
Runloop 任务分发
微博 Demo 性能优化技巧
预排版
预渲染
异步绘制
全局并发控制
更高效的异步图片加载
其他可以改进的地方
如何评测界面的流畅度

演示项目

在开始技术讨论前,你可以先下载我写的 Demo 跑到真机上体验一下:https://github.com/ibireme/YYKit。 Demo 里包含一个微博的 Feed 列表、发布视图,还包含一个 Twitter 的 Feed 列表。为了公平起见,所有界面和交互我都从官方应用原封不动的抄了过来,数据也都是从官方应用抓取的。你也可以自己抓取数据替换掉 Demo 中的数据,方便进行对比。尽管官方应用背后的功能更多更为复杂,但不至于会带来太大的交互性能差异。

weiboweibo_composetwitter

这个 Demo 最低可以运行在 iOS 6 上,所以你可以把它跑到老设备上体验一下。在我的测试中,即使在 iPhone 4S 或者 iPad 3 上,Demo 列表在快速滑动时仍然能保持 50~60 FPS 的流畅交互,而其他诸如微博、朋友圈等应用的列表视图在滑动时已经有很严重的卡顿了。

微博的 Demo 有大约四千行代码,Twitter 的只有两千行左右代码,第三方库只用到了 YYKit,文件数量比较少,方便查看。好了,下面是正文。

(更多…)

iOS 处理图片的一些小 Tip

如何把 GIF 动图保存到相册?

iOS 的相册是支持保存 GIF 和 APNG 动图的,只是不能直接播放。用 [ALAssetsLibrary writeImageDataToSavedPhotosAlbum:metadata:completionBlock] 可以直接把 APNG、GIF 的数据写入相册。如果图省事直接用 UIImageWriteToSavedPhotosAlbum() 写相册,那么图像会被强制转码为 PNG。

将 UIImage 保存到磁盘,用什么方式最好?

目前来说,保存 UIImage 有三种方式:1.直接用 NSKeyedArchiver 把 UIImage 序列化保存,2.用 UIImagePNGRepresentation() 先把图片转为 PNG 保存,3.用 UIImageJPEGRepresentation() 把图片压缩成 JPEG 保存。

实际上,NSKeyedArchiver 是调用了 UIImagePNGRepresentation 进行序列化的,用它来保存图片是消耗最大的。苹果对 JPEG 有硬编码和硬解码,保存成 JPEG 会大大缩减编码解码时间,也能减小文件体积。所以如果图片不包含透明像素时,UIImageJPEGRepresentation(0.9) 是最佳的图片保存方式,其次是 UIImagePNGRepresentation()。

UIImage 缓存是怎么回事?

通过 imageNamed 创建 UIImage 时,系统实际上只是在 Bundle 内查找到文件名,然后把这个文件名放到 UIImage 里返回,并没有进行实际的文件读取和解码。当 UIImage 第一次显示到屏幕上时,其内部的解码方法才会被调用,同时解码结果会保存到一个全局缓存去。据我观察,在图片解码后,App 第一次退到后台和收到内存警告时,该图片的缓存才会被清空,其他情况下缓存会一直存在。

我要是用 imageWithData 能不能避免缓存呢?

不能。通过数据创建 UIImage 时,UIImage 底层是调用 ImageIO 的 CGImageSourceCreateWithData() 方法。该方法有个参数叫 ShouldCache,在 64 位的设备上,这个参数是默认开启的。这个图片也是同样在第一次显示到屏幕时才会被解码,随后解码数据被缓存到 CGImage 内部。与 imageNamed 创建的图片不同,如果这个图片被释放掉,其内部的解码数据也会被立刻释放。

怎么能避免缓存呢?

1. 手动调用 CGImageSourceCreateWithData() 来创建图片,并把 ShouldCache 和 ShouldCacheImmediately 关掉。这么做会导致每次图片显示到屏幕时,解码方法都会被调用,造成很大的 CPU 占用。
2. 把图片用 CGContextDrawImage() 绘制到画布上,然后把画布的数据取出来当作图片。这也是常见的网络图片库的做法。

我能直接取到图片解码后的数据,而不是通过画布取到吗?

1.CGImageSourceCreateWithData(data) 创建 ImageSource。
2.CGImageSourceCreateImageAtIndex(source) 创建一个未解码的 CGImage。
3.CGImageGetDataProvider(image) 获取这个图片的数据源。
4.CGDataProviderCopyData(provider) 从数据源获取直接解码的数据。
ImageIO 解码发生在最后一步,这样获得的数据是没有经过颜色类型转换的原生数据(比如灰度图像)。

如何判断一个文件的图片类型?

通过读取文件或数据的头几个字节然后和对应图片格式标准进行比对。我在这里写了一个简单的函数,能很快速的判断图片格式。

怎样像浏览器那样边下载边显示图片?

首先,图片本身有 3 种常见的编码方式:

image_baseline image_interlaced image_progressive

第一种是 baseline,即逐行扫描。默认情况下,JPEG、PNG、GIF 都是这种保存方式。
第二种是 interlaced,即隔行扫描。PNG 和 GIF 在保存时可以选择这种格式。
第三种是 progressive,即渐进式。JPEG 在保存时可以选择这种方式。
在下载图片时,首先用 CGImageSourceCreateIncremental(NULL) 创建一个空的图片源,随后在获得新数据时调用
CGImageSourceUpdateData(data, false) 来更新图片源,最后在用 CGImageSourceCreateImageAtIndex() 创建图片来显示。

你可以用 PINRemoteImage 或者我写的 YYWebImage 来实现这个效果。SDWebImage 并没有用 Incremental 方式解码,所以显示效果很差。

第 1 页,共 9 页12345...最旧 »