首先简单介绍一下JSPatch:
对于iOS已经上线的应用,如果有什么bug,或者需要更新,开发者不得不重新上线一个新的版本,等待苹果审核通过之后,才能将项目更新。Objective-C是动态语言,具有运行时特性,该特性可通过类名称和方法名的字符串获取该类和该方法,并实例化和调用。JSPatch通过JavaScript文件,动态植入代码来替换旧代码。此文章是在swift3中使用JSPatch。
第一步
在项目中使用cocoapods pod 'JSPatch'导入JSPatch
在AppDelegate.swift中的didFinishLaunchingWithOptions方法中调用
//开启JSPatch
1. JPEngine.start()
2. let sourcePath = Bundle.main.path(forResource: "jsDemo", ofType: "js")
3. let script = try?String.init(contentsOfFile: sourcePath!, encoding: String.Encoding.utf8)
4. if script != nil{
5. JPEngine.evaluateScript(script)
6. }
第一行代码表示JPEngine类开始配置默认数据;第二行代码表示加载工程中名为jsDemo.js的文件,这个文件是我们使用JavaScript编写的代码,后面会介绍到;第五行代码是JSPatch开始读取并执行JavaScript文件中的内容。
以上是本地加载JS文件,网络加载JS文件写法如下:
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://cnbang.net/test.js"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[JPEngine evaluateScript:script];
}];
第二步
在工程中创建一个jsDemo.js的文件,接下来我们要开始在js文件中,使用JavaScript语法创建OC代码了。JSPatch基础语法,JSPatch是开源项目,有兴趣的朋友,可以去GitHub上查看相关文件。
现在工程中的ViewController中只有一个UITableView,如下代码:
let myTableView = UITableView()
var dataSource = [String]()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
for i in 0...10 {
dataSource.append("\(i+1)元素")
}
self.myTableView.delegate = self
self.myTableView.dataSource = self
self.myTableView.frame = self.view.bounds
self.view.addSubview(self.myTableView)
}
//MARK:UITableView代理方法
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "idetifier") ?? UITableViewCell.init(style: .default, reuseIdentifier: "idetifier")
cell.textLabel?.text = self.dataSource[indexPath.row]
return cell
}
运行结果,如下图:
一个很简单的UITableView,并添加了11个cell。现在我们要做的是修改UITableView的起始y坐标为20,并且数据长度变为5。
JS代码如下:
defineClass("ChartAndDate.ViewController",{
viewDidLoad:function(){
self.super().viewDidLoad();
var newArray = require('NSMutableArray').alloc().init();
for (var i=0;i<10;i++){
var str = "第" + (i + 1) + "个JS元素"
// console.log(typeof str) 打印str类型
newArray.addObject(str);
}
self.setDataSource(newArray);
self.myTableView().setDelegate(self);
self.myTableView().setDataSource(self);
var width = self.view().bounds().width;
var height = self.view().bounds().height - 20;
self.myTableView().setFrame({x:0, y:20, width:width, height:height});
self.view().addSubview(self.myTableView());
console.log("js脚本替换viewDidLoad方法");
},
//UITableView代理方法
tableView_numberOfRowsInSection:function(tableView,section){
console.log("js脚本替换numberOfRows方法");
return self.dataSource().count() - 5;
},
tableView_cellForRowAtIndexPath:function(tableView,indexPath){
var cell = tableView.dequeueReusableCellWithIdentifier("identifier");
if (!cell){
require('UITableViewCell')
cell = UITableViewCell.alloc().initWithStyle_reuseIdentifier(0,"identifier");
}
cell.textLabel().setText(self.dataSource().objectAtIndex(indexPath.row()));
console.log("js脚本替换cellForRowAtIndexPath方法");
return cell;
},
tableView_didSelectRowAtIndexPath:function(tableView,indexPath){
console.log("执行JS中的didSelect方法 ," + indexPath.row() + "个数");
}
})
上面的JS代码看上去是不是和swift很类似呢。我们来解释一下上面的JS代码用到了哪些JSPatch语法:
- defineClass(classDeclaration, ['propertiesA,propertiesB'] instanceMethods, classMethods)
@param classDeclaration: 字符串,类名/父类名和Protocol
@param properties: 新增property,字符串数组,可省略
@param instanceMethods: 要添加或覆盖的实例方法
@param classMethods: 要添加或覆盖的类方法
在swift中,如果要使用某个swift类,必须要用: 项目名.类名 这种方式。
- 在JS中使用OC或者swift中系统提供的类时,需要提前声明,声明方式为:
require('UIColor');
注意:如果是使用自定义类,并且该类是swift语法生成的,在初始化时一定要如下使用:
require('ChartAndDate.Person').alloc().init();
我们自定义的类如果采用以下方式初始化,则会报错;如果是系统提供的类,则可以采用此方式
require('ChartAndDate.Person');
Person.alloc().init(); //Person是自定义的swift类,此写法不行
require('NSMutableArray');
NSMutableArray.alloc().init();//NSMutableArray是系统提供的类,此写法OK
- 在JS文件中,如果要用到swift或者OC中类的属性,必须要加小括号( ) 。比如在ViewController类中设置view的背景颜色时,要这样写:
self.view().setBackgroundColor(UIColor.redColor());
- 使用swift中的数组时,对数组赋值和取值,都要使用OC中数组的方法,swift中的append方法不行。
newArray.addObject(str);//使用addObject方法,而不是append
self.dataSource().objectAtIndex(indexPath.row())
- 在JS中调用swift中的方法时,将以下swift方法在JS中调用:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//swift中的方法
}
tableView_didSelectRowAtIndexPath:function(tableView,indexPath){
//JS中使用:这里的方法一定要用OC中的方法全名,swift中的方法简写名不行
}
- 在swift3中很多方法都已经简写了,但是在JS文件中调用方法时,必须使用方法全名,这里提供一个方法,可以用来获取类的所有属性名和方法名,具体可以参考Swift Runtime分析:还像OC Runtime一样吗?
//调用:showClsRuntime(UITableViewCell.self)
func showClsRuntime(cls:AnyClass){
var methodNum:UInt32 = 0
let methodList = class_copyMethodList(cls, &methodNum)
for index in 0..<numericCast(methodNum) {
let method:Method = methodList![index]!
// print(String.init(cString: method_getTypeEncoding(method)))
print("类名" + String.init(describing: method_getName(method)))
}
var properyNum:UInt32 = 0
let properyList = class_copyPropertyList(cls, &properyNum)
for index in 0..<numericCast(properyNum) {
let property:objc_property_t = properyList![index]!
print("属性名" + String.init(cString: property_getName(property)))
// print(String.init(cString: property_getAttributes(property)))
}
}
我们来看看现在使用JS文件修改后的效果:
UITableView的起始y轴高度从20开始,cell个数也变成了只有5个。当然细心的读者可能发现了,在原代码中并没有实现UITableView的didSelectRowAtIndexPath方法,而在JS文件中确使用了这个方法。是的,JS文件中动态的给UITableView添加了一个点击cell时触发的代理方法。这时,我们点击cell时,控制台输出:
现在来将难度加大一些:给ViewController类动态添加一个数据源数组,并且数据源中存储的是自定义类,在cell上显示这个类的信息,并在点击cell时,使用block回调。
创建两个自定义类,第一个类叫Person,第二个类叫Pet。创建一个classDemo.js的文件,现在的我们工程文件结构如下:
将AppDelegate中调用的JS文件名更改为classDemo.js,现在来看看我们创建的两个类
自定义的UITableViewCell方法如下:
JS文件内容如下
//添加一个source数组的成员变量,类型为[Person]()的数组,点击cell时,调用pet的方法
require('NSString,UIAlertController,UIAlertAction')
defineClass("ChartAndDate.ViewController",['source'],{
addNewMethod:function(){
//添加第一个Person
var person = require('ChartAndDate.Person').alloc().init();
person.setName("李铭")
var dog = require('ChartAndDate.Pet').alloc().init();
dog.setPetName("金毛")
person.setPet(dog);
//添加第二个Person
var person2 = require('ChartAndDate.Person').alloc().init();
person2.setName("张桦");
var dog2 = require('ChartAndDate.Pet').alloc().init();
dog2.setPetName("德牧");
person2.setPet(dog2);
//给数组赋值
self.setSource([person,person2]);
//注意:在js中创建的数组,长度用length ,count()无效
console.log("数组长度=",self.source().length);
return self;
},
viewDidLoad:function(){
self.super().viewDidLoad();
self.view().setBackgroundColor(require('UIColor').whiteColor());
self.myTableView().setDelegate(self);
self.myTableView().setDataSource(self);
self.myTableView().setFrame({x:0,y:20,width:self.view().bounds().width,height:self.view().bounds().height-20});
self.view().addSubview(self.myTableView());
self.addNewMethod();
},
tableView_numberOfRowsInSection:function(tableView,section){
return self.source().length;
},
tableView_cellForRowAtIndexPath:function(tableView,indexPath){
var cell = tableView.dequeueReusableCellWithIdentifier("identifier");
if (!cell){
//注意:纯swift类,使用时,写法:require('ChartAndDate.PersonTableViewCell')
//强调:纯swift类,即使用 require('ChartAndDate.PersonTableViewCell')声明后,再用类名初始化也不行,必须像下面这样声明初始化👇
cell = require('ChartAndDate.PersonTableViewCell').alloc().initWithStyle_reuseIdentifier(3,"identifier");
}
//注意:js数组取值 :数组名()[下标] ,数组名().objectAtIndex(下标)是OC数组取值
//这里特别注意:cell在调用ValuesForLabel方法时,一直报unrecognized selector setValuesForLabel这个错误,当时一再确定方法名没有写错,后来用showClsRuntime方法打印方法名才发现,swift中自动将类名转为了valuesForLabelWithPerson,所以大家使用swift方法时,注意一下
cell.setValuesForLabelWithPerson(self.source()[indexPath.row()]);
cell.setSelectionStyle(0);
var weakSelf = __weak(self) //__strong(self)
//注意:这里的参数类型,如果是类,则要加*号,否则person表示地址,*person才表示取值
//块为属性时,也需要用set方法
cell.setTalk(block("ChartAndDate.Person *",function(person){
var talkContetn = NSString.stringWithFormat("%@对%@说:你好!",person.name(),person.pet().petName());
//弹框提示
var alert = UIAlertController.alertControllerWithTitle_message_preferredStyle("提示",talkContetn,1);
var action = UIAlertAction.actionWithTitle_style_handler("好",2,null);
alert.addAction(action);
weakSelf.presentViewController_animated_completion(alert,true,null);
}))
return cell;
}
})
要注意的地方,已经在JS文件中注释出来了,来看下运行结果吧
此时点击"宠物说话"的按钮效果:
最后,附上demo的github地址,如果有什么疑问的地方,留言给我,我会及时回复。如有错误,虚心请教。