初探JSPatch

前言

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
  1. 启动运行
#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 项目。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容