台式电脑

苹果电脑js技术怎么样(iOS 中的 JS)

最近主要在研究iOS中的JS这一块内容,本文打算从为什么不能单纯地搞前端、JSCore的原理和通信机制、OC底层Runtime原理、如何通过JS任意修改iOS的运行结果这4部分来阐述,旨在让前端和iOS开发同学更加了解跨端开发的原理,同时了解他俩结合起来做哪些意想不到的事情。

为什么不能单纯地搞前端?

毕业工作以来,经历过移动端H5React到RN开发,到去年的Weex开发和最近的iOS开发,越来越发现:

仅靠前端技术难以满足移动端的用户需求或体验要求

为什么只弄前端会效果不好?

可能H5同学很有感触,比如需要做一个端上H5照片上传功能,通过JS去实现往往效果会大打折扣,也很难达到业务方需要的顺滑体验,要是此时Native同学说我写好了一个Bridge,只需在客户端里用JS调用Bridge.uploadImg()这个方法就可直接用Native的上传功能,听到这句话你肯定会长舒一口气轻松地去写代码了。

还有将端上Webview由UIWebView更换成WKWebView,起到的效果也会比自己优化很久的H5顺滑滚动来得快和好。

那么潮流前端同学一般怎么弄移动端需求呢?

他们一般会借Native端的能力,譬如用Weex或者RN来开发页面,让其也有Native效果,假如有页面在微信或者支付宝,还可以升级成小程序变成内置程序一样,客户端中还可以借助端上容器优化这一块,让其可以离线我们的H5页面,并通过桥提供很多Native功能来拓展能力。

这里借力的的桥梁其实就是Bridge,让两者不在是一个孤岛,而是相互助力,我理解它可以做这些事情:

iOS 中的 JS

接下来通过JSCore的原理和通信机制这一小节给出上述的解决方案。

JSCore的原理和通信机制

JSCore是什么?

大家都知道浏览器内核的模块主要是由渲染引擎和JS引擎组成,其中JSCore就是一种JS引擎

Apple通过将WebKit的JS引擎用OC封装,提供了一套JS运行环境以及Native与JS数据类型之间的转换桥梁,常用于OC和JS代码之间的相互调用,这也意味着他可以脱离渲染单独去执行JS。

JSCore主要包括如下这些classes、协议、类结构:

iOS 中的 JS

JSCore如何运行呢?

可以通过如下这张JSCore的框架结构图和上述描述来看懂各个模块是怎么运行的。

iOS 中的 JS

从上图我们可以看到一个这样的过程:

在Native应用中我们可以开启多个线程来异步执行我们不同的需求,也就意味着我们可创建多个JSVirtualMachine虚拟机(运行资源提供者),同时相互隔离不影响,这样我们就可以并行地执行不同JS任务。

在一个JSVirtualMachine中还可以关联多个JSContext(JS执行环境上下文),并通过JSValue(值对象)来和Native进行数据传递通信,同时可以通过JSExport(协议),将Native中遵守此解析的类的方法和属性转换为JS的接口供其调用。

JS和OC数据类型互换

从上小节,可以知道JSValue可以用来让JS和OC之间无障碍的数据转换,主要原理是JSValue上面提供了如下方法,便于双方各种类型进行转换。

iOS 中的 JS

在iOS里面执行JS代码

我们可以通过evaluateScript在JSCore中执行一段JS脚本,利用这个特性我们可以来做一些多端逻辑统一的事情。

//执行一段JavaScript脚本-(JSValue*)evaluateScript:(NSString*)script;

比如业务中3端(iOS、Android、H5)有一段相当复杂的但原理一样算价逻辑,一般做法是3端用各自语言自己写一套,这样做不但麻烦、效率低而且逻辑不一定统一,同时用OC去实现复杂计算逻辑也没有JS这么灵活高效。

这里就可以利用执行JS代码这个特性,将这个逻辑抽成一个JS方法,只需要传入特定的入参,直接返回价格,这样的话,3端可以同时使用这个逻辑,还可以放到远端进行动态更新维护。

大概这样实现:

//在iOS里面执行JSJSContext*jsContext=[[JSContextalloc]init];[jsContextevaluateScript:@"varnum=500"];[jsContextevaluateScript:@"varcomputePrice=function(value){returnvalue*2}"];JSValue*value=[jsContextevaluateScript:@"computePrice(num)"];intintVal=[valuetoInt32];NSLog(@"计算结果为%d",intVal);

运算结果为:

2018-03-1620:20:28.006282+0800JSCoreDemo[4858:196086]========在iOS里面执行JS代码========2018-03-1620:20:28.006517+0800JSCoreDemo[4858:196086]计算结果为1000

我认为还可以在正则校验、动画函数、3D渲染建模等这些数据计算方面来使用它。

在iOS里面调用JS中方法

说完在iOS中执行JS代码,接下来给大家介绍下,如何在iOS中调用H5中的JS方法。

比如我们H5中有一个全局方法叫做nativeCallJS,我们可以通过执行环境的上下文jsContext[@"nativeCallJS"]获取该方法并进行调用,类似这样:

//Html中有一个JS全局方法[xss_clean]varnativeCallJS=function(parameter){alert(parameter);};[xss_clean]//在iOS运行JS方法JSContext*jsContext=[webViewvalueForKeyPath:@“documentView.webView.mainFrame.javaScriptContext”];JSValue*jsMethod=jsContext[@"nativeCallJS"];jsMethodcallWithArguments:@[@"HelloJS,IamiOS"]];

最终我们的运行结果就可以看到Native执行到了H5的Alter弹层:

iOS 中的 JS

利用这个特性我们可以让iOS获取到一些H5的信息来处理一些他想处理的东西,譬如先将信息在全局中暴露出来,通过调用方法获取到使用的版本号、运行的环境信息、端主动处理逻辑(清除缓存、控制运行)等这些事情。

在JS里面调用iOS中方法

其实对于前端同学使用得最多的应该是这个,通过JS调用端上能力来弥补H5上的不足。

这里需要和@"documentView.webView.mainFrame.javaScriptContext"这个webview相关特性结合起来,将H5调用的方法用Block以jsCallNative(调用方法名)为名传递给JSCore上下文。

比如我们H5中有一个按钮的点击回调是去调用客户端的一个方法,并在方法中输出传入参数,大致是这样实现:

//Html中按钮点击调用一个OC方法//Block以”jsCallNative"为名传递给JavaScript上下文JSContext*jsContext=[webViewvalueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];jsContext[@"jsCallNative"]=^(){NSArray*args=[JSContextcurrentArguments];for(JSValue*objinargs){NSLog(@"%@",obj);}};

最终输出是这样:

2018-03-1620:51:25.590749+0800JSCoreDemo[4970:219245]========在JS里面调用iOS中方法========2018-03-1620:51:25.591155+0800JSCoreDemo[4970:219245]HelloiOS2018-03-1620:51:25.591370+0800JSCoreDemo[4970:219245]IamJS

这个特性真正让H5可以享受到很多端上的特性,比如Native方式的跳转、Native底层能力(震动、录音、拍照)、扫码、获取设备信息、分享、设置导航栏、调用Native封装组件等这些功能,此处大家可以联系Hybrid开发模式。

通过JSExport暴露iOS方法属性给JS

这个特性可能H5的同学不是很清楚,但是对于Native同学,我认为非常有用。

通过JSExport可以很方便地将iOS对象的属性方法暴露给JS环境,让其使用起来像JS对象一样方便。

比如我们OC中有一个Person的类,包含两个属性和一个方法,此处通过让fullName方法使用JSExport协议暴露出去,这样在JS中是可以直接去调用的。

@protocolPersonProtocol-(NSString*)fullName;@end@interfacePerson:NSObject@property(nonatomic,copy)NSString*firstName;@property(nonatomic,copy)NSString*lastName;@end@implementationPerson@synthesizefirstName,lastName;(NSString*)fullName{return[NSStringstringWithFormat:@"%@%@",self.firstName,self.lastName];}@end//通过JSExport暴露iOS方法属性给JSPerson*person=[[Personalloc]init];jsContext[@"p"]=person;person.firstName=@"Fei";person.lastName=@"Zhu";NSLog(@"========通过JSExport暴露iOS方法属性给JS========");[jsContextevaluateScript:@"log(p.fullName());"];[jsContextevaluateScript:@"log(p.firstName);"];

最终运行结果为:

2018-03-1620:51:17.437688+0800JSCoreDemo[4970:219193]========通过JSExport暴露iOS方法属性给JS========2018-03-1620:51:17.438100+0800JSCoreDemo[4970:219193]FeiZhu2018-03-1620:51:17.438388+0800JSCoreDemo[4970:219193]undefined

为什么p.firstName运行后是undefined呢?因为在这里没有将其暴露到Native环境中,所以就获取不到了。

这里我们可以利用的更多的是在编程便捷性上面,让OC和JS直接可以相互调用。

在iOS里面处理JS异常

稍微成熟一点的公司的前端页面都会有运行异常监控系统,发现JS执行异常可以直接通知开发以防止线上事故的发生。

通过JSCore中的exceptionHandler可以很好的解决这个问题,当JS运行异常时候,会回调JSContext的exceptionHandler中设置的Block,这样我们可以在Block回调里面将我们的错误上传到监控平台。

比如这个例子,我运行一个返回a+1的函数,平时我们在Chromeconsole可以看到报错Can'tfindvariable:a,这里运行也会一样:

//当JavaScript运行时出现异常//会回调JSContext的exceptionHandler中设置的BlockJSContext*jsContext=[[JSContextalloc]init];jsContext.exceptionHandler=^(JSContext*context,JSValue*exception){NSLog(@"JSError:%@",exception);};[jsContextevaluateScript:@"(functionerrTest(){returna+1;})();"];

最后输出报错为:

2018-03-1711:28:07.248778+0800JSCoreDemo[15007:632219]========在iOS里面处理JS异常========2018-03-1711:28:07.252255+0800JSCoreDemo[15007:632219]JSError:ReferenceError:Can'tfindvariable:a

JS和端相互通信

最近给Weex提交了一个《Moreenhancedaboutcomponent》的PR,大概就是利用上述思路,通过实现W3C的MessageEvent规范来让组件和Weex之间可以进行互相通信,同时通过loadHTMLString直接来渲染传入的html源码功能。

具体实现为:

iOS 中的 JS

具体思路和Demo可见[WEEX-233][iOS]

JSPatch

让我们更深一步来思考上述思路可否再次进行扩展,能否通过JS直接来干预iOS代码的运行呢?答案是可以的,下面我想整理一下我对JSPatch的理解。

假如iOS想不发版改bug

假如线上APP有一段代码出现bug导致crash,可能Nativecrash会比H5问题严重很多,前者可以立马发布,后者可能需要修改好提交Apple商店数日才上线,然后可能更新率还上不去,很麻烦的。

这里就可以通过JSPatch这种类似的方案,下发一段代码覆盖掉原来有问题的方法,这样可以很快修复这个bug。

可以通过一个简单的例子来看上述过程。

用OC写了一个蓝色的HelloWorld,我们可以通过下发一段JS代码将原来蓝色的字体修改成红色并修改文字,将JS代码下发代码删除后,又可以恢复原来的蓝色HelloWorld

iOS 中的 JS

主要代码大致如下:

//一段显示蓝色HelloWorld的OC代码@implementationViewController-(void)viewDidLoad{[superviewDidLoad];[selfsimpleTest];}-(void)simpleTest{self.label.text=@"HelloWorld";self.label.textColor=[UIColorblueColor];}@end//一段符合JSPatch规则的JS覆盖代码require('UIColor');defineClass('ViewController',{simpleTest:function(){self.label().setText("你的蓝色HelloWorld被我改成红色了");varred=UIColor.redColor();self.label().setTextColor(red);},})

这里是如何做到的呢?首先需要介绍下JSPatch:

JSPatch是一个iOS动态更新框架,通过引入JSCore,就可以使用JS调用任何原生接口,可以为项目动态更新模块、替换原生代码动态修复Bug。

也即JS传递字符串给OC,OC通过Runtime接口调用和替换OC方法。

为什么可以通过JS调用任何原生接口呢?首先可以了解下OC底层Runtime的原理。

Runtime

OC语言中大概95%都是C相关的写法,为何当时苹果不直接使用C来写iOS呢?其中一个很大的原因就是OC的动态性,有一个很强大的Runtime(一套C语言的API,底层基于它来实现),核心是消息分发,Runtime会根据消息接收者是否能响应该消息而做出不同的反应。

也许上述会比较生涩,简单说就是OC方法的实现和调用指针的关系是在运行时才决定的,而非编译期,这样的话,我们可以在运行期做些事情更改原来的实现,达到热修复的目的。

OC方法的调用不像JS这种语言,直接array.push(foo)函数调用即可,他是通过消息机制来进行调用的,比如如下这个将foo插入到数组中的第5位:

[arrayinsertObject:fooatIndex:5];

在底层比这个实现更加生涩,他通过objc_msgSend这个方法将消息搭配选择器进行发送出去:

objc_msgSend(array,@selector(insertObject:atIndex:),foo,5);

运行时发消息给对象,消息是如何映射到方法的?

简单来说就是,一个对象的class保存了方法列表,是一个字典,key为selectors,IMPs为value,一个IMP是指向方法在内存中的实现,selector和IMP之间的关系是在运行时才决定的,非编译时。

-(id)doSomethingWithInt:(int)aInt{}iddoSomethingWithInt(idself,SEL_cmd,intaInt){}

通过看了下Runtime的源码,发现有如下这些常用的方法:

iOS 中的 JS

通过上述这些方法就可以做很多意想不到的事情,比如动态的变量控制、动态给一个对象增加方法、可以把消息转发给想要的对象、甚至可以动态交换两个方法的实现。

JSPatch&&Runtime

正是由于OC语言的动态性,上所有方法的调用/类的生成都通过OCRuntime在运行时进行,可通过类名称和方法名的字符串获取该类和该方法,并实例化和调用:

Classclass=NSClassFromString("UIViewController");idviewController=[[classalloc]init];SELselector=NSSelectorFromString("viewDidLoad");[viewControllerperformSelector:selector];

也可以替换某个类的方法为新的实现:

staticvoidnewViewDidLoad(idslf,SELsel){}class_replaceMethod(class,selector,newViewDidLoad,@"");

还可以新注册一个类,为类添加方法:

Classcls=objc_allocateClassPair(superCls,"JPObject",0);objc_registerClassPair(cls);class_addMethod(cls,selector,implement,typedesc);

JSPatch正是利用如上这些好的特性来实现他的热修复功能。

JSPatch中JS如何调用OC

此处JSPatch中的JS是如何和任意修改OC代码联系起来的呢?大概原理如下:

1.JSPatch在实现中是通过Require调用,在JS全局作用域上创建一个同名变量,变量指向一个对象,对象属性__clsName保存类名,同时表明这个对象是一个OCClass,通过调用require(“UIView"),我们就可以使用UIView去调用他上面对应方法了。

UIView=require(“UIView");var_require=function(clsName){if(!global[clsName]){global[clsName]={__clsName:clsName}}returnglobal[clsName]}

2.在JSCore执行前,OC方法的调用均通过新增Object(JS)原型方法__c(methodName)完成调用,假如直接调用的话,需要JS遍历当前类的所有方法,还要循环找父类的方法直到顶层,无疑是很耗时的,通过__c()元函数的唯一性,可以每次调用它时候,转发给一个指定函数去执行,就很优雅了。

iOS 中的 JS

Object.prototype.__c=function(methodName){returnfunction(){varargs=Array.prototype.slice.call(arguments)return_methodFunc(self.__obj,self.__clsName,methodName,args,self.__isSuper)}}

3.处理好JS接口问题后,接下来只需要借助前面JSCore的知识就可以做到JS和OC之间的消息互传了,也即在在_c函数中通过JSContex建立的桥接函数,用Runtime接口调用相应方法完成调用:

JS中的转发

var_methodFunc=function(instance,clsName,methodName,args,isSuper){....varret=_OC_callC(clsName,selectorName,args)return_formatOCToJS(ret)}OC中的处理

context[@"_OC_callC"]=^id(NSString*className,NSString*selectorName,JSValue*arguments){returncallSelector(className,selectorName,arguments,nil,NO);};

苹果电脑js技术怎么样(iOS 中的 JS)

感触

上面大概就是如何通过JS任意修改OC运行结果的一个原理,虽然JSPatch大部分功能被苹果禁用了,但是其中JS操作OC的思路真的很棒。

原文地址:https://zhuanlan.zhihu.com/p/34646281

相关新闻

返回顶部