0%

转载自YY大神

演示项目

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

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

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

屏幕显示图像的原理

ios_screen_scan

首先从过去的 CRT 显示器原理说起。CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。尽管现在的设备大都是液晶显示屏了,但原理仍然没有变。

ios_screen_display

通常来说,计算机系统中 CPU、GPU、显示器是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

在最简单的情况下,帧缓冲区只有一个,这时帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,显示系统通常会引入两个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。

双缓冲虽然能解决效率问题,但会引入一个新的问题。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,如下图:

ios_vsync_off

为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

那么目前主流的移动设备是什么情况呢?从网上查到的资料可以知道,iOS 设备会始终使用双缓存,并开启垂直同步。而安卓设备直到 4.1 版本,Google 才开始引入这种机制,目前安卓系统是三缓存+垂直同步。

卡顿产生的原因和解决方案

ios_frame_drop

在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。

CPU 资源消耗原因和解决方案

对象创建

对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如 CALayer 比 UIView 要轻量许多,那么不需要响应触摸事件的控件,用 CALayer 显示会更加合适。如果对象不涉及 UI 操作,则尽量放到后台线程去创建,但可惜的是包含有 CALayer 的控件,都只能在主线程创建和操作。通过 Storyboard 创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多,在性能敏感的界面里,Storyboard 并不是一个好的技术选择。

尽量推迟对象创建的时间,并把对象的创建分散到多个任务中去。尽管这实现起来比较麻烦,并且带来的优势并不多,但如果有能力做,还是要尽量尝试一下。如果对象可以复用,并且复用的代价比释放、创建新对象要小,那么这类对象应当尽量放到一个缓存池里复用。

对象调整

对象的调整也经常是消耗 CPU 资源的地方。这里特别说一下 CALayer:CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。

当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。

对象销毁

对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。

1
2
3
4
5
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});

布局计算

视图布局的计算是 App 中最为常见的消耗 CPU 资源的地方。如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了。

不论通过何种技术对视图进行布局,其最终都会落到对 UIView.frame/bounds/center 等属性的调整上。上面也说过,对这些属性的调整非常消耗资源,所以尽量提前计算好布局,在需要时一次性调整好对应属性,而不要多次、频繁的计算和调整这些属性。

Autolayout

Autolayout 是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是 Autolayout 对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout 带来的 CPU 消耗会呈指数级上升。具体数据可以看这个文章:http://pilky.me/36/。 如果你不想手动调整 frame 等属性,你可以用一些工具方法替代(比如常见的 left/right/top/bottom/width/height 快捷属性),或者使用 ComponentKit、AsyncDisplayKit 等框架。

文本计算

如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel 内部的实现方式:用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高,用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。

如果你用 CoreText 绘制文本,那就可以先生成 CoreText 排版对象,然后自己计算了,并且 CoreText 对象还能保留以供稍后绘制使用。

文本渲染

屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabel、UITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。

图片的解码

当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。

图像的绘制

图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:] 里面了。由于 CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。一个简单异步绘制的过程大致如下(实际情况会比这个复杂得多,但原理基本一致):

1
2
3
4
5
6
7
8
9
10
11
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}

GPU 资源消耗原因和解决方案

相对于 CPU 来说,GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。

纹理的渲染

所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture。不论是提交到显存的过程,还是 GPU 调整和渲染 Texture 的过程,都要消耗不少 GPU 资源。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。

当图片过大,超过 GPU 的最大纹理尺寸时,图片需要先由 CPU 进行预处理,这对 CPU 和 GPU 都会带来额外的资源消耗。目前来说,iPhone 4S 以上机型,纹理尺寸上限都是 4096x4096,更详细的资料可以看这里:iosres.com。所以,尽量不要让图片和视图的大小超过这个值。

视图的混合 (Composing)

当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。

图形的生成。

CALayer 的 border、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize 属性,但这会把原本离屏渲染的操作转嫁到 CPU 上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。

AsyncDisplayKit

AsyncDisplayKit 是 Facebook 开源的一个用于保持 iOS 界面流畅的库,我从中学到了很多东西,所以下面我会花较大的篇幅来对其进行介绍和分析。

ASDK 的由来

scott_goodson

ASDK 的作者是 Scott Goodson (Linkedin),
他曾经在苹果工作,负责 iOS 的一些内置应用的开发,比如股票、计算器、地图、钟表、设置、Safari 等,当然他也参与了 UIKit framework 的开发。后来他加入 Facebook 后,负责 Paper 的开发,创建并开源了 AsyncDisplayKit。目前他在 Pinterest 和 Instagram 负责 iOS 开发和用户体验的提升等工作。

asdk_history

ASDK 自 2014 年 6 月开源,10 月发布 1.0 版。目前 ASDK 即将要发布 2.0 版。
V2.0 增加了更多布局相关的代码,ComponentKit 团队为此贡献很多。
现在 Github 的 master 分支上的版本是 V1.9.1,已经包含了 V2.0 的全部内容。

ASDK 的资料

想要了解 ASDK 的原理和细节,最好从下面几个视频开始:
2014.10.15 NSLondon - Scott Goodson - Behind AsyncDisplayKit
2015.03.02 MCE 2015 - Scott Goodson - Effortless Responsiveness with AsyncDisplayKit
2015.10.25 AsyncDisplayKit 2.0: Intelligent User Interfaces - NSSpain 2015
前两个视频内容大同小异,都是介绍 ASDK 的基本原理,附带介绍 POP 等其他项目。
后一个视频增加了 ASDK 2.0 的新特性的介绍。

除此之外,还可以到 Github Issues 里看一下 ASDK 相关的讨论,下面是几个比较重要的内容:
关于 Runloop Dispatch
关于 ComponentKit 和 ASDK 的区别
为什么不支持 Storyboard 和 Autolayout
如何评测界面的流畅度

之后,还可以到 Google Groups 来查看和讨论更多内容:
https://groups.google.com/forum/#!forum/asyncdisplaykit

ASDK 的基本原理

asdk_design

ASDK 认为,阻塞主线程的任务,主要分为上面这三大类。文本和布局的计算、渲染、解码、绘制都可以通过各种方式异步执行,但 UIKit 和 Core Animation 相关操作必需在主线程进行。ASDK 的目标,就是尽量把这些任务从主线程挪走,而挪不走的,就尽量优化性能。

为了达成这一目标,ASDK 尝试对 UIKit 组件进行封装:

asdk_layer_backed_view

这是常见的 UIView 和 CALayer 的关系:View 持有 Layer 用于显示,View 中大部分显示属性实际是从 Layer 映射而来;Layer 的 delegate 在这里是 View,当其属性改变、动画产生时,View 能够得到通知。UIView 和 CALayer 不是线程安全的,并且只能在主线程创建、访问和销毁。

asdk_view_backed_node

ASDK 为此创建了 ASDisplayNode 类,包装了常见的视图属性(比如 frame/bounds/alpha/transform/backgroundColor/superNode/subNodes 等),然后它用 UIView->CALayer 相同的方式,实现了 ASNode->UIView 这样一个关系。

asdk_layer_backed_node

当不需要响应触摸事件时,ASDisplayNode 可以被设置为 layer backed,即 ASDisplayNode 充当了原来 UIView 的功能,节省了更多资源。

与 UIView 和 CALayer 不同,ASDisplayNode 是线程安全的,它可以在后台线程创建和修改。Node 刚创建时,并不会在内部新建 UIView 和 CALayer,直到第一次在主线程访问 view 或 layer 属性时,它才会在内部生成对应的对象。当它的属性(比如frame/transform)改变后,它并不会立刻同步到其持有的 view 或 layer 去,而是把被改变的属性保存到内部的一个中间变量,稍后在需要时,再通过某个机制一次性设置到内部的 view 或 layer。

通过模拟和封装 UIView/CALayer,开发者可以把代码中的 UIView 替换为 ASNode,很大的降低了开发和学习成本,同时能获得 ASDK 底层大量的性能优化。为了方便使用, ASDK 把大量常用控件都封装成了 ASNode 的子类,比如 Button、Control、Cell、Image、ImageView、Text、TableView、CollectionView 等。利用这些控件,开发者可以尽量避免直接使用 UIKit 相关控件,以获得更完整的性能提升。

ASDK 的图层预合成

asdk_comoose_1 asdk_compose_2

有时一个 layer 会包含很多 sub-layer,而这些 sub-layer 并不需要响应触摸事件,也不需要进行动画和位置调整。ASDK 为此实现了一个被称为 pre-composing 的技术,可以把这些 sub-layer 合成渲染为一张图片。开发时,ASNode 已经替代了 UIView 和 CALayer;直接使用各种 Node 控件并设置为 layer backed 后,ASNode 甚至可以通过预合成来避免创建内部的 UIView 和 CALayer。

通过这种方式,把一个大的层级,通过一个大的绘制方法绘制到一张图上,性能会获得很大提升。CPU 避免了创建 UIKit 对象的资源消耗,GPU 避免了多张 texture 合成和渲染的消耗,更少的 bitmap 也意味着更少的内存占用。

ASDK 异步并发操作

asdk_a9_chip

自 iPhone 4S 起,iDevice 已经都是双核 CPU 了,现在的 iPad 甚至已经更新到 3 核了。充分利用多核的优势、并发执行任务对保持界面流畅有很大作用。ASDK 把布局计算、文本排版、图片/文本/图形渲染等操作都封装成较小的任务,并利用 GCD 异步并发执行。如果开发者使用了 ASNode 相关的控件,那么这些并发操作会自动在后台进行,无需进行过多配置。

Runloop 任务分发

Runloop work distribution 是 ASDK 比较核心的一个技术,ASDK 的介绍视频和文档中都没有详细展开介绍,所以这里我会多做一些分析。如果你对 Runloop 还不太了解,可以看一下我之前的文章 深入理解RunLoop,里面对 ASDK 也有所提及。

ios_vsync_runloop

iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。

ASDK 在此处模拟了 Core Animation 的这个机制:所有针对 ASNode 的修改和提交,总有些任务是必需放入主线程执行的。当出现这种任务时,ASNode 会把任务用 ASAsyncTransaction(Group) 封装并提交到一个全局的容器去。ASDK 也在 RunLoop 中注册了一个 Observer,监视的事件和 CA 一样,但优先级比 CA 要低。当 RunLoop 进入休眠前、CA 处理完事件后,ASDK 就会执行该 loop 内提交的所有任务。具体代码见这个文件:ASAsyncTransactionGroup

通过这种机制,ASDK 可以在合适的机会把异步、并发的操作同步到主线程去,并且能获得不错的性能。

其他

ASDK 中还有封装很多高级的功能,比如滑动列表的预加载、V2.0添加的新的布局模式等。ASDK 是一个很庞大的库,它本身并不推荐你把整个 App 全部都改为 ASDK 驱动,把最需要提升交互性能的地方用 ASDK 进行优化就足够了。

微博 Demo 性能优化技巧

我为了演示 YYKit 的功能,实现了微博和 Twitter 的 Demo,并为它们做了不少性能优化,下面就是优化时用到的一些技巧。

预排版

当获取到 API JSON 数据后,我会把每条 Cell 需要的数据都在后台线程计算并封装为一个布局对象 CellLayout。CellLayout 包含所有文本的 CoreText 排版结果、Cell 内部每个控件的高度、Cell 的整体高度。每个 CellLayout 的内存占用并不多,所以当生成后,可以全部缓存到内存,以供稍后使用。这样,TableView 在请求各个高度函数时,不会消耗任何多余计算量;当把 CellLayout 设置到 Cell 内部时,Cell 内部也不用再计算布局了。

对于通常的 TableView 来说,提前在后台计算好布局结果是非常重要的一个性能优化点。为了达到最高性能,你可能需要牺牲一些开发速度,不要用 Autolayout 等技术,少用 UILabel 等文本控件。但如果你对性能的要求并不那么高,可以尝试用 TableView 的预估高度的功能,并把每个 Cell 高度缓存下来。这里有个来自百度知道团队的开源项目可以很方便的帮你实现这一点:FDTemplateLayoutCell

预渲染

微博的头像在某次改版中换成了圆形,所以我也跟进了一下。当头像下载下来后,我会在后台线程将头像预先渲染为圆形并单独保存到一个 ImageCache 中去。

对于 TableView 来说,Cell 内容的离屏渲染会带来较大的 GPU 消耗。在 Twitter Demo 中,我为了图省事儿用到了不少 layer 的圆角属性,你可以在低性能的设备(比如 iPad 3)上快速滑动一下这个列表,能感受到虽然列表并没有较大的卡顿,但是整体的平均帧数降了下来。用 Instument 查看时能够看到 GPU 已经满负荷运转,而 CPU 却比较清闲。为了避免离屏渲染,你应当尽量避免使用 layer 的 border、corner、shadow、mask 等技术,而尽量在后台线程预先绘制好对应内容。

异步绘制

我只在显示文本的控件上用到了异步绘制的功能,但效果很不错。我参考 ASDK 的原理,实现了一个简单的异步绘制控件。这块代码我单独提取出来,放到了这里:YYAsyncLayer。YYAsyncLayer 是 CALayer 的子类,当它需要显示内容(比如调用了 [layer setNeedDisplay])时,它会向 delegate,也就是 UIView 请求一个异步绘制的任务。在异步绘制时,Layer 会传递一个 BOOL(^isCancelled)() 这样的 block,绘制代码可以随时调用该 block 判断绘制任务是否已经被取消。

当 TableView 快速滑动时,会有大量异步绘制任务提交到后台线程去执行。但是有时滑动速度过快时,绘制任务还没有完成就可能已经被取消了。如果这时仍然继续绘制,就会造成大量的 CPU 资源浪费,甚至阻塞线程并造成后续的绘制任务迟迟无法完成。我的做法是尽量快速、提前判断当前绘制任务是否已经被取消;在绘制每一行文本前,我都会调用 isCancelled() 来进行判断,保证被取消的任务能及时退出,不至于影响后续操作。

目前有些第三方微博客户端(比如 VVebo、墨客等),使用了一种方式来避免高速滑动时 Cell 的绘制过程,相关实现见这个项目:VVeboTableViewDemo。它的原理是,当滑动时,松开手指后,立刻计算出滑动停止时 Cell 的位置,并预先绘制那个位置附近的几个 Cell,而忽略当前滑动中的 Cell。这个方法比较有技巧性,并且对于滑动性能来说提升也很大,唯一的缺点就是快速滑动中会出现大量空白内容。如果你不想实现比较麻烦的异步绘制但又想保证滑动的流畅性,这个技巧是个不错的选择。

全局并发控制

当我用 concurrent queue 来执行大量绘制任务时,偶尔会遇到这种问题:

ios_dispatch_blocked_1ios_dispatch_blocked_2

大量的任务提交到后台队列时,某些任务会因为某些原因(此处是 CGFont 锁)被锁住导致线程休眠,或者被阻塞,concurrent queue 随后会创建新的线程来执行其他任务。当这种情况变多时,或者 App 中使用了大量 concurrent queue 来执行较多任务时,App 在同一时刻就会存在几十个线程同时运行、创建、销毁。CPU 是用时间片轮转来实现线程并发的,尽管 concurrent queue 能控制线程的优先级,但当大量线程同时创建运行销毁时,这些操作仍然会挤占掉主线程的 CPU 资源。ASDK 有个 Feed 列表的 Demo:SocialAppLayout,当列表内 Cell 过多,并且非常快速的滑动时,界面仍然会出现少量卡顿,我谨慎的猜测可能与这个问题有关。

使用 concurrent queue 时不可避免会遇到这种问题,但使用 serial queue 又不能充分利用多核 CPU 的资源。我写了一个简单的工具 YYDispatchQueuePool,为不同优先级创建和 CPU 数量相同的 serial queue,每次从 pool 中获取 queue 时,会轮询返回其中一个 queue。我把 App 内所有异步操作,包括图像解码、对象释放、异步绘制等,都按优先级不同放入了全局的 serial queue 中执行,这样尽量避免了过多线程导致的性能问题。

更高效的异步图片加载

SDWebImage 在这个 Demo 里仍然会产生少量性能问题,并且有些地方不能满足我的需求,所以我自己实现了一个性能更高的图片加载库。在显示简单的单张图片时,利用 UIView.layer.contents 就足够了,没必要使用 UIImageView 带来额外的资源消耗,为此我在 CALayer 上添加了 setImageWithURL 等方法。除此之外,我还把图片解码等操作通过 YYDispatchQueuePool 进行管理,控制了 App 总线程数量。

其他可以改进的地方

上面这些优化做完后,微博 Demo 已经非常流畅了,但在我的设想中,仍然有一些进一步优化的技巧,但限于时间和精力我并没有实现,下面简单列一下:

列表中有不少视觉元素并不需要触摸事件,这些元素可以用 ASDK 的图层合成技术预先绘制为一张图。

再进一步减少每个 Cell 内图层的数量,用 CALayer 替换掉 UIView。

目前每个 Cell 的类型都是相同的,但显示的内容却各部一样,比如有的 Cell 有图片,有的 Cell 里是卡片。把 Cell 按类型划分,进一步减少 Cell 内不必要的视图对象和操作,应该能有一些效果。

把需要放到主线程执行的任务划分为足够小的块,并通过 Runloop 来进行调度,在每个 Loop 里判断下一次 VSync 的时间,并在下次 VSync 到来前,把当前未执行完的任务延迟到下一个机会去。这个只是我的一个设想,并不一定能实现或起作用。

如何评测界面的流畅度

最后还是要提一下,“过早的优化是万恶之源”,在需求未定,性能问题不明显时,没必要尝试做优化,而要尽量正确的实现功能。做性能优化时,也最好是走修改代码 -> Profile -> 修改代码这样一个流程,优先解决最值得优化的地方。

如果你需要一个明确的 FPS 指示器,可以尝试一下 KMCGeigerCounter。对于 CPU 的卡顿,它可以通过内置的 CADisplayLink 检测出来;对于 GPU 带来的卡顿,它用了一个 1x1 的 SKView 来进行监视。这个项目有两个小问题:SKView 虽然能监视到 GPU 的卡顿,但引入 SKView 本身就会对 CPU/GPU 带来额外的一点的资源消耗;这个项目在 iOS 9 下有一些兼容问题,需要稍作调整。

我自己也写了个简单的 FPS 指示器:FPSLabel 只有几十行代码,仅用到了 CADisplayLink 来监视 CPU 的卡顿问题。虽然不如上面这个工具完善,但日常使用没有太大问题。

最后,用 Instuments 的 GPU Driver 预设,能够实时查看到 CPU 和 GPU 的资源消耗。在这个预设内,你能查看到几乎所有与显示有关的数据,比如 Texture 数量、CA 提交的频率、GPU 消耗等,在定位界面卡顿的问题时,这是最好的工具。

iOS开发中如何检测当前运行商类型(移动、联通、电信)

OS开发中,有时需要检测设备运营商类型,如移动、联通或者电信,本文以检测联通为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (BOOL)checkIsUnicom
{
CTTelephonyNetworkInfo *info = [[CTTelephonyNetworkInfo alloc] init];
CTCarrier *carrier = info.subscriberCellularProvider;
NSString *carrierName = carrier.carrierName;
NSString *mobileCountryCode = carrier.mobileCountryCode;
NSString *mobileNetworkCode = carrier.mobileNetworkCode;
[info release];
if (!mobileNetworkCode) {
return NO;
}
if ([mobileCountryCode intValue]==460) { //国内
return [carrierName rangeOfString:@"联通"].length>0 ||
[mobileNetworkCode isEqualToString:@"01"] ||
[mobileNetworkCode isEqualToString:@"06"];
}
return [self statusBarCheckIsUnicom];
}

运行商对应的NetworkCode

正常情况下,以上代码可满足正常需求,但是对于美版或者日版卡贴iPhone,检测到的CTCarrier并非sim卡信息,此时就需要通过StatusBar实时检测当前网络运行商

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (BOOL)statusBarCheckIsUnicom
{
NSArray *subviews = [[[[UIApplication sharedApplication] valueForKey:@"statusBar"]
valueForKey:@"foregroundView"] subviews];
UIView *serviceView = nil;
Class serviceClass = NSClassFromString([NSString stringWithFormat:@"UIStat%@Serv%@%@",
@"usBar", @"ice", @"ItemView"]);
for (UIView *subview in subviews) {
if([subview isKindOfClass:[serviceClass class]]) {
serviceView = subview;
break;
}
}
if (serviceView) {
NSString *carrierName = [serviceView valueForKey:[@"service"
stringByAppendingString:@"String"]];
return [carrierName rangeOfString:@"联通"].length>0;
} else {
return NO;
}
}

iOS9 进行第三方跳转

需要在plist 加入:

<key>LSApplicationQueriesSchemes</key>
    <array>
    <string>wechat</string>
    <string>local</string>
    <string>weixin</string>
    <string>sinaweibohd</string>
    <string>sinaweibo</string>
    <string>sinaweibosso</string>
    <string>weibosdk</string>
    <string>alisdkdemo</string>
    <string>weibosdk2.5</string>
    <string>mqqapi</string>
    <string>mqqbrowser</string>
    <string>mqq</string>
    <string>mqqOpensdkSSoLogin</string>
    <string>mqqconnect</string>
    <string>mqqopensdkdataline</string>
    <string>mqqopensdkgrouptribeshare</string>
    <string>mqqopensdkfriend</string>
    <string>mqqopensdkapi</string>
    <string>mqqopensdkapiV2</string>
    <string>mqqopensdkapiV3</string>
    <string>mqzoneopensdk</string>
    <string>wtloginmqq</string>
    <string>wtloginmqq2</string>
    <string>mqqwpa</string>
    <string>safepay</string>
    <string>mqzone</string>
    <string>mqqapiwallet</string>
    <string>mqzonev2</string>
    <string>mqzoneshare</string>
    <string>wtloginqzone</string>
    <string>mqzonewx</string>
    <string>mqzoneopensdkapiV2</string>
    <string>mqzoneopensdkapi19</string>
    <string>mqzoneopensdkapi</string>
    <string>mqzoneopensdk</string>
    <string>renrenios</string>
    <string>renrenapi</string>
    <string>renren</string>
    <string>renreniphone</string>
    <string>yixin</string>
    <string>instagram</string>
    <string>whatsapp</string>
    <string>line</string>
    <string>fbapi</string>
    <string>fb-messenger-api</string>
    <string>fbauth2</string>
    <string>fbshareextension</string>
    <string>alipay</string>
    <string>cydia</string>
    <string>safepay</string>
    </array>

判断设备的型号

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
#import <sys/utsname.h>

//获得设备型号
+ (NSString *)getCurrentDeviceModel
{
struct utsname systemInfo;
uname(&systemInfo);
NSString *platform = [NSString stringWithCString:systemInfo.machine
encoding:NSUTF8StringEncoding];


if ([platform isEqualToString:@"iPhone1,1"]) return @"iPhone 2G (A1203)";
if ([platform isEqualToString:@"iPhone1,2"]) return @"iPhone 3G (A1241/A1324)";
if ([platform isEqualToString:@"iPhone2,1"]) return @"iPhone 3GS (A1303/A1325)";
if ([platform isEqualToString:@"iPhone3,1"]) return @"iPhone 4 (A1332)";
if ([platform isEqualToString:@"iPhone3,2"]) return @"iPhone 4 (A1332)";
if ([platform isEqualToString:@"iPhone3,3"]) return @"iPhone 4 (A1349)";
if ([platform isEqualToString:@"iPhone4,1"]) return @"iPhone 4S (A1387/A1431)";
if ([platform isEqualToString:@"iPhone5,1"]) return @"iPhone 5 (A1428)";
if ([platform isEqualToString:@"iPhone5,2"]) return @"iPhone 5 (A1429/A1442)";
if ([platform isEqualToString:@"iPhone5,3"]) return @"iPhone 5c (A1456/A1532)";
if ([platform isEqualToString:@"iPhone5,4"]) return @"iPhone 5c (A1507/A1516/A1526/A1529)";
if ([platform isEqualToString:@"iPhone6,1"]) return @"iPhone 5s (A1453/A1533)";
if ([platform isEqualToString:@"iPhone6,2"]) return @"iPhone 5s (A1457/A1518/A1528/A1530)";
if ([platform isEqualToString:@"iPhone7,1"]) return @"iPhone 6 Plus (A1522/A1524)";
if ([platform isEqualToString:@"iPhone7,2"]) return @"iPhone 6 (A1549/A1586)";
if ([platform isEqualToString:@"iPhone8,1"]) return @"iPhone 6s (A1633/A1688/A1691/A1700)";
if ([platform isEqualToString:@"iPhone8,2"]) return @"iPhone 6s Plus (A1634/A1687/A1690/A1699)";

if ([platform isEqualToString:@"iPod1,1"]) return @"iPod Touch 1G (A1213)";
if ([platform isEqualToString:@"iPod2,1"]) return @"iPod Touch 2G (A1288)";
if ([platform isEqualToString:@"iPod3,1"]) return @"iPod Touch 3G (A1318)";
if ([platform isEqualToString:@"iPod4,1"]) return @"iPod Touch 4G (A1367)";
if ([platform isEqualToString:@"iPod5,1"]) return @"iPod Touch 5G (A1421/A1509)";

if ([platform isEqualToString:@"iPad1,1"]) return @"iPad 1G (A1219/A1337)";

if ([platform isEqualToString:@"iPad2,1"]) return @"iPad 2 (A1395)";
if ([platform isEqualToString:@"iPad2,2"]) return @"iPad 2 (A1396)";
if ([platform isEqualToString:@"iPad2,3"]) return @"iPad 2 (A1397)";
if ([platform isEqualToString:@"iPad2,4"]) return @"iPad 2 (A1395+New Chip)";
if ([platform isEqualToString:@"iPad2,5"]) return @"iPad Mini 1G (A1432)";
if ([platform isEqualToString:@"iPad2,6"]) return @"iPad Mini 1G (A1454)";
if ([platform isEqualToString:@"iPad2,7"]) return @"iPad Mini 1G (A1455)";

if ([platform isEqualToString:@"iPad3,1"]) return @"iPad 3 (A1416)";
if ([platform isEqualToString:@"iPad3,2"]) return @"iPad 3 (A1403)";
if ([platform isEqualToString:@"iPad3,3"]) return @"iPad 3 (A1430)";
if ([platform isEqualToString:@"iPad3,4"]) return @"iPad 4 (A1458)";
if ([platform isEqualToString:@"iPad3,5"]) return @"iPad 4 (A1459)";
if ([platform isEqualToString:@"iPad3,6"]) return @"iPad 4 (A1460)";

if ([platform isEqualToString:@"iPad4,1"]) return @"iPad Air (A1474)";
if ([platform isEqualToString:@"iPad4,2"]) return @"iPad Air (A1475)";
if ([platform isEqualToString:@"iPad4,3"]) return @"iPad Air (A1476)";
if ([platform isEqualToString:@"iPad4,4"]) return @"iPad Mini 2G (A1489)";
if ([platform isEqualToString:@"iPad4,5"]) return @"iPad Mini 2G (A1490)";
if ([platform isEqualToString:@"iPad4,6"]) return @"iPad Mini 2G (A1491)";

if ([platform isEqualToString:@"i386"]) return @"iPhone Simulator";
if ([platform isEqualToString:@"x86_64"]) return @"iPhone Simulator";
return platform;
}

隐藏返回按钮的文字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[[UIBarButtonItem appearance] setBackButtonTitlePositionAdjustment:UIOffsetMake(0, -60)
forBarMetrics:UIBarMetricsDefault]; ```



### <font color = #a2c700> 判断 pickerView 是否正在滑动 </font>

```objectivec
- (BOOL)isScrolling:(UIView *)view
{
if ([view isKindOfClass:UIScrollView.class]) {
UIScrollView * scrollView = (UIScrollView *)view;
if (scrollView.dragging || scrollView.decelerating) {
return YES;
}
}

for (UIView * aView in view.subviews) {
if ([self isScrolling:aView]) {
return YES;
}
}
return NO;
}

ios8系统 点击设置隐私定位功能直接崩溃的问题

改变webView上的图片大小

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
NSString * js = [NSString stringWithFormat: @"var script = document.createElement('script');"
"script.type = 'text/javascript';"
"script.text = \"function ResizeImages() { "
"var myimg,oldwidth;"
"var maxwidth=%f;" //缩放系数
"for(i=0;i <document.images.length;i++){"
"myimg = document.images[i];"
"if(myimg.width > maxwidth){"
"oldwidth = myimg.width;"
"myimg.width = maxwidth;"
"myimg.style.width = maxwidth+'px';"
"myimg.height = myimg.height * (maxwidth/oldwidth);"
"myimg.style.height = 'auto';"
"}"
"}"
"}\";"
"document.getElementsByTagName('head')[0].appendChild(script);",kScreenWidth-10];
[webView stringByEvaluatingJavaScriptFromString:js];
[webView stringByEvaluatingJavaScriptFromString:@"ResizeImages();"];
```



### <font color = #a2c700> C文件申明冲突 </font>

头件的中的#ifndef,这是一个很关键的东西。比如你有两个C文件,这两个C文件都include了同一个头文件。而编译时,这两个C文件要一同编译成一个可运行文件,于是问题来了,大量的声明冲突。

还是把头文件的内容都放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用,你都要加上这个。一般格式是这样的:

```c
#ifndef <标识>
#define <标识>
......
......
#endif

<标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h

1
2
3
4
#ifndef _STDIO_H_
#define _STDIO_H_
......
#endif

一般是因为 delegate的问题

1
2
3
4
5
6
7
8
9
10
11
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if ([self isMovingFromParentViewController])
{
if (self.navigationController.delegate == self)
{
self.navigationController.delegate = nil;
}
}
}

pop回来后取消选中的cell

viewWillAppear 方法中加入:

1
[self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:YES];                                                                                                       

CollectionView的cell太少无法拖动出来下拉刷新

1
self.collectionView.alwaysBounceVertical = YES;

导入C文件发生冲突

在.pch文件加入

1
2
3
#ifdef __OBJC__
... oc的import
#endif

原文地址

引言

曾经觉得Objc特别方便上手,面对着 Cocoa 中大量 API,只知道简单的查文档和调用。还记得初学 Objective-C 时把[receiver message]当成简单的方法调用,而无视了“发送消息”这句话的深刻含义。其实[receiver message]会被编译器转化为:

1
objc_msgSend(receiver, selector)

如果消息含有参数,则为:

1
objc_msgSend(receiver, selector, arg1, arg2, ...)

如果消息的接收者能够找到对应的selector,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个selector对应的实现内容,要么就干脆玩完崩溃掉。

现在可以看出[receiver message]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message这条消息,而receive将要如何响应这条消息,那就要看运行时发生的情况来决定了。

Objective-C 的 Runtime 铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,但是却是每个 Objc 程序员需要了解的。

简介

因为Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。
Runtime其实有两个版本:“modern”和 “legacy”。我们现在用的 Objective-C 2.0 采用的是现行(Modern)版的Runtime系统,只能运行在 iOS 和 OS X 10.5 之后的64位程序中。而OS X较老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。
Runtime基本是用C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。

与Runtime交互

Objc 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。

Objective-C源代码

大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(比如objc_msgSend函数及其参数列表中的idSEL都是啥)

NSObject的方法

Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。

有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

Runtime的函数

Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。

Runtime术语

还记得引言中的objc_msgSend:方法吧,它的真身是这样的:

1
id objc_msgSend ( id self, SEL op, ... );

下面将会逐渐展开介绍一些术语,其实它们都对应着数据结构。

SEL

objc_msgSend函数第二个参数类型为SEL,它是selector在Objc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:

1
typedef struct objc_selector *SEL;

其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。

不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型(NSNumber一堆抽象工厂方法拿走不谢),Cocoa 中有好多长长的方法哦。

id

objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:

1
typedef struct objc_object *id;

objc_object又是啥呢:

1
struct objc_object { Class isa; };

objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。

PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档

Class

之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:

1
typedef struct objc_class *Class;

objc_class就是我们摸到的那个瓜,里面的东西多着呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。

PS:OBJC2_UNAVAILABLE之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,为的是兼容非Objective-C 2.0的遗留逻辑,但我们仍能从中获得一些有价值的信息,有兴趣的可以查看源代码。

Objective-C 2.0 的头文件虽然没暴露出objc_class结构体更详细的设计,我们依然可以从Objective-C 1.0 的定义中小窥端倪:

objc_class结构体中:ivarsobjc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。关于二级指针,可以参考这篇文章。而最新版的 Runtime 源码对这一块的描述已经有很大变化,可以参考下美团技术团队的深入理解Objective-C:Category
d
PS:任性的话可以在Category中添加@dynamic的属性,并利用运行期动态提供存取方法或干脆动态转发;或者干脆使用关联度对象(AssociatedObject)

其中objc_ivar_listobjc_method_list分别是成员变量列表和方法列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}

如果你C语言不是特别好,可以直接理解为objc_ivar_list结构体存储着objc_ivar数组列表,而objc_ivar结构体存储了类的单个成员变量的信息;同理objc_method_list结构体存储着objc_method数组列表,而objc_method结构体存储了类的某个方法的信息。

最后要提到的还有一个objc_cache,顾名思义它是缓存,它在objc_class的作用很重要,在后面会讲到。

不知道你是否注意到了objc_class中也有一个isa对象,这是因为一个 ObjC 类本身同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc] 这条消息发给类对象的时候,objc_msgSend()会去它的元类里面去查找能够响应消息的方法,如果找到了,然后对这个类对象执行方法调用。

1413628797629491

上图实线是 super_class 指针,虚线是isa指针。 有趣的是根元类的超类是NSObject,而isa指向了自己,而NSObject的超类为nil,也就是它没有超类。

Method

Method是一种代表类中的某个方法的类型。

1
typedef struct objc_method *Method;

objc_method在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:

1
2
3
4
5
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
  • 方法名类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
  • 方法类型method_types是个char指针,其实存储着方法的参数类型和返回值类型。
  • method_imp指向了方法的实现,本质上是一个函数指针,后面会详细讲到。

Ivar

Ivar是一种代表类中实例变量的类型

1
typedef struct objc_ivar *Ivar;

objc_ivar在上面的成员变量列表中也提到过:

1
2
3
4
5
6
7
8
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}

可以根据实例查找其在类中的名字,也就是“反射”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-(NSString *)nameWithInstance:(id)instance {
unsigned int numIvars = 0;
NSString *key=nil;
Ivar * ivars = class_copyIvarList([self class], &numIvars);
for(int i = 0; i < numIvars; i++) {
Ivar thisIvar = ivars[i];
const char *type = ivar_getTypeEncoding(thisIvar);
NSString *stringType = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
if (![stringType hasPrefix:@"@"]) {
continue;
}
if ((object_getIvar(self, thisIvar) == instance)) {//此处若 crash 不要慌!
key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
break;
}
}
free(ivars);
return key;
}

class_copyIvarList 函数获取的不仅有实例变量,还有属性。但会在原本的属性名前加上一个下划线。

IMP

IMPobjc.h中的定义是:

1
typedef id (*IMP)(id, SEL, ...);

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面会提到。

你会发现IMP指向的方法与objc_msgSend函数类型相同,参数都包含idSEL类型。每个方法名都对应一个SEL类型的方法选择器,而每个实例对象中的SEL对应的方法实现肯定是唯一的,通过一组idSEL参数就能确定唯一的方法实现地址;反之亦然。

Cache

runtime.hCache的定义如下:

1
typedef struct objc_cache *Cache

还记得之前objc_class结构体中有一个struct objc_cache *cache吧,它到底是缓存啥的呢,先看看objc_cache的实现:

1
2
3
4
5
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};

Cache为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa指向的类的方法列表中遍历查找能够响应消息的方法,因为这样效率太低了,而是优先在Cache中查找。Runtime 系统会把被调用的方法存到Cache中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。这根计算机组成原理中学过的 CPU 绕过主存先访问Cache的道理挺像,而我猜苹果为提高Cache命中率应该也做了努力吧。

Property

@property标记了类中的属性,这个不必多说大家都很熟悉,它是一个指向objc_property结构体的指针:

1
2
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用

可以通过class_copyPropertyListprotocol_copyPropertyList 方法来获取类和协议中的属性:

1
2
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

返回类型为指向指针的指针,哈哈,因为属性列表是个数组,每个元素内容都是一个objc_property_t指针,而这两个函数返回的值是指向这个数组的指针。
举个栗子,先声明一个类:

1
2
3
4
5
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end

你可以用下面的代码获取属性列表:

1
2
3
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

你可以用property_getName函数来查找属性名称:

1
const char *property_getName(objc_property_t property)

你可以用class_getPropertyprotocol_getProperty 通过给出的名称来在类和协议中获取属性的引用:

1
2
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)

你可以用property_getAttributes函数来发掘属性的名称和@encode类型字符串:

1
const char *property_getAttributes(objc_property_t property)

把上面的代码放一起,你就能从一个类中获取它的属性啦:

1
2
3
4
5
6
7
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}

对比下 class_copyIvarList 函数,使用 class_copyPropertyList 函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。

消息

前面做了这么多铺垫,现在终于说到了消息了。Objc 中发送消息是用中括号([])把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。

objc_msgSend函数

在引言中已经对objc_msgSend进行了一点介绍,看起来像是objc_msgSend返回了数据,其实objc_msgSend从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:

  1. 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
  2. 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
  3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
  4. 如果 cache 找不到就找一下方法分发表。
  5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到 NSObject 类为止。
  6. 如果还找不到就要开始进入动态方法解析了,后面会提到。

PS:这里说的分发表其实就是Class中的方法列表,它将方法选择器和方法实现地址联系起来。

messaging1.gif

其实编译器会根据情况在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或 objc_msgSendSuper_stret四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有”Super”的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。

值得一提的是在 i386 平台处理返回类型为浮点数的消息时,需要用到objc_msgSend_fpret函数来进行处理,这是因为返回类型为浮点数的函数对应的 ABI(Application Binary Interface) 与返回整型的函数的 ABI 不兼容。此时objc_msgSend不再适用,于是objc_msgSend_fpret被派上用场,它会对浮点数寄存器做特殊处理。不过在 PPC 或 PPC64 平台是不需要麻烦它的。

PS:有木有发现这些函数的命名规律哦?带“Super”的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别代表“struct”和“return”;“fpret”就是“fp”+“ret”,分别代表“floating-point”和“return”。

方法中的隐藏参数

我们经常在方法中使用self关键字来引用实例本身,但从没有想过为什么self就能取到调用当前方法的对象吧。其实self的内容是在方法运行时被偷偷的动态传入的。

objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

  • 接收消息的对象(也就是self指向的内容)
  • 方法选择器(_cmd指向的内容)

之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。在下面的例子中,self 引用了接收者对象,而 _cmd引用了方法本身的选择器:

1
2
3
4
5
6
7
8
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}

在这两个参数中,self 更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。

而当方法中的super关键字接收到消息时,编译器会创建一个objc_super结构体:

1
struct objc_super { id receiver; Class class; };

这个结构体指明了消息应该被传递给特定超类的定义。但receiver仍然是self本身,这点需要注意,因为当我们想通过[super class]获取超类时,编译器只是将指向selfid指针和classSEL传递给了objc_msgSendSuper函数,因为只有在NSObject类才能找到class方法,然后class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向selfid指针,与调用[self class]相同,所以我们得到的永远都是self的类型。

获取方法地址

IMP那节提到过可以避开消息绑定而直接获取方法的地址并调用方法。这种做法很少用,除非是需要持续大量重复调用某方法的极端情况,避开消息发送泛滥而直接调用该方法会更高效。

NSObject类中有个methodForSelector:实例方法,你可以用它来获取某个方法选择器对应的IMP,举个栗子:

1
2
3
4
5
6
7
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ ) {
setter(targetList[i], @selector(setFilled:), YES);
}

当方法被当做函数调用时,上节提到的两个隐藏参数就需要我们明确给出了。上面的例子调用了1000次函数,你可以试试直接给target发送1000次setFilled:消息会花多久。

PS:methodForSelector:方法是由 Cocoa 的 Runtime 系统提供的,而不是 Objc 自身的特性。

动态方法解析

你可以动态地提供一个方法的实现。例如我们可以用@dynamic关键字在类的实现文件中修饰一个属性:

1
@dynamic propertyName;

这表明我们会为这个属性动态提供存取方法,也就是说编译器不会再默认为我们生成setPropertyName:propertyName方法,而需要我们动态提供。我们可以通过分别重载resolveInstanceMethod:resolveClassMethod:方法分别添加实例方法实现和类方法实现。因为当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:resolveClassMethod:来给程序员一次动态添加方法实现的机会。我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子为resolveThisMethodDynamically方法添加了实现内容,也就是dynamicMethodIMP方法中的代码。其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding

PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector:instancesRespondToSelector:方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:返回NO

评论区有人问如何用 resolveClassMethod: 解析类方法,我将他贴出有问题的代码做了纠正后如下:
头文件:

1
2
3
4
#import <Foundation/Foundation.h>
@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
@end

m 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import "Student.h"
#import <objc/runtime.h>
@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(learnClass:)) {
class_addMethod(object_getClass([self class]), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
return YES;
}
return [class_getSuperclass([self class]) resolveClassMethod:sel];
}
+ (void)myClassMethod:(NSString *)string {
NSLog(@"myClassMethod = %@", string);
}
@end

凡是涉及到类方法时,一定要弄清楚元类、selector、IMP 等概念,这样才能做到举一反三,随机应变。

消息转发

QQ20141113-1@2x.png

重定向

在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载-(id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其他对象:

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}

毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择,不过千万别返回self,因为那样会死循环。 如果此方法返回nil或self,则会进入消息转发机制(forwardInvocation:);否则将向返回的对象重新发送消息。

转发

当动态方法解析不作处理返回NO时,消息转发机制会被触发。在这时forwardInvocation:方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:

1
2
3
4
5
6
7
8
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}

该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。

这里需要注意的是参数anInvocation是从哪的来的呢?其实在forwardInvocation:消息发送前,Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以我们在重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,否则会抛异常。

当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都从NSObject类中继承了forwardInvocation:方法。然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:。通过实现我们自己的forwardInvocation:方法,我们可以在该方法实现中将消息转发给其它对象。

forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。

注意: forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:将不可能会被调用。

转发和多继承

转发和继承相似,可以用于为Objc编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另一个对象中的方法借过来或是“继承”过来一样。

forwarding.gif

这使得不同继承体系分支下的两个类可以“继承”对方的方法,在上图中WarriorDiplomat没有继承关系,但是Warriornegotiate消息转发给了Diplomat后,就好似DiplomatWarrior的超类一样。

消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的。

替代者对象(Surrogate Objects)

转发不仅能模拟多继承,也能使轻量级对象代表重量级对象。弱小的女人背后是强大的男人,毕竟女人遇到难题都把它们转发给男人来做了。这里有一些适用案例,可以参看官方文档

转发与继承

尽管转发很像继承,但是NSObject类不会将两者混淆。像respondsToSelector:isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个Warrior对象如果被问到是否能响应negotiate消息:

1
2
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...

结果是NO,尽管它能够接受negotiate消息而不报错,因为它靠转发消息给Diplomat类来响应消息。

如果你为了某些意图偏要“弄虚作假”让别人以为Warrior继承到了Diplomatnegotiate方法,你得重新实现 respondsToSelector:isKindOfClass:来加入你的转发算法:

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}

除了respondsToSelector:isKindOfClass:之外,instancesRespondToSelector:中也应该写一份转发算法。如果使用了协议,conformsToProtocol:同样也要加入到这一行列中。类似地,如果一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:来返回准确的方法描述,这个方法会最终响应被转发的消息。比如一个对象能给它的替代者对象转发消息,它需要像下面这样实现methodSignatureForSelector:

1
2
3
4
5
6
7
8
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}

健壮的实例变量(Non Fragile ivars)

在 Runtime 的现行版本中,最大的特点就是健壮的实例变量。当一个类被编译时,实例变量的布局也就形成了,它表明访问类的实例变量的位置。从对象头部开始,实例变量依次根据自己所占空间而产生位移:

nf1.png

上图左边是NSObject类的实例变量布局,右边是我们写的类的布局,也就是在超类后面加上我们自己类的实例变量,看起来不错。但试想如果哪天苹果更新了NSObject类,发布新版本的系统的话,那就悲剧了:

nf2.png

我们自定义的类被划了两道线,那是因为那块区域跟超类重叠了。唯有苹果将超类改为以前的布局才能拯救我们,但这样也导致它们不能再拓展它们的框架了,因为成员变量布局被死死地固定了。在脆弱的实例变量(Fragile ivars) 环境下我们需要重新编译继承自 Apple 的类来恢复兼容性。那么在健壮的实例变量下会发生什么呢?

nf3.png

在健壮的实例变量下编译器生成的实例变量布局跟以前一样,但是当 runtime 系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了。

需要注意的是在健壮的实例变量下,不要使用sizeof(SomeClass),而是用class_getInstanceSize([SomeClass class])代替;也不要使用offsetof(SomeClass, SomeIvar),而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))来代替。

Objective-C Associated Objects

在 OS X 10.6 之后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有以下三个:

1
2
3
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
id objc_getAssociatedObject ( id object, const void *key );
void objc_removeAssociatedObjects ( id object );

这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:

1
2
3
4
5
6
7
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};

这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。

Method Swizzling

之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling ,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。

这里摘抄一个 NSHipster 的例子:

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
#import <objc/runtime.h> 
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end

上面的代码通过添加一个Tracking类别到UIViewController类中,将UIViewController类的viewWillAppear:方法和Tracking类别中xxx_viewWillAppear:方法的实现相互调换。Swizzling 应该在+load方法中实现,因为+load是在一个类最开始加载时调用。dispatch_onceGCD中的一个方法,它保证了代码块只执行一次,并让其为一个原子操作,线程安全是很重要的。

如果类中不存在要替换的方法,那就先用class_addMethodclass_replaceMethod函数添加和替换两个方法的实现;如果类中已经有了想要替换的方法,那么就调用method_exchangeImplementations函数交换了两个方法的 IMP,这是苹果提供给我们用于实现 Method Swizzling 的便捷方法。

可能有人注意到了这行:

// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

object_getClass((id)self)[self class] 返回的结果类型都是 Class,但前者为元类,后者为其本身,因为此时 selfClass 而不是实例.注意 [NSObject class][object class] 的区别:

1
2
3
4
5
6
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}

PS:如果类中没有想被替换实现的原方法时,class_replaceMethod相当于直接调用class_addMethod向类中添加该方法的实现;否则调用method_setImplementation方法,types参数会被忽略。method_exchangeImplementations方法做的事情与如下的原子操作等价:

1
2
3
4
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

最后xxx_viewWillAppear:方法的定义看似是递归调用引发死循环,其实不会的。因为[self xxx_viewWillAppear:animated]消息会动态找到xxx_viewWillAppear:方法的实现,而它的实现已经被我们与viewWillAppear:方法实现进行了互换,所以这段代码不仅不会死循环,如果你把[self xxx_viewWillAppear:animated]换成[self viewWillAppear:animated]反而会引发死循环。
看到有人说+load方法本身就是线程安全的,因为它在程序刚开始就被调用,很少会碰到并发问题,于是 stackoverflow 上也有大神给出了另一个 Method Swizzling 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1 {
NSLog(@"arg1 is %@", arg1);
[self replacementReceiveMessage:arg1];
}
+ (void)load {
SEL originalSelector = @selector(ReceiveMessage:);
SEL overrideSelector = @selector(replacementReceiveMessage:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, overrideMethod);
}
}

上面的代码同样要添加在某个类的类别中,相比第一个种实现,只是去掉了dispatch_once部分。

Method Swizzling 的确是一个值得深入研究的话题,Method Swizzling 的最佳实现是什么呢?小弟才疏学浅理解的不深刻,找了几篇不错的资源推荐给大家:

在用 SpriteKit 写游戏的时候,因为 API 本身有一些缺陷(增删节点时不考虑父节点是否存在啊,很容易崩溃啊有木有!),我在 Swift 上使用 Method Swizzling弥补这个缺陷:

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
extension SKNode {
class func yxy_swizzleAddChild() {
let cls = SKNode.self
let originalSelector = Selector("addChild:")
let swizzledSelector = Selector("yxy_addChild:")
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
class func yxy_swizzleRemoveFromParent() {
let cls = SKNode.self
let originalSelector = Selector("removeFromParent")
let swizzledSelector = Selector("yxy_removeFromParent")
let originalMethod = class_getInstanceMethod(cls, originalSelector)
let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
func yxy_addChild(node: SKNode) {
if node.parent == nil {
self.yxy_addChild(node)
}
else {
println("This node has already a parent!\(node.name)")
}
}
func yxy_removeFromParent() {
if parent != nil {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.yxy_removeFromParent()
})
}
else {
println("This node has no parent!\(name)")
}
}
}

然后其他地方调用那两个类方法:

1
2
SKNode.yxy_swizzleAddChild()
SKNode.yxy_swizzleRemoveFromParent()

因为 Swift 中的 extension 的特殊性,最好在某个类的load() 方法中调用上面的两个方法.我是在AppDelegate 中调用的,于是保证了应用启动时能够执行上面两个方法.

总结

我们之所以让自己的类继承NSObject不仅仅因为苹果帮我们完成了复杂的内存分配问题,更是因为这使得我们能够用上 Runtime 系统带来的便利。可能我们平时写代码时可能很少会考虑一句简单的[receiver message]背后发生了什么,而只是当做方法或函数调用。深入理解 Runtime 系统的细节更有利于我们利用消息机制写出功能更强大的代码,比如 Method Swizzling 等。

参考链接:

实现的效果如下

root.gif

项目代码

页面很简单,4个按钮、一张图片、一个 TableView 和一个半透明 View

开始撸代码。

先来写按钮

按钮包含了一个 UILabel 和一个 UIImageView ,点击的时候 UIImageView 翻转。

先看 XYButton.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
#import <UIKit/UIKit.h>
@class XYButton;

typedef void(^ clickCityBtn_block_xyButton)(XYButton * view);


@interface XYButton : UIView

@property (nonatomic, strong)UILabel * titleLabel;
@property (nonatomic, strong)UIImageView * imageView;


- (void)setTitle:(NSString *)title;

/**
* 是否 是 显示模式 YES 显示
*/
@property (nonatomic, assign)BOOL isShow;


@property (nonatomic, copy)clickCityBtn_block_xyButton clickCityBtn_block_xyButton;
- (void)clickCityBtn_block_xyButton:(void(^)(XYButton * view))clickCityBtn_block_xyButton;

@end

block 用来进行点击后的回调, isShow 用来标记状态。

点击事件通过 touchesEnded:withEvent: 来处理

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)touchesEnded
{
self.isShow = !self.isShow;
[UIView animateWithDuration:kHomeCityAnimate_time animations:^{
self.imageView.transform = CGAffineTransformRotate(self.imageView.transform, M_PI);
}];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self touchesEnded];
self.clickCityBtn_block_xyButton ? self.clickCityBtn_block_xyButton(self) : 0;
}

按钮上面的字会有长有短,为了好看,需要计算长度进行居中:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)setTitle:(NSString *)title
{
self.titleLabel.text = title;
CGFloat width = [title boundingRectWithSize:CGSizeMake(222, 20)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:9]}
context:nil].size.width;
//图片的宽度是12,所以label的宽度不能超过 width - 12
width = (width > self.width - 12) ? (self.width - 12) : width;
self.titleLabel.width = width;
self.titleLabel.mj_x = self.width / 2 - (width + 12 + 2) / 2;
self.imageView.mj_x = CGRectGetMaxX(self.titleLabel.frame) + 2;
}

至此按钮已经完成

放4个按钮的 View

为了方便控制,4个按钮需要封装到一个 View 中。

照例先看 XYNearDriverSchoolSelectView.h

1
2
3
4
5
6
7
8
9
10
11
12
#import <UIKit/UIKit.h>

@class XYButton;

typedef void (^clickSelectView_block)(XYButton * btn);

@interface XYNearDriverSchoolSelectView : UIView

@property (nonatomic, copy)clickSelectView_block clickSelectView_block;
- (void)clickSelectView_blockWithBlock:(void(^)(XYButton * btn))clickSelectView_block;

@end

只有一个 block 进行按钮被点击后的回调。

截下来创建4个按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSInteger width = (NSInteger)((self.width - 3) / 4);
NSArray * titleArray = @[@"默认排序", @"价格", @"距离", @"报名人数"];
WeakSelf(weakSelf);

for (int i = 0 ; i < titleArray.count ; i ++) {
XYButton * btn = [[XYButton alloc]
initWithFrame:CGRectMake((width + 1) * i, 1, width, self.height - 1)];
[btn setTitle:titleArray[i]];
btn.tag = 100000 + i;
[self addSubview:btn];

[btn clickCityBtn_block_xyButton:^(XYButton *view) {
weakSelf.clickSelectView_block ? weakSelf.clickSelectView_block(view) : 0 ;
}];
}

控制器

首先看属性

1
2
3
4
5
6
7
8
9
10
11
12
 @property (nonatomic, strong)XYNearDriverSchoolSelectView * selectView;

@property (nonatomic, strong)XYNearDriverSchoolSelectTableView * selectTableView;

/**
* 上一次点击的 Btn
*/
@property (nonatomic, weak)XYButton * oldBtn;
/**
* 处于 显示 模式的btn
*/
@property (nonatomic, weak)XYButton * showBtn;

然后是 ViewDidLoad

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
 - (void)viewDidLoad {

self.view.backgroundColor = kDefaultBackgroudColor;

//这是页面的内容
UIImageView * imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0,
CGRectGetMaxY(self.selectView.frame) + 1,
kScreenWidth,
kScreenHeight - CGRectGetMaxY(self.selectView.frame) - 1 -kNavigationBar_Height)];


imageView.image = kImage(@"image");
imageView.backgroundColor = kWhiteColor;
imageView.contentMode = UIViewContentModeScaleAspectFit;
[self.view addSubview:imageView];


//半透明的 View
[self.view addSubview:self.selectTableView.backgroudView];
[self.selectTableView.backgroudView clickView:^(UIView *view) {
[self.showBtn touchesEnded:[NSSet set] withEvent:nil];
}];

//TableView
[self.view addSubview:self.selectTableView];

[super viewDidLoad];
// Do any additional setup after loading the view from its nib.

//放4个按钮的 View
[self.view addSubview:self.selectView];


}

selectTableView 的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (XYNearDriverSchoolSelectTableView *)selectTableView
{
if (!_selectTableView) {
_selectTableView = [[XYNearDriverSchoolSelectTableView alloc]
initWithFrame:CGRectMake(0, 0, kScreenWidth, 1)];
/**
* 点击cell 的回调
*/
[_selectTableView didSelectRowWithBlock:^(XYNearDriverSchoolSelectTableView
*tableView, NSIndexPath *indexPath) {

}];
}
return _selectTableView;
}

selectView 的初始化

不想看代码的直接看下面

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
- (XYNearDriverSchoolSelectView *)selectView
{
if (!_selectView) {
_selectView = [[XYNearDriverSchoolSelectView alloc] initWithFrame:
CGRectMake(0, 0, kScreenWidth, 30)];

WeakSelf(weakSelf);

NSMutableArray * groupArray = @[].mutableCopy;
for (int i = 0 ; i < 4 ; i ++) {
NSMutableArray * array = @[].mutableCopy;
for (int i = 0 ; i < (arc4random()% 40) + 1; i ++) {
[array addObject:[NSString stringWithFormat:@"%d",(arc4random()% 40)]];
}
[groupArray addObject:array];
}

[_selectView clickSelectView_blockWithBlock:^(XYButton *btn) {

if (btn.isShow) {
NSLog(@" -- %@",btn.titleLabel.text);
self.showBtn = btn;
self.selectTableView.backgroudView.hidden = NO;
weakSelf.selectTableView.groupArray = groupArray[btn.tag - 100000];
}

if (self.oldBtn != btn && self.oldBtn.isShow) {
[self.oldBtn touchesEnded:[NSSet set] withEvent:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(kHomeCityAnimate_time / 2 * NSEC_PER_SEC)), dispatch_get_main_queue(),
^{
[weakSelf clickSelectViewWithBth:btn];
});
} else {
[weakSelf clickSelectViewWithBth:btn];
}

}];
}
return _selectView;
}

这里初始化完成 selectView 后,处理按钮的点击方法,4个按钮对应4个列表,我们这里只随机4组字符串:

1
2
3
4
5
6
7
8
NSMutableArray * groupArray = @[].mutableCopy;
for (int i = 0 ; i < 4 ; i ++) {
NSMutableArray * array = @[].mutableCopy;
for (int i = 0 ; i < (arc4random()% 40) + 1; i ++) {
[array addObject:[NSString stringWithFormat:@"%d",(arc4random()% 40)]];
}
[groupArray addObject:array];
}

然后开始处理点击事件:

首先判断按钮的 isShow ,这里的获取到的 isShow 是更新后的,因为在按钮的 touchesEnded:withEvent: 先给 isShow 赋值,然后旋转 imageView 最后才进行的回调,所以 isShow 如果是 YES,那么就是处于下拉状态:

1
2
3
4
5
6
if (btn.isShow) {
NSLog(@" -- %@",btn.titleLabel.text);
self.showBtn = btn;
self.selectTableView.backgroudView.hidden = NO;
weakSelf.selectTableView.groupArray = groupArray[btn.tag - 100000];
}

在这里面给 self.showBtn 赋值,把黑色半透明View的hidden置为 NO,再通过 tag 取到对应的数据。

然后进行判断点击的是否是同一个按钮:

1
2
3
4
5
6
7
8
9
10
11
//如果点击的不是同一个
if (self.oldBtn != btn && self.oldBtn.isShow) {
[self.oldBtn touchesEnded:[NSSet set] withEvent:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(kHomeCityAnimate_time / 2 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[weakSelf clickSelectViewWithBth:btn];
});
} else {
[weakSelf clickSelectViewWithBth:btn];
}

如果点击的不是同一个按钮,那么先把 oldBtn 手动点击一下,让 TableView 缩回去,然后在 kHomeCityAnimate_time / 2 秒之后调用 clickSelectViewWithBth:,如果点击的同一个按钮,直接调用 clickSelectViewWithBth:

再来说说 clickSelectViewWithBth: 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)clickSelectViewWithBth:(XYButton *)btn
{
NSInteger y = CGRectGetMaxY(self.selectView.frame) + 1;
if (!btn.isShow) {
y = - self.selectTableView.height;
}
[UIView animateWithDuration:kHomeCityAnimate_time animations:^{
self.selectTableView.mj_y = y;
}];

self.oldBtn = btn;

self.selectTableView.backgroudView.hidden = !self.showBtn.isShow;
}

依据按钮的状态来决定 TableView 的位置,每次完成后都把 oldBtn 指向该按钮,然后

self.selectTableView.backgroudView.hidden = !self.showBtn.isShow;

是为了防止点击了不同的按钮的时候,上一个按钮会把 TableView 往回缩,这时候黑色半透明View会消失,所以加入了一个 showBtn ,就是当前处于显示状态的按钮,如果处于显示状态的按钮的 isShowNO,那么说明所有按钮都不在显示状态,那么黑色半透明View应该消失。

*第一次写这样的博客,写的有点乱,时间也有限,亲们有哪里看不懂的可以留言*

ios7开始 苹果增加了页面 右滑返回的效果;具体的是以 UINavigationController 为容器的 ViewController间右滑切换页面。代码里的设置是:

self.navigationController.interactivePopGestureRecognizer.enabled = YES;(default is YES) 可以看到苹果给 navigationController 添加了一个手势(具体为 UIScreenEdgePanGestureRecognizer(边缘手势,同样是ios7以后才有的)),就是利用这个手势实现的 ios7的侧滑返回。

问题1:

然而事情并非我们想的那么简单。

  1. 当我们用系统的 UINavigationController ,并且也是利用系统的 navigateBar 的时候,是完全没有问题的
  2. 但是当我们没有用系统的 navigateBar 或者自定义了返回按钮的时候,这个时候 右滑返回是失效的。

解决(问题1)办法:

对于这种失效的情况,考虑到 interactivePopGestureRecognizer 也有 delegate 属性,替换默认的
self.navigationController.interactivePopGestureRecognizer.delegate来配置右滑返回的表现也是可行的。
我们可以在主NavigationController中设置一下:

self.navigationController.interactivePopGestureRecognizer.delegate =(id)self

问题2:

但是出现很多问题,比如说在 rootViewController 的时候这个手势也可以响应,导致整个程序页面不响应;push 了多层后,快速的触发两次手势,也会错乱

解决(问题2)办法:

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
@interface NavRootViewController : UINavigationController
@property(nonatomic,weak) UIViewController* currentShowVC;
@end

@implementation NavRootViewController
-(id)initWithRootViewController:(UIViewController *)rootViewController
{
NavRootViewController* nvc = [super initWithRootViewController:rootViewController];
self.interactivePopGestureRecognizer.delegate = self;
nvc.delegate = self;
return nvc;
}

-(void)navigationController:(UINavigationController *)navigationController
willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{}

-(void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (navigationController.viewControllers.count == 1) {
self.currentShowVC = Nil;
} else {
self.currentShowVC = viewController;
}

-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer == self.interactivePopGestureRecognizer) {
return (self.currentShowVC == self.topViewController); //the most important
}
return YES;
}

@end

借鉴了别人的方法:具体是通过 获取当前 pushView 栈里的当前显示的VC,根据这个VC来决定 是否开启手势(如果currentShowVC 是当前显示的,则开启手势;如果 currentShowVC为nil,则代表在主页面,关闭手势)

注:

当时试了一种方法 就是滑动的时候来回设置 interactivePopGestureRecognizer的delegate; 发现 会有crash,原因就是 因为 我把 当前显示的VC设置为了这个手势的 delegate ,但当这个VC消失的时候,这个 delegate 便被释放了,导致crash

至此,觉得ios7上的右滑返回大功告成了,心里正happy,妈蛋,发现了一个可耻的bug:
UIScrollView 上 右滑返回的手势失灵了,靠!!!!!!

问题三:
UIScrollView上手势失灵:
经研究,发现是UIScrollView上已经添加了 panGestureRecognizer(滑动)手势

rightSlipBackFailure

解决(问题三)办法:

参考:http://www.cnblogs.com/lexingyu/p/3702742.html

【解决方案】

苹果以UIGestureRecognizerDelegate的形式,支持多个UIGestureRecognizer共存。其中的一个方法是:

1
2
3
4
5
6
7
8
9
10
// called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be 
blocked by the other
// return YES to allow both to recognize simultaneously. the default implementation returns NO
(by default no two gestures can be recognized simultaneously)
//
// note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not
guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

一句话总结就是此方法返回YES时,手势事件会一直往下传递,不论当前层次是否对该事件进行响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation UIScrollView (AllowPanGestureEventPass)

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]
&& [otherGestureRecognizer isKindOfClass:[UIScreenEdgePanGestureRecognizer class]])
{
return YES;
}
else
{
return NO;
}
}

事实上,对UIGestureRecognizer来说,它们对事件的接收顺序和对事件的响应是可以分开设置的,即存在接收链和响应链。接收链如上文所述,和UIView绑定,由UIView的层次决定接收顺序。

而响应链在apple君的定义下,逻辑出奇的简单,只有一个方法可以设置多个gestureRecognizer的响应关系:

1
2
3
4
5
6
7
// create a relationship with another gesture recognizer that will prevent this gesture's 
actions from being called until otherGestureRecognizer transitions to
UIGestureRecognizerStateFailed // if otherGestureRecognizer transitions to
UIGestureRecognizerStateRecognized or UIGestureRecognizerStateBegan then this recognizer will
instead transition to UIGestureRecognizerStateFailed // example usage: a single ap may
require a double tap to fail
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;

每个UIGesturerecognizer都是一个有限状态机,上述方法会在两个gestureRecognizer间建立一个依托于state的依赖关系,当被依赖的gestureRecognizer.state = failed时,另一个gestureRecognizer才能对手势进行响应。

所以,只需要

[_scrollView.panGestureRecognizer requireGestureRecognizerToFail:screenEdgePanGestureRecognizer];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (UIScreenEdgePanGestureRecognizer *)screenEdgePanGestureRecognizer
{
UIScreenEdgePanGestureRecognizer *screenEdgePanGestureRecognizer = nil;
if (self.view.gestureRecognizers.count > 0)
{
for (UIGestureRecognizer *recognizer in self.view.gestureRecognizers)
{
if ([recognizer isKindOfClass:[UIScreenEdgePanGestureRecognizer class]])
{
screenEdgePanGestureRecognizer = (UIScreenEdgePanGestureRecognizer *)recognizer;
break;
}
}
}

return screenEdgePanGestureRecognizer;

}

非常重要的一点

文件夹 和 文件 的名字千万不要有汉字,24小时的血与泪告诉你们,千万不要有汉字,我就是因为有个空文件夹,名字叫 新建文件夹 因为是空的 我也没管它,结果 hexo 部署不上去了啊 ,在网上怎么都找不到解决的办法。

字体

设置了字体的颜色,必须在这段 添加上 p 标签, 否则 markdown 无法生成 p 标签,会影响到下面的布局。

1
2
3
4
5
6
<p>
<font color=#a2c700> JPEG </font>
是目前最常见的图片格式,它诞生于 1992 年,是一个很古老的格式。它只支持有损压缩,
其压缩算法可以精确控制压缩比,以图像质量换得存储空间。由于它太过常见,
以至于许多移动设备的 CPU 都支持针对它的硬编码与硬解码。
</p>

使用图片

要让图片可以点击,需要添加 html 的 a 标签

1
2
3
4
<a href = "/img/handleImageSomTip/image_baseline.gif">
<img src = "/img/handleImageSomTip/image_baseline.gif"
width = 160 height = 160 alt = "image_baseline.gif" />
</a>

生成目录

在内容前面加上这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<link rel="stylesheet" href="http://yandex.st/highlightjs/6.2/styles/googlecode.min.css">
<script src="http://code.jquery.com/jquery-1.7.2.min.js"></script>
<script src="http://yandex.st/highlightjs/6.2/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<script type="text/javascript">
$(document).ready(function(){
$("h2,h3,h4,h5,h6").each(function(i,item){
var tag = $(item).get(0).localName;
$(item).attr("id","wow"+i);
$("#category").append('<a class="new'+tag+'" href="#wow'+i+'">'+$(this).text()+'</a></br>');
$(".newh2").css("margin-left",0);
$(".newh3").css("margin-left",20);
$(".newh4").css("margin-left",40);
$(".newh5").css("margin-left",60);
$(".newh6").css("margin-left",80);
});
});
</script>
<div id="category"></div>

添加评论区

我使用了 Disqus ,在国内速度会慢一点,但是为了逼格,我还是用了它,国内可以用 多说

废话不多说,首先得去 DisqusDisqus] 注册一个账号,有一个邮箱就行,收到邮件后验证一下

signUpDisqus.png

登陆进来以后再右上角找到设置,点击 Add Disqus To Site

addDisqusToSite.png

进入后点击 Start Using Engage ,然后设置 Site

setUpDisqusNewSite.png

siteName 就填你的 github.io 地址,下面的URL会自动生成,选择一个类别,点击 Next,然后会有两个按钮 My site is part of a larger organization (我的网站是一个更大的组织的一部分)My site is just a personal site (我的网站只是个人网站), 然后填写2个问题,就进入了管理页面,

siteManager.png

点击 General

settingGeneral.png

Shortname 一会要用到,Website Name 会在评论区的顶部显示, Website URL 你博客的网址,必须填对。其他的设置不填也可以。

我使用的是 hexo 搭建的博客,进入到博客根目录,找到 _config.yml 文件,在里面添加

# Disque
disqus_shortname: 刚才的那个 Shortname

如果有这个字段,直接修改值就可以了。

然后就可以提交到 github 刷新页面,评论框就出来啦。

AVPlayer 是一个强大的视频播放器,可以播放多种格式的视频,缺点是没有控制界面,需要自己去实现。

效果图

AVPlayer

项目地址

先看下它的结构

overview
首先初始化播放器,设置播放URL。

1
2
3
self.avPlayerView = [[XYAVPlayerView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, VIDEO_HEIGHT)];
self.avPlayerView.videoUrl = m3u;
[self.view addSubview:self.avPlayerView];

初始化方法,添加一个视频控制器。

1
2
3
4
5
6
7
8
9

- (instancetype)initWithFrame:(CGRect)frame
{
    if ([super initWithFrame:frame]) {
        self.backgroundColor = [UIColor blackColor];
        [self addSubview:self.playControlView];
    }
    return self;
}

设置视频URL

1
2
3
4
5
6
7

- (void)setVideoUrl:(NSString *)videoUrl
{
    _videoUrl = videoUrl.copy;
    [self createAVPlayer];

}

初始化 AVPlayer,给_playControlView 引用AVPlayer,方便进行控制, [_playControlView addObserver];  [_playControlView playerTimerAction];会在后面说明。

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

- (void)createAVPlayer
{
    
    NSURL *url = [NSURL URLWithString:self.videoUrl];
    if (!url) {
        return;
    }
  

    /**
     *  AVPlayer
     */
    _avPlayerItem = [AVPlayerItem playerItemWithURL:url];
    _avPlayer = [AVPlayer playerWithPlayerItem:_avPlayerItem];
    _avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:_avPlayer];
    _avPlayerLayer.frame = self.bounds;
    _avPlayerLayer.videoGravity = AVLayerVideoGravityResizeAspect;//
    
    
    [self.layer addSublayer:_avPlayerLayer];
    
    [_avPlayer play];
    
    self.autoresizesSubviews = YES; //子视图Size自适应
    
    _playControlView.avPlayer = self.avPlayer;
    _playControlView.avPlayerItem = self.avPlayerItem;
    _playControlView.avPlayerLayer = self.avPlayerLayer;
    
    [_playControlView addObserver];
    [_playControlView playerTimerAction];
    
    
    [self bringSubviewToFront:self.playControlView];
}

到这里只是创建了一个View,上面加载了一个AVPlayer,一个视频控制器视图。

视频控制器代码:
我用的xib创建的View,所以初始化方法是awakeFromNib

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

- (void)awakeFromNib
{
    [super awakeFromNib];
    
    [self configureVolume];
    
    
    UITapGestureRecognizer * screenTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(screenTap)];
    [self addGestureRecognizer:screenTap];
    
    
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureRecognizerAction:)];
    pan.delegate = self;
    
    [_adjustView addGestureRecognizer:pan];
    
    
    [self.slider setThumbImage:[UIImage imageNamed:@"verify_code_button"] forState:UIControlStateNormal];
    
    // slider开始滑动事件
    [_slider addTarget:self action:@selector(progressSliderTouchBegan:) forControlEvents:UIControlEventTouchDown];
    // slider滑动中事件
    [_slider addTarget:self action:@selector(progressSliderValueChanged:) forControlEvents:UIControlEventValueChanged];
    // slider结束滑动事件
    [_slider addTarget:self action:@selector(progressSliderTouchEnded:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchUpOutside];
    // slider 添加点击手势
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(sliderTap:)];
    [_slider addGestureRecognizer:tap];
    
    
    [self hiddenViews];

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

/**
 *  获取系统音量
 */
- (void)configureVolume {
    
    MPVolumeView *volumeView = [[MPVolumeView alloc] init];
    _volumeSlider = nil;
    for (UIView *view in [volumeView subviews]){
        if ([view.class.description isEqualToString:@"MPVolumeSlider"]){
            _volumeSlider = (UISlider *)view;
            break;
        }
    }
    
    // 使用这个category的应用不会随着手机静音键打开而静音,可在手机静音下播放声音
    NSError *setCategoryError = nil;
    BOOL success = [[AVAudioSession sharedInstance]
                    setCategory: AVAudioSessionCategoryPlayback
                    error: &setCategoryError];
    
    if (!success) { /* handle the error in setCategoryError */ }
    
}

第一个手势screenTap,是控制下方控制条的显示与隐藏的

1
2
3
- (void)screenTap {
    self.controlStripView.hidden = !self.controlStripView.hidden;
}

第二个手势则是用来控制快进、快退、音量、亮度调节的

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
- (void)panGestureRecognizerAction: (UIPanGestureRecognizer *)sender {
    
    //根据在view上Pan的位置,确定是调音量还是亮度
    CGPoint locationPoint = [sender locationInView:self];
    
    CGPoint veloctyPoint = [sender velocityInView:self];
    
    switch (sender.state) {
        case UIGestureRecognizerStateBegan:{
            // 使用绝对值来判断移动的方向
            CGFloat x = fabs(veloctyPoint.x);
            CGFloat y = fabs(veloctyPoint.y);
            
            if (x > y) { //水平移动
                [self showViews];
                [self playerPause];
                self.pandirection = PanDirectionHorizontalMoved;
            }else { //垂直移动
                self.pandirection = PanDirectionVerticalMoved;
            }
            break;
        }
        case UIGestureRecognizerStateChanged:{
            switch (_pandirection) {
                case PanDirectionHorizontalMoved:{
                    [self horizontalMoved:veloctyPoint.x];
                    break;
                }
                case PanDirectionVerticalMoved:{
                    if (locationPoint.x > self.bounds.size.width / 2) {//音量调节-右侧
                        [self verticalMovedForVolume:veloctyPoint.y];
                    }else {//亮度调节-左侧
                        [self verticalMovedForBrightness:veloctyPoint.y];
                    }
                    break;
                }
            }
            break;
        }
        case UIGestureRecognizerStateEnded:{
            switch (_pandirection) {
                case PanDirectionHorizontalMoved:{
                    [self hiddenViews];
                    [self playerPlay];
                    break;
                }
                case PanDirectionVerticalMoved:{
                    break;
                }
            }
            break;
        }
        default:break;
    }
}

对于slider的方法,在开始手势的时候暂停视频播放,显示时间label

1
2
3
4
5
6
7
8
9
10

- (void)progressSliderTouchBegan: (UISlider *)sender {
    
    self.changeTimeLabel.hidden = NO;
    
    [self playerPause];
    
    
    NSLog(@" --- began touch");
}

在开始手势的时候seek到slider对应时间的时间点,然后开始视频播放,隐藏时间label

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

- (void)progressSliderTouchEnded: (UISlider *)sender {
    CMTime durationTime = self.avPlayerItem.duration;
    
    NSTimeInterval currentTime = CMTimeGetSeconds(durationTime) * sender.value;
    
    [self seekWithTime:currentTime];
    
    self.changeTimeLabel.hidden = YES;
    
    [self playerPlay];
    
    
    NSLog(@" ---- end touch");
}

slider滑动的时候,只是改变label上显示的时间

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

- (void)progressSliderValueChanged: (UISlider *)sender {
    
    CGFloat currentTime = sender.value * CMTimeGetSeconds(self.avPlayerItem.duration);
    
    NSString *tempCurrentTime = [self timeFormatterForServiceWithTimeStamp:currentTime];
    
    //当前播放时间
    self.currentTimeLabel.text = tempCurrentTime;
    
    //屏幕中间时间
    self.changeTimeLabel.text = tempCurrentTime;
    
    
    CGFloat sliderProgress = sender.value / sender.maximumValue;
    
    if (self.progressView.progress < sliderProgress) {
        self.progressView.progress = sender.value / sender.maximumValue;
    }
    
    
    
    NSLog(@" --- event touch  %f",sender.value);
    
    
}

slider的单击方法则是直接seek到对应时间点,AVPlayer会自动处理的。

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

/**
 *  Slider Tap
 */
- (void)sliderTap: (UITapGestureRecognizer *)sender {
    
    if ([sender.view isKindOfClass:[UISlider class]]) {
        
        UISlider *slider = (UISlider *)sender.view;
        CGPoint point = [sender locationInView:slider];
        CGFloat length = slider.frame.size.width;
        
        CGFloat tempValue = point.x / length;
        
        NSTimeInterval currentTime = CMTimeGetSeconds(self.avPlayerItem.duration) * tempValue;
        
        CGFloat progress = currentTime/CMTimeGetSeconds(self.avPlayerItem.duration);
        
        if (progress > slider.value) {
            self.progressView.progress = progress;
        }
        
        
        [self seekWithTime:currentTime];
    }
    
}

前面用的 addObserver方法,则是给播放器添加观察者,用来检测播放器状态,还有APP的状态。

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

- (void)addObserver
{
    //监控状态属性,注意AVPlayer也有一个status属性,通过监控它的status也可以获得播放状态
    [self.avPlayerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    //监控网络加载情况属性
    [self.avPlayerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    
    /**
     *  进入后台  暂停播放
     *
     */
    [kNotificationCenter addObserver:self selector:@selector(applicationDidEnterBackground_Notification) name:ApplicationDidEnterBackground_Notification object:nil];
    
    /**
     *  进入活跃状态  继续播放
     *
     */
    [kNotificationCenter addObserver:self selector:@selector(applicationDidBecomeActive_Notification) name:ApplicationDidBecomeActive_Notification object:nil];
    
    [kNotificationCenter addObserver:self selector:@selector(playerReset) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    
}



/** * 通过KVO监控播放器状态 *
 * @param keyPath 监控属性
 * @param object 监视器
 * @param change 状态改变
 * @param context 上下文 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    AVPlayerItem *playerItem=object;
    if ([keyPath isEqualToString:@"status"]) {
        AVPlayerStatus status= [[change objectForKey:@"new"] intValue];
        if(status==AVPlayerStatusReadyToPlay){
            //总时长
            self.allTimeLabel.text = [self timeFormatterForServiceWithTimeStamp:CMTimeGetSeconds(playerItem.duration)];
            self.currentTimeLabel.text = @"00:00:00";
            
            NSLog(@"正在播放...,视频总长度:%.2f",CMTimeGetSeconds(playerItem.duration));
        }
    }
    else if([keyPath isEqualToString:@"loadedTimeRanges"])
    {
        NSArray *array=playerItem.loadedTimeRanges;
        CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次缓冲时间范围
        float startSeconds = CMTimeGetSeconds(timeRange.start);
        float durationSeconds = CMTimeGetSeconds(timeRange.duration);
        NSTimeInterval totalBuffer = startSeconds + durationSeconds;//缓冲总长度
        NSLog(@"共缓冲:%.2f",totalBuffer);
        double ableScale = totalBuffer / CMTimeGetSeconds(playerItem.duration);
        if (ableScale <= 1) {
            self.progressView.progress = ableScale;
        }
    }
}

playerTimerAction方法则是制定每秒进行一次返回,返回当前播放进度。

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

- (void)playerTimerAction
{
    __weak XYAVControlView * weakSelf = self;
    
    [self.avPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        
        float current=CMTimeGetSeconds(time);
        if (current) {
            //当前播放进度
            weakSelf.currentTimeLabel.text = [weakSelf timeFormatterForServiceWithTimeStamp:CMTimeGetSeconds(weakSelf.avPlayerItem.currentTime)];
            //滑块进度
            double totalTempTime = CMTimeGetSeconds(weakSelf.avPlayerItem.duration);
            double scale = CMTimeGetSeconds(weakSelf.avPlayerItem.currentTime) / totalTempTime;
            weakSelf.slider.value = scale;
        }
    }];
}

最后在 ViewController 里面监控屏幕方向的变化,来处理全屏效果

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

[kNotificationCenter  addObserver:self
                             selector:@selector(onDeviceOrientationChange)
                                 name:UIDeviceOrientationDidChangeNotification
                               object:nil];






/**
 *  屏幕方向发生变化会调用这里
 */
- (void)onDeviceOrientationChange
{
    UIDeviceOrientation orientation             = [UIDevice currentDevice].orientation;
    UIInterfaceOrientation interfaceOrientation = (UIInterfaceOrientation)orientation;
    switch (interfaceOrientation) {
        case UIInterfaceOrientationPortraitUpsideDown:
        case UIInterfaceOrientationPortrait:{
            self.view.frame = (CGRect){0,0,ScreenWidth,ScreenHeight};
            self.avPlayerView.frame = CGRectMake(0, 0,ScreenWidth,VIDEO_HEIGHT);
            self.avPlayerView.playControlView.isFullScreen = NO;
            break;
        }
        case UIInterfaceOrientationLandscapeLeft:
        case UIInterfaceOrientationLandscapeRight:{
            self.view.frame = (CGRect){0,0,ScreenWidth,ScreenHeight};
            self.avPlayerView.frame = self.view.bounds;
            self.avPlayerView.playControlView.isFullScreen = YES;
            break;
        }
        default:
            break;
    }
}

到这里一个简单的视频播放器就做完了。

转载自YY大神

如何把 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) 从数据源获取直接解码的数据。
  5. ImageIO 解码发生在最后一步,这样获得的数据是没有经过颜色类型转换的原生数据(比如灰度图像)。

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

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

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

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

image_baseline.gifimage_interlaced.gifimage_progressive.gif

  1. 第一种是 baseline,即逐行扫描。默认情况下,JPEG、PNG、GIF 都是这种保存方式。
  2. 第二种是 interlaced,即隔行扫描。PNG 和 GIF 在保存时可以选择这种格式。
  3. 第三种是 progressive,即渐进式。JPEG 在保存时可以选择这种方式。

在下载图片时,首先用 CGImageSourceCreateIncremental(NULL) 创建一个空的图片源,随后在获得新数据时调用
CGImageSourceUpdateData(data, false) 来更新图片源,最后在用 CGImageSourceCreateImageAtIndex() 创建图片来显示。

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

我是UICollectionView的忠实粉丝。这个类比起它的老哥UITableView类具有更高的可定制性。现在我用collection view的次数要比用table view还多。随着iOS9的到来,它支持简单的重排。在此之前,重排不可能有现成的方法,同时这样做也是件痛苦的工作。现在让我们来看看API,你可以在GitHub找到相应的Xcode工程。

添加简单重排的最简单的方式是用UICollectionViewController。它现在有了一个新的属性叫installsStandardGestureForInteractiveMovement(为交互式移动工作设置标准手势),这个属性的添加使得我们可以用标准手势来对cell单元进行重新排序。该属性默认值为true,这意味着我们只需要重载一个方法就可以让它正常工作。

1
2
3
4
5
override func collectionView(collectionView: UICollectionView,
moveItemAtIndexPath sourceIndexPath: NSIndexPath,
toIndexPath destinationIndexPath: NSIndexPath) {
// move your data order
}

Collection view推断每个item(元素)可以被移动,因为moveItemAtIndexPath函数被重载了。

againArray_0

当我们想使用一个带有collection view的简单的UIViewController时,事情变得更加复杂。我们还需要实现之前提到的UICollectionViewDataSource的方法,但我们需要重写installsStandardGestureForInteractiveMovement。别担心,这些也很容易被支持。UILongPressGestureRecognizer是一个持续的、完全支持平移的手势识别器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
override func viewDidLoad() {
super.viewDidLoad()


longPressGesture = UILongPressGestureRecognizer(target: self, action: "handleLongGesture:")
self.collectionView.addGestureRecognizer(longPressGesture)
}


func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}

我们储存了被选择的索引路径,这个路径从 longPressGesture handler(长按手势处理器)中获得,这个路径还取决于它是否有任何我们允许的,跟平移手势相关的值。接下来我们根据手势状态调用一些新的 collection view 方法:

  • beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath)? 开始在特定的索引路径上对cell(单元)进行 Interactive Movement(交互式移动工作)。

  • updateInteractiveMovementTargetPosition(targetPosition: CGPoint)? 在手势作用期间更新交互移动的目标位置。】

  • endInteractiveMovement()? 在完成手势动作后,结束交互式移动

  • cancelInteractiveMovement()? 取消 Interactive Movement

这让处理平移手势更容易理解了。

againArray_1

机器反应跟标准的 UICollectionViewController 一样,真的很酷,但是还有更酷的–我们能对自定义的 collection view layout(collection集合视图布局)申请重排,下面是在 waterfall layout(瀑布布局)里对 Interactive Movement 的测试。

againArray_2

嗯哼,看起来很酷,但如果我们不想在移动 cell(单元)的时候改变它们的大小,那该怎么做?被选择的 cell(单元)的大小在 Interactive Movement 期间应该保持原样。这是可行的。UICollectionViewLayout 有附加的方法来处理重排。

1
2
3
4
5
6
7
8
9
func invalidationContextForInteractivelyMovingItems(targetIndexPaths: [NSIndexPath],
withTargetPosition targetPosition: CGPoint,
previousIndexPaths: [NSIndexPath],
previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext


func invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths(indexPaths: [NSIndexPath],
previousIndexPaths: [NSIndexPath],
movementCancelled: Bool) -> UICollectionViewLayoutInvalidationContext

第一个函数在元素的 Interactive Movement 期间被调用,它带有 target(目标元素)和先前的 cellindexPaths(索引地址)。第二个与第一个函数类似,但它只在 Interactive Movement 结束后才调用。通过这些知识我们能通过一点小窍门,实现我们的需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
internal override func invalidationContextForInteractivelyMovingItems(targetIndexPaths: [NSIndexPath],
withTargetPosition targetPosition: CGPoint,
previousIndexPaths: [NSIndexPath],
previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext {


var context = super.invalidationContextForInteractivelyMovingItems(targetIndexPaths,
withTargetPosition: targetPosition, previousIndexPaths: previousIndexPaths,
previousPosition: previousPosition)


self.delegate?.collectionView!(self.collectionView!, moveItemAtIndexPath: previousIndexPaths[0],
toIndexPath: targetIndexPaths[0])


return context
}

取得当前正在移动的cell的之前的和目标索引路径,然后调用 UICollectionViewDataSource 方法来移动这些 item(元素)。

againArray_3

毫无疑问,collection view的重排是一个出色的附加功能,UIKit前端框架工程师干得漂亮!:)

const

三种使用 const 的方式

const NSMutableString * constString = @"constString".mutableCopy;
NSMutableString const * stringConst = @"stringConst".mutableCopy;
NSMutableString * const string_const = @"string_const".mutableCopy;

给它们赋值,也就是改变指针 地址

constString = @"newConstString".mutableCopy; //const 不管在 NSMutableString 前后是一样的东西
stringConst = @"newStringConst".mutableCopy; 
//string_const = @"newString_const".mutableCopy; //报错 const 和 指针在一起时指针不可以指向新的地址,也就是常量指针

这里没有预料中的 直接报红 也没报黄

[constString stringByAppendingString:@"_new"];
[stringConst stringByAppendingString:@"_new"];
//但是打印出来的值 是:  ---constString = newConstString -- stringConst = newStringCon 值没有被改变,也就是 值是常量
NSLog(@" ---constString = %@ -- stringConst = %@",constString,stringConst);

//总结:const 在 NSMutableString 旁边,那么就是指针指向的值是常量,改变值不会报错,但是不会有效果。
//     const 在 指针            旁边,那么改指针就是常量指针,不可以重新赋值。

//如果 要 常量指针  指向 常量 那么 :
const NSMutableString * const const__constString = @"const__constString".mutableCopy;
//或者
NSMutableString const  * const const_constString = @"const_constString".mutableCopy;

static

//static 在 创建的时候 不能赋值给 一个 不是编译的常数
//只可以 赋值 这样的
static int a = 1;
static char b = '6';
static char c[3] = "122";
static char * d = "233";
static NSString * e = @"233";

static const NSString * static_constString_new = @"23333";//这样 也可以 我已经晕了

//    static ViewController * mySelf = [[ViewController alloc] init]; // 这样就不行了
//    static const ViewController * mySelf = [[ViewController alloc] init]; //nonono

//    static const NSMutableString * static_constString = @"2333".mutableCopy; //这样是不行的
//    static const NSString * static_constString_new_new = [NSString new]; //这样也不行

//总之 好像 就能赋值给 一个 常量

关于 static 的一点 ,它在 app 的生命周期 只执行一次,下一次如果跑到了 这里,那么这句 初始化的 代码就被直接跳过了

//先 测试 const 和 NSMutableString 在一起 指针指向 常量
static const NSMutableString * static_constString = nil;
const static NSMutableString * const_staticString = nil;
const NSMutableString static * const__staticString = nil;  //这三种方式 猜测是一样的
//    const NSMutableString * static const_string_static = nil; 这样干爆红了

static_constString = @"static_constString".mutableCopy;
const_staticString = @"const_staticString".mutableCopy;
const__staticString = @"const__staticString".mutableCopy;

// 打印 结果
//    static_constString = static_constString
//    const_staticString = const_staticString
//    const__staticString = const__staticString

NSLog(@"static_constString = %@ \n const_staticString = %@ \n const__staticString = %@",static_constString,const_staticString,const__staticString);

static_constString = @"static_constString_new".mutableCopy;
const_staticString = @"const_staticString_new".mutableCopy;
const__staticString = @"const__staticString_new".mutableCopy;

// 打印 结果
//    static_constString = static_constString_new
//    const_staticString = const_staticString_new
//    const__staticString = const__staticString_new
NSLog(@"static_constString = %@ \n const_staticString = %@ \n const__staticString = %@",static_constString,const_staticString,const__staticString);

[static_constString stringByAppendingString:@"++ new"];
[const_staticString stringByAppendingString:@"++ new"];
[const__staticString stringByAppendingString:@"++ new"];

// 打印 结果
//    static_constString = static_constString_new
//    const_staticString = const_staticString_new
//    const__staticString = const__staticString_new
NSLog(@"static_constString = %@ \n const_staticString = %@ \n const__staticString = %@",static_constString,const_staticString,const__staticString);

结论: 加了 static 对 const 和 NSMutableString 在一起的,指针指向常量这种东西 没影响,依然可以正常赋值,并且不能改变值,但是 用了 static 后,这个指针会被 存入 静态区

NSMutableString static * const static_xing_constString = nil;
static NSMutableString * const static__constString = nil;

//预料之中的 警告了
//    static_xing_constString = @"static_xing_constString".mutableCopy;
//    static__constString = @"static__constString".mutableCopy;

[static_xing_constString stringByAppendingString:@"static_xing_constString"];
[static__constString stringByAppendingString:@"static_xing_constString"];

//因为是 nil 所以。。。
NSLog(@"static_xing_constString = %@ \n static__constString = %@",static_xing_constString,static__constString);
//这样 写 好像 没用了, 初始化的时候 不能赋值,完了 还不能改,一辈子 都是 nil