注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

iOS之网络优化

一、正常一个网络请求过程正常一条网络请求需要经过:DNS解析,请求DNS服务器,获取对应的IP地址与服务端建立连接,TCP三次握手,安全协议的同步流程连接建立完成,发送和接受数据,解码数据。优化点:直接使用IP地址,除去DNS解析的流程不要每个请求都重复建立连...
继续阅读 »

一、正常一个网络请求过程

正常一条网络请求需要经过:

  • DNS解析,请求DNS服务器,获取对应的IP地址
  • 与服务端建立连接,TCP三次握手,安全协议的同步流程
  • 连接建立完成,发送和接受数据,解码数据。

优化点:

  • 直接使用IP地址,除去DNS解析的流程
  • 不要每个请求都重复建立连接,复用连接使用同一条连接(长连接)
  • 压缩数据,减少传输数据的大小

二、正常的DNS流程

DNS完整的解析流程:

  1. 先动本地系统缓存获取,偌没有就到最近的DNS服务器获取。

  2. 偌依旧没有,就到主域名服务器获取,每一层都有有缓存。

为了域名解析的实时性,每一层的缓存都有一个过期时间。

缺点:

  1. 缓存时间过长,域名更新不及时,设置的端,单量的DNS解析请求影响速度

  2. 域名劫持,容易被中间人攻击或运营商劫持,把域名解析到第三⽅IP地址,劫持率⽐较⾼。

  3. DNS解析过程不受控制,⽆法保证解析到最快的IP

  4. ⼀次请求只能解析到⼀个域名

处理DNS耗时和防劫持的方式

HTTPDNS原理就是:

⾃⼰做域名解析⼯作,通过HTTP请求后台去拿到域名对应的IP地址,可以解决上述问题。

  • 域名解析与请求分离,所有的请求都直接⽤IP,⽆需DNS解析,APP定时请求HTTPDNS服务器更 新IP地址即可

  • 通过签名等⽅式,保证HTTPDNS请求的安全性,避免被劫持

  • DNS解析有⾃⼰控制,可以确保更具⽤⼾所在地返回就近的IP地址,或者根据客⼾端测速结果使⽤ 速度最快的IP

  • ⼀次请求可以解析多个域名

三、TCP连接耗时优化

解决思路就是:复用接连

  • 不⽤每次重新建⽴连接,⽅案是⾼效的复⽤连接

HTTP1.1版本产生的问题:

  • HTTP1.1 的问题是默认开启的keep-alive,⼀次的连接只能发送接收⼀个请求,同时发起多个请求,会产⽣问题。

  • 若串⾏发送请求,可以⼀直复⽤⼀个连接,但是速度很慢,每个请求都需要等待上⼀个请求完成再发 送,也产⽣了连接的浪费,没⽤充分的利⽤带宽

  • 若并⾏发送请求,那么⾸次请求都要TCP三次握⼿建⽴新的连接,即使第⼆次请求可以复⽤连接池的 连接,但是会导致连接池的连接过多,对服务端资源产⽣浪费,若限制保持的连接数,会有超出的连 接仍要每次建⽴连接。

HTTP2.0 提出了多路复⽤的⽅式解决HTTP1.1的问题:

  • HTTP2 的多路复⽤机制也是复⽤连接,但是它的复⽤的这条连接⽀持同时处理多条请求,所有的请求 都可以并发的在这条连接上进⾏,解决了并发请求需要建⽴多次连接的问题

  • HTTP2 把连接⾥传输的数据都封装成⼀个个stream,每个stream都有⼀个标识,stream的发送和接 收可以是乱序,不依赖顺序,不会有阻塞的问题,接收端可以根据stream的标识区分属于哪个请求, 在进⾏数据拼接,最终得到数据

HTTP2.0 TCP队头阻塞

  • HTTP2还是有问题存在,就是队头阻塞,这是受限于TCP协议,TCP协议为保证数据的可靠性,若传 输过程中有⼀个TCP的包丢失,会等待这个包重传之后,才会处理后续的包.

  • HTTP2的多路复⽤让所 有的请求都在同⼀条连接上,中间有⼀个包丢失,就会阻塞等待重传,所有的请求也会被阻塞

HTTP2.0 TCP队头阻塞解决方案

  • 这个问题需要改变TCP协议,但是TCP协议依赖操作系统实现以及部分硬件的定制,所以改进缓慢。
  • 于是Google提出了QUIC协议,相当于在UDP的基础上在定义⼀套可靠的传输协议,解决TCP的缺陷, 包括队头阻塞,但是客⼾端少有介⼊

四、传输数据优化

传输数据有⼤⼩,数据也会对请求速度有影响。主要优化两个方面:

  • 压缩率,⽽是解压序列化反
    序列化的速度。使⽤Protobuf 可以⽐json的数据量⼩⾄少⼀个数量级

  • 压缩算法的选择,⽬前⽐较好的是Z-Standard HTTP的请求头数据的在HTTP2中也进⾏了压缩。

五、弱⽹优化

根据不同的⽹络设置不同的超时时间

六、数据安全优化

使⽤Https,是基于http协议上的TLS安全协议,安全协议解决了保证安全降低加密成本

1、安全上

  • 使⽤加密算法组合对传输的数据加密,避免被窃听和篡改

  • 认证对⽅⾝份,避免被第三⽅冒充

  • 加密算法保持灵活可更新,防⽌定死算法被破解后⽆法更换,禁⽌已被破解的算法

2、降低加密成本

  • ⽤对称加密算法加密传输的数据,解决⾮对称加密算法的性能低和⻓度限制的问题

  • 缓存安全协议握⼿后的秘钥等数据,加快第⼆次建⽴连接的速度

  • 3、加快握⼿过程2RTT -> 0RTT。加快握⼿的思路,原本客⼾端和服务端需要协商使⽤什么算法后才 可以加密发送数据,变成通过内置的公钥和默认算法,在握⼿的同时,就把数据发送出去,不需要等 待握⼿就开始发送数据,达到0RTT

3.充分利用缓存

  • Get请求可以被缓存,Get请求也是幂等的,
  • 简单的处理缓存的80%需求

使⽤Get请求的代码设置如下:


///objective-c代码 
NSURLCache *urlCache =[[NSURLCache alloc]initWithMemoryCapacity:4 *1024 * 1024
diskCapacity:20 * 1024 *1024 diskPath:nil];
[NSURLCachesetSharedURLCache:urlCache];

4、控制缓存的有效性

1、⽂件缓存:借助ETag或者Last-Modified判断⽂件缓存是不是有效

Last-Modified

  • ⼤多采⽤资源变动后就重新⽣成⼀个链接的做法,但是不排除没有换链接的,这种情况下就需要借助 ETagorLast-Modified判断⽂件的有效性

  • Last-Modified 资源的最后修改时间戳,与缓存时间进⾏对⽐来判断是否过期,请求时返回If- Modified-Since,返回的数据若果没变化http返回的状态码就是403(Not changed),内容为空,节省 传输数据

ETagIf-None-Match

  • HTTP 协议规格说明定义ETag为“被请求变量的实体值” 。另⼀种说法是,ETag是⼀个可以与Web 资源关联的记号(token)。
  • 它是⼀个 hash 值,⽤作 Request 缓存请求头,每⼀个资源⽂件都对应⼀ 个唯⼀的 ETag 值。如果Etag没有改变,则返回状态码304,返回的内容为空

下载的图⽚的格式最好是WebP格式,因为他是同等图⽚质量下最⼩数量的图⽚,可以降低流量损失



作者:枫叶无处漂泊
链接:https://www.jianshu.com/p/66ee6798b99a

收起阅读 »

iOS - 极其强大的性能库DoraemonKit

每一个稍微有点规模的 App,总会自带一些线下的测试功能代码,比如环境切换功能、帧率查看功能等等,这些功能的切换入口往往放在各式各样的入口中,比如一些特殊的手势,双击 statusBar,双击某一个功能区块,或者新建一个 keyWindow 始终至于 App ...
继续阅读 »


每一个稍微有点规模的 App,总会自带一些线下的测试功能代码,比如环境切换功能、帧率查看功能等等,这些功能的切换入口往往放在各式各样的入口中,比如一些特殊的手势,双击 statusBar,双击某一个功能区块,或者新建一个 keyWindow 始终至于 App 最上方等等,而且每一个 App 里面的线下附带功能模块很多是相似的,比如帧率查看、内存和 CPU 监控等等,但是现在基本上都是每个 App 都是自己实现了一份,经历了以上的问题之后,DoKit 就有了它存在的意义。

DoKit 是一个功能平台,能够让每一个 App 快速接入一些常用的或者你没有实现的一些辅助开发工具、测试效率工具、视觉辅助工具,而且能够完美在 Doraemon 面板中接入你已经实现的与业务紧密耦合的一些非通有的辅助工具,并搭配我们的dokit平台,让功能得到延伸,接入方便,便于扩展。


一、平台工具(http://www.dokit.cn)

  1. 【数据Mock】 App接口Mock解决方案,提供一套基于App网络拦截的接口Mock方案,无需修改代码即可完成对于接口数据的Mock。
  2. 【健康体检】 一键式操作,整合DoKit多项工具,数据可视化,快速准确定位问题,让你对app的性能了如指掌。
  3. 【文件同步助手】 通过终端服务,让你的终端空间在平台端完整的展现并提供强大的文件以及数据库操作能力。
  4. 【一机多控】 主从同步,释放人力,让研发测试效率提升看得见

二、常用工具

  1. 【App 信息查看】 快速查看手机信息,App 基础信息、签名相关、权限信息的渠道,避免去手机设置查找或者查看项目源代码的麻烦;
  2. 【开发者选项 Android特有】 一键跳转开发者选项,避免安卓由于平台差异导致的入口不一致
  3. 【本地语言】 一键跳转本地语言,避免安卓由于平台差异导致的入口不一致
  4. 【沙盒浏览】 App 内部文件浏览的功能,支持删除和预览, 并且能通过 AirDrop 或者其他分享方式上传到 PC 中,进行更加细致的操作;
  5. 【MockGPS】 App 能定位到全国各地,支持地图地位和手动输入经纬度;
  6. 【H5任意门】 开发测试同学可以快速输入 H5 页面地址,查看该页面效果;
  7. 【Crash查看】 方便本地打印出出现 Crash 的堆栈;
  8. 【子线程UI】 快速定位哪一些 UI 操作在非主线程中进行渲染,避免不必要的问题;(iOS独有)
  9. 【清除本地数据】 一键删除沙盒中所有数据;
  10. 【NSLog】 把所有 NSLog 信息打印到UI界面,避免没有开发证书无法调试的尴尬;
  11. 【Lumberjack】 每一条 CocoaLumberjack 的日志信息,都在在 App 的界面中显示出来,再也不需要导出日志这么麻烦;(iOS独有)
  12. 【DBView】 通过网页方便快捷的操作应用内数据库,让数据库的调试变得非常优雅;
  13. 【模拟弱网】 限制网速,模拟弱网环境下App的运行情况。(android独有)

三、性能检测

  1. 【帧率】 App 帧率信息提供波形图查看功能,让帧率监控的趋势更加明显;
  2. 【CPU】 App CPU 使用率信息提供波形图查看功能,让 CPU 监控的趋势更加形象;
  3. 【内存】 App 内存使用量信息提供波形图查看功能,让内存监控的趋势更加鲜明;
  4. 【流量监控】 拦截 App 内部流量信息,提供波形图展示、流量概要展示、流量列表展示、流量筛选、流量详情,对流量信息统一拦截,成为我们 App 中自带的 "Charles";
  5. 【卡顿】 锁定 App 出现卡顿的时刻,打印出对应的代码调用堆栈;
  6. 【大图检测】 通过流量监测,找出所有的大小超标的图片,避免下载大图造成的流量浪费和渲染大图带来的CPU消耗。
  7. 【启动耗时】 无侵入的统计出App启动过程的总共耗时;
  8. 【UI层级检查】 检查出每一个页面中层级最深的元素;
  9. 【函数耗时】 从函数级别分析app性能瓶颈;
  10. 【Load】 找出所有的Load方法,并给出耗时分析;(iOS独有)
  11. 【内存泄漏】 找出App中所有的内存泄漏的问题。

四、视觉工具

  1. 【颜色吸管】 方便设计师 UI 捉虫的时候,查看每一个组件的颜色值是否设置正确;
  2. 【组件检查】 可以抓取任意一个UI控件,查看它们的详细信息,包括控件名称、控件位置、背景色、字体颜色、字体大小;
  3. 【对齐标尺】 参考 Android 系统自带测试工具,能够实时捕获屏幕坐标,并且可以查看组件是否对齐;
  4. 【元素边框线】 绘制出每一个 UI 组件的边框,对于组件布局有一定的参考意义。

五、Weex专项工具(CML专项工具)

  1. 【console日志查看】 方便在端上查看每一个Weex文件中的console日志,提供分级和搜索功能;
  2. 【storage缓存查看】 将Weex中的storage模块的本地缓存数据可视化展示;
  3. 【容器信息】 查看每一个打开的Weex页面的基本信息和性能数据;
  4. 【DevTool】 快速开启Weex DevTool的扫码入口。

tips : 如果使用我们滴滴优秀的开源跨端方案 chameleon 也可以集成该工具集合

六、支持自定义的业务工具集成到面板中

统一维护和管理所有的测试模块,详见接入手册

七、微信小程序专项工具


收起阅读 »

微信开源框架-崩溃、卡顿和爆内存Matrix

当前工具监控范围包括:崩溃、卡顿和爆内存,包含以下两款插件:WCCrashBlockMonitorPlugin: 基于 KSCrash 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力。WCMemoryStatPlu...
继续阅读 »

当前工具监控范围包括:崩溃、卡顿和爆内存,包含以下两款插件:

  • WCCrashBlockMonitorPlugin: 基于 KSCrash 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力。

  • WCMemoryStatPlugin: 一款性能优化到极致的爆内存监控工具,能够全面捕获应用爆内存时的内存分配以及调用堆栈情况。

特性

WCCrashBlockMonitorPlugin

  • 接入简单,代码无侵入
  • 通过检查 Runloop 运行状态判断应用是否卡顿,同时支持 iOS/macOS 平台
  • 增加耗时堆栈提取,卡顿线程快照日志中附加最近时间最耗时的主线程堆栈

WCMemoryStatPlugin

  • 在应用运行期间获取对象存活以及相应的堆栈信息,在检测到应用爆内存时进行上报
  • 使用平衡二叉树存储存活对象,使用 Hash Table 存储堆栈,将性能优化到极致

使用方法

安装

  • 通过 Cocoapods 安装

    1. 先安装 CocoaPods
    2. 通过 pod repo update 更新 matrix 的 Cocoapods 版本;
    3. 在 Podfile 对应的 target 中,添加 pod 'matrix-wechat',并执行 pod install;
    4. 在项目中使用 Cocoapods 生成的 .xcworkspace运行工程;
    5. 在你的代码文件头引入头文件 #import ,就可以接入微信的性能探针工具了!
  • 通过静态库安装

    1. 获取 Matrix 源码;
    2. 打开命令行,在 matrix/matrix-iOS 代码目录下执行 make 进行编译生成静态库;编译完成后,iOS 平台的库在 matrix/matrix-iOS/build_ios 目录下,macOS 平台的库在 matrix/matrix-iOS/build_macos目录下;
    3. 工程引入静态库:
    • iOS 平台:使用 matrix/matrix-iOS/build_ios 路径下的 Matrix.framework,将 Matrix.framework以静态库的方式引入工程;
    • macOS 平台:使用 matrix/matrix-iOS/build_macos 路径下的 Matrix.framework,将 Matrix.framework 以静态库的方式引入工程。
    1. 添加头文件 #import ,就可以接入微信的性能探针工具了!

启动监控

在以下地方:

  • 程序 main 函数入口;
  • AppDelegate 中的 application:didFinishLaunchingWithOptions:
  • 或者其他应用启动比较早的时间点。

添加类似如下代码,启动插件:

#import 

Matrix *matrix = [Matrix sharedInstance];
MatrixBuilder *curBuilder = [[MatrixBuilder alloc] init];
curBuilder.pluginListener = self; // pluginListener 回调 plugin 的相关事件

WCCrashBlockMonitorPlugin *crashBlockPlugin = [[WCCrashBlockMonitorPlugin alloc] init];
[curBuilder addPlugin:crashBlockPlugin]; // 添加卡顿和崩溃监控

WCMemoryStatPlugin *memoryStatPlugin = [[WCMemoryStatPlugin alloc] init];
[curBuilder addPlugin:memoryStatPlugin]; // 添加内存监控功能

[matrix addMatrixBuilder:curBuilder];

[crashBlockPlugin start]; // 开启卡顿和崩溃监控
// [memoryStatPlugin start];
// 开启内存监控,注意 memoryStatPlugin 开启之后对性能损耗较大,建议按需开启

接收回调获得监控数据

设置 MatrixBuilder 对象中的 pluginListener,实现 MatrixPluginListenerDelegate。

// 设置 delegate

MatrixBuilder *curBuilder = [[MatrixBuilder alloc] init];
curBuilder.pluginListener = <一个遵循 MatrixPluginListenerDelegate 的对象>;

// MatrixPluginListenerDelegate

- (void)onInit:(id)plugin;
- (void)onStart:(id)plugin;
- (void)onStop:(id)plugin;
- (void)onDestroy:(id)plugin;
- (void)onReportIssue:(MatrixIssue *)issue;

各个添加到 MatrixBuilder 的 plugin 会将对应的事件通过 pluginListener 回调。

重要:通过 onReportIssue: 获得 Matrix 处理后的数据,监控数据格式详见:Matrix for iOS/macOS 数据格式说明

Demo

至此,Matrix 已经集成到应用中并且开始收集崩溃、ANR、卡顿和爆内存数据,如仍有疑问,请查看示例:samples/sample-apple/MatrixDemo


常见问题及源码下载:https://github.com/Tencent/matrix#matrix_ios_cn



收起阅读 »

Objective-C & Swift 最轻量级 Hook 方案-SDMagicHook

本文从一个 iOS 日常开发的 hook 案例入手,首先简要介绍了 Objective-C 的动态特性以及传统 hook 方式常见的命名冲突、操作繁琐、hook 链意外断裂、hook 作用范围不可控制等缺陷,然后详细介绍了一套基于消息转发机制的 instanc...
继续阅读 »

本文从一个 iOS 日常开发的 hook 案例入手,首先简要介绍了 Objective-C 的动态特性以及传统 hook 方式常见的命名冲突、操作繁琐、hook 链意外断裂、hook 作用范围不可控制等缺陷,然后详细介绍了一套基于消息转发机制的 instance 粒度的轻量级 hook 方案:SDMagicHook


背景

某年某月的某一天,产品小 S 向开发君小 Q 提出了一个简约而不简单的需求:扩大一下某个 button 的点击区域。小 Q 听完暗自窃喜:还好,这是一个我自定义的 button,只需要重写一下 button 的 pointInside:withEvent:方法即可。只见小 Q 手起刀落在产品小 S 崇拜的目光中轻松完成。代码如下:



次日,产品小 S 又一次满怀期待地找到开发君小 Q:欧巴~,帮我把这个 button 也扩大一下点击区域吧。小 Q 这次却犯了难,心中暗自思忖:这是系统提供的标准 UI 组件里面的 button 啊,我只能拿来用没法改呀,我看你这分明就是故意为难我胖虎!我…我…我.----小 Q 卒。

在这个 case 中,小 Q 的遭遇着实令人同情。但是痛定思痛,难道产品提出的这个问题真的无解吗?其实不然,各位看官静息安坐,且听我慢慢分析:


1. Objective-C 的动态特性

Objective-C 作为一门古老而又灵活的语言有很多动态特性为开发者所津津乐道,这其中尤其以动态类型(Dynamic typing)、动态绑定(Dynamic binding)、动态加载(Dynamic loading)等特性最为著名,许多在其他语言中看似不可能实现的功能也可以在 OC 中利用这些动态特性达到事半功倍的效果。

1.1 动态类型(Dynamic typing)

动态类型就是说运行时才确定对象的真正类型。例如我们可以向一个 id 类型的对象发送任何消息,这在编译期都是合法的,因为类型是可以动态确定的,消息真正起作用的时机也是在运行时这个对象的类型确定以后,这个下面就会讲到。我们甚至可以在运行时动态修改一个对象的 isa 指针从而修改其类型,OC 中 KVO 的实现正是对动态类型的典型应用。

1.2 动态绑定(Dynamic binding)

当一个对象的类型被确定后,其对应的属性和可响应的消息也被确定,这就是动态绑定。绑定完成之后就可以在运行时根据对象的类型在类型信息中查找真正的函数地址然后执行。

1.3 动态加载(Dynamic loading)

根据需求加载所需要的素材资源和代码资源,用户可根据需求加载一些可执行的代码资源,而不是在在启动的时候就加载所有的组件,可执行代码可以含有新的类。

了解了 OC 的这些动态特性之后,让我们再次回顾一下产品的需求要领:产品只想任性地修改任何一个 button 的点击区域,而恰巧这次这个 button 是系统原生组件中的一个子 View。所以当前要解决的关键问题就是如何去改变一个用系统原生类实例化出来的组件的“点击区域检测方法”。刚才在 OC 动态类型特性的介绍中我们说过“消息真正起作用的时机是在运行时这个对象的类型确定以后”、“我们甚至可以在运行时动态修改一个对象的 isa 指针从而修改其类型,OC 中 KVO 的实现正是对动态类型的典型应用”。看到这里,你应该大概有了一些思路,我们不妨照猫画虎模仿 KVO 的原理来实现一下。

2. 初版 SDMagicHook 方案

要想使用这种类似 KVO 的替换 isa 指针的方案,首先需要解决以下几个问题:

2.1 如何动态创建一个新的类

在 OC 中,我们可以调用 runtime 的 objc_allocateClassPairobjc_registerClassPair 函数动态地生成新的类,然后调用 object_setClass 函数去将某个对象的 isa 替换为我们自建的临时类。

2.2 如何给这些新建的临时类命名

作为一个有意义的临时类名,首先得可以直观地看出这个临时类与其基类的关系,所以我们可以这样拼接新的类名[NSString stringWithFormat:@“SDHook*%s”, originalClsName],但这有一个很明显的问题就是无法做到一个对象独享一个专有类,为此我们可以继续扩充下,不妨在类名中加上一个对象的唯一标记–内存地址,新的类名组成是这样的[NSString stringWithFormat:@“SDHook_%s_%p”, originalClsName, self],这次看起来似乎完美了,但在极端的情况下还会出问题,例如我们在一个一万次的 for 循环中不断创建同一种类型的对象,那么就会大概率出现新对象的内存地址和之前已经释放了的对象的内存地址一样,而我们会在一个对象析构后很快就会去释放它所使用的临时类,这就会有概率导致那个新生成的对象正在使用的类被释放了然后就发生了 crash。为解决此类问题,我们需要再在这个临时的类名中添加一个随机标记来降低这种情况发生的概率,最终的类名组成是这样的[NSString stringWithFormat:@“SDHook_%s_%p_%d”, originalClsName, self, mgr.randomFlag]


2.3 何时销毁这些临时类

我们通过 objc_setAssociatedObject 的方式可以为每个 NSObject 对象动态关联上一个 SDNewClassManager 实例,在 SDNewClassManager 实例里面持有当前对象所使用的临时类。当前对象销毁时也会销毁这个 SDNewClassManager 实例,然后我们就可以在 SDNewClassManager 实例的 dealloc 方法里面做一些销毁临时类的操作。但这里我们又不能立即做销毁临时类的操作,因为此时这个对象还没有完全析构,它还在做一些其它善后操作,如果此时去销毁那个临时类必然会造成 crash,所以我们需要稍微延迟一段时间来做这些临时类的销毁操作,代码如下:




好了,到目前为止我们已经实现了第一版 hook 方案,不过这里两个明显的问题:

  1. 每次 hook 都要增加一个 category 定义一个函数相对比较麻烦;
  2. 如果我们在某个 Class 的两个 category 里面分别实现了一个同名的方法就会导致只有一个方法最终能被调用到。

为此,我们研发了第二版针对第一版的不足予以改进和优化。

3. 优化版 SDMagicHook 方案

针对上面提到的两个问题,我们可以通过用 block 生成 IMP 然后将这个 IMP 替换到目标 Selector 对应的 method 上即可,API 示例代码如下:


这个 block 方案看上去确实简洁和方便了很多,但同样面临着任何一个 hook 方案都避不开的问题那就是,如何在 block 里面调用原生的对应方法呢?

3.1 关键点一:如何在 block 里面调用原生方法

在初版方案中,我们在一个类的 category 中增加了一个 hook 专用的方法,然后在完成方法交换之后通过向实例发送 hook 专用的方法自身对应的 selector 消息即可实现对原生方法的回调。但是现在我们是使用的 block 创建了一个“匿名函数”来替换原生方法,既然是匿名函数也就没有明确的 selector,这也就意味着我们根本没有办法在方法交换后找到它的原生方法了!

那么眼下的关键问题就是找到一个合适的 Selector 来映射到被 hook 的原生函数。而目前来看,我们唯一可以在当前编译环境下方便调用且和这个 block 还有一定关联关系的 Selector 就是原方法的 Selector 也就是我们的 demo 中的pointInside:withEvent:了。这样一来pointInside:withEvent:这个 Selector 就变成了一个一对多的映射 key,当有人在外部向我们的 button 发送 pointInside:withEvent:消息时,我们应该首先将 pointInside:withEvent:转发给我们自定义的 block 实现的 IMP,然后当在 block 内部再次向 button 发送 pointInside:withEvent:消息时就将这个消息转发给系统原生的方法实现,如此一来就可以完成了一次完美的方法调度了。

3.2 关键点二:如何设计消息调度方案

在 OC 中要想调度方法派发就需要拿到消息转发的控制权,而要想获得这个消息转发控制权就需要强制让这个 receiver 每次收到这个消息都触发其消息转发机制然后我们在消息转发的过程中做对应的调度。在这个例子中我们将目标 button 的 pointInside:withEvent:对应的 method 的 imp 指针替换为_objc_msgForward,这样每当有人调用这个 button 的 pointInside:withEvent:方法时最终都会走到消息转发方法 forwardInvocation:里面,我们实现这个方法来完成具体的方法调度工作。

因为目标 button 的 pointInside:withEvent:对应的 method 的 imp 指针被替换成了_objc_msgForward,所以我们需要另外新增一个方法 A 和方法 B 来分别存储目标 button 的 pointInside:withEvent:方法的 block 自定义实现和原生实现。然后当需要在自定义的方法内部调用原始方法时通过调用 callOriginalMethodInBlock:这个 api 来显式告知,示例代码如下:



当目标 button 实例收到 pointInside:withEvent:消息时会启用我们自定义的消息调度机制,检查如果 OriginalCallFlag 为 false 就去调用自定义实现方法 A,否则就去调用原始实现方法 B,从而顺利实现一次方法调度。流程图及示例代码如下:




想象这样一个应用场景:有一个全局的 keywindow,各个业务都想监听一下 keywindow 的 layoutSubviews 方法,那我们该如何去管理和维护添加到 keywindow 上的多个 hook 实现之间的关系呢?如果一个对象要销毁了,它需要移除掉之前对 keywindow 的 hook,这时又该如何处理呢?

我们的解决方案是为每个被 hook 的目标原生方法生成一张 hook 表,按照 hook 发生的顺序依次为其生成内部 selector 并加入到 hook 表中。当 keywindow 收到 layoutSubviews 消息时,我们从 hook 表中取出该次消息对应的 hook selector 发送给 keywindow 让它执行对应的动作。如果删除某个 hook 也只需将其对应的 selector 从 hook 表中移除即可。代码如下:




4. 防止 hook 链意外断裂

我们都知道在对某个方法进行 hook 操作时都需要在我们的 hook 代码方法体中调用一下被 hook 的那个原始方法,如果遗漏了此步操作就会造成 hook 链断裂,这样就会导致被 hook 的那个原始方法永远不会被调用到,如果有人在你之前也 hook 了这个方法的话就会导致在你之前的所有 hook 都莫名失效了,因为这是一个很隐蔽的问题所以你往往很难意识到你的 hook 操作已经给其他人造成了严重的问题。

为了方便 hook 操作者快速及时发现这一问题,我们在 DEBUG 模式下增加了一套“hook 链断裂检测机制”,其实现原理大致如下:

前面已经提到过,我们实现了对 hook 目标方法的自定义调度,这就使得我们有机会在这些方法调用结束后检测其是否在方法执行过程中通过 callOriginalMethodInBlock 调用原始方法。如果发现某个方法体不是被 hook 的目标函数的最原始的方法体且这次方法执行结束之后也没有调用过原始方法就会通过 raise(SIGTRAP)方式发送一个中断信号暂停当前的程序以提醒开发者当次 hook 操作没有调用原始方法。



5. SDMagicHook 的优缺点

与传统的在 category 中新增一个自定义方法然后进行 hook 的方案对比,SDMagicHook 的优缺点如下:

优点:

  1. 只用一个 block 即可对任意一个实例的任意方法实现 hook 操作,不需要新增任何 category,简洁高效,可以大大提高你调试程序的效率;
  2. hook 的作用域可以控制在单个实例粒度内,将 hook 的副作用降到最低;
  3. 可以对任意普通实例甚至任意类进行 hook 操作,无论这个实例或者类是你自己生成的还是第三方提供的;
  4. 可以随时添加或去除者任意 hook,易于对 hook 进行管理。

缺点:

  1. 为了保证增删 hook 时的线程安全,SDMagicHook 进行增删 hook 相关的操作时在实例粒度内增加了读写锁,如果有在多线程频繁的 hook 操作可能会带来一点线程等待开销,但是大多数情况下可以忽略不计;
  2. 因为是基于实例维度的所以比较适合处理对某个类的个别实例进行 hook 的场景,如果你需要你的 hook 对某个类的所有实例都生效建议继续沿用传统方式的 hook。

总结

SDMagicHook 方案在 OC 中和 Swift 的 UIKit 层均可直接使用,而且 hook 作用域可以限制在你指定的某个实例范围内从而避免污染其它不相关的实例。Api 设计简洁易用,你只需要花费一分钟的时间即可轻松快速上手,希望我们的这套方案可以给你带来更美妙的 iOS 开发体验。



Github 项目地址:https://github.com/larksuite/SDMagicHook

源码下载:SDMagicHook-master.zip



收起阅读 »

iOS抖音的转场动画

转场调用代码- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { AwemeListV...
继续阅读 »



转场调用代码


- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
AwemeListViewController *awemeVC = [[AwemeListViewController alloc] init];
awemeVC.transitioningDelegate = self; //0

// 1
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
// 2
CGRect cellFrame = cell.frame;
// 3
CGRect cellConvertedFrame = [collectionView convertRect:cellFrame toView:collectionView.superview];

//弹窗转场
self.presentScaleAnimation.cellConvertFrame = cellConvertedFrame; //4

//消失转场
self.dismissScaleAnimation.selectCell = cell; // 5
self.dismissScaleAnimation.originCellFrame = cellFrame; //6
self.dismissScaleAnimation.finalCellFrame = cellConvertedFrame; //7

awemeVC.modalPresentationStyle = UIModalPresentationOverCurrentContext; //8
self.modalPresentationStyle = UIModalPresentationCurrentContext; //9

[self.leftDragInteractiveTransition wireToViewController:awemeVC];
[self presentViewController:awemeVC animated:YES completion:nil];
}

0 处代码使我们需要把当前的类做为转场的代理
1 这里我们要拿出cell这个view
2 拿出当前Cell的frame坐标
3 cell的坐标转成屏幕坐标
4 设置弹出时候需要cell在屏幕的位置坐标
5 设置消失转场需要的选中cell视图
6 设置消失转场原始cell坐标位置
7 设置消失转场最终得cell屏幕坐标位置 用于消失完成回到原来位置的动画
8 设置弹出得vc弹出样式 这个用于显示弹出VC得时候 默认底部使blua的高斯模糊
9 设置当前VC的模态弹出样式为当前的弹出上下文

5~7 步设置的消失转场动画 下面会讲解
这里我们用的是前面讲上下滑的VC对象 大家不必担心 当它是一个普通的UIViewController即可

实现转场所需要的代理

首先在需要实现UIViewControllerTransitioningDelegate这个代理


 #pragma mark -
#pragma mark - UIViewControllerAnimatedTransitioning Delegate
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {

return self.presentScaleAnimation; //present VC
}

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return self.dismissScaleAnimation; //dismiss VC
}

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
return self.leftDragInteractiveTransition.isInteracting? self.leftDragInteractiveTransition: nil;
}

这里面我们看到我们分别返回了

  • 弹出动画实例self.presentScaleAnimation
  • dismiss动画实例self.dismissScaleAnimation
  • 以及self.leftDragInteractiveTransition实例用于负责转场切换的具体实现

所以我们需要在 当前的VC中声明3个成员变量 并初始化

@property (nonatomic, strong) PresentScaleAnimation *presentScaleAnimation;
@property (nonatomic, strong) DismissScaleAnimation *dismissScaleAnimation;
@property (nonatomic, strong) DragLeftInteractiveTransition *leftDragInteractiveTransition;

并在viewDidLoad:方法中初始化一下

 //转场的两个动画
self.presentScaleAnimation = [[PresentScaleAnimation alloc] init];
self.dismissScaleAnimation = [[DismissScaleAnimation alloc] init];
self.leftDragInteractiveTransition = [DragLeftInteractiveTransition new];

这里我说一下这三个成员都负责啥事
首先DragLeftInteractiveTransition类负责转场的 手势 过程,就是pan手势在这个类里面实现,并继承自UIPercentDrivenInteractiveTransition类,这是iOS7以后系统提供的转场基类必须在interactionControllerForDismissal:代理协议中返回这个类或者子类的实例对象,所以我们生成一个成员变量self.leftDragInteractiveTransition

其次是弹出present和消失dismiss的动画类,这俩类其实是负责简单的手势完成之后的动画.

这两个类都是继承自NSObject并实现UIViewControllerAnimatedTransitioning协议的类,这个协议里面有 需要你复写某些方法返回具体的动画执行时间,和中间过程中我们需要的相关的容器视图以及控制器的视图实例,当我们自己执行完成之后调用相关的block回答告知转场是否完成就行了.

 @implementation PresentScaleAnimation

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
return 0.3f;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
if (CGRectEqualToRect(self.cellConvertFrame, CGRectZero)) {
[transitionContext completeTransition:YES];
return;
}
CGRect initialFrame = self.cellConvertFrame;

UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];

CGRect finalFrame = [transitionContext finalFrameForViewController:toVC];
NSTimeInterval duration = [self transitionDuration:transitionContext];

toVC.view.center = CGPointMake(initialFrame.origin.x + initialFrame.size.width/2, initialFrame.origin.y + initialFrame.size.height/2);
toVC.view.transform = CGAffineTransformMakeScale(initialFrame.size.width/finalFrame.size.width, initialFrame.size.height/finalFrame.size.height);

[UIView animateWithDuration:duration
delay:0
usingSpringWithDamping:0.8
initialSpringVelocity:1
options:UIViewAnimationOptionLayoutSubviews
animations:^{
toVC.view.center = CGPointMake(finalFrame.origin.x + finalFrame.size.width/2, finalFrame.origin.y + finalFrame.size.height/2);
toVC.view.transform = CGAffineTransformMakeScale(1, 1);
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
@end

很简单.

消失的动画 同上边差不多

@interface DismissScaleAnimation ()

@end

@implementation DismissScaleAnimation

- (instancetype)init {
self = [super init];
if (self) {
_centerFrame = CGRectMake((ScreenWidth - 5)/2, (ScreenHeight - 5)/2, 5, 5);
}
return self;
}

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
return 0.25f;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
// UINavigationController *toNavigation = (UINavigationController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
// UIViewController *toVC = [toNavigation viewControllers].firstObject;


UIView *snapshotView;
CGFloat scaleRatio;
CGRect finalFrame = self.finalCellFrame;
if(self.selectCell && !CGRectEqualToRect(finalFrame, CGRectZero)) {
snapshotView = [self.selectCell snapshotViewAfterScreenUpdates:NO];
scaleRatio = fromVC.view.frame.size.width/self.selectCell.frame.size.width;
snapshotView.layer.zPosition = 20;
}else {
snapshotView = [fromVC.view snapshotViewAfterScreenUpdates:NO];
scaleRatio = fromVC.view.frame.size.width/ScreenWidth;
finalFrame = _centerFrame;
}

UIView *containerView = [transitionContext containerView];
[containerView addSubview:snapshotView];

NSTimeInterval duration = [self transitionDuration:transitionContext];

fromVC.view.alpha = 0.0f;
snapshotView.center = fromVC.view.center;
snapshotView.transform = CGAffineTransformMakeScale(scaleRatio, scaleRatio);
[UIView animateWithDuration:duration
delay:0
usingSpringWithDamping:0.8
initialSpringVelocity:0.2
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
snapshotView.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
snapshotView.frame = finalFrame;
} completion:^(BOOL finished) {
[transitionContext finishInteractiveTransition];
[transitionContext completeTransition:YES];
[snapshotView removeFromSuperview];
}];
}



@end
我们重点需要说一下 转场过渡的类DragLeftInteractiveTransition继承自UIPercentDrivenInteractiveTransition负责转场过程,
头文件的声明

@interface DragLeftInteractiveTransition : UIPercentDrivenInteractiveTransition

/** 是否正在拖动返回 标识是否正在使用转场的交互中 */
@property (nonatomic, assign) BOOL isInteracting;


/**
设置需要返回的VC

@param viewController 控制器实例
*/

-(void)wireToViewController:(UIViewController *)viewController;


@end


实现


@interface DragLeftInteractiveTransition ()

@property (nonatomic, strong) UIViewController *presentingVC;
@property (nonatomic, assign) CGPoint viewControllerCenter;
@property (nonatomic, strong) CALayer *transitionMaskLayer;

@end

@implementation DragLeftInteractiveTransition

#pragma mark -
#pragma mark - override methods 复写方法
-(CGFloat)completionSpeed{
return 1 - self.percentComplete;
}

- (void)updateInteractiveTransition:(CGFloat)percentComplete {
NSLog(@"%.2f",percentComplete);

}

- (void)cancelInteractiveTransition {
NSLog(@"转场取消");
}

- (void)finishInteractiveTransition {
NSLog(@"转场完成");
}


- (CALayer *)transitionMaskLayer {
if (_transitionMaskLayer == nil) {
_transitionMaskLayer = [CALayer layer];
}
return _transitionMaskLayer;
}

#pragma mark -
#pragma mark - private methods 私有方法
- (void)prepareGestureRecognizerInView:(UIView*)view {
UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
[view addGestureRecognizer:gesture];
}

#pragma mark -
#pragma mark - event response 所有触发的事件响应 按钮、通知、分段控件等
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer {
UIView *vcView = gestureRecognizer.view;
CGPoint translation = [gestureRecognizer translationInView:vcView.superview];
if(!self.isInteracting &&
(translation.x < 0 ||
translation.y < 0 ||
translation.x < translation.y)) {
return;
}
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:{
//修复当从右侧向左滑动的时候的bug 避免开始的时候从又向左滑动 当未开始的时候
CGPoint vel = [gestureRecognizer velocityInView:gestureRecognizer.view];
if (!self.isInteracting && vel.x < 0) {
self.isInteracting = NO;
return;
}
self.transitionMaskLayer.frame = vcView.frame;
self.transitionMaskLayer.opaque = NO;
self.transitionMaskLayer.opacity = 1;
self.transitionMaskLayer.backgroundColor = [UIColor whiteColor].CGColor; //必须有颜色不能透明
[self.transitionMaskLayer setNeedsDisplay];
[self.transitionMaskLayer displayIfNeeded];
self.transitionMaskLayer.anchorPoint = CGPointMake(0.5, 0.5);
self.transitionMaskLayer.position = CGPointMake(vcView.frame.size.width/2.0f, vcView.frame.size.height/2.0f);
vcView.layer.mask = self.transitionMaskLayer;
vcView.layer.masksToBounds = YES;

self.isInteracting = YES;
}
break;
case UIGestureRecognizerStateChanged: {
CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width;
progress = fminf(fmaxf(progress, 0.0), 1.0);

CGFloat ratio = 1.0f - progress*0.5f;
[_presentingVC.view setCenter:CGPointMake(_viewControllerCenter.x + translation.x * ratio, _viewControllerCenter.y + translation.y * ratio)];
_presentingVC.view.transform = CGAffineTransformMakeScale(ratio, ratio);
[self updateInteractiveTransition:progress];
break;
}
case UIGestureRecognizerStateCancelled:
case UIGestureRecognizerStateEnded:{
CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width;
progress = fminf(fmaxf(progress, 0.0), 1.0);
if (progress < 0.2){
[UIView animateWithDuration:progress
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGFloat w = [UIScreen mainScreen].bounds.size.width;
CGFloat h = [UIScreen mainScreen].bounds.size.height;
[self.presentingVC.view setCenter:CGPointMake(w/2, h/2)];
self.presentingVC.view.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} completion:^(BOOL finished) {
self.isInteracting = NO;
[self cancelInteractiveTransition];
}];
}else {
_isInteracting = NO;
[self finishInteractiveTransition];
[_presentingVC dismissViewControllerAnimated:YES completion:nil];
}
//移除 遮罩
[self.transitionMaskLayer removeFromSuperlayer];
self.transitionMaskLayer = nil;
}
break;
default:
break;
}
}

#pragma mark -
#pragma mark - public methods 公有方法
-(void)wireToViewController:(UIViewController *)viewController {
self.presentingVC = viewController;
self.viewControllerCenter = viewController.view.center;
[self prepareGestureRecognizerInView:viewController.view];
}

@end


关键的核心代码

[self updateInteractiveTransition:progress];

最后 手势结束

CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width;
progress = fminf(fmaxf(progress, 0.0), 1.0);
if (progress < 0.2){
[UIView animateWithDuration:progress
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGFloat w = [UIScreen mainScreen].bounds.size.width;
CGFloat h = [UIScreen mainScreen].bounds.size.height;
[self.presentingVC.view setCenter:CGPointMake(w/2, h/2)];
self.presentingVC.view.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} completion:^(BOOL finished) {
self.isInteracting = NO;
[self cancelInteractiveTransition];
}];
}else {
_isInteracting = NO;
[self finishInteractiveTransition];
[_presentingVC dismissViewControllerAnimated:YES completion:nil];
}
//移除 遮罩
[self.transitionMaskLayer removeFromSuperlayer];
self.transitionMaskLayer = nil;


demo及常见问题:https://github.com/sunyazhou13/AwemeDemoTransition



收起阅读 »

UITableView 建模

tableview 是开发中项目中常用的视图控件,并且是重复的使用,布局类似,只是数据源及Cell更改,所以会出现很多重复的内容,并且即使新建一个基础的列表也要重复这些固定逻辑的代码,这对于开发效率很不友好。本文的重点是抽取重复的逻辑代码,简化列表页面的搭建,...
继续阅读 »
tableview 是开发中项目中常用的视图控件,并且是重复的使用,布局类似,只是数据源及Cell更改,所以会出现很多重复的内容,并且即使新建一个基础的列表也要重复这些固定逻辑的代码,这对于开发效率很不友好。
本文的重点是抽取重复的逻辑代码简化列表页面的搭建,达到数据驱动列表

说明:
首先tableview有两个代理delegate 和 datasource(基于单一职责设计规则)
delegate :负责交互事件;
datasource :负责cell创建及数据填充,这也是本文探讨的重点。
(1)基本原则
苹果将tableView的数据通过一个二维数组构建(组,行),这是一个很重要的设计点,要沿着这套规则继续发展,设计模式的继承,才是避免坏代码产生的基础。
(2)组
“组”是这套逻辑的根基先有组再有行,并且列表动态修改的内容都是以为基础,的结构相对固定,因此本文将抽离成一个数据模型而不是接口


#import <Foundation/Foundation.h>
#import "RWCellViewModelProtocol.h"

@interface RWSectionModel : NSObject
/// item数组:元素必须是遵守RWCellViewModel协议
@property (nonatomic, strong) NSMutableArray <id<RWCellViewModel>>*itemsArray;

/// section头部高度
@property (nonatomic, assign) CGFloat sectionHeaderHeight;
/// section尾部高度
@property (nonatomic, assign) CGFloat sectionFooterHeight;
/// sectionHeaderView: 必须是UITableViewHeaderFooterView或其子类,并且遵循RWHeaderFooterDataSource协议
@property (nonatomic, strong) Class headerReuseClass;
/// sectionFooterView: 必须是UITableViewHeaderFooterView或其子类,并且遵循RWHeaderFooterDataSource协议
@property (nonatomic, strong) Class footerReuseClass;

/// headerData
@property (nonatomic, strong) id headerData;
/// footerData
@property (nonatomic, strong) id footerData;
@end

(2)行
最核心的有三大CellCell高度Cell数据
这次的设计参考MVVM设计模式,对于行的要素提取成一个ViewModel,并且ViewModel要做成接口的方式,因为行除了这三个基本的元素外,可能要需要Cell填充的数据,比如titleString,subTitleString,headerImage等等,这样便于扩展。


#ifndef RWCellViewModel_h
#define RWCellViewModel_h

@import UIKit;

@protocol RWCellViewModel <NSObject>
/// Cell 的类型
@property (nonatomic, strong) Class cellClass;
/// Cell的高度: 0 则是UITableViewAutomaticDimension
@property (nonatomic, assign) CGFloat cellHeight;
@end

#endif /* RWCellViewModel_h */

(3)tableView
此处不用使用tableViewController的方式,而使用view的方式,这样嵌入更方便。并且对外提供基本的接口,用于列表数据的获取,及点击事件处理。

备注:
关于数据,这里提供了多组和单组的两个接口,为了减少使用的过程中外部新建RWSectionModel这一步,但是其内部还是基于RWSectionModel这一个模型。


#import <UIKit/UIKit.h>
#import "RWCellViewModelProtocol.h"
#import "RWSectionModel.h"

@protocol RWTableViewDelegate;

@interface RWTableView : UITableView
/// rwdelegate
@property (nonatomic, weak) id<RWTableViewDelegate> rwdelegate;

/// 构建方法
/// @param delegate 是指rwdelegate
- (instancetype)initWithDelegate:(id<RWTableViewDelegate>)delegate;

@end


@protocol RWTableViewDelegate <NSObject>
@optional
/// 多组构建数据
- (NSArray <RWSectionModel*>*)tableViewWithMutilSectionDataArray;

/// 单组构建数据
- (NSArray <id<RWCellViewModel>>*)tableViewWithSigleSectionDataArray;


/// cell点击事件
/// @param data cell数据模型
/// @param indexPath indexPath
- (void)tableViewDidSelectedCellWithDataModel:(id)data indexPath:(NSIndexPath *)indexPath;

RWTableview.m

#pragma mark - dataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
/// 数据源始终保持“二维数组的状态”,即SectionModel中包裹items的方式
if ([self.rwdelegate respondsToSelector:@selector(tableViewWithMutilSectionDataArray)]) {
self.dataArray = [self.rwdelegate tableViewWithMutilSectionDataArray];
return self.dataArray.count;
}
else if ([self.rwdelegate respondsToSelector:@selector(tableViewWithSigleSectionDataArray)]) {
RWSectionModel *sectionModel = [[RWSectionModel alloc]init];
sectionModel.itemsArray = [self.rwdelegate tableViewWithSigleSectionDataArray].mutableCopy;
self.dataArray = @[sectionModel];
return 1;
}
return 0;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:section];
return sectionModel.itemsArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
/// 此处只做Cell的复用或创建
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellViewModel.cellClass)];
if (cell == nil) {
cell = [[cellViewModel.cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellViewModel.cellClass)];
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}

(4)Cell上子控件的交互事件处理
腾讯QQ部门的大神峰之巅提供了一个很好的解决办法,基于苹果现有的响应链(真的很牛逼),将点击事件传递给下个响应者,而不需要为事件的传递搭建更多的依赖关系。这是一篇鸡汤文章,有很多营养,比如tableview模块化,这也是我接下来要学习的。

#import <UIKit/UIKit.h>
#import "RWEvent.h"

@interface UIResponder (RWEvent)

- (void)respondEvent:(NSObject<RWEvent> *)event;

@end
#import "UIResponder+RWEvent.h"

@implementation UIResponder (RWEvent)

- (void)respondEvent:(NSObject<RWEvent> *)event {
[self.nextResponder respondEvent:event];
}

@end


2020年11月18日 更新

鉴于此tableView封装在实际项目遇到的问题进行改善,主要内容如下:
(1)使用分类的方式替换协议
优点:分类能更便捷的扩展原有类,并且使用更方便,不需要再导入协议文件及遵守协议
【RWCellDataSource协议】替换成:【UITableViewCell (RWData)】
【RWHeaderFooterDataSource协议】 替换成:【UITableViewHeaderFooterView (RWData)】

(2)cell高度缓存的勘误
willDisplayCell:中要想获取准确的Cell高度,那么必须在heightForRowAtIndexPath:方法中给Cell赋值,因为系统计算Cell的高度是在这个方法中进行的

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellViewModel.cellClass)];
/// Cell创建
if (cell == nil) {
cell = [[cellViewModel.cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellViewModel.cellClass)];
}
/// Cell赋值
[cell rw_setData:cellViewModel];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
return cellViewModel.cellHeight ? : UITableViewAutomaticDimension;
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
/// 高度缓存
/// 此处高度做一个缓存是为了高度自适应的Cell,避免重复计算的工作量,对于性能优化有些帮助
/// 如果想要在willDisplayCell获取到准确的Cell高度,那么必须在cellForRowAtIndexPath:方法给Cell赋值
/// 同时可以避免由于高度自适应导致Cell的定位不准确,比如置顶或者滑动到某一个Cell的位置
/// 如果自动布局要更新高度,可以将cellViewModel设置为0
cellViewModel.cellHeight = cell.frame.size.height;
}













收起阅读 »

iOS-分页控制器

使用:1、创建方法1.1 导入头文件#import "XLPageViewController.h"1.2 遵守协议@interface ViewController ()<XLPageViewControllerDelegate, XLPageView...
继续阅读 »




使用:

1、创建方法

1.1 导入头文件

#import "XLPageViewController.h"
1.2 遵守协议
@interface ViewController ()<XLPageViewControllerDelegate, XLPageViewControllerDataSrouce>
1.3 创建外观配置类

注:config负责所有的外观配置,defaultConfig方法设定了默认参数,使用时可按需配置。 →Config属性列表

  XLPageViewControllerConfig *config = [XLPageViewControllerConfig defaultConfig];

1.4 创建分页控制器

注:需要把pageViewController添加为当前视图控制器的子视图控制器,才能实现子视图控制器中的界面跳转。

  XLPageViewController *pageViewController = [[XLPageViewController alloc] initWithConfig:config];
pageViewController.view.frame = self.view.bounds;
pageViewController.delegate = self;
pageViewController.dataSource = self;
[self.view addSubview:pageViewController.view];
[self addChildViewController:pageViewController];
2、协议

2.1 XLPageViewControllerDelegate

//回调切换位置
- (void)pageViewController:(XLPageViewController *)pageViewController didSelectedAtIndex:(NSInteger)index;

2.2 XLPageViewControllerDataSrouce

@required

//根据index创建对应的视图控制器,每个试图控制器只会被创建一次。
- (UIViewController *)pageViewController:(XLPageViewController *)pageViewController viewControllerForIndex:(NSInteger)index;
//根据index返回对应的标题
- (NSString *)pageViewController:(XLPageViewController *)pageViewController titleForIndex:(NSInteger)index;
//返回分页数
- (NSInteger)pageViewControllerNumberOfPage;

@optional

//标题cell复用方法,自定义标题cell时用到
- (__kindof XLPageTitleCell *)pageViewController:(XLPageViewController *)pageViewController titleViewCellForItemAtIndex:(NSInteger)index;

3、自定义标题cell

3.1 创建一个XLPageTitleCell的子类

#import "XLPageTitleCell.h"

@interface CustomPageTitleCell : XLPageTitleCell

@end

3.2 注册cell、添加创建cell

//需要先注册cell
[self.pageViewController registerClass:CustomPageTitleCell.class forTitleViewCellWithReuseIdentifier:@"CustomPageTitleCell"];
//自定义标题cell创建方法
- (XLPageTitleCell *)pageViewController:(XLPageViewController *)pageViewController titleViewCellForItemAtIndex:(NSInteger)index {
CustomPageTitleCell *cell = [pageViewController dequeueReusableTitleViewCellWithIdentifier:@"CustomPageTitleCell" forIndex:index];
return cell;
}

3.3 复写cell父类方法

//通过此父类方法配置标题cell是否被选中样式
- (void)configCellOfSelected:(BOOL)selected {

}

//通过此父类方法配置标题cell动画;type:区分是当前选中cell/将要被选中的cell;progress:动画进度0~1
- (void)showAnimationOfProgress:(CGFloat)progress type:(XLPageTitleCellAnimationType)type {

}

4、特殊情况处理

4.1 和子view手势冲突问题

pageViewController的子视图中存在可滚动的子view,例如UISlider、UIScrollView等,如果子view和pageViewController发生滚动冲突时,可设置子view的xl_letMeScrollFirst属性为true。

  UISlider *slider = [[UISlider alloc] init];
slider.xl_letMeScrollFirst = true;
[childVC.view addSubview:slider];

4.2 全屏返回手势问题

pageViewController和全屏返回手势一起使用时,需要将其它手势的delegate的类名添加到respondOtherGestureDelegateClassList属性中。当滚动到第一个分页时,向右滑动会优先响应全屏返回。以FDFullscreenPopGesture为例:

self.pageViewController.respondOtherGestureDelegateClassList = @[@"_FDFullscreenPopGestureRecognizerDelegate"];

5、注意事项

使用时需注意标题不要重复标题是定位ViewController的唯一ID。


源码下载:XLPageViewController-master.zip 

常见问题及demo:https://github.com/mengxianliang/XLPageViewController





收起阅读 »

iOS - 呼吸动画库

先看效果

先看效果



需求和实现思路

具体要求

  • 内部头像呼吸放大缩小 无限循环
  • 每次放大同时需要背景还有一张图也放大 并且透明
  • 点击缩放整个背景视图


实现思路

首先 需要使用创建一个Layer 装第一个无限放大缩小的呼吸的图 背景也需要一个Layer 做 放大+透明度渐变的动画组并且也放置一张需要放大渐变的图片

最后点击触发. 添加一个一次性的缩放动画即可

呼吸动画layer和动画

呼吸layer

CALayer *layer = [CALayer layer];
layer.position = CGPointMake(kHeartSizeWidth/2.0f, kHeartSizeHeight/2.0f);
layer.bounds = CGRectMake(0, 0, kHeartSizeWidth/2.0f, kHeartSizeHeight/2.0f);
layer.backgroundColor = [UIColor clearColor].CGColor;
layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"breathImage"].CGImage);
layer.contentsGravity = kCAGravityResizeAspect;
[self.heartView.layer addSublayer:layer];
复制代码

kHeartSizeHeight 和kHeartSizeWidth 是常量 demo中写好了100

加帧动画

CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
animation.values = @[@1.f, @1.4f, @1.f];
animation.keyTimes = @[@0.f, @0.5f, @1.f];
animation.duration = 1; //1000ms
animation.repeatCount = FLT_MAX;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[animation setValue:kBreathAnimationKey forKey:kBreathAnimationName];
[layer addAnimation:animation forKey:kBreathAnimationKey];
复制代码

差值器也可以自定义 例如:

[CAMediaTimingFunction functionWithControlPoints:0.33 :0 :0.67 :1]
复制代码

这里我做的持续时常1秒

放大渐变动画group

创建新layer

CALayer *breathLayer = [CALayer layer];
breathLayer.position = layer.position;
breathLayer.bounds = layer.bounds;
breathLayer.backgroundColor = [UIColor clearColor].CGColor;
breathLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"breathImage"].CGImage);
breathLayer.contentsGravity = kCAGravityResizeAspect;
[self.heartView.layer insertSublayer:breathLayer below:layer];
//[self.heartView.layer addSublayer:breathLayer];
复制代码

这里用的是放在 呼吸layer后边 如果想放在呼吸layer前边 就把里面注释打开 然后注掉 inert那行代码

动画组 包含 放大 渐变


//缩放
CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.values = @[@1.f, @2.4f];
scaleAnimation.keyTimes = @[@0.f,@1.f];
scaleAnimation.duration = animation.duration;
scaleAnimation.repeatCount = FLT_MAX;
scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
//透明度
CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animation];
opacityAnimation.keyPath = @"opacity";
opacityAnimation.values = @[@1.f, @0.f];
opacityAnimation.duration = 0.4f;
opacityAnimation.keyTimes = @[@0.f, @1.f];
opacityAnimation.repeatCount = FLT_MAX;
opacityAnimation.duration = animation.duration;
opacityAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];

//动画组
CAAnimationGroup *scaleOpacityGroup = [CAAnimationGroup animation];
scaleOpacityGroup.animations = @[scaleAnimation, opacityAnimation];
scaleOpacityGroup.removedOnCompletion = NO;
scaleOpacityGroup.fillMode = kCAFillModeForwards;
scaleOpacityGroup.duration = animation.duration;
scaleOpacityGroup.repeatCount = FLT_MAX;
[breathLayer addAnimation:scaleOpacityGroup forKey:kBreathScaleName];
复制代码

点击缩放动画

跟第一个一样 只不过 执行次数默认一次 执行完就可以了

- (void)shakeAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
animation.values = @[@1.0f, @0.8f, @1.f];
animation.keyTimes = @[@0.f,@0.5f, @1.f];
animation.duration = 0.35f;
animation.timingFunctions = @[[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[self.heartView.layer addAnimation:animation forKey:@""];
}
复制代码

手势触发的时候 调用一下 

源码及demo地址:https://github.com/sunyazhou13/BreathAnimation



iOS 攻防 - ptrace

在破解一款App的时候,在实际破解之前肯定是在做调试。LLDB之所以能附加进程时因为debugserver,而debugserver附加是通过ptrace函数来trace process的。ptrace是系统函数,此函数提供一个进程去监听和控制另一个进程,并且...
继续阅读 »

在破解一款App的时候,在实际破解之前肯定是在做调试。LLDB之所以能附加进程时因为debugserver,而debugserver附加是通过ptrace函数来trace process的。
ptrace是系统函数,此函数提供一个进程去监听和控制另一个进程,并且可以检测被控制进程的内存和寄存器里面的数据。ptrace可以用来实现断点调试和系统调用跟踪。

一、反调试ptrace

iOS#import 头文件不能直接导入,所以需要我们自己导出头文件引入调用。当然也可以声明ptrace函数直接调用。

1.1 ptrace 头文件

  1. 直接创建一个macOS程序导入#import 头文件,点进去拷贝生成一个.h文件就可以了:


/*
* Copyright (c) 2000-2005 Apple Computer, Inc. All rights reserved.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. The rights granted to you under the License
* may not be used to create, or enable the creation or redistribution of,
* unlawful or unlicensed copies of an Apple operating system, or to
* circumvent, violate, or enable the circumvention or violation of, any
* terms of an Apple operating system software license agreement.
*
* Please obtain a copy of the License at
*
http://www.opensource.apple.com/apsl/
and read it before using this file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_END@
*/

/* Copyright (c) 1995 NeXT Computer, Inc. All Rights Reserved */
/*-
* Copyright (c) 1984, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* @(#)ptrace.h 8.2 (Berkeley) 1/4/94
*/


#ifndef _SYS_PTRACE_H_
#define _SYS_PTRACE_H_

#include
#include

enum {
ePtAttachDeprecated __deprecated_enum_msg("PT_ATTACH is deprecated. See PT_ATTACHEXC") = 10
};


#define PT_TRACE_ME 0 /* child declares it's being traced */
#define PT_READ_I 1 /* read word in child's I space */
#define PT_READ_D 2 /* read word in child's D space */
#define PT_READ_U 3 /* read word in child's user structure */
#define PT_WRITE_I 4 /* write word in child's I space */
#define PT_WRITE_D 5 /* write word in child's D space */
#define PT_WRITE_U 6 /* write word in child's user structure */
#define PT_CONTINUE 7 /* continue the child */
#define PT_KILL 8 /* kill the child process */
#define PT_STEP 9 /* single step the child */
#define PT_ATTACH ePtAttachDeprecated /* trace some running process */
#define PT_DETACH 11 /* stop tracing a process */
#define PT_SIGEXC 12 /* signals as exceptions for current_proc */
#define PT_THUPDATE 13 /* signal for thread# */
#define PT_ATTACHEXC 14 /* attach to running process with signal exception */

#define PT_FORCEQUOTA 30 /* Enforce quota for root */
#define PT_DENY_ATTACH 31

#define PT_FIRSTMACH 32 /* for machine-specific requests */

__BEGIN_DECLS


int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);


__END_DECLS

#endif /* !_SYS_PTRACE_H_ */

  1. 直接声明函数:
int ptrace(int _request, pid_t _pid, caddr_t _addr, int _data);
  • _request:要处理的事情
  • _pid:要操作的进程
  • _addr_data:取决于_pid参数,要传递的数据地址和数据本身。

1.2 ptrace调用

//告诉系统当前进程拒绝被debugserver附加
ptrace(PT_DENY_ATTACH, 0, 0, 0);
//ptrace(31, 0, 0, 0);

PT_DENY_ATTACH表示拒绝附加,值为31。如果仅仅是声明函数就传31就好了。_pid0表示当前进程。这里不传递任何数据。

分别在以下方法中调用

  1. load方法中调用:
+ (void)load {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
  1. constructor中调用:
__attribute__((constructor)) static void entry() {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
  1. main函数中调用:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;

@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
ptrace(PT_DENY_ATTACH, 0, 0, 0);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}


  1. didFinishLaunchingWithOptions中调用:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
ptrace(PT_DENY_ATTACH, 0, 0, 0);
return YES;
}

123情况下Xcode启动调试后调试直接断开,App能正常操作不能调试。4在调试情况下App直接闪退,正常打开没问题。同时调用的情况下以第一次为准。

也就是说 :ptracemain函数之后调用App会直接闪退,main以及之前调用会停止进程附加,以第一次调用为准。正常打开App没有问题,只影响LLDB调试。

通过上面的验证说明在程序没有加载的时候调用ptrace会设置一个标志,后续程序就不会被附加了,如果在已经被附加了的情况下调用ptrace会直接退出(因为这里ptrace附加传递的pid0主程序本身)。


PT_DENY_ATTACH
This request is the other operation used by the traced process; it allows a process that is not currently being traced to deny future traces by its parent. All other arguments are ignored. If the process is currently being traced, it will exit with the exit status of ENOTSUP; otherwise, it sets a flag that denies future traces. An attempt by the parent to trace a process which has set this flag will result in a segmentation violation in the parent.
ENOTSUP含义如下:
define ENOTSUP 45 //Operation not supported
之前在手机端通过debugserver附加程序直接报错11,定义如下:
PT_DETACH 11 // stop tracing a process

二、 破解ptrace

ptrace的特征:附加不了、Xcode运行闪退/停止附加、使用正常。

既然ptrace可以组织调试,那么我们只要Hook了这个函数绕过PT_DENY_ATTACH的调用就可以了。首先想到的就是fishhook


#import "fishhook.h"

int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);

int hp_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data){
if (_request != 31) {//不是拒绝附加
return ptrace_p(_request, _pid, _addr, _data);
}
return 0;
}

void hp_hook_ptrace() {
struct rebinding ptrace_rb;
ptrace_rb.name ="ptrace";
ptrace_rb.replacement = hp_ptrace;
ptrace_rb.replaced = (void *)&ptrace_p;

struct rebinding bds[] = {ptrace_rb};
rebind_symbols(bds, 1);
}

+ (void)load {
hp_hook_ptrace();
}

这样就能够进行附加调试了。


三、防止ptrace被破解


3.1 提前Hook防止ptrace被Hook


既然ptrace能够被Hook,那么自己先Hookptrace。调用的时候直接调用自己存储的地址就可以了。我们可以在自己的项目中增加一个Framework。这个库在Link Binary With Libraries中尽可能的靠前。这与dyld加载动态库的顺序有关。
这样就可以不被ptrace Hook了。代码逻辑和1.2中相同,只不过调用要换成ptrace_p
记的头文件中导出ptrace_p

CF_EXPORT int (*ptrace_p)(int _request, pid_t _pid, caddr_t _addr, int _data);

创建一个Monkey工程,将3.1生成的.app包拖入工程重签名,这个时候主程序通过调用ptrace已经不能阻止我们调试了,但是调用ptrace_p的地方Monkey Hook不到了。

3.2 修改二进制破解提前Hook ptrace


Monkey的工程中打ptrace符号断点:

这个时候可以看到是didFinishLaunchingWithOptions中调用了ptrace_p函数:
Hopper打开MachO文件找到didFinishLaunchingWithOptions方法:

然后一直点下去找到ptrace_p是属于Inject.framework的:

.appFrameworks中找到Inject.frameworkHopper打开,可以看到_rebind_symbols,上面的参数是ptrace

这里我们可以直接修改ptrace让先Hook的变成另外一个函数,但是有风险点是App内部调用ptrace_p的时候如果没有判断空就crash了。如果判断了可以这么处理。
还有另外一个方式是修改didFinishLaunchingWithOptions代码中的汇编,修改blr x8NOP这样就绕过了ptrace_p的调用。





作者:HotPotCat
链接:https://www.jianshu.com/p/9ed2de5e7497












收起阅读 »

iOS 自定义键盘

很多项目中都使用自定义键盘,实现自定义键盘有很多方法,本文讲的是修改UITextField/UITextView的inputView来实现自定义键盘。如何修改已经知道了,但是怎么修改。有两种思路:自定义CustomTextField/CustomTextVie...
继续阅读 »

很多项目中都使用自定义键盘,实现自定义键盘有很多方法,本文讲的是修改UITextField/UITextView的inputView来实现自定义键盘。
如何修改已经知道了,但是怎么修改。有两种思路:

  1. 自定义CustomTextField/CustomTextView,直接实现如下代码
textField.inputView = customView;   
textView.inputView = customView;

但是这样写有个弊端,就是通用性不强。比如项目中可能要实现某个具体业务逻辑,这个textField/textView是继承ATextField/ATextView,其他地方又有用到的是继承BTextField/BTextView,那我们再写代码时候,可能需要写n个自定义textField/textView,用起来就非常麻烦了,所以这种方法不推荐。

  1. 使用分类来实现自定义键盘
    思路就是在分类中增加一个枚举,这个枚举定义了不同类型的键盘
typedef NS_ENUM(NSUInteger, SJKeyboardType)
{
SJKeyboardTypeDefault, // 使用默认键盘
SJKeyboardTypeNumber // 使用自定义数字键盘
// 还可以根据需求 自定义其他样式...
};

写一个属性,来标记键盘类型

@property (nonatomic, assign) SJKeyboardType sjKeyboardType;
在.m文件中实现getter和setter方法

static NSString *sjKeyboardTypeKey = @"sjKeyboardTypeKey";
- (SJKeyboardType)sjKeyboardType
{
return [objc_getAssociatedObject(self, &sjKeyboardTypeKey) integerValue];
}

- (void)setSjKeyboardType:(SJKeyboardType)sjKeyboardType
{
objc_setAssociatedObject(self, &sjKeyboardTypeKey, @(sjKeyboardType), OBJC_ASSOCIATION_ASSIGN);
[self setupKeyboard:sjKeyboardType];
}

在set方法中来实现自定义键盘视图设置及对应点击方法实现

- (void)setupKeyboard:(SJKeyboardType)sjKeyboardType
{

switch (sjKeyboardType) {
case SJKeyboardTypeDefault:
break;
case SJKeyboardTypeNumber: {
SJCustomKeyboardView *numberInputView = [[[NSBundle mainBundle] loadNibNamed:@"SJCustomKeyboardView" owner:self options:nil] lastObject];
numberInputView.frame = CGRectMake(0, 0, SJSCREEN_WIDTH, SJNumberKeyboardHeight + SJCustomKeyboardBottomMargin);
self.inputView = numberInputView;
numberInputView.textFieldReplacementString = ^(NSString * _Nonnull string) {
BOOL canEditor = YES;
if ([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) {
canEditor = [self.delegate textField:self shouldChangeCharactersInRange:NSMakeRange(self.text.length, 0) replacementString:string];
}

if (canEditor) {
[self replaceRange:self.selectedTextRange withText:string];
}
};
numberInputView.textFieldShouldDelete = ^{
BOOL canEditor = YES;
if ([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)] && self.text.length) {
canEditor = [self.delegate textField:self shouldChangeCharactersInRange:NSMakeRange(self.text.length - 1, 1) replacementString:@""];
}
if (canEditor) {
[self deleteBackward];
}
};
numberInputView.textFieldShouldClear = ^{
BOOL canClear = YES;
if ([self.delegate respondsToSelector:@selector(textFieldShouldClear:)]) {
canClear = [self.delegate textFieldShouldClear:self];
}
if (canClear) {
[self setText:@""];
}
};
numberInputView.textFieldShouldReturn = ^{
if ([self.delegate respondsToSelector:@selector(textFieldShouldReturn:)]) {
[self.delegate textFieldShouldReturn:self];
}
};
break;
}
}
}
之后就需要实现自定义键盘视图,这里需要注意一点,就是如果使用新建子类实现自定义键盘,个人感觉按钮响应用代理实现会看起来逻辑更清晰

/* 用代理看的更清楚 但是分类不能实现代理 所以只能用block实现回调 如果自定义textField可以用代理 @protocol SJCustomKeyboardViewDelegate - (void)textFieldReplacementString:(NSString *_Nullable)string; - (BOOL)textFieldShouldDelete; - (BOOL)textFieldShouldClear; - (BOOL)textFieldShouldReturn; @end */

但是分类不能实现代理,所以只能用block来实现回调


@property (nonatomic, copy) void (^textFieldReplacementString)(NSString *string);
@property (nonatomic, copy) void (^textFieldShouldDelete)(void);
@property (nonatomic, copy) void (^textFieldShouldClear)(void);
@property (nonatomic, copy) void (^textFieldShouldReturn)(void);

.m中只需要实现按钮的点击方法和对应的回调方法即可。
这样好处是只需要引入头文件,修改一个属性即可实现自定义键盘,不会影响项目中其他的业务逻辑。

self.textField = [[UITextField alloc] initWithFrame:CGRectMake(20, 100, SJSCREEN_WIDTH - 40, 40)];  
self.textField.placeholder = @"input";
self.textField.borderStyle = UITextBorderStyleBezel;
self.textField.delegate = self;
[self.view addSubview:self.textField];

self.textField.sjKeyboardType = SJKeyboardTypeNumber;





收起阅读 »

iOS 任务调度器:为 CPU 和内存减负

GitHub 地址:YBTaskScheduler支持 cocopods,使用简便,效率不错,一个性能优化的基础组件。前言前些时间有好几个技术朋友问过笔者类似的问题:主线程需要执行大量的任务导致卡顿如何处理?异步任务量级过大导致 CPU 和内存压力过高如何优化...
继续阅读 »

GitHub 地址:YBTaskScheduler
支持 cocopods,使用简便,效率不错,一个性能优化的基础组件。

前言

前些时间有好几个技术朋友问过笔者类似的问题:主线程需要执行大量的任务导致卡顿如何处理?异步任务量级过大导致 CPU 和内存压力过高如何优化?

解决类似的问题可以用几个思路:降频、淘汰、优先级调度。

本来解决这些问题并不需要很复杂的代码,但是涉及到一些 C 代码并且要注意线程安全的问题,所以笔者就做了这样一个轮子,以解决任务调度引发的性能问题。

本文讲述 YBTaskScheduler 的原理,读者朋友需要有一定的 iOS 基础,了解一些性能优化的知识,基本用法可以先看看 GitHub README,DEMO 中也有一个相册列表的应用案例。

一、需求分析

就拿 DEMO 中的案例来说明,一个显示相册图片的列表:


实现图中业务,必然考虑到几个耗时操作:

1、从相册读取图片
2、解压图片
3、圆角处理
4、绘制图片

理所当然的想到处理方案(DEMO中有实现):

1、异步读取图片
2、异步裁剪图片为正方形(这个过程中就解压了)
3、异步裁剪圆角
4、回到主线程绘制图片

一整套流程下来,貌似需求很好的解决了,但是当快速滑动列表时,会发现 CPU 和内存的占用会比较高(这取决于从相册中读取并显示多大的图片)。当然 DEMO 中按照屏幕的物理像素处理,就算不使用任务调度器组件快速滑动列表也基本不会有掉帧的现象。考虑到老旧设备或者技术人员的水平,很多时候这种需求会导致严重的 CPU 和内存负担,甚至导致闪退。

以上处理方案可能存在的性能瓶颈:

从相册读取图片、裁剪图片,处理圆角、主线程绘制等操作会导致 CPU 计算压力过大。
同时解压的图片、同时绘制的图片过多导致内存峰值飙升(更不要说做了图片的缓存)。
任何一种情况都可能导致客户端卡死或者闪退,结合业务来分析问题,会发现优化的思路还是不难找到:

· 滑出屏幕的图片不会存在绘制压力,而当前屏幕中的图片会在一个 RunLoop 循环周期绘制,可能造成掉帧。所以可以减少一个 RunLoop 循环周期所绘制的图片数量。
· 快速滑动列表,大量的异步任务直接交由 CPU 执行,然而滑出屏幕的图片已经没有处理它的意义了。所以可以提前删除掉已经滑出屏幕的异步任务,以此来降低 CPU 和内存压力。

没错, YBTaskScheduler 组件就是替你做了这些事情 ,而且还不止于此。

二、命令模式与 RunLoop

想要管理这些复杂的任务,并且在合适的时机调用它们,自然而然的就想到了命令模式。意味着任务不能直接执行,而是把任务作为一个命令装入容器。

在 Objective-C 中,显然 Block 代码块能解决延迟执行这个问题:

[_scheduler addTask:^{
/*
具体任务代码
解压图片、裁剪图片、访问磁盘等
*/
}];

然后组件将这些代码块“装起来”,组件由此“掌握”了所有的任务,可以自由的决定何时调用这些代码块,何时对某些代码块进行淘汰,还可以实现优先级调度。

既然是命令模式,还差一个 Invoker (调用程序),即何时去触发这些任务。结合 iOS 的技术特点,可以监听 RunLoop 循环周期来实现:

static void addRunLoopObserver() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
taskSchedulers = [NSHashTable weakObjectsHashTable];
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit, true, 0xFFFFFF, runLoopObserverCallBack, NULL);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
});
}

然后在回调函数中进行任务的调度。

三、策略模式

考虑到任务的淘汰策略和优先级调度,必然需要一些高效数据结构来支撑,为了提高处理效率,笔者直接使用了 C++ 的数据结构:deque和priority_queue。

因为要实现任务淘汰,所以使用deque双端队列来模拟栈和队列,而不是直接使用stack和queue。使用priority_queue优先队列来处理自定义的优先级调度,它的缺点是不能删除低优先级节点,为了节约时间成本姑且够用。

具体的策略:

栈:后加入的任务先执行(可以理解为后加入的任务优先级高),优先淘汰先加入的任务。
队列:先加入的任务先执行(可以理解为先加入的任务优先级高),优先淘汰后加入的任务。
优先队列:自定义任务优先级,不支持任务淘汰。
实际上组件是推荐使用栈和队列这两种策略,因为插入和取出的时间复杂度是常数级的,需要定制任务的优先级时才考虑使用优先队列,因为其插入复杂度是 O(logN) 的。

至此,整个组件的业务是比较清晰了,组件需要让这三种处理方式可以自由的变动,所以采用策略模式来处理,下面是 UML 类图:


嗯,这是个挺标准的策略模式。

四、线程安全

由于任务的调度可能在任意线程,所以必须要做好容器(栈、队列、优先队列)访问的线程安全问题,组件是使用pthread_mutex_t和dispatch_once来保证线程安全,同时笔者尽量减少临界区来提高性能。值得注意的是,如果不会存在线程安全的代码就不要去加锁了。

后语

部分技术细节就不多说了,组件代码量比较少,如果感兴趣可以直接看源码。实际上这个组件的应用场景并不是很多,在项目稳定需要做深度的性能优化时可能会比较需要它,并且希望使用它的人也能了解一些原理,做到胸有成竹,才能灵活的运用。

转自:https://www.jianshu.com/p/f2a610c77d26

收起阅读 »

OLLVM代码混淆移植与使用

简介OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度。github上地址是https://github.com/obfusc...
继续阅读 »

简介

OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度。github上地址是https://github.com/obfuscator-llvm/obfuscator,只不过仅更新到llvm的4.0,2017年开始就没在更新。

移植

OLLVM如果自己想拿最新版的LLVM和Clang进行移植功能其实也并不是很难,整理一下其实改动很小,接下来将会讲一下移植的方法。

个人整理

先放一下个人移植好的版本地址https://github.com/heroims/obfuscator.git,个人fork原版后又加入了llvm5.0,6.0,7.0以及swift-llvm5.0的版本,应该能满足大部分需求了,如果有新版本下面的讲解,各位也可以自己动手去下载自己需要的llvm和clang进行移植。git上的提交每次都很独立如下图,方便各位cherry-pick。


下载LLVM

llvm地址:https://github.com/llvm-mirror
swift-llvm地址:https://github.com/apple
大家可以从上面的地址下载最新的自己需要的llvm和clang

#下载llvm源码
wget https://codeload.github.com/llvm-mirror/llvm/zip/release_70
unzip llvm-release_70.zip
mv llvm-release_70 llvm


#下载clang源码
wget https://codeload.github.com/llvm-mirror/clang/zip/release_70
unzip clang-release_70.zip
mv clang-release_70 llvm/tools/clang

添加混淆代码

如果用git的话只需要执行git cherry-pick xxxx把xxxx换成对应的我的版本上的提交哈希填上即可。极度推荐用git搞定。

如果手动一点点加的话,第一步就是把我改过的OLLVM文件夹里/include/llvm/Transforms/Obfuscation和/lib/llvm/Transforms/Obfuscation移动到刚才下载好的llvm源码文件夹相同的位置。

git clone https://github.com/heroims/obfuscator.git
cd obfuscator
git checkout llvm-7.0
cp include/llvm/Transforms/Obfuscation llvm/include/llvm/Transforms/Obfuscation
cp lib/llvm/Transforms/Obfuscation llvm/lib/llvm/Transforms/Obfuscation

然后手动修改8个文件如下:









编译

mkdir build
cd build
#如果不想跑测试用例加上-DLLVM_INCLUDE_TESTS=OFF
cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_CREATE_XCODE_TOOLCHAIN=ON ../obfuscator/
make -j7

使用

这里原版提供了3种混淆方式分别是控制流扁平化,指令替换,虚假控制流程,用起来都是加cflags的方式。下面简单说下这几种模式。

控制流扁平化

这个模式主要是把一些if-else语句,嵌套成do-while语句

-mllvm -fla:激活控制流扁平化
-mllvm -split:激活基本块分割。在一起使用时改善展平。
-mllvm -split_num=3:如果激活了传递,则在每个基本块上应用3次。默认值:1

指令替换

这个模式主要用功能上等效但更复杂的指令序列替换标准二元运算符(+ , – , & , | 和 ^)

-mllvm -sub:激活指令替换
-mllvm -sub_loop=3:如果激活了传递,则在函数上应用3次。默认值:1

虚假控制流程

这个模式主要嵌套几层判断逻辑,一个简单的运算都会在外面包几层if-else,所以这个模式加上编译速度会慢很多因为要做几层假的逻辑包裹真正有用的代码。

另外说一下这个模式编译的时候要浪费相当长时间包哪几层不是闹得!

-mllvm -bcf:激活虚假控制流程
-mllvm -bcf_loop=3:如果激活了传递,则在函数上应用3次。默认值:1
-mllvm -bcf_prob=40:如果激活了传递,基本块将以40%的概率进行模糊处理。默认值:30

上面说完模式下面讲一下几种使用方式

直接用二进制文件

直接使用编译的二进制文件build/bin/clang test.c -o test -mllvm -sub -mllvm -fla -mllvm -bcf

NDK集成

这里分为工具链的制作和项目里的配置。

制作Toolchains

这里以修改最新的ndk r18为例,老的ndk版本比这更容易都在ndk-bundle/toolchains里放着需要修改的文件。

#复制ndk的toolschain里的llvm
cp -r ndk-bundle/toolchains/llvm ndk-bundle/toolchains/ollvm
#删除prebuilt文件夹下的文件夹的bin和lib64,prebuilt文件夹下根据系统不同命名也不同
rm -rf ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/bin
rm -rf ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/lib64
#把我们之前编译好的ollvm下的bin和lib移到我们刚才删除bin和lib64的目录下
mv build/bin ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/
mv build/lib ndk-bundle/toolchains/ollvm/prebuilt/darwin-x86_64/
#复制ndk-bundle⁩/⁨build⁩/⁨core⁩/⁨toolchains的文件夹,这里根据自己对CPU架构的需求自己复制然后修改
cp -r ndk-bundle⁩/⁨build⁩/⁨core⁩/⁨toolchains/arm-linux-androideabi-clang⁩ ndk-bundle⁩/⁨build⁩/⁨core⁩/⁨toolchains/arm-linux-androideabi-clang-ollvm

最后把arm-linux-androideabi-clang-ollvm里的setup.mk文件进行修改

TOOLCHAIN_NAME := ollvm
TOOLCHAIN_ROOT := $(call get-toolchain-root,$(TOOLCHAIN_NAME))
TOOLCHAIN_PREFIX := $(TOOLCHAIN_ROOT)/bin

config.mk里是CPU架构,刚才是复制出来的所以不用修改,但如果要添加其他的自定义架构需要严格按照格式规范命名最初的文件夹,如mips的需要添加文件夹mipsel-linux-android-clang-ollvm,setup.mk和刚才的修改一样即可。

项目中配置

到了项目里还需要修改两个文件:
在Android.mk 中添加混淆编译参数

LOCAL_CFLAGS += -mllvm -sub -mllvm -bcf -mllvm -fla

Application.mk中配置NDK_TOOLCHAIN_VERSION

#根据需要添加
APP_ABI := x86 armeabi-v7a x86_64 arm64-v8a mips armeabi mips64
#使用刚才我们做好的编译链
NDK_TOOLCHAIN_VERSION := ollvm

Visual Studio集成

编译ollvm的时候,使用cmake-gui选择Visual Studio2015或者命令行选择cmake -G "Visual Studio 14 2015" -DCMAKE_BUILD_TYPE=Release ../obfuscator/
然后cmake会产生一个visual studio工程,用vs编译即可!
至于将Visual Studio的默认编译器换成clang编译,参考https://www.ishani.org/projects/ClangVSX/

Visual Studio2015起官方开始支持Clang,具体做法:
文件->新建->项目->已安装->Visual C++->跨平台->安装Clang with Microsoft CodeGen
Clang是一个完全不同的命令行工具链,这时候可以在工程配置中,平台工具集选项里找到Clang,然后使用ollvm的clang替换该clang即可。

XCode集成

XCode里集成需要看版本,XCode10之前和之后是一个分水岭,XCode9之前和之后有一个小配置不同。

XCode10以前

$ cd /Applications/Xcode.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/
$ sudo cp -r Clang\ LLVM\ 1.0.xcplugin/ Obfuscator.xcplugin
$ cd Obfuscator.xcplugin/Contents/
$ sudo plutil -convert xml1 Info.plist
$ sudo vim Info.plist

修改:

<string>com.apple.compilers.clang</string> -> <string>com.apple.compilers.obfuscator</string>
<string>Clang LLVM 1.0 Compiler Xcode Plug-in</string> -> <string>Obfuscator Xcode Plug-in</string>

执行:

$ sudo plutil -convert binary1 Info.plist
$ cd Resources/
$ sudo mv Clang\ LLVM\ 1.0.xcspec Obfuscator.xcspec
$ sudo vim Obfuscator.xcspec

修改:

<key>Description</key>
<string>Apple LLVM 8.0 compiler</string> -> <string>Obfuscator 4.0 compiler</string>
<key>ExecPath</key>
<string>clang</string> -> <string>/path/to/obfuscator_bin/clang</string>
<key>Identifier</key>
<string>com.apple.compilers.llvm.clang.1_0</string> -> <string>com.apple.compilers.llvm.obfuscator.4_0</string>
<key>Name</key>
<string>Apple LLVM 8.0</string> -> <string>Obfuscator 4.0</string>
<key>Vendor</key>
<string>Apple</string> -> <string>HEIG-VD</string>
<key>Version</key>
<string>7.0</string> -> <string>4.0</string>

执行:

$ cd English.lproj/
$ sudo mv Apple\ LLVM\ 5.1.strings "Obfuscator 3.4.strings"
$ sudo plutil -convert xml1 Obfuscator\ 3.4.strings
$ sudo vim Obfuscator\ 3.4.strings

修改:

<key>Description</key>
<string>Apple LLVM 8.0 compiler</string> -> <string>Obfuscator 4.0 compiler</string>
<key>Name</key>
<string>Apple LLVM 8.0</string> -> <string>Obfuscator 4.0</string>
<key>Vendor</key>
<string>Apple</string> -> <string>HEIG-VD</string>
<key>Version</key>
<string>7.0</string> -> <string>4.0</string>

执行:

$ sudo plutil -convert binary1 Obfuscator\ 3.4.strings

XCode9之后要设置Enable Index-While-Building成NO



XCode10之后

xcode10之后无法使用添加ideplugin的方法,但添加编译链跑的依然可行,另外网上一些人说不能开bitcode,不能提交AppStore,用原版llvm改的ollvm的确有可能出现上述情况,所以我用苹果的swift-llvm改了一版暂时没去试着提交,或许可以,有兴趣的也可以自己下载使用试试obfuscator这版,特别备注由于修改没有针对swift部分所以用swift写的代码没混淆,回头有空的话再弄。

创建XCode的toolchain然后把生成的文件夹放到/Library/Developer/下

cd build
sudo make install-xcode-toolchain
mv /usr/local/Toolchains /Library/Developer/

Toolchains下的.xctoolchain文件就是一个文件夹,进去修改info.plist

<key>CFBundleIdentifier</key>
<string>org.llvm.7.0.0svn</string> -> <string>org.ollvm-swift.5.0</string>

修改完在XCode的Toolchains下就会显示相应的名称

然后如图打开XCode选择Toolchaiins




按这些配置好后就算是可以用了。

最后

简单展示一下混淆后的成果

源码


反编译未混淆代码


反编译混淆后代码


扩展:字符串混淆

原版是没有这功能的本来,Armariris 提供了这个功能,我这也移植过来了,毕竟不难。
首先把StringObfuscation的.h,.cpp文件放到对应的Obfuscation文件夹下,然后分别修改下面的文件。


用法

-mllvm -sobf:编译时候添加选项开启字符串加密
-mllvm -seed=0xdeadbeaf:指定随机数生成器种子

效果

看个添加了-mllvm -sub -mllvm -sobf -mllvm -fla -mllvm -bcf这么一串的效果。

源码


反编译未混淆代码


反编译混淆后代码


转自:https://www.jianshu.com/p/e0637f3169a3

收起阅读 »

汇编-函数本质(上)

栈函数调用栈恢复后数据并不销毁,拉伸栈空间后会先覆盖再读取。内存读写指令⚠️:读/写 数据都是往高地址读/写,也就是放数据从高地址往低地址放。比如读取16字节的数据,给的地址是0x02,那么读取的就是0x02和0x03。str(store register)指...
继续阅读 »


栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)


SP和FP寄存器

  • sp寄存器在任意时刻会保存我们栈顶的地址。
  • fp寄存器也称为x29寄存器属于通用寄存器,但是在某些时刻我们利用它保存栈底的地址!(没有出现函数嵌套调用的时候不需要fp,相当于分界点)
    ⚠️:ARM64开始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stpARM64里面 对栈的操作是16字节对齐的!!

ARM64是先开辟一段栈空间,fp移动到栈顶再往栈中存放内容(编译期就已经确定大小)。不存在push操作。在iOS中栈是往低地址开辟空间




函数调用栈

常见的函数调用开辟和恢复的栈空间:

//开辟栈空间
sub sp, sp, #0x40 ; 拉伸0x4064字节)空间
stp x29, x30, [sp, #0x30] ;x29\x30 寄存器入栈保护
add x29, sp, #0x30 ; x29指向栈帧的底部
...
//恢复栈空间
ldp x29, x30, [sp, #0x30] ;恢复x29/x30 寄存器的值
add sp, sp, #0x40 ;栈平衡
ret

恢复后数据并不销毁,拉伸栈空间后会先覆盖再读取。

内存读写指令

⚠️:读/写 数据都是往高地址读/写,也就是放数据从高地址往低地址放。比如读取16字节的数据,给的地址是0x02,那么读取的就是0x020x03

str(store register)指令
将数据从寄存器中读出来,存到内存中。

ldr(load register)指令
将数据从内存中读出来,存到寄存器中。

ldr 和 str 的变种 ldp 和 stp 还可以操作2个寄存器。


堆栈操作案例

使用32个字节空间作为这段程序的栈空间,然后利用栈将x0x1的值进行交换。

.text
.global _C

_C:
sub sp, sp, #0x20 ;拉伸栈空间32个字节
stp x0, x1, [sp, #0x10] ;sp 偏移 16字节存放 x0和x1 []的意思是寻址。这sp并没有改变
ldp x1, x0, [sp, #0x10] ;将sp偏移16个字节的值取出来,放入x1 和 x0。这里内存相当于temp 交换了 x0 和 x1。寄存器中的值交换了,内存中的值不变。
add sp, sp, #0x20 ;恢复栈空间
ret
这段代码相当于 x0,x1遍历,sp和内存没有变。
栈空间分配:





断点调试

0x102e6e518断点处对x0x1分别赋值0xa0xb。然后单步执行:




拉伸后sp也变了。

(lldb) register write x0 0xa
(lldb) register write x1 0xb
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb) register read sp
sp = 0x000000016cf95b10
(lldb)

看下0x000000016cf95b10的空间:




目前还没有写入内存,是脏数据。接着单步执行:



这个时候x0x1的数据完成了交换。内存的数据并没有变化。
继续单步执行:

(lldb) register write x0 0xa
(lldb) register write x1 0xb
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb) register read sp
sp = 0x000000016cf95b10
(lldb) register read sp
sp = 0x000000016cf95b30
(lldb)

sp还原了,栈空间释放,这时候0xa0xb还依然存在内存中,等待下次拉伸栈空间写数据覆盖:




bl和ret指令

bl标号

  • 将下一条指令的地址放入lr(x30)寄存器
  • 转到标号处执行指令

b就是跳转,l将下一条指令的地址放入lr(x30)寄存器。


lr相当于保存的”回家的路“。


ret

  • 默认使用lr(x30)寄存器的值,通过底层指令提示CPU此处作为下条指令地址!

ret只会看lr


ARM64平台的特色指令,它面向硬件做了优化处理。



x30寄存器

x30寄存器存放的是函数的返回地址.当ret指令执行时刻,会寻找x30寄存器保存的地址值!
一个嵌套调用的案例,汇编代码如下:

.text
.global _C, _D

_C:
mov x0,#0xaaaa
bl _D
mov x0,#0xaaaa
ret

_D:
mov x0,#0xbbbb
ret
ViewController.m中调用:

int C();
int D();
- (void)viewDidLoad {
[super viewDidLoad];
printf("C");
C();
printf("D");
}
C();打断点执行,进入C中:






继续执行发现一直在0x104c8e4f80x104c8e4fc中跳转返不回去viewDidLoad中了,发生了死循环。

->  0x104c8e4f8 <+8>:  mov    x0, #0xaaaa
0x104c8e4fc <+12>: ret
那么如果要返回,就必须将viewDidLoad中下一条指令告诉lr,这个时候就必须在bl之前保护lr寄存器(遇到bllr就会改变。需要保护“回家的路”)。那么这个时候能不能把lr保存到其它寄存器?这里我们没法保证其它寄存器不会被使用。这个时候唯一属于当前函数的也就是自己的栈区了。保存到栈区应该就能解决了。
可以看下系统是怎么实现的,写一个c函数断点调试看下:

void c() {
d();
return;;
}

void d() {

}

- (void)viewDidLoad {
[super viewDidLoad];
c();
}

系统的实现如下:



TestDemo`c:
//边开辟空间边写入 x29(fp) x30(lr) 的值。[sp, #-0x10]! !代表赋值给sp,相当于 sp -= 0x10
-> 0x102a21e84 <+0>: stp x29, x30, [sp, #-0x10]!
0x102a21e88 <+4>: mov x29, sp
0x102a21e8c <+8>: bl 0x102a21e98 ; d at ViewController.m:34:1
//将sp所指向的地址读取给x29,x30。[sp], #0x10 等价于 sp += 0x10
0x102a21e90 <+12>: ldp x29, x30, [sp], #0x10
0x102a21e94 <+16>: ret

可以看到系统先开辟栈空间,然后将x29x30寄存器的值存入栈区。在ret之前恢复x29x30的值。

  • stp x29, x30, [sp, #-0x10]!:开辟空间并将x29x30存入栈区。!代表赋值给sp,相当于 sp -= 0x10
  • ldp x29, x30, [sp], #0x10:将栈区的值给x29x30并回收空间。[sp], #0x10 等价于 sp += 0x10

那么对于CD的案例自己实现下保存和恢复lr寄存器。


.text
.global _C, _D

_C:
//sub sp,sp,#0x10
//str x30,[sp] ;等价
str x30, [sp,#-0x10]! ;16字节对齐,必须最小0x10
mov x0,#0xaaaa
bl _D
mov x0,#0xaaaa
//ldr x30,[sp]
//add sp,#0x10 ;等价
ldr x30,[sp],#0x10
ret

_D:
mov x0,#0xbbbb
ret



这个时候进入Dlr值已经发生变化。



继续执行正常返回viewDidload了,这个时候死循环就已经解决了。

⚠️:在函数嵌套调用的时候,需要将x30入栈!开辟空间需要16字节对齐。如果开辟8字节再读的时候会坏地址访问。写的时候没问题。




函数的参数和返回值

先看下系统的实现:

int sum(int a, int b) {
return a + b;
}

- (void)viewDidLoad {
[super viewDidLoad];
sum(10,20);
}




可以看到变量1020分别存入了w0w1
sum调用如下(release模式下编译器会优化):

TestDemo`sum:
//开辟空间
-> 0x100121e68 <+0>: sub sp, sp, #0x10 ; =0x10
//w0 w1 存入栈中
0x100121e6c <+4>: str w0, [sp, #0xc]
0x100121e70 <+8>: str w1, [sp, #0x8]
//从栈中读取参数
0x100121e74 <+12>: ldr w8, [sp, #0xc]
0x100121e78 <+16>: ldr w9, [sp, #0x8]
//参数相加存入w0
0x100121e7c <+20>: add w0, w8, w9
//恢复栈空间
0x100121e80 <+24>: add sp, sp, #0x10 ; =0x10
//返回
0x100121e84 <+28>: ret
从上面可以看出返回值在w0中。那么自己实现sum函数的汇编代码:

.text
.global _suma

_suma:
add x0,x0,x1
ret
调用:

int suma(int a, int b);
- (void)viewDidLoad {
[super viewDidLoad];
printf("%d",suma(10,20));
}

⚠️ARM64下,函数的参数是存放在X0X7(W0W7)这8个寄存器里面的。如果超过8个参数就会入栈。那么oc的方法最好不要超过6个(selfcmd)。
函数的返回值是放在X0寄存器里面的。


参数超过8个


int test(int a, int b, int c ,int d, int e, int f, int g, int h, int i) {
return a + b + c + d + e + f + g + h + i;
}

- (void)viewDidLoad {
[super viewDidLoad];
test(1, 2, 3, 4, 5, 6, 7, 8, 9);
}




可以看到前8个参数分别保存在w0~w7寄存器中,第9个参数先保存在w10中,然后写入x8中(这个时候x8指向sp,相当于第9个参数写入了当前函数栈中)。


TestDemo`-[ViewController viewDidLoad]:
//拉伸栈空间,保存fp lr
0x100f09e5c <+0>: sub sp, sp, #0x40 ; =0x40
0x100f09e60 <+4>: stp x29, x30, [sp, #0x30]

//fp指向 sp+0x30
0x100f09e64 <+8>: add x29, sp, #0x30 ; =0x30
//fp-0x8 存放x0
0x100f09e68 <+12>: stur x0, [x29, #-0x8]
//fp-0x10 存放x1
0x100f09e6c <+16>: stur x1, [x29, #-0x10]
//fp-0x8 给到 x8
0x100f09e70 <+20>: ldur x8, [x29, #-0x8]
//sp+0x10 指针给到 x9
0x100f09e74 <+24>: add x9, sp, #0x10 ; =0x10
//x8写入 sp+0x10
0x100f09e78 <+28>: str x8, [sp, #0x10]

//adrp = address page 内存中取数据
0x100f09e7c <+32>: adrp x8, 4
0x100f09e80 <+36>: add x8, x8, #0x418 ; =0x418
//x8所指向的内容去出来
0x100f09e84 <+40>: ldr x8, [x8]
//x8写入栈中,这个时候x9指向地址,这个时候是一个新的x8
0x100f09e88 <+44>: str x8, [x9, #0x8]
0x100f09e8c <+48>: adrp x8, 4
0x100f09e90 <+52>: add x8, x8, #0x3e8 ; =0x3e8
0x100f09e94 <+56>: ldr x1, [x8]
0x100f09e98 <+60>: mov x0, x9
0x100f09e9c <+64>: bl 0x100f0a568 ; symbol stub for: objc_msgSendSuper2

//sp 一直没有改变过,w0~w7 分别存放前8个参数
0x100f09ea0 <+68>: mov w0, #0x1
0x100f09ea4 <+72>: mov w1, #0x2
0x100f09ea8 <+76>: mov w2, #0x3
0x100f09eac <+80>: mov w3, #0x4
0x100f09eb0 <+84>: mov w4, #0x5
0x100f09eb4 <+88>: mov w5, #0x6
0x100f09eb8 <+92>: mov w6, #0x7
0x100f09ebc <+96>: mov w7, #0x8
//x8 指向 sp
-> 0x100f09ec0 <+100>: mov x8, sp
//参数 9 存入 w10
0x100f09ec4 <+104>: mov w10, #0x9
//w10 存入 x8地址中,也就是sp栈底中
0x100f09ec8 <+108>: str w10, [x8]

0x100f09ecc <+112>: bl 0x100f09de4 ; test at ViewController.m:41
0x100f09ed0 <+116>: ldp x29, x30, [sp, #0x30]
0x100f09ed4 <+120>: add sp, sp, #0x40 ; =0x40
0x100f09ed8 <+124>: ret



接着往下直接跳转到test函数中:

TestDemo`test:
//开辟空间48字节
0x100f09de4 <+0>: sub sp, sp, #0x30 ; =0x30

//从viewDidLoad栈中取数据 第9个参数(读写往高地址)
0x100f09de8 <+4>: ldr w8, [sp, #0x30]

//参数入栈,分别占4个字节
0x100f09dec <+8>: str w0, [sp, #0x2c]
0x100f09df0 <+12>: str w1, [sp, #0x28]
0x100f09df4 <+16>: str w2, [sp, #0x24]
0x100f09df8 <+20>: str w3, [sp, #0x20]
0x100f09dfc <+24>: str w4, [sp, #0x1c]
0x100f09e00 <+28>: str w5, [sp, #0x18]
0x100f09e04 <+32>: str w6, [sp, #0x14]
0x100f09e08 <+36>: str w7, [sp, #0x10]
0x100f09e0c <+40>: str w8, [sp, #0xc]

-> 0x100f09e10 <+44>: ldr w8, [sp, #0x2c]
0x100f09e14 <+48>: ldr w9, [sp, #0x28]
0x100f09e18 <+52>: add w8, w8, w9
0x100f09e1c <+56>: ldr w9, [sp, #0x24]
0x100f09e20 <+60>: add w8, w8, w9
0x100f09e24 <+64>: ldr w9, [sp, #0x20]
0x100f09e28 <+68>: add w8, w8, w9
0x100f09e2c <+72>: ldr w9, [sp, #0x1c]
0x100f09e30 <+76>: add w8, w8, w9
0x100f09e34 <+80>: ldr w9, [sp, #0x18]
0x100f09e38 <+84>: add w8, w8, w9
0x100f09e3c <+88>: ldr w9, [sp, #0x14]
0x100f09e40 <+92>: add w8, w8, w9
0x100f09e44 <+96>: ldr w9, [sp, #0x10]
0x100f09e48 <+100>: add w8, w8, w9
0x100f09e4c <+104>: ldr w9, [sp, #0xc]
//最终相加结果给 w0
0x100f09e50 <+108>: add w0, w8, w9
//栈平衡
0x100f09e54 <+112>: add sp, sp, #0x30 ; =0x30
0x100f09e58 <+116>: ret



最终函数返回值放入w0中,如果在release模式下test不会被调用(被优化掉,因为没有意义,有没有对app没有影响。)

自己实现一个简单有参数并且嵌套调用的汇编:


.text
.global _func,_sum

_func:
//sub sp,sp,#0x10
//stp x29,x30,[sp]
stp x29,x30,[sp, #-0x10]!
bl _sum
//ldp x29,x30,[sp]
//add sp,sp,#0x10
ldp x29,x30,[sp],#0x10
ret
_sum:
add x0,x0,x1
ret


篇幅限制 分为2篇

作者:HotPotCat
链接:https://www.jianshu.com/p/69b9c49b0e71




收起阅读 »

汇编-基本概念

在逆向开发中,非常重要的一个环节就是静态分析。对于逆向iOS app来说,一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制。是由手机上的CPU执行的,静态分析是建立在分析二进制上面。汇编语言的发展机器语言由0...
继续阅读 »


在逆向开发中,非常重要的一个环节就是静态分析。对于逆向iOS app来说,一个APP安装在手机上面的可执行文件本质上是二进制文件。因为iPhone手机本质上执行的指令是二进制。是由手机上的CPU执行的,静态分析是建立在分析二进制上面。


汇编语言的发展

机器语言

01组成的机器指令。0代表有电,1代表没电。

  • 加:0100 0000
  • 减:0100 1000
  • 乘:1111 0111 1110 0000
  • 除:1111 0111 1111 0000

汇编语言(assembly language)

为了高效的写代码出现了助记符,使用助记符代替机器语言,如:

  • 加:INC EAX 通过编译器 0100 0000
  • 减:DEC EAX 通过编译器 0100 1000
  • 乘:MUL EAX 通过编译器 1111 0111 1110 0000
  • 除:DIV EAX 通过编译器 1111 0111 1111 0000

助记符就是汇编语言的前身,当有专门的编译器出现的时候就有了汇编语言。

高级语言(High-level programming language)

C\C++\Java\OC\Swift,更加接近人类的自然语言。
比如C语言:

  • 加:A + B 通过编译器 0100 0000
  • 减:A - B 通过编译器 0100 1000
  • 乘:A * B 通过编译器 1111 0111 1110 0000
  • 除:A / B 通过编译器 1111 0111 1111 0000

代码在终端设备上的过程:





  • 汇编语言机器语言一一对应,每一条机器指令都有与之对应的汇编指令
  • 汇编语言可以通过编译得到机器语言机器语言可以通过反汇编得到汇编语言
  • 高级语言可以通过编译得到汇编语言 \ 机器语言,但汇编语言\机器语言几乎不可能还原成高级语言(不是一一对应关系,反推出是不准确的,只能大致。)

汇编语言的特点


  • 可以直接访问、控制各种硬件设备,比如存储器、CPU等,能最大限度地发挥硬件的功能
  • 能够不受编译器的限制,对生成的二进制代码进行完全的控制
  • 目标代码简短,占用内存少,执行速度快
  • 汇编指令是机器指令的助记符,同机器指令一一对应。每一种CPU都有自己的机器指令集\汇编指令集,所以汇编语言不具备可移植性
  • 开发者需要对CPU等硬件结构有所了解,不易于编写、调试、维护
  • 不区分大小写,比如movMOV是一样的

汇编的用途

  • 编写驱动程序、操作系统(比如Linux内核的某些关键部分)
  • 对性能要求极高的程序或者代码片段,可与高级语言混合使用(内联汇编
  • 软件安全
    1.病毒分析与防治
    2.逆向\加壳\脱壳\破解\外挂\免杀\加密解密\漏洞\黑客
  • 理解整个计算机系统的最佳起点和最有效途径
  • 为编写高效代码打下基础
  • 弄清代码的本质

汇编语言的种类

目前讨论比较多的汇编语言有:

  • 8086汇编(8086处理器是16bitCPU
  • Win32汇编
  • Win64汇编
  • ARM汇编(嵌入式、MaciOS
  • ......

iPhone里面用到的是ARM汇编,但是不同的设备也有差异(因CPU的架构不同)。

位数架构设备
32armv6iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch
32armv7iPhone3GS, iPhone4, iPhone4S,iPad, iPad2, iPad3(The New iPad), iPad mini, iPod Touch 3G, iPod Touch4
32armv7siPhone5, iPhone5C, iPad4(iPad with Retina Display)
64arm64iPhone5s,iPhone6、7、8,iPhone6、7、8 Plus,iPhone X,iPad Air,iPad mini2(iPad mini with Retina Display)
64arm64eXS/XS Max/XR/ iPhone 11, iPhone 11 pro 及以后
64x86_64模拟器64位处理器 (intel)
32i386模拟器32位处理器(intel)

⚠️:苹果A7处理器支持两个不同的指令集:32ARM指令集(armv6|armv7|armv7s)和64ARM指令集(arm64

汇编相关的学习需要了解CPU等硬件结构,最为重要的是CPU/内存。在汇编中,大部分指令都是和CPU与内存相关的。
APP/程序的执行过程:





执行过程:
1.地址总线先去内存地址。
2.控制读取发送读/写命令。
3.数据总线写数据->内存/ 内存发送数据->数据总线

地址总线

  • 它的宽度决定了CPU的寻址能力(也就是寻址范围)
  • 8086的地址总线宽度是20,所以寻址能力是1M( 220)(这里的M是大小,数量单位)




内存中的MB是容量单位。如果内存很大, 地址总线宽度不够怎么处理?以前的cpu是通过2次寻址相加得到一个最终的值来访问内存,现在的cpu没有寻址能力的问题。
数量单位:M,K。1M = 1024K,1K= 1024。比如:10,100
容量单位:字节Byte。 1024B = 1KB,1024KB = 1MB。比如:10个,100只。(大部分计算机都是以1个字节为单位。银行系统的IBM电脑例外是2个字节为单位。)
对于100M 宽带,这里的100M是100Mbps(每秒钟传递多少二进制位,bit位。所以100M带宽理论下载速度12.5MB/s)。

数据总线

  • 它的宽度决定了CPU的单次数据传送量,也就是数据传送速度(吞吐量)
  • 8086的数据总线宽度是16,所以单次最大传递2个字节的数据

我们现在常说的32位,64位cpu说的就是它的数据吞吐量。1次放电分别4字节,8字节数据。

控制总线

  • 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制

案例:
1.一个CPU 的寻址能力为8KB,那么它的地址总线的宽度为____
答案:8KB对应 8192, 213 = 8192 所以为13。

  1. 8080,8088,80286,80386 的地址总线宽度分别为16根,20根,24根,32根。那么他们的寻址能力分别为多少____KB, ____MB,____MB,____GB?
    答案:1kb = 210 = 1024
    1kb * 26 = 64kb
    1kb * 1kb = 1mb
    1mb * 24 = 16mb
    1kb * 1kb * 1kb * 22 = 4gb

  2. 8080,8088,8086,80286,80386 的数据总线宽度分别为8根,8根,16根,16根,32根.那么它们一次可以传输的数据为:____B,____B,____B,____B,____B
    答案:1 、1、2、2、4

4.从内存中读取1024字节的数据,8086至少要读____次,80386至少要读取____次.
答案:8086 数据总线宽度为16。8086一次读2个字节,那么需要512次,80286数据总线宽度为32,一次4个字节,需要256次。

内存




  • 内存地址空间的大小受CPU地址总线宽度的限制。8086的地址总线宽度为20,可以定位220个不同的内存单元(内存地址范围0x00000~0xFFFFF),所以8086的内存空间大小为1MB
  • 0x00000~0x9FFFF:主存储器。可读可写
  • 0xA0000~0xBFFFF:向显存中写入数据,这些数据会被显卡输出到显示器。可读可写
  • 0xC0000~0xFFFFF:存储各种硬件\系统信息。只读

  • 进制

    想学好进制首先要忘掉十进制,也要忘掉进制间的转换。

    进制的定义

    • 八进制由8个符号组成:0 1 2 3 4 5 6 7 逢八进一
    • 十进制由10个符号组成:0 1 2 3 4 5 6 7 8 9逢十进一
    • N进制就是由N个符号组成:逢N进一

    ⚠️:进制的本质是符号。

    案例

    1. 1 + 1 在____情况下等于 3 ?
      除了算错的情况下。在十进制由10个符号组成,假如由: 0 1 3 2 8 A B E S 7组成逢十进一,那么在这种情况下1+1=3

    传统定义的十进制和自定义的十进制不一样。那么这10个符号如果我们不告诉别人这个符号表,别人是没办法拿到我们的具体数据的,可以用于加密!
    ⚠️:十进制由十个符号组成,逢十进一,符号是可以自定义的!!!

    1. 八进制运算:
    • 2 + 3 = __ , 2 * 3 = __ ,4 + 5 = __ ,4 * 5 = __.
      答案:5,6,11,24
    • 277 + 333 = __ , 276 * 54 = __ , 237 - 54 = __ , 234 / 4 = __ .
      答案:632, 20250, 163,47


    八进制加法表
    0 1 2 3 4 5 6 7
    10 11 12 13 14 15 16 17
    20 21 22 23 24 25 26 27
    ...

    1+1 = 2                     
    1+2 = 3 2+2 = 4
    1+3 = 4 2+3 = 5 3+3 = 6
    1+4 = 5 2+4 = 6 3+4 = 7 4+4 = 10
    1+5 = 6 2+5 = 7 3+5 = 10 4+5 = 11 5+5 = 12
    1+6 = 7 2+6 = 10 3+6 = 11 4+6 = 12 5+6 = 13 6+6 = 14
    1+7 = 10 2+7 = 11 3+7 = 12 4+7 = 13 5+7 = 14 6+7 = 15 7+7 = 16

    八进制乘法表
    0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 27...

    1*1 = 1                     
    1*2 = 2 2*2 = 4
    1*3 = 3 2*3 = 6 3*3 = 11
    1*4 = 4 2*4 = 10 3*4 = 14 4*4 = 20
    1*5 = 5 2*5 = 12 3*5 = 17 4*5 = 24 5*5 = 31
    1*6 = 6 2*6 = 14 3*6 = 22 4*6 = 30 5*6 = 36 6*6 = 44
    1*7 = 7 2*7 = 16 3*7 = 25 4*7 = 34 5*7 = 43 6*7 = 52 7*7 = 61

    二进制的简写形式

                   二进制: 1 0 1 1 1 0 1 1 1 1 0 0
    三个二进制一组: 101 110 111 100
                    八进制:    5     6     7      4
    四个二进制一组: 1011 1011 1100
                十六进制:     b        b       c

    二进制:从 0 写到 1111
    0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
    这种二进制使用起来太麻烦,改成更简单一点的符号:
    0 1 2 3 4 5 6 7 8 9 A B C D E F 这就是十六进制了

    数据的宽度

    数学上的数字,是没有大小限制的,可以无限的大。但在计算机中,由于受硬件的制约,数据都是有长度限制的(我们称为数据宽度),超过最多宽度的数据会被丢弃。


    int test() {
    int cTemp = 0x1FFFFFFFF;
    return cTemp;
    }

    - (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%x",test());
    }
    输出:

    ffffffff
    数据溢出了。刚开始cTemp默认值1,溢出后变为-1第一位符号位,1代表负数,0代表正数。往后逐位取反,末尾加1。)。

    (lldb) p cTemp
    (int) $0 = 1
    (lldb) p cTemp
    (int) $1 = -1
    (lldb) p &cTemp
    (int *) $2 = 0x000000016b3a9b1c
    (lldb) x 0x000000016b3a9b1c
    0x16b3a9b1c: ff ff ff ff 10 00 00 00 00 00 00 00 ef 98 3a 6b ..............:k
    0x16b3a9b2c: 01 00 00 00 70 a9 f0 59 01 00 00 00 50 d4 a5 04 ....p..Y....P...
    (lldb) p (uint)cTemp
    (uint) $3 = 4294967295

    Debug -> Debug Workflow -> View Memory中也可以查看(这里查看内容更新后需要翻页刷新然后切换回来才能显示新值):




    再看下汇编代码(Debug -> Debug Workflow -> Always Show Disassembly):


    TestDemo`test:
    0x104a59ec8 <+0>: sub sp, sp, #0x10 ; =0x10
    0x104a59ecc <+4>: mov w8, #-0x1
    0x104a59ed0 <+8>: str w8, [sp, #0xc]
    -> 0x104a59ed4 <+12>: ldr w0, [sp, #0xc]
    0x104a59ed8 <+16>: add sp, sp, #0x10 ; =0x10
    0x104a59edc <+20>: ret

    可以看到直接将-1给力w8。指令在内存中占用4字节。

    计算机中常见的数据宽度

    • 位(Bit): 1个位就是1个二进制位。0或者1
    • 字节(Byte): 1个字节由8个Bit组成(8位)。内存中的最小单元Byte
    • 字(Word): 1个字由2个字节组成(16位),这2个字节分别称为高字节低字节
    • 双字(Doubleword): 1个双字由两个字组成(32位)。

    计算机存储数据会分为有符号数和无符号数(对于数据本身内容没有变化,取决于你怎么看):


    无符号数,直接换算!
    有符号数:
    正数: 0 1 2 3 4 5 6 7
    负数: F E D B C A 9 8
    -1 -2 -3 -4 -5 -6 -7 -8

    自定义进制符号

    案例:

    • 现在有10进制数10个符号分别是:2,9,1,7,6,5,4, 8,3 , A 逢10进1 那么: 123 + 234 = ____

    十进制:
    0 1 2 3 4 5 6 7 8 9
    自定义:
    2 9 1 7 6 5 4 8 3 A
    92 99 91 97 96 95 94 98 93 9A
    12 19 11 17 16 15 14 18 13 1A
    72 79 71 77 76 75 74 78 73 7A
    62 69 61 67 66 65 64 68 63 6A
    52 59 51 57 56 55 54 58 53 5A
    42 49 41 47 46 45 44 48 43 4A
    82 89 81 87 86 85 84 88 83 8A
    32 39 31 37 36 35 34 38 33 3A
    922

    转换后加法表:

    9+9 = 1                 
    9+1 = 7 1+1 = 6
    9+7 = 6 1+7 = 5 7+7 = 4
    9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
    9+5 = 4 1+5 = 8 7+5 = 3 6+5 = A 5+5 = 92
    9+4 = 8 1+4 = 3 7+4 = a 6+4 = 92 5+4 = 99 4+4 = 91
    9+8 = 3 1+8 = A 7+8 = 92 6+8 = 99 5+8 = 91 4+8 = 97 8+8 = 96
    9+3 = A 1+3 = 92 7+3 = 99 6+3 = 91 5+3 = 97 4+3 = 96 8+3 = 95 3+3 = 94
    9+A = 92 1+A = 99 7+A = 91 6+A = 97 5+A = 96 4+A = 95 8+A = 94 3+A = 98 A+A = 93

    123 + 234 = 1A6

    • 现在有9进制数 9个符号分别是:2,9,1,7,6,5,4, 8,3 逢9进1 那么: 123 + 234 = __

    十进制:
    0 1 2 3 4 5 6 7 8
    自定义:
    2 9 1 7 6 5 4 8 3
    92 99 91 97 96 95 94 98 93
    12 19 11 17 16 15 14 18 13
    72 79 71 77 76 75 74 78 73
    62 69 61 67 66 65 64 68 63
    52 59 51 57 56 55 54 58 53
    42 49 41 47 46 45 44 48 43
    82 89 81 87 86 85 84 88 83
    32 39 31 37 36 35 34 38 33
    922

    转换后加法表:

    9+9 = 1                 
    9+1 = 7 1+1 = 6
    9+7 = 6 1+7 = 5 7+7 = 4
    9+6 = 5 1+6 = 4 7+6 = 8 6+6 = 3
    9+5 = 4 1+5 = 8 7+5 = 3 6+5 = 92 5+5 = 99
    9+4 = 8 1+4 = 3 7+4 = 92 6+4 = 99 5+4 = 91 4+4 = 97
    9+8 = 3 1+8 = 92 7+8 = 99 6+8 = 91 5+8 = 97 4+8 = 96 8+8 = 95
    9+3 = 92 1+3 = 99 7+3 = 91 6+3 = 97 5+3 = 96 4+3 = 95 8+3 = 94 3+3 = 98

    123 + 234 = 725

    CPU&寄存器

    内部部件之间由总线连接


    CPU除了有控制器、运算器还有寄存器。其中寄存器的作用就是进行数据的临时存储。

    CPU的运算速度是非常快的,为了性能CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。我们称这一小块临时存储区域为寄存器

    对于arm64系的CPU来说, 如果寄存器以x开头则表明的是一个64位的寄存器,如果以w开头则表明是一个32位的寄存器,在系统中没有提供16位和8位的寄存器供访问和使用。其中32位的寄存器是64位寄存器的低32位部分并不是独立存在的

    • 对程序员来说,CPU中最主要部件是寄存器,可以通过改变寄存器的内容来实现对CPU的控制
    • 不同的CPU,寄存器的个数、结构是不相同的

    浮点寄存器

    因为浮点数的存储以及其运算的特殊性,CPU中专门提供浮点数寄存器来处理浮点数。

    • 浮点寄存器 64位D0 - D31 32位: S0 - S31

    向量寄存器

    现在的CPU支持向量运算。(向量运算在图形处理相关的领域用得非常的多)为了支持向量计算系统了也提供了众多的向量寄存器.

    • 向量寄存器 128位:V0-V31

    通用寄存器

    • 通用寄存器也称数据地址寄存器通常用来做数据计算的临时存储、做累加、计数、地址保存等功能。定义这些寄存器的作用主要是用于在CPU指令中保存操作数,在CPU中当做一些常规变量来使用。
    • ARM64拥有32个64位的通用寄存器x0x30,以及XZR(零寄存器),这些通用寄存器有时也有特定用途。
      1.64位X0-X30, XZR(零寄存器)w0 到 w28 这些是32位的。因为64位CPU可以兼容32位.所以可以只使用64位寄存器的低32位.
      2.32位W0-W30, WZR(零寄存器)。 w0 就是 x0 的低32位!

    ⚠️:了解过8086汇编的都知道,有一种特殊的寄存器段寄存器:CS,DS,SS,ES四个寄存器来保存这些段的基地址,这个属于Intel架构CPU中。在ARM中并没有。

    在"Xcode"中我们可以查看具体寄存器的内容:




    分别看一下x0w0的值:
    x0  unsigned long   0x0000000159f0a970
    w0 unsigned int 0x59f0a970

    验证了w0x0的低32位。

    通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算
    假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间:




    pc寄存器

    单步执行汇编代码(pc始终指向下一条指令):




  • 为指令指针寄存器,它指示了CPU当前要读取指令的地址(指向下一条即将执行的指令
  • 在内存或者磁盘上,指令和数据没有任何区别,都是二进制信息
  • CPU在工作的时候把有的信息看做指令,有的信息看做数据,为同样的信息赋予了不同的意义

  • 比如 1110 0000 0000 0011 0000 1000 1010 1010,
    可以当做数据 0xE003008AA。
    也可以当做指令 mov x0, x8

    • CPU根据什么将内存中的信息看做指令?

    CPU将pc指向的内存单元的内容看做指令
    如果内存中的某段内容曾被CPU执行过,那么它所在的内存单元必然被pc指向过。

    高速缓存

    iPhoneX上搭载的ARM处理器A11它的1级缓存的容量是64KB,2级缓存的容量8M。

    CPU每执行一条指令前都需要从内存中将指令读取到CPU内并执行。而寄存器的运行速度相比内存读写要快很多,为了性能,CPU还集成了一个高速缓存存储区域.当程序在运行时,先将要执行的指令代码以及数据复制到高速缓存中去(由操作系统完成)。CPU直接从高速缓存依次读取指令来执行。

    bl指令

    bl分位bl:
    b:跳转。
    l:lr寄存器。

    • CPU从何处执行指令是由pc中的内容决定的,我们可以通过改变pc的内容来控制CPU执行目标指令
    • ARM64提供了一个mov指令(传送指令),可以用来修改大部分寄存器的值,比如:
      mov x0,#10、mov x1,#20
    • 但是,mov指令不能用于设置pc的值,ARM64没有提供这样的功能
    • ARM64提供了另外的指令来修改PC的值,这些指令统称为转移指令,最简单的是bl指令

    案例
    现在有两段代码!假设程序先执行A,请写出指令执行顺序。最终寄存器x0的值是多少?

    _A:
    mov x0,#0xa0
    mov x1,#0x00
    add x1, x0, #0x14
    mov x0,x1
    bl _B
    mov x0,#0x0
    ret

    _B:
    add x0, x0, #0x10
    ret

    分析:


    Xcode中创建Empty文件命名为asm.s.s汇编代码会被Xcode自动识别编译)。


    //asm.s
    .text // 告诉是代码
    .global _A, _B //.global 是标号

    _A:
    mov x0,#0xa0 //a0 给 x0 x0 = 0xa0
    mov x1,#0x00 //00 给x1 x1 = 0x00
    add x1, x0, #0x14 //x0 + 0x14 给 x1 x1 = 0xb4
    mov x0,x1 //x1 的值给 x0 x0 = 0xb4
    bl _B //跳转B
    mov x0,#0x0 //0x0 给 x0 x0 = 0x0
    ret //return 上层调用的地方

    _B:
    add x0, x0, #0x10 //x0 + 0x10 给 x0 x0 = 0xc4
    ret //return A

    oc调用汇编:

    //ViewController.m
    int A();

    - (void)viewDidLoad {
    [super viewDidLoad];
    A();
    }
    swift调用汇编:

    //声明方法A。Swift中C和汇编都可以这么暴露。
    @_silgen_name("A")
    func A()

    class ViewController: UIViewController {

    override func viewDidLoad() {
    super.viewDidLoad()
    A();
    }

    }
    答案:0x00



    断点验证了x0最终值为0x00。这里有个问题是发生死循环了。(bl跳转指令导致的,lr寄存器在跳转后需要保护现场还原。)


    总结

    • 汇编概述:
      • 使用助记符代替集齐指令的一种编程语言。
      • 汇编和及其指令是一一对应的关系,拿到二进制就可以反汇编。
      • 由于汇编和CPU指令集是对应的,所以汇编不具备移植性。
    • 总线:是一堆导线的集合
      • 地址总线:地址总线的宽度决定了寻址能力
      • 数据总线:数据总线的宽度决定了CPU的吞吐量
    • 进制
      • 任意进制都是由对应个数的符号组成的。符号可以自定义。
      • 2/8/16是相对完美的集智,他们之间的关系
        • 3个2进制使用一个8进制标识
        • 4个2进制使用一个16进制标识
        • 两个16进制位可以标识一个字节
      • 数量单位
        • 1024 = 1K;1024K = 1M;1024M = 1G
      • 容量单位
        • 1024B = 1KB;1024KB = 1MB; 1024MB = 1GB
        • B:byte(字节)1B = 8bit
        • bit(比特):一个二进制位
      • 数据的宽度
        • 计算机中的数据是有宽度的,超过了就会溢出
    • 寄存器:CPU为了性能,在内部开辟了一小块临时存储区域
      • 浮点向量寄存器
      • 异常状态寄存器
      • 通用寄存器:除了存放数据有时候也有特殊的用途
        • ARM64拥有32个64位的通用寄存器X0—X30以及XZR(令寄存器)
        • 为了兼容32位,所以ARM64拥有W0—W28\WZR 30个32位寄存器
        • 32位寄存器并不是独立存在的,比如W0是X0的低32位
      • PC寄存器:指令指针寄存器
        • PC寄存器里面的值保存的就是CPU接下来需要执行的指令地址!
        • 改变PC的值可以改变程序的执行流程!


    作者:HotPotCat
    链接:https://www.jianshu.com/p/e8ea78cb10f0



    收起阅读 »

    iOS - Path menu 的动画效果

    AwesomeMenu 是一个与Path的故事菜单外观相同的菜单。通过设置菜单项来创建菜单:UIImage *storyMenuItemImage = [UIImage imageNamed:@"bg-menuitem.png"]; UIImage *sto...
    继续阅读 »

    AwesomeMenu 是一个与Path的故事菜单外观相同的菜单

    通过设置菜单项来创建菜单:

    UIImage *storyMenuItemImage = [UIImage imageNamed:@"bg-menuitem.png"];
    UIImage *storyMenuItemImagePressed = [UIImage imageNamed:@"bg-menuitem-highlighted.png"];
    UIImage *starImage = [UIImage imageNamed:@"icon-star.png"];
    AwesomeMenuItem *starMenuItem1 = [[AwesomeMenuItem alloc] initWithImage:storyMenuItemImage
    highlightedImage:storyMenuItemImagePressed
    ContentImage:starImage
    highlightedContentImage:nil];
    AwesomeMenuItem *starMenuItem2 = [[AwesomeMenuItem alloc] initWithImage:storyMenuItemImage
    highlightedImage:storyMenuItemImagePressed
    ContentImage:starImage
    highlightedContentImage:nil];
    // the start item, similar to "add" button of Path
    AwesomeMenuItem *startItem = [[AwesomeMenuItem alloc] initWithImage:[UIImage imageNamed:@"bg-addbutton.png"]
    highlightedImage:[UIImage imageNamed:@"bg-addbutton-highlighted.png"]
    ContentImage:[UIImage imageNamed:@"icon-plus.png"]
    highlightedContentImage:[UIImage imageNamed:@"icon-plus-highlighted.png"]];




    然后,设置菜单和选项:


    AwesomeMenu *menu = [[AwesomeMenu alloc] initWithFrame:self.window.bounds startItem:startItem optionMenus:[NSArray arrayWithObjects:starMenuItem1, starMenuItem2]];
    menu.delegate = self;
    [self.window addSubview:menu];

    您还可以使用菜单选项:

    找到“添加”按钮的中心:

    menu.startPoint = CGPointMake(160.0, 240.0);

    设置旋转角度:

    menu.rotateAngle = 0.0;

    设置整个菜单角度:

    menu.menuWholeAngle = M_PI * 2;

    设置每个菜单飞出动画的延迟:

    menu.timeOffset = 0.036f;

    调整弹跳动画:

    menu.farRadius = 140.0f;
    menu.nearRadius = 110.0f;

    设置“添加”按钮和菜单项之间的距离:

    menu.endRadius = 120.0f;


    常见问题及demo下载:https://github.com/levey/AwesomeMenu

    源码下载:AwesomeMenu-master.zip

    收起阅读 »

    iOS 滑动效果cell - SWTableViewCell

    SWTableViewCell一个易于使用的 UITableViewCell 子类,它实现了一个可滑动的内容视图,它公开了实用程序按钮(类似于 iOS 7 邮件应用程序)在你的 Podfile 中:- (void)tableView:(UITableView ...
    继续阅读 »

    SWTableViewCell

    一个易于使用的 UITableViewCell 子类,它实现了一个可滑动的内容视图,它公开了实用程序按钮(类似于 iOS 7 邮件应用程序)

    在你的 Podfile 中:

    pod 'SWTableViewCell', '~> 0.3.7'


    或者只是克隆这个 repo 并手动将源添加到项目

    当用户向左滑动时,在表格视图单元格右侧可见的实用程序按钮。此行为类似于在 iOS 应用程序邮件和提醒中看到的行为。



    实用程序按钮 当用户向右滑动时,在表格视图单元格左侧可见的实用程序按钮。


    • 动态实用程序按钮缩放。当您向单元格添加更多按钮时,该侧的其他按钮会变小以腾出空间
    • 智能选择:单元格将拾取触摸事件并将单元格滚动回中心或触发委托方法 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

    因此,当实用程序按钮可见时,当用户触摸单元格时,单元格不会被视为选中,相反,单元格将滑回原位(与 iOS 7 邮件应用程序功能相同) * 创建带有标题或图标的实用程序按钮以及RGB 颜色 * 在 iOS 6.1 及更高版本上测试,包括 iOS 7

    用法

    标准表格视图单元格

    在您的tableView:cellForRowAtIndexPath:方法中,您设置 SWTableView 单元格并使用包含的NSMutableArray+SWUtilityButtons类别向其中添加任意数量的实用程序按钮


    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellIdentifier = @"Cell";

    SWTableViewCell *cell = (SWTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if (cell == nil) {
    cell = [[SWTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
    cell.leftUtilityButtons = [self leftButtons];
    cell.rightUtilityButtons = [self rightButtons];
    cell.delegate = self;
    }

    NSDate *dateObject = _testArray[indexPath.row];
    cell.textLabel.text = [dateObject description];
    cell.detailTextLabel.text = @"Some detail text";

    return cell;
    }

    - (NSArray *)rightButtons
    {
    NSMutableArray *rightUtilityButtons = [NSMutableArray new];
    [rightUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:0.78f green:0.78f blue:0.8f alpha:1.0]
    title:@"More"];
    [rightUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:1.0f green:0.231f blue:0.188 alpha:1.0f]
    title:@"Delete"];

    return rightUtilityButtons;
    }

    - (NSArray *)leftButtons
    {
    NSMutableArray *leftUtilityButtons = [NSMutableArray new];

    [leftUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:0.07 green:0.75f blue:0.16f alpha:1.0]
    icon:[UIImage imageNamed:@"check.png"]];
    [leftUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:1.0f green:1.0f blue:0.35f alpha:1.0]
    icon:[UIImage imageNamed:@"clock.png"]];
    [leftUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:1.0f green:0.231f blue:0.188f alpha:1.0]
    icon:[UIImage imageNamed:@"cross.png"]];
    [leftUtilityButtons sw_addUtilityButtonWithColor:
    [UIColor colorWithRed:0.55f green:0.27f blue:0.07f alpha:1.0]
    icon:[UIImage imageNamed:@"list.png"]];

    return leftUtilityButtons;
    }

    ###Custom Table View Cells

    Thanks to Matt Bowman you can now create custom table view cells using Interface Builder that have the capabilities of an SWTableViewCell

    The first step is to design your cell either in a standalone nib or inside of a table view using prototype cells. Make sure to set the custom class on the cell in interface builder to the subclass you made for it:

    Then set the cell reuse identifier:

    When writing your custom table view cell's code, make sure your cell is a subclass of SWTableViewCell:

    #import <SWTableViewCell.h>

    @interface MyCustomTableViewCell : SWTableViewCell

    @property (weak, nonatomic) UILabel *customLabel;
    @property (weak, nonatomic) UIImageView *customImageView;

    @end

    If you are using a separate nib and not a prototype cell, you'll need to be sure to register the nib in your table view:

    - (void)viewDidLoad
    {
    [super viewDidLoad];

    [self.tableView registerNib:[UINib nibWithNibName:@"MyCustomTableViewCellNibFileName" bundle:nil] forCellReuseIdentifier:@"MyCustomCell"];
    }

    Then, in the tableView:cellForRowAtIndexPath: method of your UITableViewDataSource (usually your view controller), initialize your custom cell:

    - (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
    {
    static NSString *cellIdentifier = @"MyCustomCell";

    MyCustomTableViewCell *cell = (MyCustomTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier
    forIndexPath:indexPath];

    cell.leftUtilityButtons = [self leftButtons];
    cell.rightUtilityButtons = [self rightButtons];
    cell.delegate = self;

    cell.customLabel.text = @"Some Text";
    cell.customImageView.image = [UIImage imageNamed:@"MyAwesomeTableCellImage"];
    [cell setCellHeight:cell.frame.size.height];
    return cell;
    }

    代理方法

    // click event on left utility button
    - (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerLeftUtilityButtonWithIndex:(NSInteger)index;

    // click event on right utility button
    - (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerRightUtilityButtonWithIndex:(NSInteger)index;

    // utility button open/close event
    - (void)swipeableTableViewCell:(SWTableViewCell *)cell scrollingToState:(SWCellState)state;

    // prevent multiple cells from showing utilty buttons simultaneously
    - (BOOL)swipeableTableViewCellShouldHideUtilityButtonsOnSwipe:(SWTableViewCell *)cell;

    // prevent cell(s) from displaying left/right utility buttons
    - (BOOL)swipeableTableViewCell:(SWTableViewCell *)cell canSwipeToState:(SWCellState)state;


    常见问题及demo下载:https://github.com/CEWendel/SWTableViewCell

    源码下载:SWTableViewCell-master.zip


    收起阅读 »

    iOS 标签浮动-JVFloatLabeledTextField

    JVFloatLabeledTextFieldJVFloatLabeledTextField是 UX 模式的第一个实现,后来被称为“浮动标签模式”。由于移动设备的空间限制,通常仅依靠占位符来标记字段。这带来了 UX 问题,因为一旦用户开始填写表单,就不会出现任...
    继续阅读 »

    JVFloatLabeledTextField

    JVFloatLabeledTextField是 UX 模式的第一个实现,后来被称为“浮动标签模式”

    由于移动设备的空间限制,通常仅依靠占位符来标记字段。这带来了 UX 问题,因为一旦用户开始填写表单,就不会出现任何标签。

    这个 UI 组件库包括 aUITextFieldUITextView子类,旨在通过将占位符转换为浮动标签来改善用户体验,这些标签在填充文本后悬停在字段上方。

    马特 D. 史密斯的设计


    通过 CocoaPods 

    sudo gem install cocoapods

    Podfile在您的项目目录中创建一个

    pod init

    将以下内容添加到您的Podfile项目目标中:

    pod 'JVFloatLabeledTextField'

    然后运行 CocoaPods pod install

    最后,将JVFloatLabeledTextField.h包含JVFloatLabeledTextView.h在您的项目中。

    Carthage

    brew update
    brew install carthage

    Cartfile在您的项目目录中创建一个包含:

    github "jverdi/JVFloatLabeledTextField"

    然后运行 carthagecarthage updateJVFloatLabeledText.frameworkCarthage/Build/iOS目录中添加到您的项目中

    最后,JVFloatLabeledText.h在您的项目中包含

    #import <JVFloatLabeledText/JVFloatLabeledText.h>


    常见问题及demo下载:https://github.com/jverdi/JVFloatLabeledTextField

    源码下载:JVFloatLabeledTextField-main.zip








    收起阅读 »

    Swift - 第三方日历组件CVCalendar使用详解1(配置、基本用法)

    CVCalendar 是一款超好用的第三方日历组件,不仅功能强大,而且可以方便地进行样式自定义。同时,CVCalendar 还提供月视图、周视图两种展示模式,我们可以根据需求自由选择使用。一、安装配置1. 从 GitHub 上下载最新的代码:https://g...
    继续阅读 »

    CVCalendar 是一款超好用的第三方日历组件,不仅功能强大,而且可以方便地进行样式自定义。同时,CVCalendar 还提供月视图、周视图两种展示模式,我们可以根据需求自由选择使用。

    一、安装配置

    1. 从 GitHub 上下载最新的代码:https://github.com/Mozharovsky/CVCalendar
    2. 将下载下来的源码包中 CVCalendar.xcodeproj 拖拽至你的工程中 


    3. 工程 -> General -> Embedded Binaries 项,把 iOS 版的 framework 添加进来:CVCalendar.framework


    4. 最后,在需要使用 CVCalendar 的地方 import 进来就可以了

    import CVCalendar

    二、基本用法

    1,月视图使用样例 

    ① 效果图
    1. 初始化的时候自动显示当月日历,且“今天”的日期文字是红色的。
    2. 顶部导航栏标题显示当前日历的年、月信息,日历左右滑动切换的时候,标题内容也会随之改变。
    3. 点击导航栏右侧的“今天”按钮,日历又会跳回到当前日期。
    4. 点击日历上的任一日期时间后,该日期背景色会变蓝色(如果是今天则变红色)。同时我们在日期选择响应中,将选择的日期弹出显示。

          

    ② 样例代码
    日历组件分为:CVCalendarMenuView 和 CVCalendarView 两部分。前者是显示星期的菜单栏,后者是日期表格视图。这二者的位置和大小我们可以随意调整设置。
    组件提供了许多代理协议让我进行样式调整或功能响应,我们可以选择使用。但其中 CVCalendarViewDelegate, CVCalendarMenuViewDelegate 这两个协议是必须的。

    import UIKit
    import CVCalendar

    class ViewController: UIViewController {
    //星期菜单栏
    private var menuView: CVCalendarMenuView!

    //日历主视图
    private var calendarView: CVCalendarView!

    var currentCalendar: Calendar!

    override func viewDidLoad() {
    super.viewDidLoad()

    currentCalendar = Calendar.init(identifier: .gregorian)

    //初始化的时候导航栏显示当年当月
    self.title = CVDate(date: Date(), calendar: currentCalendar).globalDescription

    //初始化星期菜单栏
    self.menuView = CVCalendarMenuView(frame: CGRect(x:0, y:80, width:300, height:15))

    //初始化日历主视图
    self.calendarView = CVCalendarView(frame: CGRect(x:0, y:110, width:300,
    height:450))

    //星期菜单栏代理
    self.menuView.menuViewDelegate = self

    //日历代理
    self.calendarView.calendarDelegate = self

    //将菜单视图和日历视图添加到主视图上
    self.view.addSubview(menuView)
    self.view.addSubview(calendarView)
    }

    //今天按钮点击
    @IBAction func todayButtonTapped(_ sender: AnyObject) {
    let today = Date()
    self.calendarView.toggleViewWithDate(today)
    }

    override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    //更新日历frame
    self.menuView.commitMenuViewUpdate()
    self.calendarView.commitCalendarViewUpdate()
    }

    override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    }
    }

    extension ViewController: CVCalendarViewDelegate,CVCalendarMenuViewDelegate {
    //视图模式
    func presentationMode() -> CalendarMode {
    //使用月视图
    return .monthView
    }

    //每周的第一天
    func firstWeekday() -> Weekday {
    //从星期一开始
    return .monday
    }

    func presentedDateUpdated(_ date: CVDate) {
    //导航栏显示当前日历的年月
    self.title = date.globalDescription
    }

    //每个日期上面是否添加横线(连在一起就形成每行的分隔线)
    func topMarker(shouldDisplayOnDayView dayView: CVCalendarDayView) -> Bool {
    return true
    }

    //切换月的时候日历是否自动选择某一天(本月为今天,其它月为第一天)
    func shouldAutoSelectDayOnMonthChange() -> Bool {
    return false
    }

    //日期选择响应
    func didSelectDayView(_ dayView: CVCalendarDayView, animationDidFinish: Bool) {
    //获取日期
    let date = dayView.date.convertedDate()!
    // 创建一个日期格式器
    let dformatter = DateFormatter()
    dformatter.dateFormat = "yyyy年MM月dd日"
    let message = "当前选择的日期是:\(dformatter.string(from: date))"
    //将选择的日期弹出显示
    let alertController = UIAlertController(title: "", message: message,
    preferredStyle: .alert)
    let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
    alertController.addAction(okAction)
    self.present(alertController, animated: true, completion: nil)
    }
    }

    2,周视图使用样例

    同月视图模式相比,周视图日历区域只有一行(每次显示7天日期)。其它方面和月视图相比差别不大,也都是左右滑动切换显示下一周、下一周日期。


    import UIKit
    import CVCalendar

    class ViewController: UIViewController {
    //星期菜单栏
    private var menuView: CVCalendarMenuView!

    //日历主视图
    private var calendarView: CVCalendarView!

    var currentCalendar: Calendar!

    override func viewDidLoad() {
    super.viewDidLoad()

    currentCalendar = Calendar.init(identifier: .gregorian)

    //初始化的时候导航栏显示当年当月
    self.title = CVDate(date: Date(), calendar: currentCalendar).globalDescription

    //初始化星期菜单栏
    self.menuView = CVCalendarMenuView(frame: CGRect(x:0, y:80, width:300, height:15))

    //初始化日历主视图
    self.calendarView = CVCalendarView(frame: CGRect(x:0, y:110, width:300,
    height:50))

    //星期菜单栏代理
    self.menuView.menuViewDelegate = self

    //日历代理
    self.calendarView.calendarDelegate = self

    //将菜单视图和日历视图添加到主视图上
    self.view.addSubview(menuView)
    self.view.addSubview(calendarView)
    }

    //今天按钮点击
    @IBAction func todayButtonTapped(_ sender: AnyObject) {
    let today = Date()
    self.calendarView.toggleViewWithDate(today)
    }

    override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    //更新日历frame
    self.menuView.commitMenuViewUpdate()
    self.calendarView.commitCalendarViewUpdate()
    }

    override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    }
    }

    extension ViewController: CVCalendarViewDelegate,CVCalendarMenuViewDelegate {
    //视图模式
    func presentationMode() -> CalendarMode {
    //使用周视图
    return .weekView
    }

    //每周的第一天
    func firstWeekday() -> Weekday {
    //从星期一开始
    return .monday
    }

    func presentedDateUpdated(_ date: CVDate) {
    //导航栏显示当前日历的年月
    self.title = date.globalDescription
    }

    //每个日期上面是否添加横线(连在一起就形成每行的分隔线)
    func topMarker(shouldDisplayOnDayView dayView: CVCalendarDayView) -> Bool {
    return true
    }

    //切换周的时候日历是否自动选择某一天(本周为今天,其它周为第一天)
    func shouldAutoSelectDayOnWeekChange() -> Bool {
    return false
    }

    //日期选择响应
    func didSelectDayView(_ dayView: CVCalendarDayView, animationDidFinish: Bool) {
    //获取日期
    let date = dayView.date.convertedDate(calendar: currentCalendar)!
    // 创建一个日期格式器
    let dformatter = DateFormatter()
    dformatter.dateFormat = "yyyy年MM月dd日"
    let message = "当前选择的日期是:\(dformatter.string(from: date))"
    //将选择的日期弹出显示
    let alertController = UIAlertController(title: "", message: message,
    preferredStyle: .alert)
    let okAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
    alertController.addAction(okAction)
    self.present(alertController, animated: true, completion: nil)
    }
    }

    转自:https://www.hangge.com/blog/cache/detail_1504.html#

    收起阅读 »

    iOS-使用SDCycleScrollView定制各种自定义样式的上下滚动的跑马灯

    SDCycleScrollView的优点及实现技巧:1.利用UICollectionView的复用机制,只会创建屏幕可见个cell。2.如果是无限循环 ,会存在100*self.imagePathsGroup.count个item,第一次出现的位置在(100*...
    继续阅读 »

    SDCycleScrollView的优点及实现技巧:

    1.利用UICollectionView的复用机制,只会创建屏幕可见个cell。
    2.如果是无限循环 ,会存在100*self.imagePathsGroup.count个item,第一次出现的位置在(100*self.imagePathsGroup.count)/2的位置。
    3.每次滚动到100*self.imagePathsGroup.count位置的item自动切换到(100*self.imagePathsGroup.count)/2的位置。
    4.使用取余index % self.imagePathsGroup.count确定现在显示的imageView

    缺点:

    手动拖拽到最后、不会跳到初始位置

    原因:

    因为作者设置的100足够大、未对拖拽最后一个item做处理

    解决方法:

    同时监听NSTimer和拖拽,在(100 - 1)*self.imagePathsGroup.count和self.imagePathsGroup.count位置时实现切换到(100*self.imagePathsGroup.count)/2的位置

    使用SDCycleScrollView制作各种自定义样式的上下滚动的跑马灯

    效果图:


    .m

    @interface ViewController () <SDCycleScrollViewDelegate>
    @end
    @implementation ViewController
    {
    NSArray *_imagesURLStrings;
    SDCycleScrollView *_customCellScrollViewDemo;
    }

    - (void)customCellScrollView {

    // 如果要实现自定义cell的轮播图,必须先实现customCollectionViewCellClassForCycleScrollView:和 setupCustomCell:forIndex:代理方法

    _customCellScrollViewDemo = [SDCycleScrollView cycleScrollViewWithFrame:CGRectMake(0, 820, w, 40) delegate:self placeholderImage:[UIImage imageNamed:@"placeholder"]];
    _customCellScrollViewDemo.currentPageDotImage = [UIImage imageNamed:@"pageControlCurrentDot"];
    _customCellScrollViewDemo.pageDotImage = [UIImage imageNamed:@"pageControlDot"];
    _customCellScrollViewDemo.imageURLStringsGroup = imagesURLStrings;
    _customCellScrollViewDemo.scrollDirection = UICollectionViewScrollDirectionVertical;
    _customCellScrollViewDemo.showPageControl = NO;
    [demoContainerView addSubview:_customCellScrollViewDemo];
    }

    // 不需要自定义轮播cell的请忽略下面的代理方法

    // 如果要实现自定义cell的轮播图,必须先实现customCollectionViewCellClassForCycleScrollView:和setupCustomCell:forIndex:代理方法
    - (Class)customCollectionViewCellClassForCycleScrollView:(SDCycleScrollView *)view
    {
    if (view != _customCellScrollViewDemo) {
    return nil;
    }
    return [CustomCollectionViewCell class];
    }

    - (void)setupCustomCell:(UICollectionViewCell *)cell forIndex:(NSInteger)index cycleScrollView:(SDCycleScrollView *)view
    {
    CustomCollectionViewCell *myCell = (CustomCollectionViewCell *)cell;
    //[myCell.imageView sd_setImageWithURL:_imagesURLStrings[index]];

    NSArray *titleArray = @[@"新闻",
    @"娱乐",
    @"体育"];
    NSArray *contentArray = @[@"新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻新闻",
    @"娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐娱乐",
    @"体育体育体育体育体育体育体育体育体育体育体育体育"];
    myCell.titleLabel.text = titleArray[index];
    myCell.contentLabel.text = contentArray[index];
    }

    自定义cell-根据不同的cell定制各种自定义样式的上下滚动的跑马灯

    .h
    #import <UIKit/UIKit.h>

    @interface CustomCollectionViewCell : UICollectionViewCell

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

    @end
    .m
    #import "CustomCollectionViewCell.h"
    #import "UIView+SDExtension.h"

    @implementation CustomCollectionViewCell

    #pragma mark - 懒加载
    - (UIImageView *)imageView {
    if (!_imageView) {
    _imageView = [UIImageView new];
    _imageView.layer.borderColor = [[UIColor redColor] CGColor];
    _imageView.layer.borderWidth = 0;
    _imageView.hidden = YES;
    }
    return _imageView;
    }
    - (UILabel *)titleLabel {
    if (!_titleLabel) {
    _titleLabel = [[UILabel alloc]init];
    _titleLabel.text = @"新闻";
    _titleLabel.textColor = [UIColor redColor];
    _titleLabel.numberOfLines = 0;
    _titleLabel.textAlignment = NSTextAlignmentCenter;
    _titleLabel.font = [UIFont systemFontOfSize:12];
    _titleLabel.backgroundColor = [UIColor yellowColor];
    _titleLabel.layer.masksToBounds = YES;
    _titleLabel.layer.cornerRadius = 5;
    _titleLabel.layer.borderColor = [UIColor redColor].CGColor;
    _titleLabel.layer.borderWidth = 1.f;
    }
    return _titleLabel;
    }
    - (UILabel *)contentLabel {
    if (!_contentLabel) {
    _contentLabel = [[UILabel alloc]init];
    _contentLabel.text = @"我是label的内容";
    _contentLabel.textColor = [UIColor blackColor];
    _contentLabel.numberOfLines = 0;
    _contentLabel.font = [UIFont systemFontOfSize:12];
    }
    return _contentLabel;
    }
    #pragma mark - 页面初始化
    - (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
    self.contentView.backgroundColor = [UIColor whiteColor];
    [self setupViews];
    }
    return self;
    }

    #pragma mark - 添加子控件
    - (void)setupViews {
    [self.contentView addSubview:self.imageView];
    [self.contentView addSubview:self.titleLabel];
    [self.contentView addSubview:self.contentLabel];
    }

    #pragma mark - 布局子控件
    - (void)layoutSubviews {
    [super layoutSubviews];
    _imageView.frame = self.bounds;
    _titleLabel.frame = CGRectMake(15, 10, 45, 20);
    _contentLabel.frame = CGRectMake(15 + 45 + 15, 10, 200, 20);
    }

    实际情况自己可下载SDCycleScrollView自行研究。。。

    转自:https://www.jianshu.com/p/641403879f7b

    收起阅读 »

    iOS第三方——JazzHands

    JazzHands是UIKit一个简单的关键帧基础动画框架。可通过手势、scrollView,kvo或者ReactiveCocoa控制动画。JazzHands很适合用来创建很酷的引导页。Swift中的JazzHands想在Swift中使用Jazz Hands?...
    继续阅读 »

    JazzHands是UIKit一个简单的关键帧基础动画框架。可通过手势、scrollView,kvo或者ReactiveCocoa控制动画。JazzHands很适合用来创建很酷的引导页。


    Swift中的JazzHands

    想在Swift中使用Jazz Hands?可以试试RazzleDazzle。

    安装

    JazzHands可以通过CocoaPods安装,在Podfile中加入如下的一行:

    pod "JazzHands"

    你也可以把JazzHands文件夹的内容复制到工程中。

    快速开始

    首先,在UIViewController中加入JazzHands:

    #import <IFTTTJazzHands.h>

    现在创建一个Animator来管理UIViewController中所有的动画。

    @property (nonatomic, strong) IFTTTAnimator *animator;

    // later...

    self.animator = [IFTTTAnimator new];

    为你想要动画的view,创建一个animation。这儿有许多可以应用到view的animation。例如,我们使用IFTTTAlphaAnimation,可以使view淡入淡出。

    IFTTTAlphaAnimation *alphaAnimation = [IFTTTAlphaAnimation animationWithView: viewThatYouWantToAnimate];

    使用animator注册这个animation。

    [self.animator addAnimation: alphaAnimation];

    为animation添加一些keyframe关键帧。我们让这个view在times的30和60之间变淡(Let’s fade this view out between times 30 and 60)。

    [alphaAnimation addKeyframeForTime:30 alpha:1.f];
    [alphaAnimation addKeyframeForTime:60 alpha:0.f];

    现在,让view动起来,要让animator知道what time it is。例如,把这个animation和UIScrollView绑定起来,在scroll的代理方法中来通知animator。

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
    [super scrollViewDidScroll:scrollView];
    [self.animator animate:scrollView.contentOffset.x];
    }

    这样会产生的效果是,view在滚动位置的0到30之间时,view会淡入,变的可见。在滚动位置的30到60之间,view会淡出,变的不可见。而且在滚动位置大于60的时候会保持fade out。

    动画的类型

    Jazz Hands支持多种动画:

    IFTTTAlphaAnimation 动画的是 alpha 属性 (创造的是淡入淡出的效果).
    IFTTTRotationAnimation 动画的是旋转变换 (旋转效果).
    IFTTTBackgroundColorAnimation 动画的是 backgroundColor 属性.
    IFTTTCornerRadiusAnimation 动画的是 layer.cornerRadius 属性.
    IFTTTHideAnimation 动画的是 hidden属性 (隐藏和展示view).
    IFTTTScaleAnimation 应用一个缩放变换 (缩放尺寸).
    IFTTTTranslationAnimation 应用一个平移变换 (平移view的位置).
    IFTTTTransform3DAnimation 动画的是 layer.transform 属性 (是3D变换).
    IFTTTTextColorAnimation 动画的是UILabel的 textColor 属性。
    IFTTTFillColorAnimation 动画的是CAShapeLayer的fillColor属性。
    IFTTTStrokeStartAnimation 动画的是CAShapeLayer的strokeStart属性。(does not work with IFTTTStrokeEndAnimation).
    IFTTTStrokeEndAnimation 动画的是CAShapeLayer的strokeEnd属性。 (does not work with IFTTTStrokeStartAnimation).
    IFTTTPathPositionAnimation 动画的是UIView的layer.position属性。
    IFTTTConstraintConstantAnimation animates an AutoLayout constraint constant.
    IFTTTConstraintMultiplierAnimation animates an AutoLayout constraint constant as a multiple of an attribute of another view (to offset or resize views based on another view’s size)
    IFTTTScrollViewPageConstraintAnimation animates an AutoLayout constraint constant to place a view on a scroll view page (to position views on a scrollView using AutoLayout)
    IFTTTFrameAnimation animates the frame property (moves and sizes views. Not compatible with AutoLayout).

    更多例子

    Easy Paging Scrollview Layouts in an AutoLayout World
    JazzHands的IFTTTAnimatedPagingScrollViewController中的 keepView:onPage:方法,可以非常简单的在scroll view上布局分页。

    调用keepView:onPages: 可以在多个pages上展示一个view,当其它view滚动的时候。

    具体应用的例子

    在开源项目coding/Coding-iOS中的IntroductionViewController有使用到,IntroductionViewController继承自IFTTTAnimatedPagingScrollViewController。

    - (void)configureTipAndTitleViewAnimations{
    for (int index = 0; index < self.numberOfPages; index++) {
    NSString *viewKey = [self viewKeyForIndex:index];
    UIView *iconView = [self.iconsDict objectForKey:viewKey];
    UIView *tipView = [self.tipsDict objectForKey:viewKey];
    if (iconView) {
    if (index == 0) {//第一个页面
    [self keepView:iconView onPages:@[@(index +1), @(index)] atTimes:@[@(index - 1), @(index)]];

    [iconView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.mas_equalTo(kScreen_Height/7);
    }];
    }else{
    [self keepView:iconView onPage:index];

    [iconView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.centerY.mas_equalTo(-kScreen_Height/6);//位置往上偏移
    }];
    }
    IFTTTAlphaAnimation *iconAlphaAnimation = [IFTTTAlphaAnimation animationWithView:iconView];
    [iconAlphaAnimation addKeyframeForTime:index -0.5 alpha:0.f];
    [iconAlphaAnimation addKeyframeForTime:index alpha:1.f];
    [iconAlphaAnimation addKeyframeForTime:index +0.5 alpha:0.f];
    [self.animator addAnimation:iconAlphaAnimation];
    }
    if (tipView) {
    [self keepView:tipView onPages:@[@(index +1), @(index), @(index-1)] atTimes:@[@(index - 1), @(index), @(index + 1)]];

    IFTTTAlphaAnimation *tipAlphaAnimation = [IFTTTAlphaAnimation animationWithView:tipView];
    [tipAlphaAnimation addKeyframeForTime:index -0.5 alpha:0.f];
    [tipAlphaAnimation addKeyframeForTime:index alpha:1.f];
    [tipAlphaAnimation addKeyframeForTime:index +0.5 alpha:0.f];
    [self.animator addAnimation:tipAlphaAnimation];

    [tipView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(iconView.mas_bottom).offset(kScaleFrom_iPhone5_Desgin(45));
    }];
    }
    }
    }

    效果如下:


    转自:https://blog.csdn.net/u014084081/article/details/53610215

    收起阅读 »

    【iOS】自动布局之Purelayout

    masonry这个第三方库件在github上很出名,貌似也很好用,但是我在看过masonry的介绍和使用方法之后,觉得有点隐隐的蛋疼。因为本人工作时间不多,加上一直都用的是Objective-C,看着masonry提供的方法基本上都是点语法,我的[]呢?!!怎...
    继续阅读 »

    masonry这个第三方库件在github上很出名,貌似也很好用,但是我在看过masonry的介绍和使用方法之后,觉得有点隐隐的蛋疼。
    因为本人工作时间不多,加上一直都用的是Objective-C,看着masonry提供的方法基本上都是点语法,我的[]呢?!!怎么不在了?

    于是在github上搜索到另外一个较出名的布局,便有了这段Purelayout的尝试。

    生成一个UIView:

    UIView *view = [UIView newAutoLayoutView];
    + (instancetype)newAutoLayoutView
    {
    ALView *view = [self new];
    view.translatesAutoresizingMaskIntoConstraints = NO;
    return view;
    }

    newAutoLayoutView是UIView的一个扩展方法,其实达到的目的就是生成一个UIView实例,并把该实例的translatesAutoresizingMaskIntoConstraints属性置为NO。这个属性值在默认情况下是YES,如果设置为 NO,那么在运行时,程序不会自动将AutoresizingMask转化成 Constraint。

    1.view相对于父容器间距的位置

    [view autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:10];//相对于父容器顶部距离10
    [view autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:10];//相对于父容器左部距离10
    [view autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:10];//相对于父容器右部距离10
    [view autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:10];//相对于父容器底部距离10

    值得注意的是Purelayout对UILabel做了一些人性化的处理:
    在有的国家地区文字是从右至左的,以下代码就是将label的起始位置距离父容器10

    [label autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:10];

    2.相对于父容器的中心位置:

    [view autoCenterInSuperview];//view在父容器中心位置
    [view autoAlignAxisToSuperviewAxis:ALAxisHorizontal];//view在父容器水平中心位置
    [view autoAlignAxisToSuperviewAxis:ALAxisVertical];//view在父容器垂直中心位置

    3.设置大小

    [view autoSetDimensionsToSize:CGSizeMake(300, 300)];//设置view的大小为300*300
    [view autoSetDimension:ALDimensionHeight toSize:300];//设置view的高度为300
    [view autoSetDimension:ALDimensionWidth toSize:300];//设置view的宽度为300

    4.相对位置
    NSLayoutRelation是一个枚举类型:

    typedef NS_ENUM(NSInteger, NSLayoutRelation) {
    NSLayoutRelationLessThanOrEqual = -1,
    NSLayoutRelationEqual = 0,
    NSLayoutRelationGreaterThanOrEqual = 1,
    };

    见名知意,你懂的。

    [view1 autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:view2 withOffset:20 relation:NSLayoutRelationEqual];//view1的顶部在view2的底部的20像素的位置

    5.中心对齐

    [view1 autoAlignAxis:ALAxisVertical toSameAxisOfView:view2];//view1相对于view2保持在同一个垂直中心上

    view1相对于view2保持在同一个垂直中心上

    6.相对大小

    [view1 autoMatchDimension:ALDimensionWidth toDimension:ALDimensionWidth ofView:view2];

    view1的宽度和view2的宽度相等

    在使用purelayout的时候值得注意:
    1.purelayout提供的方法有些是只支持iOS8及以上的,如果iOS7及以下的调用了是会奔溃的,本人就是因为这个被搞得欲仙欲死。好在purelayout在方法中都有介绍。以上介绍的几种使用场景的方法,也都是支持iOS7及以下系统的。
    2.在view父容器为nil的时候,执行purelayout的方法会崩溃。

    有兴趣的可以直接去github下载官方的demo,写的也是相当ok的。

    持续更新~~~

    链接:https://www.jianshu.com/p/15bb1bfec5e9

    收起阅读 »

    SVProgressHUD简单使用以及自定义动画

    SVProgressHUD 是一个干净,易于使用的HUD,旨在显示iOS和tvOS正在进行的任务的进展。常用的还有MBProgressHUD.这两个都是很常用的HUD,大体相似,但是还是有一些不同的.MBProgressHUD和SVProgressHUD的区别...
    继续阅读 »

    SVProgressHUD 是一个干净,易于使用的HUD,旨在显示iOS和tvOS正在进行的任务的进展。
    常用的还有MBProgressHUD.这两个都是很常用的HUD,大体相似,但是还是有一些不同的.
    MBProgressHUD和SVProgressHUD的区别:
    svprogresshud 使用起来很方便,但 可定制 差一些,看它的接口貌似只能添加一个全屏的HUD,不能把它添加到某个视图上面去.
    MBProgressHUD 功能全一些,可定制 高一些,而且可以指定加到某一个View上去.用起来可能就没上面那个方便了.
    具体还要看你的使用场景.
    附上GitHub源码地址:
    SVProgressHUD:https://github.com/SVProgressHUD/SVProgressHUD
    MBProgressHUD:https://github.com/jdg/MBProgressHUD
    今天我们不对二者的区别做详解,有空我会专门写文章对它们的区别做一个详解.
    今天我们主要简单介绍一下SVProgressHUD的使用.


    安装

    通过CocoaPods安装,在Podfile中加入pod 'SVProgressHUD',这里不多做介绍.可以参考文章: CocoaPods的简单使用

    使用

    SVProgressHUD是已经被创建为单例的,所以不需要被实例化了,可以直接使用.调用它的方法[SVProgressHUD method].

    [SVProgressHUD show ];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^ {
    //耗时的任务
    dispatch_async(dispatch_get_main_queue(),^ {
    [SVProgressHUD dismiss ];
    });
    });

    显示HUD

    可以在下拉刷新或者执行其他耗时任务的时候,使用下面方法之一,来显示不确定任务的状态:

    + (void)show;
    + (void)showWithStatus:(NSString*)string;

    效果图分别为:



    如果你希望HUD反应任务的进度,可以使用下面方法的其中一个:

    + (void)showProgress:(CGFloat)progress;
    + (void)showProgress:(CGFloat)progress status:(NSString*)status;

    通过其他方式可以实现进度条的速度把控.比如:

    - (IBAction)clickButtonsShowWithProgress:(id)sender {
    progress = 0.0f;
    [SVProgressHUD showProgress:0 status:@"Loading"];
    [self performSelector:@selector(increaseProgress) withObject:nil afterDelay:0.1f];
    }

    - (void)increaseProgress {
    progress += 0.05f;
    [SVProgressHUD showProgress:progress status:@"xuanhe Loading"];

    if(progress < 1.0f){
    [self performSelector:@selector(increaseProgress) withObject:nil afterDelay:0.1f];
    } else {
    [self performSelector:@selector(dismiss) withObject:nil afterDelay:0.4f];
    }
    }

    效果如下


    还有其他常用的语法:

    +(void)showInfoWithStatus :( NSString *)string;
    +(void)showSuccessWithStatus :( NSString *)string;
    +(void)showErrorWithStatus :( NSString *)string;
    +(void)showImage:(UIImage *)image status :( NSString *)string;

    取消HUD

    HUD可以使用以下方式解除:

    +(void)dismiss;
    +(void)dismissWithDelay :( NSTimeInterval)delay;
    + (void)dismissWithDelay:(NSTimeInterval)delay completion:(SVProgressHUDDismissCompletion)completion;

    可以对这些代码进行改进,比如,在弹框结束后执行其他操作.可以封装一个方法,弹框结束后,执行Block.

    定制

    SVProgressHUD 可以通过以下方法定制:

    + (void)setDefaultStyle:(SVProgressHUDStyle)style;                  // default is SVProgressHUDStyleLight
    + (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType; // default is SVProgressHUDMaskTypeNone
    + (void)setDefaultAnimationType:(SVProgressHUDAnimationType)type; // default is SVProgressHUDAnimationTypeFlat
    + (void)setContainerView:(UIView*)containerView; // default is window level
    + (void)setMinimumSize:(CGSize)minimumSize; // default is CGSizeZero, can be used to avoid resizing
    + (void)setRingThickness:(CGFloat)width; // default is 2 pt
    + (void)setRingRadius:(CGFloat)radius; // default is 18 pt
    + (void)setRingNoTextRadius:(CGFloat)radius; // default is 24 pt
    + (void)setCornerRadius:(CGFloat)cornerRadius; // default is 14 pt
    + (void)setBorderColor:(nonnull UIColor*)color; // default is nil
    + (void)setBorderWidth:(CGFloat)width; // default is 0
    + (void)setFont:(UIFont*)font; // default is [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]
    + (void)setForegroundColor:(UIColor*)color; // default is [UIColor blackColor], only used for SVProgressHUDStyleCustom
    + (void)setBackgroundColor:(UIColor*)color; // default is [UIColor whiteColor], only used for SVProgressHUDStyleCustom
    + (void)setBackgroundLayerColor:(UIColor*)color; // default is [UIColor colorWithWhite:0 alpha:0.4], only used for SVProgressHUDMaskTypeCustom
    + (void)setImageViewSize:(CGSize)size; // default is 28x28 pt
    + (void)setInfoImage:(UIImage*)image; // default is the bundled info image provided by Freepik
    + (void)setSuccessImage:(UIImage*)image; // default is bundled success image from Freepik
    + (void)setErrorImage:(UIImage*)image; // default is bundled error image from Freepik
    + (void)setViewForExtension:(UIView*)view; // default is nil, only used if #define SV_APP_EXTENSIONS is set
    + (void)setGraceTimeInterval:(NSTimeInterval)interval; // default is 0 seconds
    + (void)setMinimumDismissTimeInterval:(NSTimeInterval)interval; // default is 5.0 seconds
    + (void)setMaximumDismissTimeInterval:(NSTimeInterval)interval; // default is CGFLOAT_MAX
    + (void)setFadeInAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds
    + (void)setFadeOutAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds
    + (void)setMaxSupportedWindowLevel:(UIWindowLevel)windowLevel; // default is UIWindowLevelNormal
    + (void)setHapticsEnabled:(BOOL)hapticsEnabled; // default is NO

    样式

    作为标准SVProgressHUD提供两种预先配置的样式:

    SVProgressHUDStyleLight白色背景黑色图标和文字
    SVProgressHUDStyleDark黑色背景与白色图标和文本
    如果要使用自定义颜色使用setForegroundColor和setBackgroundColor:。这些方法将HUD的风格置为SVProgressHUDStyleCustom。

    触觉反馈

    对于具有较新设备的用户(从iPhone 7开始),SVProgressHUD可以根据显示的HUD来自动触发触觉反馈。反馈图如下:

    showSuccessWithStatus: < - > UINotificationFeedbackTypeSuccess

    showInfoWithStatus: < - > UINotificationFeedbackTypeWarning

    showErrorWithStatus: < - > UINotificationFeedbackTypeError

    要启用此功能,请使用setHapticsEnabled: 。

    具有iPhone 7之前的设备的用户将不会改变功能。

    通知

    SVProgressHUD发布四个通知,NSNotificationCenter以响应被显示/拒绝:

    SVProgressHUDWillAppearNotification 提示框即将出现
    SVProgressHUDDidAppearNotification 提示框已经出现
    SVProgressHUDWillDisappearNotification 提示框即将消失
    SVProgressHUDDidDisappearNotification 提示框已经消失

    每个通知通过一个userInfo保存HUD状态字符串(如果有的话)的字典,可以通过检索SVProgressHUDStatusUserInfoKey。

    SVProgressHUD SVProgressHUDDidReceiveTouchEventNotification当用户触摸整个屏幕或SVProgressHUDDidTouchDownInsideNotification用户直接触摸HUD时也会发布。由于此通知userInfo未被传递,而对象参数包含UIEvent与触摸相关的参数。

    应用扩展

    这里对这个功能不做详解.自行摸索.

    自定义动画

    SVProgressHUD提供了方法可以自定义图片.但是不支持gif格式,直接利用下面的方法依然显示一张静态的图片

    [SVProgressHUD showImage:[UIImage imageNamed:@"loading.gif"] status:@"加载中..."];

    我们可以把gif转化为一个动态的image.
    下面是我在百度上搜的一个方法.仅供参考.

    #import <UIKit/UIKit.h>

    typedef void (^GIFimageBlock)(UIImage *GIFImage);
    @interface UIImage (GIFImage)

    /** 根据本地GIF图片名 获得GIF image对象 */
    + (UIImage *)imageWithGIFNamed:(NSString *)name;

    /** 根据一个GIF图片的data数据 获得GIF image对象 */
    + (UIImage *)imageWithGIFData:(NSData *)data;

    /** 根据一个GIF图片的URL 获得GIF image对象 */
    + (void)imageWithGIFUrl:(NSString *)url and:(GIFimageBlock)gifImageBlock;

    下面是.m的方法实现.

    #import "UIImage+GIFImage.h"
    #import <ImageIO/ImageIO.h>
    @implementation UIImage (GIFImage)
    + (UIImage *)imageWithGIFData:(NSData *)data{

    if (!data) return nil;
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    size_t count = CGImageSourceGetCount(source);
    UIImage *animatedImage;
    if (count <= 1) {
    animatedImage = [[UIImage alloc] initWithData:data];
    } else {
    NSMutableArray *images = [NSMutableArray array];
    NSTimeInterval duration = 0.0f;
    for (size_t i = 0; i < count; i++) {
    // 拿出了Gif的每一帧图片
    CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
    //Learning... 设置动画时长 算出每一帧显示的时长(帧时长)
    NSTimeInterval frameDuration = [UIImage sd_frameDurationAtIndex:i source:source];
    duration += frameDuration;
    // 将每帧图片添加到数组中
    [images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
    // 释放真图片对象
    CFRelease(image);
    }
    // 设置动画时长
    if (!duration) {
    duration = (1.0f / 10.0f) * count;
    }
    animatedImage = [UIImage animatedImageWithImages:images duration:duration];
    }

    // 释放源Gif图片
    CFRelease(source);
    return animatedImage;
    }
    + (UIImage *)imageWithGIFNamed:(NSString *)name{
    NSUInteger scale = (NSUInteger)[UIScreen mainScreen].scale;
    return [self GIFName:name scale:scale];
    }

    + (UIImage *)GIFName:(NSString *)name scale:(NSUInteger)scale{
    NSString *imagePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@@%zdx", name, scale] ofType:@"gif"];
    if (!imagePath) {
    (scale + 1 > 3) ? (scale -= 1) : (scale += 1);
    imagePath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"%@@%zdx", name, scale] ofType:@"gif"];
    }
    if (imagePath) {
    // 传入图片名(不包含@Nx)
    NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
    return [UIImage imageWithGIFData:imageData];
    } else {
    imagePath = [[NSBundle mainBundle] pathForResource:name ofType:@"gif"];
    if (imagePath) {
    // 传入的图片名已包含@Nx or 传入图片只有一张 不分@Nx
    NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
    return [UIImage imageWithGIFData:imageData];
    } else {
    // 不是一张GIF图片(后缀不是gif)
    return [UIImage imageNamed:name];
    }
    }
    }
    + (void)imageWithGIFUrl:(NSString *)url and:(GIFimageBlock)gifImageBlock{
    NSURL *GIFUrl = [NSURL URLWithString:url];
    if (!GIFUrl) return;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSData *CIFData = [NSData dataWithContentsOfURL:GIFUrl];
    // 刷新UI在主线程
    dispatch_async(dispatch_get_main_queue(), ^{
    gifImageBlock([UIImage imageWithGIFData:CIFData]);
    });
    });
    }
    #pragma mark - <关于GIF图片帧时长(Learning...)>
    + (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
    float frameDuration = 0.1f;
    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
    NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
    NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary];
    NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime];
    if (delayTimeUnclampedProp) {
    frameDuration = [delayTimeUnclampedProp floatValue];
    }
    else {
    NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime];
    if (delayTimeProp) {
    frameDuration = [delayTimeProp floatValue];
    }
    }
    // Many annoying ads specify a 0 duration to make an image flash as quickly as possible.
    // We follow Firefox's behavior and use a duration of 100 ms for any frames that specify
    // a duration of <= 10 ms. See and
    // for more information.
    if (frameDuration < 0.011f) {
    frameDuration = 0.100f;
    }
    CFRelease(cfFrameProperties);
    return frameDuration;
    }
    @end

    这个是UIimage的分类,在用到的控制器里面调用代码方法即可.这个分类实现我也不太懂.只会用.

    _imgView1.image = [UIImage imageWithGIFNamed:@"xuanxuan"];

    NSString *path = [[NSBundle mainBundle] pathForResource:@"xuanxuan" ofType:@"gif"];
    NSData *imgData = [NSData dataWithContentsOfFile:path];
    _imgView2.image = [UIImage imageWithGIFData:imgData];


    [UIImage imageWithGIFUrl:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1495708809771&di=da92fc5cf3bdd684711ab5124ee43183&imgtype=0&src=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fw%253D580%2Fsign%3D91bd6cd2d42a60595210e1121835342d%2F212eb9389b504fc215d0301ee6dde71190ef6d1a.jpg" and:^(UIImage *GIFImage) {
    _imgView3.image = GIFImage;
    }];

    转自:https://www.jianshu.com/p/fa22b7c27e1d

    收起阅读 »

    ios中应用Lottie解决动画问题

    Lottie的简单介绍:使用Lottie开发的流程是: 设计师在AE中设计完成你的动画,通过bodymoving插件导出纪录动画信息的JSON文件,然后开发人员使用 Lottie 的Android,iOS,React Native apps开源动画库读取这份J...
    继续阅读 »

    Lottie的简单介绍:

    使用Lottie开发的流程是: 设计师在AE中设计完成你的动画,通过bodymoving插件导出纪录动画信息的JSON文件,然后开发人员使用 Lottie 的Android,iOS,React Native apps开源动画库读取这份JSON文件, 解析动画结构和参数信息并渲染。

    Lottie的优点:

    1、设计即所见: 设计师用AE设计好动画后直接导出Json文件,Lottie 解析Json文件后调Core Animation的API绘制渲染。还原度更好,开发成本更低。
    2、跨平台: 支持iOS、Android、React Native。
    3、性能:Lottie对于从AE导出的Json文件,用Core Animation做矢量动画, 性能较佳。Lottie 对解析后的数据模型有内存缓存。但是对多图片帧动画,性能比较差。
    支持动画属性多:比起脸书的Keyframes,Lottie支持了更多AE动画属性,比如Mask, Trim Paths,Stroke (shape layer)等。
    4、包大小,相比动辄上百K的帧动画,Json文件包大小很小。有图片资源的情况下,同一张图片也可以被多个图层复用,而且运行时内存中只有一个UIImage对象(iOS)。

    Lottie在iOS中的使用

    1、pod 'lottie-ios' 使用cocoaPods来加载Lottie。
    2、在使用的界面添加头文件#import <Lottie/Lottie.h>
    3、简单的使用介绍(要想深入学习,还需要自己点击进入源代码中去深究每一个方法和属性,在此就不一一列举了)

    LOTAnimationView * animation = [LOTAnimationView animationNamed:@"HappyBirthday"];
    animation.loopAnimation = YES; //是否是循环播放
    animation.frame = self.view.bounds;
    [self.view addSubview:animation];
    animation.backgroundColor = [UIColor whiteColor];
    [animation playWithCompletion:^(BOOL animationFinished) {
    //播放完成,循环播放则不进入此方法
    }];
    //可以以动画为北京来添加子控件
    UILabel * newV = [[UILabel alloc]initWithFrame:CGRectMake(100,100,200,100)];
    newV.backgroundColor = [UIColor clearColor];
    newV.textColor = [UIColor blackColor];
    newV.text = @"Lottie的使用教程";
    [animation addSubview:newV];

    另外的创建方法

    /// Load animation by name from the default bundle, Images are also loaded from the bundle
    + (nonnull instancetype)animationNamed:(nonnull NSString *)animationName NS_SWIFT_NAME(init(name:));

    /// Loads animation by name from specified bundle, Images are also loaded from the bundle
    + (nonnull instancetype)animationNamed:(nonnull NSString *)animationName inBundle:(nonnull NSBundle *)bundle NS_SWIFT_NAME(init(name:bundle:));

    /// Creates an animation from the deserialized JSON Dictionary
    + (nonnull instancetype)animationFromJSON:(nonnull NSDictionary *)animationJSON NS_SWIFT_NAME(init(json:));

    /// Loads an animation from a specific file path. WARNING Do not use a web URL for file path.
    + (nonnull instancetype)animationWithFilePath:(nonnull NSString *)filePath NS_SWIFT_NAME(init(filePath:));

    /// Creates an animation from the deserialized JSON Dictionary, images are loaded from the specified bundle
    + (nonnull instancetype)animationFromJSON:(nullable NSDictionary *)animationJSON inBundle:(nullable NSBundle *)bundle NS_SWIFT_NAME(init(json:bundle:));

    /// Creates an animation from the LOTComposition, images are loaded from the specified bundle
    - (nonnull instancetype)initWithModel:(nullable LOTComposition *)model inBundle:(nullable NSBundle *)bundle;

    /// Loads animation asynchrounously from the specified URL
    - (nonnull instancetype)initWithContentsOfURL:(nonnull NSURL *)url;

    LOTAnimationView的属性

    /// Flag is YES when the animation is playing
    @property (nonatomic, readonly) BOOL isAnimationPlaying;

    /// Tells the animation to loop indefinitely.
    @property (nonatomic, assign) BOOL loopAnimation;

    /// The animation will play forward and then backwards if loopAnimation is also YES
    @property (nonatomic, assign) BOOL autoReverseAnimation;

    /// Sets a progress from 0 - 1 of the animation. If the animation is playing it will stop and the compeltion block will be called.
    /// The current progress of the animation in absolute time.
    /// e.g. a value of 0.75 always represents the same point in the animation, regardless of positive
    /// or negative speed.
    @property (nonatomic, assign) CGFloat animationProgress;

    /// Sets the speed of the animation. Accepts a negative value for reversing animation.
    @property (nonatomic, assign) CGFloat animationSpeed;

    /// Read only of the duration in seconds of the animation at speed of 1
    @property (nonatomic, readonly) CGFloat animationDuration;

    /// Enables or disables caching of the backing animation model. Defaults to YES
    @property (nonatomic, assign) BOOL cacheEnable;

    /// Sets a completion block to call when the animation has completed
    @property (nonatomic, copy, nullable) LOTAnimationCompletionBlock completionBlock;

    /// Set the amimation data
    @property (nonatomic, strong, nullable) LOTComposition *sceneModel;

    4、简单应用的场景:(1)App的动画引导页。(2)一些特定的动画界面。(3)来作为Tabbar来使用。
    5、这里来介绍下作为Tabbar的使用gitHub上原作者
    6、Lottie动画资源网站
    7、后续有新的学习会更新的。

    链接:https://www.jianshu.com/p/7af085a6a20a

    收起阅读 »

    iOS 音视频编解码基本概念

    内容元素:图像(Image)⾳频(Audio)元信息(Metadata)编码格式: • Video: H264Audio: AAC容器封装: • MP4/MOV/FLV/RM/RMVB/AVI.视频相关基础概念1.视频文件格式相信大家平时接触的word文件后面...
    继续阅读 »



    内容元素:

    • 图像(Image)

    • ⾳频(Audio)

    • 元信息(Metadata)

    • 编码格式: • Video: H264

    • Audio: AAC

    • 容器封装: • MP4/MOV/FLV/RM/RMVB/AVI

    • .视频相关基础概念

      • 1.视频文件格式

        相信大家平时接触的word文件后面带的.doc,图片后缀带有.png/.jpg等,我们常见的视频文件后缀有:.mov、.avi、.mpg、.vob、.mkv、.rm、.rmvb 等等。这些后缀名通常在操作系统上用相应的应用程序打开,比如.doc用word打开。对于视频来说,为什么会有这么多的文件格式了,那是因为通过了不同的方式来实现了视频这件事---------视频的封装格式。
    • 2.视频的封装格式

      视频封装格式,通常我们把它称作为视频格式,它相当于一种容器,比如可乐的瓶子,矿泉水瓶等等。它里面包含了视频的相关信息(视频信息,音频信息,解码方式等等),一种封装格式直接反应了视频的文件格式,封装格式:就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳.通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的.




    • 相关视频封装格式的优缺点:

      • 1.AVI 格式:这种视频格式的优点是图像质量好,无损 AVI 可以保存 alpha 通道。缺点是体积过于庞大,并且压缩标准不统一,存在较多的高低版本兼容问题。
      • 2.WMV 格式:可以直接在网上实时观看视频节目的文件压缩格式。在同等视频质量下,WMV 格式的文件可以边下载边播放,因此很适合在网上播放和传输。
      • 3.MPEG 格式:为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。
      • 4.Matroska 格式:是一种新的视频封装格式,它可将多种不同编码的视频及 16 条以上不同格式的音频和不同语言的字幕流封装到一个 Matroska Media 文件当中。
      • 5.Real Video 格式:用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。
      • 6.QuickTime File Format 格式:是 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。这种封装格式具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。
      • 7.Flash Video 格式: Adobe Flash 延伸出来的一种网络视频封装格式。这种格式被很多视频网站所采用。
    • 视频的编码格式

    • 视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.在做视频编解码时,需要考虑以下这些因素的平衡:

      • 视频的质量、
      • 用来表示视频所需要的数据量(通常称之为码率)、
      • 编码算法和解码算法的复杂度
      • 针对数据丢失和错误的鲁棒性(Robustness)
      • 编辑的方便性
      • 随机访问
      • 编码算法设计的完美性
      • 端到端的延时以及其它一些因素
    • 常见的编码方式:

    • H.26X 系列,由国际电传视讯联盟远程通信标准化组织(ITU-T)主导,包括 H.261、H.262、H.263、H.264、H.265

      • H.261,主要用于老的视频会议和视频电话系统。是第一个使用的数字视频压缩标准。实质上说,之后的所有的标准视频编解码器都是基于它设计的。
      • H.262,等同于 MPEG-2 第二部分,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
      • H.263,主要用于视频会议、视频电话和网络视频相关产品。在对逐行扫描的视频源进行压缩的方面,H.263 比它之前的视频编码标准在性能上有了较大的提升。尤其是在低码率端,它可以在保证一定质量的前提下大大的节约码率。
      • H.264,等同于 MPEG-4 第十部分,也被称为高级视频编码(Advanced Video Coding,简称 AVC),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。该标准引入了一系列新的能够大大提高压缩性能的技术,并能够同时在高码率端和低码率端大大超越以前的诸标准。
      • H.265,被称为高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,是 H.264 的继任者。HEVC 被认为不仅提升图像质量,同时也能达到 H.264 两倍的压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。
    • 当前不建议用H.265是因为太过于消耗CPU,而且目前H.264已经满足了大多的视频需求,虽然H.265是H.264的升级版,期待后续硬件跟上

    • MPEG 系列,由国际标准组织机构(ISO)下属的运动图象专家组(MPEG)开发。

      • MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。
      • MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
      • MPEG-4 第二部分,可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和第一版的 H.263,它的压缩性能有所提高。
      • MPEG-4 第十部分,等同于 H.264,是这两个编码组织合作诞生的标准。
        其他,AMV、AVS、Bink、CineForm 等等,这里就不做多的介绍了。
    • 可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264

    • 音频编码方式

      • 视频中除了画面通常还有声音,所以这就涉及到音频编解码。在视频中经常使用的音频编码方式有

      • AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。

      • MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。

      • WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。

    直播/小视频中的编码格式

    • 视频编码格式

      • H264编码的优势:
        低码率
        高质量的图像
        容错能力强
        网络适应性强
    • 总结: H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
      举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
      音频编码格式:

    • AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式.

    • LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)

    • HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)

    • 优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码,适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码

    关于H264

    • H.264 是现在广泛采用的一种编码方式。关于 H.264 相关的概念,从大到小排序依次是:序列、图像、片组、片、NALU、宏块、亚宏块、块、像素。

    • 图像

      • H.264 中,「图像」是个集合的概念,帧、顶场、底场都可以称为图像。一帧通常就是一幅完整的图像。

    当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。

    当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。

    「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好




    • 片(Slice),每一帧图像可以分为多个片

    网络提取层单元(NALU, Network Abstraction Layer Unit),
    NALU 是用来将编码的数据进行打包的,一个分片(Slice)可以编码到一个 NALU 单元。不过一个 NALU 单元中除了容纳分片(Slice)编码的码流外,还可以容纳其他数据,比如序列参数集 SPS。对于客户端其主要任务则是接收数据包,从数据包中解析出 NALU 单元,然后进行解码播放。

    宏块(Macroblock),分片是由宏块组成。




    作者:枫紫
    链接:https://www.jianshu.com/p/9602f3c9b82b


    收起阅读 »

    iOS 特效 - iCarousel

    iCarousel 是一个旨在简化 iPhone、iPad 和 Mac OS 上各种类型的轮播(分页、滚动视图)的实现的类。iCarousel 实现了许多常见的效果,例如圆柱形、平面和“CoverFlow”风格的轮播,并提供钩子来实现您自己的定制效果。与许多其...
    继续阅读 »

    iCarousel 是一个旨在简化 iPhone、iPad 和 Mac OS 上各种类型的轮播(分页、滚动视图)的实现的类。iCarousel 实现了许多常见的效果,例如圆柱形、平面和“CoverFlow”风格的轮播,并提供钩子来实现您自己的定制效果。与许多其他“CoverFlow”库不同,iCarousel 可以处理任何类型的视图,而不仅仅是图像,因此它非常适合在您的应用程序中以流畅且令人印象深刻的方式呈现分页数据。它还使得以最少的代码更改在不同的轮播效果之间切换变得非常容易。

    支持的操作系统和 SDK 版本

    • 支持的构建目标 - iOS 10.0 / Mac OS 10.12(Xcode 8.0,Apple LLVM 编译器 8.0)
    • 最早支持的部署目标 - iOS 5.0 / Mac OS 10.7
    • 最早的兼容部署目标 - iOS 4.3 / Mac OS 10.6

    注意:“支持”表示该库已经过此版本的测试。“兼容”意味着库应该在这个操作系统版本上工作(即它不依赖于任何不可用的 SDK 功能)但不再进行兼容性测试,可能需要调整或错误修复才能正确运行。

    ARC兼容性

    从 1.8 版开始,iCarousel 需要 ARC。如果您希望在非 ARC 项目中使用 iCarousel,只需将 -fobjc-arc 编译器标志添加到 iCarousel.m 类。为此,请转到目标设置中的 Build Phases 选项卡,打开 Compile Sources 组,双击列表中的 iCarousel.m 并在弹出窗口中键入 -fobjc-arc。

    如果您希望将整个项目转换为 ARC,请在 iCarousel.m 中注释掉 #error 行,然后在 Xcode 中运行 Edit > Refactor > Convert to Objective-C ARC... 工具并确保您希望转换的所有文件使用 ARC 进行(包括 iCarousel.m)检查。

    线程安全

    iCarousel 派生自 UIView 并且 - 与所有 UIKit 组件一样 - 它只能从主线程访问。您可能希望使用线程来加载或更新轮播内容或项目,但始终确保一旦您的内容加载完毕,您就可以在更新轮播前切换回主线程。

    安装

    要在应用程序中使用 iCarousel 类,只需将 iCarousel 类文件(不需要演示文件和资产)拖到您的项目中并添加 QuartzCore 框架。您也可以使用 Cocoapods 以正常方式安装它。


    轮播类型

    iCarousel 支持以下内置显示类型:

    • iCarouselTypeLinear
    • iCarouselTypeRotary
    • iCarouselTypeInvertedRotary
    • iCarouselTypeCylinder
    • iCarouselTypeInvertedCylinder
    • iCarouselTypeWheel
    • iCarouselTypeInvertedWheel
    • iCarouselTypeCoverFlow
    • iCarouselTypeCoverFlow2
    • iCarouselTypeTimeMachine
    • iCarouselTypeInvertedTimeMachine

    您还可以使用iCarouselTypeCustomcarousel:itemTransformForOffset:baseTransform:委托方法实现自己的定制轮播样式

    注意:iCarouselTypeCoverFlowiCarouselTypeCoverFlow2类型之间的区别非常微妙,但是 for 的逻辑要iCarouselTypeCoverFlow2复杂得多。如果您轻弹转盘,它们基本上是相同的,但是如果您用手指缓慢拖动转盘,则差异应该很明显。iCarouselTypeCoverFlow2旨在尽可能接近地模拟标准 Apple CoverFlow 效果,并且将来可能会为了该目标而进行微妙的更改。

    显示类型可视化示例

    线性

    线性

    旋转式

    旋转式


    倒转

    倒转


    圆筒

    圆筒

    倒置气缸

    倒置气缸

    Cover Flow功能

    Cover Flow功能



    特性

    iCarousel 具有以下属性(注意:对于 Mac OS,在使用属性时将 NSView 替换为 UIView):

    @property (nonatomic, weak) IBOutlet id dataSource;

    一个支持 iCarouselDataSource 协议并可以提供视图来填充轮播的对象。

    @property (nonatomic, weak) IBOutlet id delegate;

    一个支持 iCarouselDelegate 协议并且可以响应轮播事件和布局请求的对象。

    @property (nonatomic, assign) iCarouselType type;

    用于切换轮播显示类型(详见上文)。

    @property (nonatomic, assign) CGFloat perspective;

    用于调整各种 3D 轮播视图的透视缩短效果。应为负值,小于 0 且大于 -0.01。超出此范围的值将产生非常奇怪的结果。默认值为 -1/500 或 -0.005;

    @property (nonatomic, assign) CGSize contentOffset;

    此属性用于调整轮播项目视图相对于轮播中心的偏移。它默认为 CGSizeZero,这意味着轮播项目居中。更改此值会移动轮播项目而不改变其视角,即消失点随轮播项目移动,因此如果您将轮播项目向下移动,则不会看起来好像您在俯视轮播。

    @property (nonatomic, assign) CGSize viewpointOffset;

    此属性用于调整相对于轮播项目的用户视角。它与调整 contentOffset 有相反的效果,即如果您向上移动视点,则轮播似乎向下移动。与 contentOffset 不同,移动视点也会改变相对于旋转木马项目的透视消失点,因此如果您向上移动视点,它会看起来好像您在俯视旋转木马。

    @property (nonatomic, assign) CGFloat decelerationRate;

    旋转木马在轻弹时减速的速率。较高的值意味着较慢的减速。默认值为 0.95。值应在 0.0(释放时旋转木马立即停止)到 1.0(旋转木马无限期地继续而不减速,除非它到达终点)的范围内。

    @property (nonatomic, assign) BOOL bounces;

    设置旋转木马是应该弹过终点并返回,还是停止不动。请注意,这对设计为包装的轮播类型或 carouselShouldWrap 委托方法返回 YES 的类型没有影响。

    @property (nonatomic, assign) CGFloat bounceDistance;

    未包裹的传送带越过末端时反弹的最大距离。这是以 itemWidth 的倍数来衡量的,因此值 1.0 表示轮播将反弹整个项目宽度,值 0.5 表示项目宽度的一半,依此类推。默认值为 1.0;

    @property (nonatomic, assign, getter = isScrollEnabled) BOOL scrollEnabled;

    启用和禁用用户滚动轮播。如果此属性设置为 NO,则仍然可以通过编程方式滚动轮播。

    @property (nonatomic, readonly, getter = isWrapEnabled) BOOL wrapEnabled;

    如果启用包装,则返回 YES,否则返回 NO。此属性是只读的。如果您希望覆盖默认值,请实现carousel:valueForOption:withDefault:委托方法并为 返回一个值iCarouselOptionWrap

    @property (nonatomic, assign, getter = isPagingEnabled) BOOL pagingEnabled;

    启用和禁用分页。启用分页后,轮播将在用户滚动时在每个项目视图处停止,这与 UIScrollView 的 pagingEnabled 属性非常相似。

    @property (nonatomic, readonly) NSInteger numberOfItems;

    轮播中的项目数(只读)。要设置它,请实现numberOfItemsInCarousel:dataSource 方法。请注意,并非所有这些项目视图都会在给定的时间点加载或可见 - 轮播在滚动时按需加载项目视图。

    @property (nonatomic, readonly) NSInteger numberOfPlaceholders;

    要在轮播中显示的占位符视图的数量(只读)。要设置它,请实现numberOfPlaceholdersInCarousel:dataSource 方法。

    @property (nonatomic, readonly) NSInteger numberOfVisibleItems;

    屏幕上同时显示的轮播项目视图的最大数量(只读)。此属性对于性能优化很重要,并且会根据轮播类型和视图框架自动计算。如果您希望覆盖默认值,请实现carousel:valueForOption:withDefault:委托方法并为 iCarouselOptionVisibleItems 返回一个值。

    @property (nonatomic, strong, readonly) NSArray *indexesForVisibleItems;

    一个数组,包含当前加载和在轮播中可见的所有项目视图的索引,包括占位符视图。该数组包含 NSNumber 对象,其整数值与视图的索引匹配。项目视图的索引从零开始并匹配传递给 dataSource 以加载视图的索引,但是任何可见占位符视图的索引要么是负数(小于零)要么大于或等于numberOfItems此数组中占位符视图的索引等同于与 dataSource 一起使用的占位符视图索引。

    @property (nonatomic, strong, readonly) NSArray *visibleItemViews;

    当前显示在轮播中的所有项目视图的数组(只读)。这包括任何可见的占位符视图。此数组中的视图索引与项目索引不匹配,但是这些视图的顺序与 visibleItemIndexes 数组属性的顺序匹配,即您可以通过从visibleItemIndexes 数组(或者,您可以只使用该indexOfItemView:方法,这要容易得多)。

    @property (nonatomic, strong, readonly) UIView *contentView;

    包含轮播项目视图的视图。如果您想将它们与轮播项目散布,您可以向此视图添加子视图。如果您希望视图出现在所有轮播项目的前面或后面,您应该将其直接添加到 iCarousel 视图本身。请注意,当应用程序运行时, contentView 中的视图顺序会经常发生且未记录的更改。添加到 contentView 的任何视图都应将其 userInteractionEnabled 属性设置为 NO 以防止与 iCarousel 的触摸事件处理发生冲突。

    @property (nonatomic, assign) CGFloat scrollOffset;

    这是轮播的当前滚动偏移量,是 itemWidth 的倍数。这个值,四舍五入到最接近的整数,是 currentItemIndex 值。您可以使用此值在轮播移动时定位其他屏幕元素。如果您希望以编程方式将轮播滚动到特定偏移量,也可以设置该值。如果您希望禁用内置手势处理并提供您自己的实现,这可能很有用。

    @property (nonatomic, readonly) CGFloat offsetMultiplier;

    这是用户用手指拖动轮播时使用的偏移乘数。它不影响编程滚动或减速速度。对于大多数轮播类型,这默认为 1.0,但对于 CoverFlow 风格的轮播默认为 2.0,以补偿它们的项目间隔更近的事实,因此必须进一步拖动以移动相同的距离。您不能直接设置此属性,但可以通过实现carouselOffsetMultiplier:委托方法来覆盖默认值

    @property (nonatomic, assign) NSInteger currentItemIndex;

    轮播中当前居中项目的索引。设置此属性等效于scrollToItemAtIndex:animated:将动画参数设置为 NO进行调用

    @property (nonatomic, strong, readonly) UIView *currentItemView;

    轮播中当前居中的项目视图。此视图的索引匹配currentItemIndex

    @property (nonatomic, readonly) CGFloat itemWidth;

    轮播中项目的显示宽度(只读)。这是从使用carousel:viewForItemAtIndex:reusingView:dataSource 方法传递给轮播的第一个视图自动派生的您还可以使用carouselItemWidth:委托方法覆盖此值,这将更改为轮播项目分配的空间(但不会调整项目视图的大小或缩放)。

    @property (nonatomic, assign) BOOL centerItemWhenSelected;

    当设置为 YES 时,点击轮播中除与 currentItemIndex 匹配的项目之外的任何项目都会使其平滑地动画到中心。点击当前选定的项目将不起作用。默认为是。

    @property (nonatomic, assign) CGFloat scrollSpeed;

    这是用户用手指轻弹轮播时的滚动速度倍增器。默认为 1.0。

    @property (nonatomic, readonly) CGFloat toggle;

    此属性用于iCarouselTypeCoverFlow2轮播变换。它是公开的,以便您可以使用carousel:itemTransformForOffset:baseTransform:委托方法实现自己的 CoverFlow2 样式变体

    @property (nonatomic, assign) BOOL stopAtItemBoundary;

    默认情况下,轮播将在轻弹时停在确切的项目边界处。如果将此属性设置为 NO,它将自然停止,然后 - 如果 scrollToItemBoundary 设置为 YES - 向后或向前滚动到最近的边界。

    @property (nonatomic, assign) BOOL scrollToItemBoundary;

    默认情况下,当轮播停止移动时,它会自动滚动到最近的项目边界。如果将此属性设置为 NO,则轮播在停止后将不会滚动并停留在它所在的位置,即使它在当前索引上没有完全对齐。例外情况是,如果 wrapping 被禁用并bounces设置为 YES,那么无论此设置如何,如果轮播结束后停止,轮播将自动滚动回第一个或最后一个项目索引。

    @property (nonatomic, assign, getter = isVertical) BOOL vertical;

    此属性切换轮播是在屏幕上水平显示还是垂直显示。所有内置的轮播类型都适用于两个方向。切换到垂直会更改轮播的布局以及屏幕上滑动检测的方向。请注意,自定义轮播变换不受此属性影响,但滑动手势方向仍会受到影响。

    @property (nonatomic, readonly, getter = isDragging) BOOL dragging;

    如果用户已开始滚动轮播但尚未释放它,则返回 YES。

    @property (nonatomic, readonly, getter = isDecelerating) BOOL decelerating;

    如果用户不再拖动轮播,但它仍在移动,则返回 YES。

    @property (nonatomic, readonly, getter = isScrolling) BOOL scrolling;

    如果当前正在以编程方式滚动轮播,则返回 YES。

    @property (nonatomic, assign) BOOL ignorePerpendicularSwipes;

    如果是,则轮播将忽略与轮播方向垂直的滑动手势。所以对于水平轮播,垂直滑动不会被拦截。这意味着您可以在旋转木马项目视图中拥有一个垂直滚动的 scrollView,它仍然可以正常工作。默认为是。

    @property (nonatomic, assign) BOOL clipsToBounds;

    这实际上不是 iCarousel 的属性,而是继承自 UIView。它包含在此处是因为它是一个经常被遗漏的功能。将此设置为 YES 以防止轮播项目视图溢出其边界。您可以通过勾选“剪辑子视图”选项在界面生成器中设置此属性。默认为否。

    @property (nonatomic, assign) CGFloat autoscroll;

    此属性可用于设置轮播以恒定速度滚动。值为 1.0 将以每秒一项的速度向前滚动轮播。自动滚动值可以为正也可以为负,默认为 0.0(固定)。如果用户与轮播交互,自动滚动将停止,并在他们停止时恢复。

    方法

    iCarousel 类具有以下方法(注意:对于 Mac OS,在方法参数中用 NSView 替换 UIView):

    - (void)scrollToItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这将使轮播在指定的项目上居中,无论是立即还是平滑的动画。对于包裹式轮播,轮播将自动确定要滚动的最短(直接或环绕)距离。如果您需要控制滚动方向,或者想要滚动一圈以上,请改用 scrollByNumberOfItems 方法。

    - (void)scrollToItemAtIndex:(NSInteger)index duration:(NSTimeInterval)scrollDuration;

    此方法允许您控制轮播滚动到指定索引所需的时间。

    - (void)scrollByNumberOfItems:(NSInteger)itemCount duration:(NSTimeInterval)duration;

    此方法允许您将轮播滚动固定距离,以轮播项目宽度为单位。可以为 itemCount 指定正值或负值,具体取决于您希望滚动的方向。iCarousel 优雅地处理边界问题,因此如果您指定的距离大于轮播中项目的数量,滚动将在到达轮播结束时被限制(如果环绕被禁用)或无缝环绕。

    - (void)scrollToOffset:(CGFloat)offset duration:(NSTimeInterval)duration;

    这与 的工作方式相同scrollToItemAtIndex:,但允许您滚动到小数偏移量。如果您希望获得非常精确的动画效果,这可能很有用。请注意,如果该scrollToItemBoundary属性设置为 YES,则调用此方法后,轮播将自动滚动到最近的项目索引。反正。

    - (void)scrollByOffset:(CGFloat)offset duration:(NSTimeInterval)duration;

    这与 的工作方式相同scrollByNumberOfItems:,但允许您滚动项目的小数部分。如果您希望获得非常精确的动画效果,这可能很有用。请注意,如果该scrollToItemBoundary属性设置为 YES,则无论如何调用此方法后,轮播都会自动滚动到最近的项目索引。

    - (void)reloadData;

    这将从数据源重新加载所有轮播视图并刷新轮播显示。

    - (UIView *)itemViewAtIndex:(NSInteger)index;

    返回具有指定索引的可见项视图。请注意,索引与轮播中的位置有关,而不是在visibleItemViews数组中的位置,这可能会有所不同。传递负索引或大于或等于的索引numberOfItems以检索占位符视图。该方法仅适用于可见的项目视图,如果指定索引处的视图尚未加载,或者索引超出范围,则返回 nil。

    - (NSInteger)indexOfItemView:(UIView *)view;

    轮播中给定项目视图的索引。适用于项目视图和占位符视图,但是占位符视图索引与数据源使用的索引不匹配,并且可能为负数(indexesForVisibleItems有关更多详细信息,请参阅上面的属性)。此方法仅适用于可见的项目视图,并且将为当前未加载的视图返回 NSNotFound。要获取所有当前加载的视图的列表,请使用该visibleItemViews属性。

    - (NSInteger)indexOfItemViewOrSubview:(UIView *)view

    此方法为您提供传递的视图或包含作为参数传递的视图的视图的项目索引。它的工作方式是从传递的视图开始沿着视图层次结构向上移动,直到找到一个项目视图并在轮播中返回其索引。如果未找到当前加载的项目视图,则返回 NSNotFound。此方法对于处理嵌入在项目视图中的控件上的事件非常有用。这允许您将所有项目控件绑定到视图控制器上的单个操作方法,然后确定触发操作的控件与哪个项目相关。您可以在Controls Demo示例项目中看到此技术的示例。

    - (CGFloat)offsetForItemAtIndex:(NSInteger)index;

    itemWidth以中心位置的倍数返回指定项索引的偏移量这与用于计算视图变换和 alpha 的值相同,可用于根据它们在轮播中的位置自定义项目视图。每当carouselDidScroll:调用委托方法时,每个视图的这个值都会发生变化

    - (UIView *)itemViewAtPoint:(CGPoint)point;

    返回轮播边界内指定点的最前面的项目视图。用于实现您自己的点击检测。

    - (void)removeItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这将从轮播中删除一个项目。其余项目将滑过以填补空白。请注意,调用此方法时数据源不会自动更新,因此后续调用 reloadData 将恢复已删除的项目。

    - (void)insertItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    这会将一个项目插入到轮播中。新的item会从dataSource中请求,所以在调用这个方法之前要确保新的item已经添加到数据源data中,否则会在carousel中得到重复的item,或者其他怪事。

    - (void)reloadItemAtIndex:(NSInteger)index animated:(BOOL)animated;

    此方法将重新加载指定的项目视图。将从数据源请求新项目。如果动画参数为 YES,它将从旧项目视图交叉淡入淡出到新项目视图,否则将立即交换。

    协议

    iCarousel 通过提供两个协议接口 iCarouselDataSource 和 iCarouselDelegate 来遵循 Apple 的数据驱动视图约定。iCarouselDataSource 协议具有以下必需的方法(注意:对于 Mac OS,在方法参数中用 NSView 替换 UIView):

    - (NSUInteger)numberOfItemsInCarousel:(iCarousel *)carousel;

    返回轮播中的项目(视图)数。

    - (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSUInteger)index reusingView:(UIView *)view;

    返回要显示在轮播中指定索引处的视图。reusingView参数的工作方式类似于 UIPickerView,其中先前显示在轮播中的视图被传递回要回收的方法。如果这个参数不是 nil,你可以设置它的属性并返回它,而不是创建一个新的视图实例,这会稍微提高性能。与 UITableView 不同,没有用于区分不同轮播视图类型的重用标识符,因此如果您的轮播包含多个不同的视图类型,那么您应该忽略此参数并在每次调用该方法时返回一个新视图。您应该确保每次carousel:viewForItemAtIndex:reusingView: 方法被调用时,它要么返回 reusingView 要么返回一个全新的视图实例,而不是维护自己的可回收视图池,因为为不同的轮播项目索引返回同一视图的多个副本可能会导致轮播显示问题。

    iCarouselDataSource 协议有以下可选方法:

    - (NSUInteger)numberOfPlaceholdersInCarousel:(iCarousel *)carousel;

    返回要在轮播中显示的占位符视图的数量。当轮播中的项目数量太少而无法填充轮播宽度,并且您希望在空白空间中显示某些内容时,将使用占位符视图。它们与轮播一起移动并且行为与任何其他轮播项目一样,但它们不计入 numberOfItems 值,并且不能设置为当前选定的项目。启用换行时,占位符会隐藏。占位符出现在轮播项目的两侧。对于 n 个占位符视图,前 n/2 个项目将出现在项目视图的左侧,接下来的 n/2 个项目将出现在右侧。您可以有奇数个占位符,在这种情况下,轮播将是不对称的。

    - (UIView *)carousel:(iCarousel *)carousel placeholderViewAtIndex:(NSUInteger)index reusingView:(UIView *)view;

    返回要显示为占位符视图的视图。工作方式与carousel:viewForItemAtIndex:reusingView:占位符 reusingViews 与用于常规轮播的 reusingViews 存储在单独的池中,因此如果您的占位符视图与项目视图不同,这不是问题。

    iCarouselDelegate 协议具有以下可选方法:

    - (void)carouselWillBeginScrollingAnimation:(iCarousel *)carousel;

    每当轮播开始动画滚动时,都会调用此方法。这可以在用户完成滚动轮播后以编程方式或自动触发,因为轮播会重新对齐自身。

    - (void)carouselDidEndScrollingAnimation:(iCarousel *)carousel;

    当轮播结束动画滚动时调用此方法。

    - (void)carouselDidScroll:(iCarousel *)carousel;

    每当滚动轮播时都会调用此方法。无论轮播是通过编程还是通过用户交互滚动,它都会被调用。

    - (void)carouselCurrentItemIndexDidChange:(iCarousel *)carousel;

    每当轮播滚动到足以改变 currentItemIndex 属性时,就会调用此方法。无论项目索引是以编程方式更新还是通过用户交互更新,都会调用它。

    - (void)carouselWillBeginDragging:(iCarousel *)carousel;

    当用户开始拖动轮播时调用此方法。如果用户点击/点击轮播,或者轮播以编程方式滚动,它不会触发。

    - (void)carouselDidEndDragging:(iCarousel *)carousel willDecelerate:(BOOL)decelerate;

    当用户停止拖动轮播时调用此方法。willDecelerate 参数指示转盘是否行得足够快以至于它在停止之前需要减速(即当前索引不一定是它将停止的索引),或者它是否会在它所在的位置停止。请注意,即使 willDecelerate 为 NO,轮播仍会自动滚动,直到它与当前索引完全对齐。如果您需要知道它何时完全停止移动,请使用 carouselDidEndScrollingAnimation 委托方法。

    - (void)carouselWillBeginDecelerating:(iCarousel *)carousel;

    当轮播开始减速时调用此方法。它通常会在 carouselDidEndDragging:willDecelerate: 方法之后立即调用,假设 willDecelerate 为 YES。

    - (void)carouselDidEndDecelerating:(iCarousel *)carousel;

    当轮播完成减速时调用此方法,您可以假设此时的 currentItemIndex 是最终停止值。与以前的版本不同,在大多数情况下,轮播现在将准确地停在最终索引位置。唯一的例外是启用了弹跳的非包裹式转盘,如果最终停止位置超出转盘的末端,则转盘将自动滚动,直到它与结束索引完全对齐。为了向后兼容,轮播将始终scrollToItemAtIndex:animated:在完成减速后调用如果您需要确定轮播何时完全停止移动,请使用carouselDidEndScrollingAnimation委托方法。

    - (CGFloat)carouselItemWidth:(iCarousel *)carousel;

    返回轮播中每个项目的宽度 - 即每个项目视图的间距。如果未实现该方法,则默认为carousel:viewForItemAtIndex:reusingView:dataSource 方法返回的第一个项目视图的宽度如果从返回的视图carousel:viewForItemAtIndex:reusingView:不正确(例如,如果视图大小不同,或者在其背景图像中包含影响其大小的投影或外部发光),则此方法应仅用于裁剪或填充项目视图- 如果您只是想要将视图隔开一点,那么最好使用该iCarouselOptionSpacing值。

    - (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform;

    此方法可用于为每个轮播视图提供自定义转换。offset 参数是视图与旋转木马中间的距离。当前居中的项目视图的偏移量为 0.0,右侧的偏移值为 1.0,左侧的偏移值为 -1.0,依此类推。要实现线性轮播样式,您只需将偏移值乘以项目宽度并将其用作变换的 x 值。仅当轮播类型为 iCarouselTypeCustom 时才会调用此方法。

    - (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value;

    该方法用于自定义标准轮播类型的参数。通过实施此方法,您可以调整选项,例如圆形转盘中显示的项目数量,或coverflow 转盘中的倾斜量,以及转盘是否应环绕以及是否应在末端淡出等. 对于任何您不想调整的选项,只需返回默认值即可。这些选项的含义在下面的iCarouselOption 值下列出检查选项演示以获取使用此方法的高级示例。

    - (void)carousel:(iCarousel *)carousel didSelectItemAtIndex:(NSInteger)index;

    如果用户点击任何轮播项目视图(不包括占位符视图),包括当前选择的视图,则会触发此方法。如果用户点击当前选定视图中的控件(即作为 UIControl 子类的任何视图),则不会触发此方法。

    - (BOOL)carousel:(iCarousel *)carousel shouldSelectItemAtIndex:(NSInteger)index;

    如果用户点击任何轮播项目视图(不包括占位符视图),包括当前选择的视图,则会触发此方法。方法的目的是让您有机会忽略轮播上的点击。如果你从方法中返回 YES,或者没有实现它,tap 将正常处理并carousel:didSelectItemAtIndex:调用方法。如果您返回 NO,轮播将忽略点击并继续向上传播视图层次结构。这是防止轮播拦截打算由另一个视图处理的点击事件的好方法。