在swift3中,GCD的语法已经全面修改,变得更加swift化了。
使用Carthage管理ios依赖
Carthage官网已经针对cocopods和carthage进行了详细的说明:
IP TCP 和HTTP
当app和服务器进行通信的额时候,大多数情况下,都是采用http协议。http最初是为web浏览器而定制的,如果在浏览器输入 http://www.baidu.com
。浏览器就会通过http协议和baidu所对应的服务器进行通信。
ReactiveSwift入门
Signal
一个signal类型的实例,代表了一个有时序的并且可以被观察(类似订阅)的事件流。
信号通常被用来表示正在进行中的事件流,比如通知,用户输入等。用户(或者只要能造成事件的东西)产生的事件发送或者被接受,事件就被传递到信号上,并且被推送(push-Driven)到任何观察者哪里,并且所有观察者都是同时收到这些事件。
如果你想访问一系列的事件,就必须观察一个信号,观察一个信号并不会触发任何副作用,可以这样理解。信号是由生产者生产和推动的,消费者(观察者)是不会对事件的生命周期有任何影响。在观察一个信号时,发送了什么事件,只能对这个事件操作,因为信号是由时序的,不能随机的访问其他事件。
信号可以通过原函数去操作,比如filter,map,reduce,也可以同时操作多个信号如zip,这些原函数只在nextEvents生效(也就是对complete,failure等不生效)
在一个信号的生命周期里,可以发送无数次的NextEvents事件,直到他们被终结,类似compleye,Failed,InterRupper.终止事件没有数据值,所以他们必须被单独处理。
Subscription
一个信号通常被用来表示正在进行中的事件流,有时候他们被叫做热信号,这意味着订阅者可以错过一些在它订阅前发送的事件。订阅一个信号不会触发任何副作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
|
因为Swift有泛型的存在,这样的话我们可以把Signal当做任何数据类型的容器,而不是像OC中利用上帝类型Id,更加方便传递数据
首先我们通过Signal.pipe()创建了一个信号和一个观察者。
奇怪的是,在RACOC部分中,我们很少主动创建观察者,我们通常直接订阅信号就可以。
在Swift中,通过pipe创建的信号是个热信号,类似于OC中的RACSubject系列,在RACSubject继承自RACSiganl又继承RACStream,RACStream是一个Monad,它可以代表数据和数据的一系列的操作如map,flatterMap,bind
RACSubject又遵守了RACSubscriber协议,这个协议定义了可以发送数据的操作。
所以RACSubject即是一个信号,又是一个观察者。
在Swift部分的实现中,Signal并没有实现发送数据的方法。所以它需要一个内部的Observer去发送数据。所以它被pipe直接返回。
在外部我们需要自己实例化一个Observer观察者。去订阅事件。
可能在你查看Pipe的实现的时候并不好理解。把尾随闭包补全相对好理解点。
做个总结:
- RACOC中:RACSubject = RACSignal + RACSubscriper,在订阅的时候,订阅者被放在了RACSubject内部存放,我们只需要去关注订阅的block实现即可。
- RACSwift中:Signal 仅仅就是一个信号,所以需要一个内部观察者去充当发送数据的工具。外部的订阅需要自己手动实例观察者
- 热信号:由于pipe方法返回的是热信号,所以一个订阅者会错过在订阅之前发送的事件 *
empty
空信号直接发送一个interrupted事件
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Never
一个Never信号不会发送任何事件
1 2 3 4 5 6 7 8 9 10 11 |
|
uniqueValues唯一值
仅从集合中发送一次相同事件–类似与arrayQueue变成了Setqueue
注意:这会造成被发送的值被保留下来,用于以后发送的时候来检查是否重复,你可以编写一个函数来过滤重复值,这样可以减少内存消耗。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
map
把每一个发送的值转换成新的值
1 2 3 4 5 6 7 8 9 10 11 |
|
mapError
把收到的error值变成新的error值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
filter
用于过滤一些值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
ignoreNil
在发送的值为可选类型中:如果有值,把值解包,如果是nil丢弃掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
take
take(num)只取前num此值得信号
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
collect
在发送complete事件之后,观察者会收到一个由之前事件组成的数组
注意:如果在发送complete事件的时候,没有任何事件发送,观察者会收到一个空的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
SignalProducer
一个信号发生器,是SignalProducer类型的实例,它可以创建信号(signals)并施加副作用(side effects)
信号发生器用来表示操作或者任务,比如网络请求,每一次对它的调用start()将会生成一个新的潜在操作,并允许调用者观察它的结果,还有一个startWithSignal()方法,会给出产生的信号,允许在必要的情况下监听多次。
根据start()方法的动作方式,被同一个信号发生器生成的信号可能会有不同的事件顺序或版本,甚至事件流完全不一样!和普通的信号不同,在观察者连接上之前,信号发生器不会开始工作(也就没有事件会生成),并且在每一个新的监听器连接上时其工作都会重新开始一个单独的工作流。
启动一个信号发生器会返回一个销毁器(disposable),它可用来打断或取消被生成信号的工作
和信号一样,信号生成器可以通过map,filter等原函数操作,使用lift方法,所有信号的原函数可以被提升成为以信号生成器为对象的操作,除此以外,还有一些用来控制何时与如何启动信号生成器的原函数,比如times.
通过lift函数可以让热信号转变为冷信号。
Subscription
一个信号生成器代表了一种可以在需要的时候才被启动的操作(不像signal是自启动的),这种信号是冷信号,在刚开始这个信号的状态也为冷(未激活),既然是冷信号,那么就意味着这一个观察者不会错过任何被信号生成器发出的值。
补充:像signal是创建的时候状态为cold(理解为未激活),被订阅时状态为hot(理解为激活)
但是冷信号和热信号与状态为冷热是两个不同的概念,冷信号会带来副作用,热信号不会
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
像不像是RACDynamicSignal的创建方式,这不过不同与Sinal的是,这里的发送信号的观察者是在内部通过Signal.pipe()生成的,不需要外部创建。
SignalProduce是冷信号,任何一个订阅者/观察者都不会错过任何事件
start方类似Signal的 signal.observe()方法,只不过Signal的方法只有一个作用,就是关联一个观察者,而SignalProduce的start方法还多了一个激活信号的功能
Empty
一个会立即调用complete事件的信号生成器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Signal调用的是interrup方法,暂时不知道为什么,可能是为了区分语义吧,Signal是有时序的,SignalProduce是没有时序的。
Never
一个什么都不会发送的信号器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
buffer
创建一个事件队列可以回放已经发送的事件
当一个值被发送的时候,它会被放进缓冲区内,如果缓冲区已经溢出,就会丢弃旧的值
这些被缓存的值将会被保留,直到这个信号被终结,当一个信号启动的时候,如果队列里没有任何值,所有被发送的新值都会被自动转发到观察者那里,直到管着着收到一个终止事件。
当一个终止事件被发送到队列中,观察者不会再收到任何值,并且这个事件不会被计算buffer的缓冲区大小,所以没有缓存的值都会被丢弃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
startWithSignal
通过Producer返回一个Signal,当闭包调用时返回signal开始发送事件
闭包返回一个Disponsable,可以用来中断Signal或者完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
startWithNext
通过信号生成器创建一个信号,并且给这个信号内部直接构建一个观察者,在指定的闭包中会直接订阅next事件。
返回一个Disposable,可以中断这个信号,中断之后这个闭包不会再被调用
1 2 3 4 5 6 7 8 |
|
这个订阅只能接受next事件
startWithCompleted
同startWithNext,只不过只能接受complete事件
1 2 3 4 5 6 7 8 |
|
startWithFailed
同startWithNext, 只不过只能接受Failer事件事件
1 2 3 4 5 6 7 8 |
|
startWithInterrupted
同startWithNext,只不过只能接受interrupted事件
1 2 3 4 5 6 7 8 9 |
|
lift
这个相对难理解点,大致类似于RAC_OC部分中的bind函数,monad中bind函数
可以理解为所有的原函数都是通过lift去实现的,借用中间信号来实现一系列的信号变换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
map
把每个值都转换为新的值
1 2 3 4 5 6 7 8 9 |
|
mapError
把收到的error转换为新的error
1 2 3 4 5 6 7 8 9 |
|
filter
过滤不符合条件的值
1 2 3 4 5 6 7 8 9 |
|
take
take(num) 只取前几次的值
1 2 3 4 5 6 7 8 9 10 |
|
observeOn
在指定调度器上分发事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
collect
在发送完成的时候将一系列的值聚合为一个数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
collect(count:)
在发送数据的时候(不需要发送complete)的时候将一系列的值聚合为数组,数组的长度为count,如果有很多数据,将会返回多个数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
collect(predicate:) matching values inclusively
通过谓词将一系列的值聚合为一个数组,注意在发送complete时候,如果前面只剩下一个值,就不需要聚合(因为没有其它元素和最后一个元素聚合),直接返回一个只有一个元素的数组。如果没有数据则返回一个空数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
尝试打开注释看看会有什么结果
collect(predicate:) matching values exclusively
和上一个不同的是,如果谓词成功就把之前的聚合在一起,可以理解为把成功的界限当做分隔符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
combineLatestWith
将第一个信号生成器的values和被聚合信号生成器的最后一个值聚合为一个元组
新产生的信号生成器不会发送任何值,只是转发,任何一个原来的信号被中断,这个新的信号生成器也会中断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
skip
skip(num),跳过num此发送的事件
1 2 3 4 5 6 7 8 9 10 11 |
|
materialize
将被发送的值(value)编程Event,允许他们被修改。还句话说,允许他们被修改,把一个值变成一个Monad
当收到一个complete或者Failure事件,这个新的信号生成器,会发送事件并且结束。当收到一个interrupted事件,这个新的信号生成器也会中断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
sampleOn
当sampler(被操作的信号生成器)发送任何事件的时候,都转发原来信号生成器的最后一个值
如果当一个sampler启动时,当前的值没有被观察者,没有任何事情发生
新产生的信号生成器从源信号生成器哪里发送数据,如果两个信号生成器任何一个complete或者interrupt,新产生的都会中断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
combinePrevious
向前合并,没法送一个值就结合历史发送数据的最后一个构造成一个新的元组返回。在第一个发送时由于没有历史数据,所以combinePrevious传递了一个默认值。当做第一次的合并。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
scan
类似reduce,将值聚合为一个新的值,每次聚合都保留结果作为下次的默认值,首次需给出默认值
每次聚合都会发送这个值
1 2 3 4 5 6 7 8 9 10 11 12 |
|
reduce
和scan类似,区别为reduce只发送聚合后的值并且立即结束
1 2 3 4 5 6 7 8 9 |
|
skipRepeats
跳过表达式里返回true的值,第一个值不会被跳过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
skipWhile
对每个值都去做判断,知道返回false,之前的值会被跳过
1 2 3 4 5 6 7 8 9 10 11 12 |
|
takeUntilReplacement
在被替换的信号发生器发送信号之后,发送被替换的信号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
takeLast
在发送complete事件后只取count此数据
1 2 3 4 5 6 7 8 9 10 11 |
|
ignoreNil
如果发送的事件是可选类型,解包这些可选类型,并且丢弃nil值
1 2 3 4 5 6 7 8 9 10 11 12 |
|
zipWith
压缩信号生成器,只有再两个信号都有数据发送之后,新的信号生成器才会发送数据
新的数据被组合为元组
1 2 3 4 5 6 7 8 9 10 11 12 |
|
后面应为第二个没有数据了,所以不会再聚合了
times
time(count)重复发送count数据,每次重复必须上次发送完成事件
1 2 3 4 5 6 7 8 9 10 11 12 |
|
retry
如果收到失败事件重试retry(count)次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
当第一个信号发送complete时,第二个信号被替换成信号发送线路上,如果有任何失败事件,后面的就替换失败。
第一个信号发送的所有事件都会被忽略
flatMap
将收到的每个事件都映射为新的Product,然后摊平,如果原来的producer发送失败,新产生也得立即失败
1 2 3 4 5 6 7 8 9 10 11 12 |
|
flatMapError
把收到的failer事件映射为新的Producer,并且摊平它
1 2 3 4 5 6 7 8 |
|
Swift Package Manager入门
大部分语言都有官方的代码分配解决方案,幸好苹果也在开发替代Cocoapods和Carthage的管理工具,Swift Package Manager(Swift包管理器,下面我们简称SPM)就是一个用来管理Swift代码的分配的官方工具,它为Swift编译系统集成了自动进行下载,编译和连接依赖的过程
目前,SPM还处于早起阶段,现在仅仅支持OS X和linux系统,尚不支持Ios,watchOS以及tvOS平台,但未来很大希望会支持上述平台。
概念概述
在swift中我们使用模块来管理代码,每个模块指定一个命名空间并强制指定模块外那些部分是代码是可以被访问控制的。
一个程序可以将它所有代码聚合到一个模块中,也可以将它作为依赖关系导入到其他模块,除了少量系统提供的模块,像OS X中的Darwin或者 Linux中的Glibc等大多数依赖需要代码被下载或者内置才能被使用。
当你将编写额解决待定问题的代码独立成一个模块时,这段代码可以在其他情况下呗重新利用。例如,一个模块提供了发起网络请求的功能,在一个照片分享的app或者一个天气的app里它都是可以使用的。使用模块可以让你的代码建立在其他开发者的代码之上,而不是你自己去重复实现相同的功能。
一个包由Swift源文件和一个清单文件组成,这个清单文件称为Package.swift
,定义包或者它的内容使用PackageDescription
模块。
一个包邮一个或者多个目标,每个目标制定一个铲平并且可能声明一个后者多个依赖。
一个目标可能构建一个库或者一个可执行文件作为其产品。库是包含可以被其它Swift代码导入的模块。可执行文件是一段可以被操作系统运行的程序
目标依赖是指保重代码必须添加的模块。依赖由包资源的绝对或者相对URL和一些可以被使用的包的版本要求所组成。包管理器的作用是通过自动为工程下载和编译所有依赖的过程中,减少协调的成本。这是一个递归的过程:依赖能有自己的依赖,其中每一个也可以具有依赖,形成一个依赖的相关图。包管理器下载和编译所需要满足整个依赖相关图的一切。
开源Swift入门
关于使用REPL和LLDB调试器的内容具体可以参阅官方文档使用REPL和使用LLDB调试器
下载和安装Swift
刚开始下载和安装swift需要下载并安装编译器和其它必备组件,进入到 https://swift.org/download/#releases按目标平台的说明进行。
下载完成后,点击按步骤安装就可以
在OS X上下载工具链的默认地址是:/Library/Developer/Toolchains
.接着,我们可以输入以下命令导出编译路径:
1
|
|
首先需要安装clang:
1
|
|
如果你在Linux上安装的Swift工具链在系统根目录以外的目录,你需要使用你安装Swift的实际路径来运行下面的命令:
1
|
|
导出路径之后,你可以通过输入 swift 命令并传入 –version 标志来校验你是否运行了 Swift 的预期版本
1 2 |
|
在版本号的后缀 -dev 用来表明它是一个开发的编译,而不是一个发布的版本
使用REPL
使用编译系统
Swift编译系统为编译库,可执行文件和不同工程之间共享代码提供了基本的约定。
创建一个新的Swift包,首先创建并进入到一个新的目录命令为Hello:
1 2 |
|
每个包在其根目录下都必须拥有一个命名为Package.swift
清单文件,如果清单文件为空,那包管理器将会使用常规默认的方式来编译包,创建一个空的清空文件使用命令:
1
|
|
当使用默认方式时,包管理器预计将包含在Source/子目录下的所有源代码。创建方式:
1
|
|
编译可执行文件
默认方式下,目录中包含一个文件称为main.swift
将会将文件编译成与包名称相同的二进制可执行文件。
在这个例子中,包将生成一个可以输出hello world
的可执行文件为 hello
在Source/目录下创建一个命名为main.swift
的文件,并使用你喜欢的任意一种编译器输入如下代码:
1
|
|
返回到 Hello 目录中,通过运行 swift build 命令来编译包:
1
|
|
当命令完成之后,编译产品将会出现在 .build 目录中。通过如下命令运行 Hello 程序:
1 2 |
|
下一步,让我们在新的资源文件里定义一个新的方法 sayHello(_:)
然后直接用print(_:)
替换执行调用的内容。
多了源文件协作
在Sources/
目录下创建一个新文件命名为Greeter.swift
然后输入如下代码:
1 2 3 |
|
sayHello(_:)
方法带一个单一的字符串参数,然后在前面打印一个"hello",后面跟着函数参数单词"World".
现在打开main.swift
,然后替换原来的内容为下面代码:
1 2 3 4 5 6 |
|
跟之前的硬编码不同,main.swift
现在从命令行参数中读取。替代之前直接调用print(_:)
,main.swift
现在调用sayHello(_:)
方法,因为这个方法是Hello
模块的一部分,所以不需要使用到import
语句。
运行swift build
并尝试Hello
的新版本:
1 2 |
|
目前为止,你已经能够运用开源Swift来运行一些你想要的程序了。接下来我们就可以进入正题开始入手SPM.
快速入门实例
在本章节中,我们简单地学会了编译一个"`Hello world"程序。
为了了解SPM究竟能做什么,我们来看一下下面这个由4个独立的包组成的例子:
- O2PlayingCard-定义了O2PlayingCard , O2Suit , O2Rank , 3个类型
- O2FisherYates-定义了 shuffle() 和 shuffleInPlace() 方法实现的扩展
- O2DeckOfPlayingCards-定义了一个 O2Deck 类型对 O2PlayingCard 值得数据进行洗牌和抽牌。
- O2Dealer-定义了一个用来创建 O2DeckOfPlayingCards 进行洗牌和抽出前10个卡片的可执行文件。
你可以从O2Dealer from GitHub 编译并运行完整例子,然后运行如下命令:
1 2 3 |
|
创建一个库包
我们将从创建一个代表一副标准的52张扑克牌的模块开始。 O2PlayingCard 模块定义了 由 O2Suit 枚举值(Clubs, Diamonds, Hearts, spades)和 O2Rank 枚举值(Ace, Two, Three, …, Jack, Queen, King)组成的 O2PlayingCard 类。各个类的核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
一般来说,一个包包括位于Source/的源文件
1 2 3 4 5 6 |
|
由于O2PlayingCard
模块并不会生成可执行文件,这里应该成为库。库表示被编译成一个可以被其它包导入的模块的包,默认情况下,库模块公开所有位于Sources/
目录下的源代码中声明的公共类型的方法。
运行 swift build 开始启动 Swift 编译的过程。如果一切进行顺利,将会在 .build/debug 目录下生成 O2PlayingCard.build 目录。
接下来,我们在Package.swift
文件中定义包名,代码如下:
1 2 3 4 5 |
|
然后我们只要将O2PlayingCard
提交到Github上,并且给他发布一个Release版本即可完成该库包,这里可以自己手动添加一个.gitignore
文件,忽略掉/.build
,因为我们的包是不需要包括生成的编译结果的内容的。
使用编译配置语句
下一个即将编译的模块是O2FisherYates
.跟之前O2PlayingCard
有所不同,该模块没有定义新的类,取而代之的是该模块拓展了一个已经存在的特殊的CollectionType
和MutableCollectionType
接口协议,用来添加shuffle()
方法和对应的shuffleInPlace()
方法。
在 OS X 中,系统模块是 Darwin , 提供的函数是 arc4random_uniform(_:) 。在 Linux 中, 系统模块是 Glibc , 提供的函数是 random() :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
剩下的步骤和前面的类似,编译通过后上传到GitHub,发布Release版本。
导入依赖
O2DeckOfPlayingCards
包把前两个包聚合到一起:它定义了一个O2PlayingCard
数组中使用O2FisherYates
的shuffle()
方法的Deck类型。
为了使用 O2FisherYates 和 O2PlayingCards 模块, O2DeckOfPlayingCards 包必须在 Package.Swift 清单中将上述模块声明为依赖。
1 2 3 4 5 6 7 8 9 10 11 |
|
每个依赖都需要指定一个源URL和版本号,源URL是指允许当前用户解析到对应的Git仓库。版本号遵循 语义化版本号 2.0.0 的约定,用来决定检出或者使用哪个Git标签版本来建立依赖。对于FisherYates
和PlayingCard
这两个依赖来说, 最新的将要被使用的主版本号为1.
当你运行swift build
命令时,包管理器将会下载所有的依赖,并将它们编译成静态库,再把它们链接到包模块中。这样将会使O2DeckOfPlayingCards
可以访问依赖import语句的模块的公共成员
你可以看到这些资源被下载到你工程根目录的 Packages 目录下,并且会生成编译产品在你工程根目录的 .build 目录下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Package
目录包含了被复制的包依赖的所有仓库,这样将使你能修改源代码并直接推送这些修改到它们的源,而不需要再对每个包在单独进行复制。
Swift是一门先进的语言,SPM的社区也在不断地完善中。在swift开源之后,我们很容可以看到它的潜力,看来掌握这门语言必将是一个大趋势。
iOS中的热修复
背景需求
为什么我们需要热修复
- 工作中容易犯错,bug难以避免
- 开发和测试人力有限
- 苹果AppStore审核周期太长,一旦出现严重bug难以快速上线新版本
JSPatch简介
JSPatch诞生于2015年5月,最初是腾讯广研高级ios开发@bang的人格项目。它能够使用JavaScripit调用Objective-C的原声接口,从而动态植入代码来替换旧代码,以实现修复线上bug.
JSPatch与wax对比
最关键的是JSpath可实现方法粒度的线上代码替换,能修复一切代码引起的bug.而Wax无法实现。
JSPatch实现原理
基础原理
Objective-C是动态语言,具有运行时特性,该特性可通过类名称和方法名的字符换获取该类和该方法,并实例化调用
1 2 3 4 |
|
也可以替换某个类的方法为新的实现:
1 2 |
|
还可以注册一个类,为类添加方法:
1 2 3 |
|
JavaScript调用
我们可以用JavaScript对象定义一个Objective-C类:
1 2 3 4 |
|
在OC执行JS脚本前,通过正则把所有方法调用都改成__c()函数,再执行这个JS脚本,做到了类似OC/Lua/Ruby等的消息转发机制:
1 2 3 |
|
给JS对象基类Object的prototype加上c成员,这样所有对象都可以调用到c,根据当前对象类型判断进行不同操作:
1 2 3 4 5 6 7 8 |
|
互传消息
JS和OC是通过JavaScriptCore互传消息的。OC端在启动JSPatch引擎会创建一个JSContext实例,JSContext是js代码的执行环境,可以给JSContext添加方法。JS通过调用JSContext定义的方法把数据传给OC,OC通过返回值传回给JS.调用这种方法,它的参数/返回值 javaScripotCore都会自动转换,OC里的NSArray,NSdictionary ,NSString,NSNumber,NSBlock会分别转为JS端的数组/对象/字符串/数字/函数类型 对于一个自定义ID对象,JavaScriptCore会把这个自定义对象的指针传给JS,这个对象在JS无法使用,但在回传给OC时OC可以找到这个对象。对于这个对象声明周期的管理,如果JS有变量引用时,这个OC对象引用计数就加1,JS变量的引用释放了就减一,如果OC上没别的持有者,这个OC对象的生命周期就跟着JS走了,会在JS进行垃圾回收时释放。
方法替换
- 把UIViewContrller的
-viewWillAppear:
方法通过class_replaceMethod()
接口指向_objc_msgForward
,这是一个全局IMP,OC调用方法不存在时都会转发到这个IMP上,这里直接把方法替换成这个IMP,这样调用这个方法时就会走到-forwardInvocation:
- 为UIViewController添加
-ORIGviewWillAppear:
和-_JPviewWillAppear:
两个方法,前者指向原来的IMP实现,后者是新的实现,稍后会在这个实现里回调JS函数 - 改写UIViewController的
-forwardInvocation:
方法为自定义实现。一旦OC里调用UIViewController的-viewWillAppear:
方法,经过上面的处理会把这个调用转发到forwardInvocation:
,这时已经组装好了一个NSInvocation,包含了这个调用的参数。在这里把参数从NSInvocation反解出来,待着参数调用删除新增加的方法-JPviewWillAppear:
,在这个新方法里获取到参数传给JS,调用JS的实现函数,整个调用过程就结束了,整个过程图示如下:
最后一个问题,我们把UIViewController的-forwardInvocation:
方法的实现给替换掉了,如果程序里挣得有用到这个方法对消息进行转发,原来的逻辑怎么办?首先我们在替换-forwardInvocation:
方法前会新建一个方法-ORIGforwardInvocation:
,保存原来的实现IMP,在新的-forwardInvocation:
实现了做个判断,如果转发的方法是我们想改写的,就走我们的逻辑,若不是,就调用-ORIGforwardInvocation:
走原来的流程
JSPatch代码示例
jspatch在oc上的调用十分简单
1 2 3 4 5 6 7 |
|
一个JavaScript修复Objective-C的bug的示例:
1 2 3 4 5 6 7 8 9 10 |
|
上述代码中取数组元素出可能会超出数组范围导致crash.如果在项目里引用了JSPatch,就可以发JS脚本修复这个bug:
1 2 3 4 5 6 7 8 9 10 |
|
热修复的解决方案
版本更新策略
考虑到下一个提交的App版本已经修复了上一个版本的bug,所以不同的App版本对应的补丁肯定也不同,同一个App版本下,可以出现递增的补丁版本
- 补丁为全量更新,即最新的版本补丁包括旧版的补丁的内容,更新后新版补丁覆盖旧版补丁
- 补丁分为可选补丁和必选补丁,必选补丁用于重大bug的修复,如果不更新必须补丁则App无法继续使用。如下图2中,补丁版本v1234对应各自版本的用户,补丁v3为必须更新,补丁v1,v2,v4为可选补丁,则v1,v2必须更新到v4才可使用;而v3的哟过户可先使用,同事后台静默更新到v4
安全策略
安全问题在于JS脚本可能被中间人攻击替换代码。可采取一下三种方法
- 对称加密: 如zip的加密压缩,Aes等加密算法。优点是简单,缺点是安全性低,易被破解。若客户端被反编译,密码字段泄露,则完全破解。
- HTTPS:%E4%BC%98%E7%82%B9%E6%98%AF%E5%AE%89%E5%85%A8%E6%80%A7%E9%AB%98%EF%BC%8C%E8%AF%81%E4%B9%A6%E5%9C%A8%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%9C%AA%E6%B3%84%E9%9C%B2%EF%BC%8C%E5%B0%B1%E4%B8%8D%E4%BC%9A%E8%A2%AB%E7%A0%B4%E8%A7%A3%EF%BC%8C%E7%BC%BA%E7%82%B9%E6%98%AF%E9%83%A8%E7%BD%B2%E9%BA%BB%E7%83%A6%EF%BC%8C%E5%A6%82%E6%9E%9C%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%9C%AC%E6%9D%A5%E5%B0%B1%E6%94%AF%E6%8C%81Https,%E4%BD%BF%E7%94%A8%E8%BF%99%E7%A7%8D%E6%96%B9%E6%A1%88%E4%B9%9F%E6%98%AF%E4%B8%80%E7%A7%8D%E4%B8%8D%E9%94%99%E7%9A%84%E9%80%89%E6%8B%A9%E3%80%82
- RSA校验:安全性高,部署简单
详细校验步骤如下:
- 服务器计算出脚本文件的MD5值,作为这个文件的数字签名
- 服务器通过私钥加密算出的MD5值,得到一个加密后的md5值
- 把脚本文件和加密后的md5值一起发给客户端
- 客户端拿到加密后的md5值,通过保存在客户端的公钥解密
- 客户端计算脚本文件的md5值
- 对比第 4/5 步的两个md5值(分别是客户端和服务器端计算出来的MD5值),若相等则通过校验
客户端策略
客户端具体策略如下图:
- 用户打开App时,同步进行本地补丁的加载
- 用户打开App时,后台进程发起异步网络请求,获取服务器中当前App版本所对应的最新补丁版本和必须的补丁版本
- 获取补丁版本的请求回来后,跟本地的补丁版本进行对比
- 如果本地补丁版本小于必须版本,则提示用户,展示下载补丁界面,进行进程同步的补丁下载。下载完成后重新加载App和最新补丁,再进入App
- 如果本地补丁版本不小于必须版本,但小于最新版本,则进入App,不影响用户操作。同时进行后台进程异步静默下载,下载后补丁保存在本地,下次App启动时再加载最新补丁。
- 如果版本为最新,则进入App
iOS中常见的面试题二
如何进行真机调试
- 首先需要钥匙串创建一个钥匙(key)
- 将钥匙串上传到官网,获取ios Development证书
- 创建APP Id即我们应用程序中的BundleId
- 添加Device ID 即 UDID;
- 通过勾选前面所创建的证书:App ID, Deveice id
- 生成mobileProvision文件
- 先决条件:申请开发者账号 99美刀
Ios中常见的面试题及答案
ios中深拷贝和浅拷贝
在ios开发中,经常涉及到深拷贝和浅拷贝的问题,针对深拷贝和浅拷贝,为了方便大家的理解,专门总结如下:
自定义UICollectionViewLayout
UICollectionView,自从ios6之后就被引入到开发中,现在已经变成了最流程的UI元素之一,它最吸引人的特性就是将数据和布局进行分离,依靠分离的数据元素去处理布局,这个布局对象是决定占位元素和视图的元素。
Ios10 UICollectionView 新特性
关于ios 10 UIcolelctionView的新特性,主要还是体现在如下三个方面
- 顺滑的滑动体验
- 针对self=sizing的改进
- Interactive recording重排