前言
iOS平台的有很多热修复框架,原理都是差不多,都是利使用 Runtime 进行属性、方法修改。
JSPatch 是现今比较主流、轻量级的热修复框架。利使用内置的 JavaScript 引擎(JavaScriptCore)结合 JavaScript 在运行时进行对 Object-C 对象修改。
接入文档
JSPatch 的官方接入文档写的很详细,不过也很简洁。对于 Objective-C 项目已经足够用了但是对于 Swift 项目的接入介绍还是略显简略。目前,因为 Apple 公司对热修复的打压以及等等其余起因,使得 JSPatch 分为JSPatch平台版和 Github 的开源代码版。
Github 的开源代码版:
# Your Podfileplatform :ios, '6.0'pod 'JSPatch'
JSPatch 平台版:
JSPatch 平台版只支持手动集成方式, 没有放到CocoaPods专门管理。
将
JSPatchPlatform.framework
拖入项目中,勾选 “Copy items if needed”,并确保 “Add to target” 勾选了相应的 target。增加依赖框架:TARGETS -> Build Phases -> Link Binary With Libraries -> + 增加
libz.dylib
和JavaScriptCore.framework
。生成和配置RSA密钥。
openssl >genrsa -out rsa_private_key.pem 1024pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM –nocryptrsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
- 启动运行
#import <JSPatchPlatform/JSPatch.h>@implementation AppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [JSPatch startWithAppKey:@"你的AppKey"]; [JSPatch setupRSAPublicKey:@"你的公钥"]; [JSPatch sync]; ...}@end
注意事项:
Swift 项目,因为 JSPatch 平台版因为 JSPatchPlatform.framework
里的 “Header”文件定义了与热修复类、方法相同的宏,导致 Swift 无法直接桥接。
#define JSPatch Eb_tCode#define startWithAppKey stwa_43#define setupRSAPublicKey strs_3x#define setupTestScriptFileName sttsc_3#define updateConfigWithAppKey udcak#define testScriptInBundle tests_sinbund#define JPCallbackType jtspc_b#define JPErrorCode DRkcos#define setupCallback sefjtpsytecal
处理方法:
定义一个 Object-C 的桥接对象,进行桥接。
#import <JSPatchPlatform/JSPatch.h>@interface Patch : NSObject/**开始配置热修复 */+ (void)start;/** 同步补丁 */+ (void)sync;@end@implementation Patch+ (void)start { [JSPatch startWithAppKey:appKey]; [JSPatch setupRSAPublicKey:@"你的公钥"];}+ (void)sync { [JSPatch sync];}@end
桥接头文件导入 Patch.h
,之后即可以在Swift中调使用:
class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptionslaunchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { Patch.start() //配置热修复 Patch.sync() //同步下载补丁,这个方法可放在其余地方调使用 return true }}
编写工具
JSPatch 编写工具体验上都不太好,一般编写和调试的工具都是分开。调试工具一般能调试JavaScript的浏览器就可。编写工具种类比较多,只需能友好的编写 JavaScript 的就行。
编写工具推荐
- Sublime Text 轻量级文本编辑器
- Atom 很多东西需要翻墙用
- AppCode 重量级的IDE适合当做Xcode用
调试工具推荐
- Safari浏览器
- Google浏览器
基本用
JSPatch 基本用,官方文档也已经有详细说明。可以说学习 JSPatch 的门槛比较低,官网提供的少量工具方便并提升了开发效率,不过有一点需要注意的是不要太依赖官方的工具(只支持常规的语法,而且很容易出错),所以需要对脚本进行语法检查。本文主要补充少量 Swift 项目的用以及注意事项说明。
Objective-C 项目
JSPatch 尽管已经很方便对代码进行热修复,但是对少量的支持并不是很好,比方:
Struct 支持部分系统结构体,其余的需要在项目中和脚本中写
C 函数 用 JPCFunction 扩展支持
Block 用 JPBlock 扩展支持
GCD 用 JPDispatch 扩展支持
指针 用 JPMemory 扩展支持
常量、枚举、宏、全局变量 无法支持
参照 官方文档
Swift 项目
JSPatch 是利使用 Objective-C 的 Runtime 进行改写、修改的;而 Swift 是利使用 C++ 的那一套静态机制,编译的时候已经决定了不能修改,所以纯 Swift 项目是不支持热修复的。为了让 Swift 项目也能支持热修复,所以需要把 Swift 使用到的类 进行桥接到 Objective-C 对应的对象,这样就能实现热修复了。
官方文档说明:
1. 只支持调使用继承自 NSObject 的 Swift 类2. 继承自 NSObject 的 Swift 类,其继承自父类的方法和属性可以在 JS 调使用,其余自己设置方法和属性同样需要加 @objc 和 dynamic 关键字才行。3. 若方法的参数/属性类型为 Swift 特有(如 Character / Tuple),则此方法和属性无法通过 JS 调使用。4. Swift 项目在 JSPatch 新添加类与 OC 无异,可以正常用。
编写 JavaScript 脚本
因为 Swift 不能直接支持热修复,所以只能把需要修改的 Swift 语言写的类、属性、方法转成对应的 Objective-C 代码。一般编写脚本步骤:
1. 利使用Xcode混编项目,在 Objective-C 文件中用将要改变的 Swift 的代码。目的为了查看转成 Swift 对象转成 OC 对象的方法名。2. Swift 类名 = 项目名.类名3. 将替换的 OC 代码 -> JS 脚本
对于第二点,这里说明一下,比方我有一个项目 SwiftDemo
需要改写 TestProject
类下面的实例方法 testLog
,就需要如下写:
defineClass("SwiftDemo.TestProject", { testLog: function() { console.log("打印 JS Log") //不能使用 NSLog('xx'),应该使用 console.log('xx') }})
总结:
编写 JavaScript 脚本主要的转换流程 Swift
-> Objective-C
-> JavaScript
。
无法实现这条链路转换的都无法进行热修复。
编写项目
为了能把 Swift 代码转换为 Objective-C 代码,需要对 Swift 代码进行一系列的修改。所以,本文对 Swift 代码定义少量规范:
Struct 结构体不能用,由于无法桥接成 OC 对象。无法拥有动态属性
公告 Class 需要继承
NSObject
,并且对属性和方法进行动态说明,也就是需要增加相应的@objc
,dynamic
,@objcMembers
关键字。
- 属性修改值,只要要
@objc
就可- JSPatch 调使用的方法只具备
@objc
就可,不需要dynamic
。- JSPatch 重写的方法需要具有
@objc
和dynamic
性质。
修改的 Swift 代码如下:
open class TestProject: NSObject { @objc var pname: String = "原始名字" //不需要 dynamic 特性 @objc private var name: String = "原始名字" //不需要 dynamic 特性 @objc static var same: String = "原始名字" //不需要 dynamic 特性 public override init() { super.init() } @objc func start() { self.testLog() } @objc dynamic func testLog() { //重写需要 @objc dynamic 性质 print("原始打印log") } @objc fileprivate func orgMethod() { //调使用的方法不使用 dynamic print("原始orgMethod") print("pname = \(self.pname)") print("name = \(self.name)") print("static same = \(DCTestProject.same)") print("执行完成") } }@objcMembersopen class TestProject: NSObject { var pname: String = "原始名字" //不需要 dynamic 特性 @objc private var name: String = "原始名字" //不需要 dynamic 特性 static var same: String = "原始名字" //不需要 dynamic 特性 public override init() { super.init() } func start() { self.testLog() } dynamic func testLog() { //重写需要 @objc dynamic 性质 print("原始打印log") } @objc fileprivate func orgMethod() { //调使用的方法不使用 dynamic, 但私有方法需要手动加 @objc print("原始orgMethod") print("pname = \(self.pname)") print("name = \(self.name)") print("static same = \(DCTestProject.same)") print("执行完成") } }
JSPatch 脚本如下:
defineClass("SwiftDemo.TestProject", { testLog: function() { console.log("打印 JS Log"); self.setPname("打印 JS"); self.setName("打印 JS"); require('SwiftDemo.TestProject').setSame("打印 JS"); self.orgMethod(); }})
- Enum 枚举尽量少使用,需要少量特殊解决,并且枚举中不能有其余方法。即便桥接成OC枚举,JavaScript没办法获取。
@objc public enum NVActivityIndicatorType: Int { case Blank case BallPulse case BallGridPulse case BallClipRotate case SquareSpin }
- Protocol 协议需要在相应的地方增加
@objc
关键字, 并且继承NSObjectProtocol
协议。
@objc protocol TestDelegate: NSObjectProtocol { @objc func TestClick(Str: String)}
元组类型不能用。
需要在 JavaScript 调使用或者者修改的方法都必需具备动态属性,而且方法所使用到的参数以及返回的对象都必需具备动态属性。
调使用 C 函数 函数很麻烦需要做绑定操作,所以尽量少使用,而且不能保证所有的 C 函数 都能绑定调使用。尤其是内联函数。
常量、枚举、宏、全局变量不要用,由于 JavaScript 没办法获取。
指针尽量不要用,对于 Swift 和 JavaScript 语言来说,指针用麻烦,容易出错。指针用方法请看JPMemory用文档
方法里的代码尽量不能太多,尽量不要超过 30 行。对臃肿代码,尤其是逻辑比较重要的代码进行方法拆分。
重写或者者调使用的方法的参数和返回类型也必需需要能桥接到 Objective-C 代码中。
项目中对于公使用工具类最好具有动态属性,而且假如是纯 Swift 写的就尽量中间封装动态中间类。
注意事项
说明一下 Swift 4.0 之后的两个修饰的关键字 @objc
和 @objcMembers
比照:
- Swift 4.0 之后的
@objc
和dynamic
关键字功能分开,也就是只增加 @objc 是不具备动态性的。 - @objcMembers 会在类、类扩展、子类的所有非 private 的方法和属性前增加 @objc 修饰,并且不会增加 dynamic 特性。
总结
热修复只是使用来线上紧急的 BugFix,没必要使用来做其余功能开发不必要的操作。对于 Swift 项目,还是平时注意一下代码编写逻辑,毕竟热修复针对的是 Objective-C 项目。