美文网首页iOS开发精进iOS Dev
swift3.0中使用JSPatch热更新

swift3.0中使用JSPatch热更新

作者: 披萨配可乐 | 来源:发表于2016-12-02 14:47 被阅读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语法:

    • 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文件修改后的效果:

    修改后的效果.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地址,如果有什么疑问的地方,留言给我,我会及时回复。如有错误,虚心请教。

    相关文章

      网友评论

        本文标题:swift3.0中使用JSPatch热更新

        本文链接:https://www.haomeiwen.com/subject/uhtqmttx.html