TTPatch 0.9.1

TTPatch 0.9.1

tianyu 维护。



TTPatch 0.9.1

TTPatch

Cocoapods Cocoapods CocoaPods

热修复、热更新、JS 代码动态下发、动态创建类

TTPatch 升级至 2.0,核心实现替换为 libffi 实现。同时代码重构,修改敏感命名。TTPatch 更新为 TTDFKit

风险提示:仅供技术交流使用,上架有风险!!!!

热更新交流群:978337686

1. 使用文档

2. 基础用法

3. 在线工具

4. 常见问题

5. 进阶用法

1. 功能列表

功能特性 备注限制
支持手动设置系统 Block 签名 例如 WKWebView 一些系统级 block 缺失签名,无法动态调用
替换指定 ObjectC 方法实现 实例/静态方法均可替换实现
动态创建方法供 Native/Js 调用 需传入方法签名
支持 block ObjectC传入 JSJS传入 ObjectC 均已支持
支持添加属性 为已存在的 class 添加属性
支持基础数据类型 非id类型,如 intbool 均已支持
支持下发纯 JS 页面 JS 代码映射原生代码,动态发布
实现协议 2020年04月01日新增
支持真机无线预览 详细说明
支持 Native 代码转成 JS 脚本 在线地址
支持原生网络请求 使用示例

2. 安装

CocoaPods pod 2.1.1

  1. 在 Podfile 中添加 pod 'TTDFKit'
  2. 执行 pod installpod update
  3. 导入 "TTDFKit.h"

演示项目: Example.xcodeproj

运行效果图

效果图.gif

在线下发补丁执行

在线下发补丁执行.gif

重启后加载已下发补丁

重启后加载已下发补丁.gif

3. 基础用法

我要参照TTPatch-JS模板

0. build

TTPatch的使用流程

  1. 源文件编写(伪js代码,不可直接执行).
  2. 执行 build.js 脚本
  3. 通过 build.js 语法转义,变成 js 可执行代码。输出路径 ./outputs(具体要下发到app的js文件)

⚠️./outputs目录不要修改,每次执行过 build.js 后会替换 ./outputs 目录

1. import

在使用Objective-C类之前需要调用 _import('className’)

_import('UIView')
var view = UIView.alloc().init()

可以用逗号 , 分隔,一次性导入多个类

_import('UIView, UIColor')
var view = UIView.alloc().init()
var red = UIColor.redColor()

2. 调用 OC 方法

调用类方法

var redColor = UIColor.redColor();

调用实例方法

var view = UIView.alloc().init();
view.setNeedsLayout();

参数传递

与 OC 一样传递参数

在这里需要注意,对于有参数的情况,参数前需要加 _。在 Obj-C 方法中使用的 : 和 JS 中使用的 _ 是一一对应的,如果有遗漏则会报错。

var view = UIView.alloc().init();
var superView = UIView.alloc().init()
superView.addSubview_(view)

Property

与声明和实例方法平级

data:property(),

获取/修改 通过 getter / setter 方法,获取时记得加 ()

view.setBackgroundColor_(redColor);
var bgColor = view.backgroundColor();

方法名转换

多参数方法名使用 _ 分隔:

var indexPath = NSIndexPath.indexPathForRow_inSection_(0, 1);

如果原 OC 方法名中包含下划线 _,在 JS 中使用双下划线 __ 代替:

// Obj-C: [JPObject _privateMethod];
JPObject.__privateMethod()

3. defineClass

声明 Class,实现协议 Protocol

API

// class:superClass<protocolA,protocolB,...>
defineClass('ViewController:UIViewController<UITableViewDelegate,UITableViewDataSource>',
{
    instanceMethods...
},
{
    classMethods...
});

@param classDeclaration: 字符串: 类名:父类名<Protocol> @param instanceMethods: 要添加或覆盖的实例方法
@param classMethods: 要添加或覆盖的类方法

覆盖方法

1.在 defineClass 里定义 OC 已存在的方法即可覆盖,方法名规则与调用规则一样,使用 _ 分隔

// OC
@implementation JPTableViewController
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}
@end
// JS
defineClass("JPTableViewController", {
  tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
    ...
  },
})

2.使用双下划线 __ 代表原OC方法名里的下划线 _

// OC
@implementation JPTableViewController
- (NSArray *) _dataSource {
}
@end
// JS
defineClass("JPTableViewController", {
  __dataSource: function() {
  },
})

3.在方法名前加 tt 即可调用未覆盖前的 OC 原方法

// OC
@implementation JPTableViewController
- (void)viewDidLoad {
}
@end
// JS
defineClass("JPTableViewController", {
  viewDidLoad: function() {
     self.ttviewDidLoad();
  },
})

覆盖类方法

defineClass() 第三个参数就是要添加或覆盖的类方法,规则与上述覆盖实例方法一致:

// OC
@implementation JPTestObject
+ (void)shareInstance
{
}
@end
// JS
defineClass("JPTableViewController", {
  //实例方法
}, {
  //类方法
  shareInstance: function() {
    ...
  },
})

覆盖 Category 方法

覆盖 Category 方法与覆盖普通方法一样:

@implementation UIView (custom)
- (void)methodA {
}
+ (void)clsMethodB {
}
@end
defineClass('UIView', {
  methodA: function() {
  }
}, {
  clsMethodB: function() {
  }
});

添加新方法

TTPatch动态添加的方法分两类

  1. 仅供JS端调用,此种方法因供JS端调用,所以采用普通方式声明即可.

  2. 供JS&Oc调用,此种访问因Native调用所以需要提供动态方法签名,写法如下

    方法名 关键字 返回值,参数 方法实现

    funcName:dynamic("void, int", function(){})

    如方法无参/无返回值可简化:dynamic(function(){}),也可以不写dynamic.

    Native动态方法签名默认: `v@:v'

方法签名对照表

// OC
@implementation JPTableViewController
- (void)viewDidLoad
{
    [self funcWithParams:@"悟空"];
    [self funcWithParams:@"熊大" param2:@"熊二"];
    [self funcWithParams:@"百度" param2:@"腾讯" param3:@"阿里"];
}
@end
// JS

defineClass("JPTableViewController", {
 	funcWithParams_:dynamic('void,id',function(param1){
		Utils.log_info('[1]动态方法入参:'+param1);
	}),
	funcWithParams_param2_:dynamic('void,id,id',function(param1,param2){
		Utils.log_info('[2]动态方法入参:'+param1+','+param2);
	}),
	funcWithParams_param2_param3_:dynamic('void,id,id,id',function(param1,param2,param3){
		Utils.log_info('[3]动态方法入参:'+param1+','+param2+','+param3);
	}),
})

Super

使用 Super() 接口代表 super 关键字,调用 super 方法

// JS
defineClass("JPTableViewController", {
  viewDidLoad: function() {
     Super().viewDidLoad();
  }
})

属性

获取/修改 OC 定义的 Property

使用 getter / setter 方法获取/修改已定义的 Property

动态新增 Property

name:property() 構造函数用于属性

defineClass("JPTableViewController", {
  //添加属性
  name:property(),
  totalCount:property(),
  viewDidLoad: function() {
     Super().viewDidLoad();
     self.setName_("TTPatch");   //设置 Property 值
     var name = self.name();    //获取 Property 值
     var totalCount = self.totalCount()
  },
},{});

私有成员变量

使用 valueForKey()setValue_forKey() 方法获取/修改私有成员变量

// OC
@implementation JPTableViewController {
     NSArray *_data;
}
@end
// JS
defineClass("JPTableViewController", {
  viewDidLoad: function() {
     var data = self.valueForKey_("_data")     //get member variables
     self.setValue_forKey_(["Patch"], "_data")     //set member variables
  },
})

4. 特殊类型

Struct

支持使用 JavaScript 对象表示 CGRect / CGPoint / CGSize / UIEdgeInsets 这四个 struct 类型

// Obj-C
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 100, 100)];
[view setCenter:CGPointMake(10,10)];
[view sizeThatFits:CGSizeMake(100, 100)];


// JS
var view = UIView.alloc().initWithFrame(new TTReact(x:20, y:20, width:100, height:100))
view.setCenter_(new TTPoint(x: 10, y: 10))
view.sizeThatFits_(new TTSize(width: 100, height:100))

Selector

在 JS 中使用字符串来表示 Selector

//Obj-C
[self performSelector:@selector(viewWillAppear:) withObject:@(YES)];
//JS
self.performSelector_withObject_("viewWillAppear:", 1)

5. 块

新增原生方法中

调用Obj-C传入的block,需传入方法签名?代表block动态生成的方法参数中包含block,要注意block是否包含signature信息,如确实则不可动态调用

iOS 13下WKWebView代理方法中block缺少signature.

上述情况需要在调用时手动设置签名

    decisionHandler(block(',int'),1);

签名形式为'block(',int')',具体规则如下

方法签名对照表


callBlock_:dynamic(',?',function(callback){
    if(callback){
        //自动获取签名
        callback(10);
    })
},

webView_decidePolicyForNavigationAction_decisionHandler_:dynamic(',id,id,?',function(webView, navigationAction, 
decisionHandler) {
    //手动设置签名
    decisionHandler(block(',int'),1);
}),

新增纯JS方法中

callBlock_:function(callback){
    if(callback){
	callback(10);
    }
},

Obj-C调用JS传入block,并接受回调

将JavaScript的block传入Obj-c时要注意,block应声明方法参数及返回值类型,分割,返回值在第一位

runBlock:function(){

    self.testCall2_(block("id,id"),function(arg){
        Utils.log_info('--------JS传入OC方法,接受到回调--------- 有参数,有返回值:string  '+arg);
        return '这是有返回值的哦';
    });
}

6. 调试

目前支持三种级别日志

Utils.log只在debug环境下的js中输出,Utils.log_info在js和xcode中输出,Utils.log_error在js和xcode中输出并输出错误信息

log不支持多参数,只支持参数拼接

var view = UIView.alloc().init();
var str = "test";
var num = 1;
Utils.log(str + num);   //直接在JS拼接字符串

也可以通过Safari的调试工具对JS进行断点调试,详见JS断点调试

4. 环境配置及使用

简单体验 I

首先需要下载我们的demo工程,然后只需修改src目录下的.js文件,接着运行npm run build命令。这条命令会将我们刚刚修改的工作区代码(src)经过转义压缩输出到outputs目录下,outputs目录下的文件供app读取使用。

⚠️⚠️app不能直接读取src工作区的文件哦!!!!

简单体验 II

如果你已经熟练使用了步骤 I,是不是觉得每次都要经过下面三步,很麻烦。那么你可以往下看

  • save
  • run build
  • run xocde

目前demo已支持模拟器/真机在线实时预览修改内容了~~~~~

下面为实时预览的准备工作

  1. JS目录下的node.js依赖下载成功。执行npm install即可。
  2. 执行npm run server 开启本地服务
  3. 将真机/模拟器调至同一WIFI
  4. 运行demo

如步骤1失败请检查本地npm,node版本,下面给出我电脑版本供参考npm -v 6.9.0 / node -v v10.16.0

此时你的准备工作已经全部完成,接下来用你最喜欢的IDE打开src目录下的任意js文件进行编辑。在点击保存之后你会发现手机数据也跟着刷新了~~~~

实际使用 III

实际使用的话,就需要一些JS相关的支持,要确保本机已安装npm。如果不知道的同学可以百度安装。如果已经安装好npm可以往下操作

  1. cd /demo/JS 执行 npm install
  2. npm run server

⚠️⚠️执行后,我们本地已经有可以执行js的环境了。然后我们就可以在/src文件夹内修改.js源文件,修改后本地服务会自动执行打包更新并预览。

⚠️⚠️实际使用不要直接修改outputs目录,因为每次buildoutputs目录会被全量替换

关于build说明

执行npm run build 将文件转成各自对应的js。

执行npm run packagesrc目录下文件打包成一个文件。(demo中使用此种方式进行演示。)

您的喜好就是我更新的动力