swift3.0中使用JSPatch热更新

首先简单介绍一下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地址,如果有什么疑问的地方,留言给我,我会及时回复。如有错误,虚心请教。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,029评论 4 62
  • 亲爱的妹妹, 饭后散步,姐姐不愿外出,时间完完全全交给你。 你挑最喜欢的裙子陪我去散步,我们一起看天上的月亮,一起...
    helenxxf阅读 169评论 0 0
  • 我迎着风来到了海边 像个浪子追逐着海洋另一个广阔的世界 看到了那些为等待而进入了枯景的它们 海风吹乱了我的头发 空...
    苏翊轩阅读 338评论 1 0
  • 临江仙~信州作 晁补之 谪宦江城无屋买,残僧野寺相依。松间药臼竹间衣。水穷行到处,云起坐看时。 一个幽禽缘底事,苦...
    S_be84阅读 144评论 0 0
  • 第六章第一节心理技术杂谈篇 抑郁症的发病的老根是缺爱。缺爱多是儿时父母吵架和父母常打自己、上学被老师同学打。...
    逍遥真龙哥哥阅读 1,137评论 0 2