前言
iOS平台的有很多热修复框架,原理都是差不多,都是利用 Runtime 进行属性、方法修改。
JSPatch 是现今比较主流、轻量级的热修复框架。利用内置的 JavaScript 引擎(JavaScriptCore)结合 JavaScript 在运行时进行对 Object-C 对象修改。
接入文档
JSPatch 的官方接入文档写的很详细,不过也很简洁。对于 Objective-C 项目已经足够使用了但是对于 Swift 项目的接入详情还是略显简略。目前,由于 Apple 公司对热修复的打压以及等等其他原因,使得 JSPatch 分为JSPatch平台版和 Github 的开源代码版。
Github 的开源代码版:
# Your Podfile
platform :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 1024
pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM –nocrypt
rsa -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("执行完成")
}
}
@objcMembers
open 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 项目。