干货 | 携程酒店iOS动态View的探索 - 携程技术

作者简介

姜睿东,2009年加入携程,从事无线研发,现在大住宿事业群负责酒店无线研发工作。

一直以来,Native App因为审核的原因,新版本不能很及时地上线。尤其是iOS,碰到点审核问题,有时候一连几天都不能上架,严重影响业务和产品的体验。

大家一直都在寻求能够动态更新业务的方法,关于这方面的框架也是层出不穷。自从Facebook推出React Native以后,便以其良好的兼容性和性能优势占据了这方面的领先地位,携程也在此基础上开源了CRN框架

如果是新业务,用CRN开发是非常合适的,开发效率高,双平台兼容性好。但如果要把已有的Native页面转CRN,复杂的核心页面成本会有点高。在不增加人手的情况下,要想同时进行业务的迭代和CRN的转换,会有点力不从心。

如果硬转,周期会很长。以携程酒店主流程页面之一的订单详情页为例,在没有额外增加人手的情况下,前后花了几个月时间,才陆陆续续完成了90%的功能转CRN,过程尤为艰辛。订单详情页是主流程页面中相对简单的,如果要转酒店详情页,光是几百行的ViewModel就已经让人望而却步了。

对此,我们考虑能不能采用一种让Native和CRN共存的方式,这样既可以保留Native的业务逻辑,又可以在UI层面做到灵活应变。最关键的是,可以分模块的开发,而不用像转CRN那样必须整个页面一起上。

当然,Native和CRN混合的解决方案早就有了,但是当CRN作为一个子View出现在Native页面里的时候,由于CRN的框架比较重量级,在性能上并不是特别理想,而且和Native的交互也不是特别方便,所以我们开始考虑有没有更为轻便的解决方案

在比较了多种跨平台方案之后,首先排除了类似Lua这种需要依赖第三方库,且语法非主流的方案,最终决定采用原生系统就自带支持的,且语法有着广泛群众基础的JavaScript。

在iOS7之前,要在Native环境中和JavaScript交互是非常简单且功能有限的,基本上只有依靠Webview的EvaluateJavaScript 来注入执行一段JS脚本。从iOS7开始,苹果引入了JavaScriptCore这个库,顿时给iOS的开发带来了翻天覆地的变化。

为什么会这么说呢,首先来看一下JavaScriptCore中所包含的两个关键类,JSContext和JSValue:

JSContext

JSContext提供了一个在APP中执行JavaScript代码的环境,使得我们可以直接在Objective-C或Swift代码中直接调用JavaScript代码,并得到返回结果,反过来也可以暴露方法和类供JavaScript调用。

JSValue

JSValue则是一个JavaScript数据类型在Objective-C或Swift中的包装对象,借助于这个对象我们可以在Native代码和JavaScript代码之间互相传值,这两者之间的对应关系如下图所示:

Objective-C (and Swift) Types JavaScript Types
nil undefined
NSNull null
NSString (Swift String) String
NSNumber and primitive numeric types Number, Boolean
NSDictionary (Swift Dictionary) Object
NSArray (Swift Array) Array
NSDate Date
Objective-C or Swift object (id or AnyObject) Objective-C or Swift class (Class or AnyClass) Object
Structure types: NSRange, CGRect, CGPoint, CGSize Object
Objective-C block (Swift closure) Function

简单总结一下,JSContext提供JavaScript和Native互相调用的接口,JSValue提供互相调用之间的数据类型转换,这样的调用方法比之前的Webview要强大灵活许多,想象空间也大了很多。所以我们接下去就准备在这基础之上做点文章。

第一步,先创建一个JavaScript对象,用来描述对应iOS中的UIView,代码用ES6如下:


Class View { 
    constructor() { 
         this.x = 0; 
         this.y = 0; 
         this.width = 0; 
         this.height = 0; 
         this.borderWidth = 0; 
         this.borderColor = ‘’; 
         this.cornerRadius = 0; 
         this.masksToBounds = false; 
         this.subviews = []; 
     } 
     initWithFrame(x, y, width, height) { 
        …… 
     } 
     addSubview(v) { 
        ……    
     } 
     setOnclick(click) { 
        ……    
     } 
     …… 
} 

这些属性和方法都是iOS中UIView比较常用的,如同在iOS中UILabel是继承自UIView一样,我们继续创建一个JavaScript的Label对象,并继承自刚才在上面创建的View对象。


Class Label extends View { 
    constructor() { 
         super(); 
         this.text = ‘’; 
         this.textColor = ‘’; 
         this.textSize = 14; 
         this.fontStyle = 0; 
         this.textAlignment = 0; 
         this.lineBreakMode = 4; 
         this.numberOfLines = 1; 
    }     
} 

以此类推,我们继续创建诸如Imageview,Button,ScrollView等iOS中常用的组件,只要愿意,所有的组件都可以用这种方式来描绘。

有了这些基础的JavaScript组件,接下去就可以如同在iOS中布局一样,开始用这些组件进行布局,如下代码片段示例了如何对一张图片进行布局。


createImage() { 
       var container = View.initWithFrame(0, 0, 50, 50); 
       container.backgroundColor = "#FFFFFF"; 
       var image = Image.initWithFrame(0, 0, 50, 50); 
       image. imageUrl = ‘http://m.ctrip.com/xxxxx.png’; 
       container.addSubView(image); 
       return container; 
 } 

对于熟悉iOS开发的同学来说,会觉得这段代码非常眼熟。没错,这就是一段用JavaScript来写的iOS代码,依此类推,稍微复杂一点的布局也可以用这种方式完成。

最后来看一下布局完成以后的返回值,暂时还是先以上面的Image控件来做示例:


render() { 
        varcontainer = View.initWithFrame(0, 0, 50, 50); 
       container.backgroundColor = "#FFFFFF"; 
       var image = Image.initWithFrame(0, 0, 50, 50); 
       image. imageUrl = ‘http://m.ctrip.com/xxxxx.png’; 
       container.addSubView(image); 

       var demoView = View.initWithFrame(0,0,180,180); 
       demoView.addSubView(container) 
       return demoView; 
} 

如果在浏览器或者JavaScript环境中运行上述代码,会得到一个自定义的递归对象,根对象会包含一个Subview数组,数组中的每个元素都有可能是另外一组UI对象,当然实际操作中并不建议层次太多,一般1-2层。

做到这里,JavaScript的部分暂告一段落。接下来回到Native当中,还记得上文提到的JSContext么?这是一个在Native当中的JavaScript执行环境,我们在Native环境中用JSContext来执行刚才那个Demo,就会得到一个对应的JSValue值,这个JSValue的值用[JSValuetoObjct]来转换成Object-C对象的话,最终就得到了一个字典,NSDictionary。

继续递归地拆解这个字典,拆解到底,每个元素最终都会转成OC的Object,然后根据每个Object预先定义好的Type类型,实例化成相应的Native组件,并且每个组件有一个对应的数据Model。

还是以上述那个Label为例,其对应的OC Label代码如下:


@implementation Label 

- (void)setModel:(HTLDynamicLabelModel *)model{ 
   self.dynamicViewModel = model; 
   self.text = model.text; 
   self.textColor = model.textColor; 
   self.font = model.font; 
   self.lineBreakMode = model.lineBreakMode; 
   self.numberOfLines = model.numberOfLines; 

    if(model.richText && model.richText.attributedString) { 
       self.attributedText = model.richText.attributedString; 
    } 
} 

@end 

到此为止,就完成了所有之前在JavaScript中描绘的控件在Native里的转换,剩下的事情就是对这些Native组件进行渲染了,具体就不在这里描述了。

总体来说,这个思路在原理上跟RN或者CRN是一样的,但更为轻量一点,几乎0配置就能使用。通过配置增量更新,从服务端下载最新的JS文件,可以做到类似CRN在线更新的效果。

从性能上来看,因为不需要额外加载任何框架代码,JS执行的消耗几乎可以忽略,所以和Native混合在一起的时候,几乎看不出有任何延迟。

这个方案非常适合做一些轻量级的又需要经常不定期更新的UI

,比如节日氛围或者城市包装的UI。这些UI经常会跟随节假日更新,用这个方案可以轻松在线更新UI,不用通过服务端下发一堆样式来控制,减轻了服务发布的压力和不必要的服务交互。

综上所述,这是我们团队对新事物的一些探讨和研究,并不存在要代替CRN或其他框架一说,每个框架都有其适用的场景,没有绝对的优劣之分。

在研究这个解决方案的过程中,我们也认真地深入了解了JavaScriptCore的一些机制,原理都是万变不离其宗的,但可以结合不同的场景,进行不同的演变,就看怎么灵活运用了。

所以,与其说本文是在探索iOS中动态View的解决方案,也不妨说成是对JSContex和JSValue如何运用的一些探讨,从实际的摸索中来看,灵活运用好JavaScriptCore,可以有无限多的可能。