初探JSPatch

Swift混编项目使用JSPatch热修复

Posted by Anyeler on July 3, 2018

前言

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专门管理。

  1. JSPatchPlatform.framework 拖入项目中,勾选 “Copy items if needed”,并确保 “Add to target” 勾选了相应的 target。

  2. 添加依赖框架:TARGETS -> Build Phases -> Link Binary With Libraries -> + 添加 libz.dylibJavaScriptCore.framework

  3. 生成和配置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
    
  4. 启动运行
    #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-CRuntime 进行改写、修改的;而 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,并且对属性和方法进行动态说明,也就是需要添加相应的 @objcdynamic@objcMembers 关键字。

  1. 属性修改值,只需要 @objc 即可
  2. JSPatch 调用的方法只具有 @objc 即可,不需要 dynamic
  3. JSPatch 重写的方法需要具备 @objcdynamic 性质。

修改的 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 没办法获取。

  • 指针尽量不要使用,对于 SwiftJavaScript 语言来说,指针使用麻烦,容易出错。指针使用方法请看JPMemory使用文档

  • 方法里的代码尽量不能太多,尽量不要超过 30 行。对臃肿代码,尤其是逻辑比较重要的代码进行方法拆分。

  • 重写或者调用的方法的参数返回类型也必须需要能桥接到 Objective-C 代码中。

  • 项目中对于公用工具类最好具备动态属性,而且如果是纯 Swift 写的就尽量中间封装动态中间类。

注意事项

说明一下 Swift 4.0 之后的两个修饰的关键字 @objc@objcMembers 对比:

  • Swift 4.0 之后的 @objcdynamic 关键字功能分开,也就是只添加 @objc 是不具有动态性的。
  • @objcMembers 会在类扩展子类的所有非 private 的方法和属性前添加 @objc 修饰,并且不会添加 dynamic 特性。

总结

热修复只是用来线上紧急的 BugFix,没必要用来做其他功能开发不必要的操作。对于 Swift 项目,还是平常注意一下代码编写逻辑,毕竟热修复针对的是 Objective-C 项目。