TTPatch
热修复、热更新、JS 代码动态下发、动态创建类
TTPatch 升级至 2.0,核心实现替换为 libffi 实现。同时代码重构,修改敏感命名。TTPatch 更新为 TTDFKit
风险提示:仅供技术交流使用,上架有风险!!!!
热更新交流群:978337686
1. 功能列表
功能特性 | 备注限制 |
---|---|
支持手动设置系统 Block 签名 | 例如 WKWebView 一些系统级 block 缺失签名,无法动态调用 |
替换指定 ObjectC 方法实现 |
实例/静态方法均可替换实现 |
动态创建方法供 Native/Js 调用 | 需传入方法签名 |
支持 block |
ObjectC 传入 JS ,JS 传入 ObjectC 均已支持 |
支持添加属性 | 为已存在的 class 添加属性 |
支持基础数据类型 | 非id类型,如 int ,bool 均已支持 |
支持下发纯 JS 页面 |
纯 JS 代码映射原生代码,动态发布 |
实现协议 | 2020年04月01日新增 |
支持真机无线预览 | 详细说明 |
支持 Native 代码转成 JS 脚本 |
在线地址 |
支持原生网络请求 | 使用示例 |
2. 安装
pod 2.1.1
CocoaPods - 在 Podfile 中添加
pod 'TTDFKit'
。 - 执行
pod install
或pod update
。 - 导入 "TTDFKit.h"
演示项目: Example.xcodeproj
运行效果图
在线下发补丁执行
重启后加载已下发补丁
3. 基础用法
0. build
TTPatch
的使用流程
- 源文件编写(伪
js
代码,不可直接执行). - 执行
build.js
脚本 - 通过
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动态添加的方法分两类
-
仅供JS端调用,此种方法因供JS端调用,所以采用普通方式声明即可.
-
供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读取使用。
src
工作区的文件哦!!!!
简单体验 II
如果你已经熟练使用了步骤 I
,是不是觉得每次都要经过下面三步,很麻烦。那么你可以往下看
save
run build
run xocde
目前demo
已支持模拟器/真机在线实时预览修改内容了~~~~~
下面为实时预览的准备工作
- 将
JS
目录下的node.js
依赖下载成功。执行npm install
即可。 - 执行
npm run server
开启本地服务 - 将真机/模拟器调至同一
WIFI
下 - 运行
demo
如步骤1失败请检查本地
npm,node
版本,下面给出我电脑版本供参考npm -v 6.9.0
/node -v v10.16.0
此时你的准备工作已经全部完成,接下来用你最喜欢的IDE
打开src
目录下的任意js
文件进行编辑。在点击保存之后你会发现手机数据也跟着刷新了~~~~
实际使用 III
实际使用的话,就需要一些JS相关的支持,要确保本机已安装npm
。如果不知道的同学可以百度安装。如果已经安装好npm
可以往下操作
cd
/demo/JS 执行npm install
npm run server
js
的环境了。然后我们就可以在/src
文件夹内修改.js
源文件,修改后本地服务会自动执行打包更新并预览。
outputs
目录,因为每次build
后outputs
目录会被全量替换
关于build说明
执行
npm run build
将文件转成各自对应的js。
执行
npm run package
将src
目录下文件打包成一个文件。(demo中使用此种方式进行演示。)
您的喜好就是我更新的动力