不再安全的 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 方式解码,所以显示效果很差。

移动端图片格式调研

图片通常是移动端流量耗费最多的部分,并且占据着重要的视觉空间。合理的图片格式选用和优化可以为你节省带宽、提升视觉效果。在这篇文章里我会分析一下目前主流和新兴的几种图片格式的特点、性能分析、参数调优,以及相关开源库的选择。

Index
几种图片格式简介
移动端图片类型的支持情况
静态图片的编码与解码
JPEG
PNG
WebP
BPG
动态图片的编码与解码
GIF
APNG
WebP
BPG
动图性能对比

(更多…)

YYCache 设计思路

cache_all_the_things

iOS 开发中总会用到各种缓存,最初我是用的一些开源的缓存库,但到总觉得缺少某些功能,或某些 API 设计的不够好用。YYCache (https://github.com/ibireme/YYCache) 是我新造的一个轮子,下面说一下这个轮子的设计思路。

内存缓存

通常一个缓存是由内存缓存和磁盘缓存组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。相对于磁盘缓存来说,内存缓存的设计要更简单些,下面是我调查的一些常见的内存缓存。

NSCache 是苹果提供的一个简单的内存缓存,它有着和 NSDictionary 类似的 API,不同点是它是线程安全的,并且不会 retain key。我在测试时发现了它的几个特点:NSCache 底层并没有用 NSDictionary 等已有的类,而是直接调用了 libcache.dylib,其中线程安全是由 pthread_mutex 完成的。另外,它的性能和 key 的相似度有关,如果有大量相似的 key (比如 "1", "2", "3", ...),NSCache 的存取性能会下降得非常厉害,大量的时间被消耗在 CFStringEqual() 上,不知这是不是 NSCache 本身设计的缺陷。

TMMemoryCacheTMCache 的内存缓存实现,最初由 Tumblr 开发,但现在已经不再维护了。TMMemoryCache 实现有很多 NSCache 并没有提供的功能,比如数量限制、总容量限制、存活时间限制、内存警告或应用退到后台时清空缓存等。TMMemoryCache 在设计时,主要目标是线程安全,它把所有读写操作都放到了同一个 concurrent queue 中,然后用 dispatch_barrier_async 来保证任务能顺序执行。它错误的用了大量异步 block 回调来实现存取功能,以至于产生了很大的性能和死锁问题。

PINMemoryCache 是 Tumblr 宣布不在维护 TMCache 后,由 Pinterest 维护和改进的一个内存缓存。它的功能和接口基本和 TMMemoryCache 一样,但修复了性能和死锁的问题。它同样也用 dispatch_semaphore 来保证线程安全,但去掉了dispatch_barrier_async,避免了线程切换带来的巨大开销,也避免了可能的死锁。

YYMemoryCache 是我开发的一个内存缓存,相对于 PINMemoryCache 来说,我去掉了异步访问的接口,尽量优化了同步访问的性能,用 OSSpinLock 来保证线程安全。另外,缓存内部用双向链表和 NSDictionary 实现了 LRU 淘汰算法,相对于上面几个算是一点进步吧。

下面的单线程的 Memory Cache 性能基准测试:

memory_cache_bench_result

可以看到 YYMemoryCache 的性能不错,仅次于 NSDictionary + OSSpinLock;
NSCache 的写入性能稍差,读取性能不错;
PINMemoryCache 的读写性能也还可以,但读取速度差于 NSCache;
TMMemoryCache 性能太差以至于图上都看不出来了。

磁盘缓存

为了设计一个比较好的磁盘缓存,我调查了大量的开源库,包括 TMDiskCache、PINDiskCache、SDWebImage、FastImageCache 等,也调查了一些闭源的实现,包括 NSURLCache、Facebook 的 FBDiskCache 等。他们的实现技术大致分为三类:基于文件读写、基于 mmap 文件内存映射、基于数据库。

TMDiskCache, PINDiskCache, SDWebImage 等缓存,都是基于文件系统的,即一个 Value 对应一个文件,通过文件读写来缓存数据。他们的实现都比较简单,性能也都相近,缺点也是同样的:不方便扩展、没有元数据、难以实现较好的淘汰算法、数据统计缓慢。

FastImageCache 采用的是 mmap 将文件映射到内存。用过 MongoDB 的人应该很熟悉 mmap 的缺陷:热数据的文件不要超过物理内存大小,不然 mmap 会导致内存交换严重降低性能;另外内存中的数据是定时 flush 到文件的,如果数据还未同步时程序挂掉,就会导致数据错误。抛开这些缺陷来说,mmap 性能非常高。

NSURLCache、FBDiskCache 都是基于 SQLite 数据库的。基于数据库的缓存可以很好的支持元数据、扩展方便、数据统计速度快,也很容易实现 LRU 或其他淘汰算法,唯一不确定的就是数据库读写的性能,为此我评测了一下 SQLite 在真机上的表现。iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。这和 SQLite 官网的描述基本一致。另外,直接从官网下载最新的 SQLite 源码编译,会比 iOS 系统自带的 sqlite3.dylib 性能要高很多。基于 SQLite 的这种表现,磁盘缓存最好是把 SQLite 和文件存储结合起来:key-value 元数据保存在 SQLite 中,而 value 数据则根据大小不同选择 SQLite 或文件存储。NSURLCache 选定的数据大小的阈值是 16K;FBDiskCache 则把所有 value 数据都保存成了文件。

我的 YYDiskCache 也是采用的 SQLite 配合文件的存储方式,在 iPhone 6 64G 上的性能基准测试结果见下图。在存取小数据 (NSNumber) 时,YYDiskCache 的性能远远高出基于文件存储的库;而较大数据的存取性能则比较接近了。但得益于 SQLite 存储的元数据,YYDiskCache 实现了 LRU 淘汰算法、更快的数据统计,更多的容量控制选项。

disk_cache_bench_result

备注:

关于锁:

OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。对于内存缓存的存取来说,它非常合适。

dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。

关于 Realm:

Realm 是一个比较新的数据库,针对移动应用所设计。它的 API 对于开发者来说非常友好,比 SQLite、CoreData 要易用很多,但相对的坑也有不少。我在测试 SQLite 性能时,也尝试对它做了些简单的评测。我从 Realm 官网下载了它提供的 benchmark 项目,更新 SQLite 到官网最新的版本,并启用了 SQLite 的 sqlite3_stmt 缓存。我的评测结果显示 Realm 在写入性能上差于 SQLite,读取小数据时也差 SQLite 不少,读取较大数据时 Realm 有很大的优势。当然这只是我个人的评测,可能并不能反映真实项目中具体的使用情况。我想看看它的实现原理,但发现 Realm 的核心 realm-core 是闭源的(评论里 Realm 员工提到目前有在 Apache 2.0 授权下的开源计划),能知道的是 Realm 应该用 了 mmap 把文件映射到内存,所以才在较大数据读取时获得很高的性能。另外我注意到添加了 Realm 的 App 会在启动时向某几个 IP 发送数据,评论中有 Realm 员工反馈这是发送匿名统计数据,并且只针对模拟器和 Debug 模式。这部分代码目前是开源的,并且可以通过环境变量 REALM_DISABLE_ANALYTICS 来关闭,如果有使用 Realm 的可以注意一下。

iOS JSON 模型转换库评测

iOS 开发中总会用到各种 JSON 模型转换库,这篇文章将会对常见的几个开源库进行一下评测。评测的内容主要集中在性能、功能、容错性这几个方面。

评测的对象:

Manually
手动进行 JSON/Model 转换,不用任何开源库,可以进行高效、自由的转换,但手写代码非常繁琐,而且容易出错。

YYModel
我造的一个新轮子,比较轻量(算上 .h 只有 5 个文件),支持自动的 JSON/Model 转换,支持定义映射过程。API 简洁,功能也比较简单。

FastEasyMapping
Yalantis 开发的一个 JSON 模型转换库,可以自定义详细的 Model 映射过程,支持 CoreData。使用者较少。

JSONModel
一个 JSON 模型转换库,有着比较简洁的接口。Model 需要继承自 JSONModel。

Mantle
Github 官方团队开发的 JSON 模型转换库,Model 需要继承自 MTLModel。功能丰富,文档完善,使用广泛。

MJExtension
国内开发者"小码哥"开发的 JSON 模型库,号称性能超过 JSONModel 和 Mantle,使用简单无侵入。国内有大量使用者。

(更多…)

最近一段时间的工作整理

正式转为全职 iOS 工程师已经有一年多了,这一年里工作并不忙,使得我有更多的时间和精力来研究些有意思的东西。最近整理了下这一年攒下来的代码,拆分成了几个库,在接下来的一段时间内会陆续开源:

YYModel 类似 Mantle/JSONModel 的工具,性能比 Mantle 高一个数量级,有更好的容错性,更简洁的 API。
YYCache 类似 TMCache 那样的工具,有着更好的性能,支持 LRU,磁盘缓存支持 SQLite。
YYImage iOS图像库,支持高性能的 APNG/WebP/GIF 动图播放、编码和解码,支持帧动画等。
YYWebImage 类似 SDWebImage 的工具,基于 YYImage 和 YYCache,有更好的性能、更丰富的功能。
YYText UILabel 和 UITextView 的开源实现,支持异步排版渲染、图文混排、更多文字特效/点击效果、动画/表情输入、竖排版等。

YYKeyboardManager 从 YYText 分离出来的一个键盘监听工具,能实时监听和获取键盘视图、位置、动画。
YYDispatchQueuePool 从 YYText 分离出来的一个很简单的队列管理工具,用于管理全局并发任务。
YYAsyncLayer 从 YYText 分离出来的一个很简单的 CALayer 的子类,用于进行异步绘制和显示。
YYCategories Category 类型的工具库。

YYKit 上面所有工具的打包工具集,全部工具都兼容 iOS6~9。
YYKitDemo YYKit 的功能/性能演示,实现有 Twitter 和 Weibo 的 Feed 列表、发布视图,有着和官方 App 完全一致的 UI 和更流畅的交互体验。

每个库都会配有几篇博客来介绍相关技术、性能评测,这也算是我最近一年工作的总结吧。

深入理解RunLoop

RunLoop 是 iOS 和 OSX 开发中非常基础的一个概念,这篇文章将从 CFRunLoop 的源码入手,介绍 RunLoop 的概念以及底层实现原理。之后会介绍一下在 iOS 中,苹果是如何利用 RunLoop 实现自动释放池、延迟回调、触摸事件、屏幕刷新等功能的。

Index
RunLoop 的概念
RunLoop 与线程的关系
RunLoop 对外的接口
RunLoop 的 Mode
RunLoop 的内部逻辑
RunLoop 的底层实现
苹果用 RunLoop 实现的功能
AutoreleasePool
事件响应
手势识别
界面更新
定时器
PerformSelecter
关于GCD
关于网络请求
RunLoop 的实际应用举例
AFNetworking
AsyncDisplayKit

(更多…)

2015年了呢...

又是新的一年了呢。。偶尔也写写年终总结之类的东西,然后顺便稍微展望一下未来吧。

算起来,来到帝都工作已经快三年了呢;相对于刚毕业前后那段紧张绝望迷茫的日子来说,刚过去的这一年已经算得上不错了。这一年里换了一个新工作,从 Java 转回了 iOS,上班从酒仙桥挪到了中关村,工资涨了点儿,活儿轻松了点儿。和小伙伴租的房子,一年一搬。新的房子有个面朝阳台的书房,午后的阳光很棒;书房有一个很大的书架,能放下我在这边买的所有的书。看上去,这一年过得还不错。

但是到底是怎样的呢?我也不知道。能说上话的人越来越少,从前的朋友和同学也都各奔东西,不再联系。朋友们有的去留学了,有的留学又回来了,有的去创业了,有的仍留在出生的城市工作。大家都有自己的圈子,即使能有空聚一聚,或者在网上聊一聊,也都没什么话题了。我自己也不太擅长与人交流吧,所以总是习惯了一个人。用村上的话来说:"哪里会有人喜欢孤独,不过是不喜欢失望罢了"。

2015年来了,我要做些什么呢?如果可以的话,腾出更多个人时间来做些有意思的事儿。再仔细学一下 dsp 相关的东西,希望能做出类似 GarageBand 那样的 App 吧,我想要在 iPad 上编辑和录制 midi。在屋里腾出块空间来放下我的电钢,试试看能不能捡起一些过去的技能。换一块儿好点的数位板,认真学一下数码绘,希望能创造点东西,而不是总在复制。然后,一定要多读书!想得太多读书少这肯定是不行的。我知道现在如果立下一堆计划怕是用处不大(笑),但是不管怎么样,希望未来能更好一些吧。

最后还是想说一下我的第一份工作。08年我第一次注册人人网,加上了很多大学好友,玩得不亦乐乎。11年底,我拿到人人的 Offer,到人人网实习,做一些移动方面的东西。12年毕业后,便正式加入了人人网,只是转做起了 Java 后台,先后负责开发开放平台、应用中心、支付等一些项目。虽然当时由移动开发转做 Java 有点不情愿,但现在看来这让我增长了很多技能,眼界也开阔了很多。在这期间,部门的头头(给我面试机会、招我进去,算是有知遇之恩吧)走了,一同去的几个同学也先后走了,部门同事也前前后后走了很多,最后我也走了。。我在人人网玩了六年多,工作了两年多,到现在每天新鲜事儿都刷不出来几条了,多少有点感伤,稍微祝福一下人人吧。。

第 1 页,共 5 页12345