iOS开发精进iOS Dev

swift3.0中使用JSPatch热更新

2016-12-02  本文已影响1357人  披萨配可乐

首先简单介绍一下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
   }

运行结果,如下图:


上面代码运行结果.png

一个很简单的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语法:

@param classDeclaration: 字符串,类名/父类名和Protocol
@param properties: 新增property,字符串数组,可省略
@param instanceMethods: 要添加或覆盖的实例方法
@param classMethods: 要添加或覆盖的类方法

在swift中,如果要使用某个swift类,必须要用: 项目名.类名 这种方式。

require('UIColor');

注意:如果是使用自定义类,并且该类是swift语法生成的,在初始化时一定要如下使用:

require('ChartAndDate.Person').alloc().init();

我们自定义的类如果采用以下方式初始化,则会报错;如果是系统提供的类,则可以采用此方式

require('ChartAndDate.Person');
Person.alloc().init(); //Person是自定义的swift类,此写法不行
require('NSMutableArray');
NSMutableArray.alloc().init();//NSMutableArray是系统提供的类,此写法OK
self.view().setBackgroundColor(UIColor.redColor());
newArray.addObject(str);//使用addObject方法,而不是append
self.dataSource().objectAtIndex(indexPath.row())
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        //swift中的方法
    }
tableView_didSelectRowAtIndexPath:function(tableView,indexPath){
           //JS中使用:这里的方法一定要用OC中的方法全名,swift中的方法简写名不行
   }
//调用: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文件修改后的效果:

修改后的效果.png

UITableView的起始y轴高度从20开始,cell个数也变成了只有5个。当然细心的读者可能发现了,在原代码中并没有实现UITableView的didSelectRowAtIndexPath方法,而在JS文件中确使用了这个方法。是的,JS文件中动态的给UITableView添加了一个点击cell时触发的代理方法。这时,我们点击cell时,控制台输出:

控制台输出.png

现在来将难度加大一些:给ViewController类动态添加一个数据源数组,并且数据源中存储的是自定义类,在cell上显示这个类的信息,并在点击cell时,使用block回调。
创建两个自定义类,第一个类叫Person,第二个类叫Pet。创建一个classDemo.js的文件,现在的我们工程文件结构如下:

工程文件结构.png

将AppDelegate中调用的JS文件名更改为classDemo.js,现在来看看我们创建的两个类

Person类.png Pet类.png

自定义的UITableViewCell方法如下:

PersonTableViewCell.png

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文件中注释出来了,来看下运行结果吧

运行结果.png

此时点击"宠物说话"的按钮效果:

block回调.png

最后,附上demo的github地址,如果有什么疑问的地方,留言给我,我会及时回复。如有错误,虚心请教。

上一篇 下一篇

猜你喜欢

热点阅读