上文中,中间部分是 WebKit,其他部分由浏览器实现,而 WebKit 中主要分为四大部分:
1. WebKit Embedding Api提供和浏览器进行交互的一些接口,比如前进后腿、新开窗口、新开页面、关闭页面等;
2. WebKit Ports(Platform Api)跨平台 Api,感觉应该是 WebKit 暴露一些没有实现接口,供不同的平台去实现,最后完成统一的功能,如:渲染、SSL 认证、音视频等;
最典型的比如在 iOS 的 WKWebView 上,使用 CoreGraphic 来处理相关的渲染逻辑,而在 Android 上则使用 Skia 来处理渲染逻辑;
3. WebCore该部分的功能是解析 HTML 生成 DOM 和 CSSOM ,合并之后生成渲染树最终生成纹理(Graphic Context):
渲染流程在以 WebKit 为基础的浏览器中,几乎所有的浏览器都没有重写 WebCore,而是直接使用的 WebKit 源码中的 WebCore 来完成 HTML 的渲染。不同浏览器之间的主要区别则在于 JSCore 的实现;
4. JSCore早期因为只有 HTML 和浏览器两个角色。浏览器向后台发送表单时,即使是一个必选框没有填写,浏览器也无法获取 HTML/DOM,所以只能直接提交给服务端。因为当时网速慢,耗时很长之后让用户重新填写表单的体验很差,所以亟需一个角色来 *** 作 HTML 进行验证;
浏览器 Javascript 语言的诞生的目的是为浏览器和 HTML 进行交互提供一个桥梁。而 Javascript 的环境提供、解析、运行、runtime 管理都由 JSCore 负责;
JSCore 的角色类似于编译器和虚拟机的结合;
JVM 虚拟机不承担编译的工作,编译器将 .java 文件编译生成字节码文件 .class,而 JVM 则直接解析字节码文件,根据不同的设备特性生成对应的二进制码,并且在整个 runtime 过程中管理内存和 GC *** 作,JVM 的工作内容:
将字节码转化成二进制码(机器码);执行并管理 Runtime;而 OC 中通过编译器生成 Mach-O 文件之后,当程序被执行时,二进制代码直接被加载到虚拟内存并印射到物理内存中进行执行;
JSCore 则承担了编译器和虚拟机的角色。Javascript 首先被 JSCore 转化成字节码最终转化成二进制代码并执行,同时在 runtime 中管理内存和 GC,即 JSCore 的工作内容:
将 JS 代码编译成字节码;将字节码转化成二进制码(机器码);执行并管理 Runtime; JSCore解析流程 二、JSCore 的特性 1. 基于寄存器的指令集结构代码执行效率更高,但是占用内存更大;
2. 单线程一个 JS 虚拟机只能单线程执行代码,如果要开启多线程,则只能开启多个 JS 虚拟机,比如开启两个浏览器窗口;
3. 事件响应机制JS 虽然只支持单线程,但却可以异步执行。 JS 通过事件响应机制,将耗时或者异步 *** 作丢到 WorkThread 中执行,执行完毕获取回调事件之后继续在 JS 线程执行相关代码:
事件响应机制JS 的事件响应机制机制和主流的事件响应机制(RunLoop)并无太大差别,不再赘述;
三、JSCore 在 iOS 上的使用 1. JSVirtualMachine表示一个 JS 虚拟机。
其实并没有 JS 虚拟机的概念,这个概念更像是一个封装之后的概念,封装的内容包括:JS 运行环境、JS 的解析功能、JS 的 Runtime 管理等;
一个 JSVirtualMachine 对象内部只能单线程执行,如果需要多线程执行 JS 代码,则需要开启多个 JSVirtualMachine,但是两个 JSVirtualMachine 之间的资源不共享;
2. JSContextJSContext 可以理解成窗口,或者可以等加成 window 对象,而 JSContext 中的 globalObject 就是这个 window 对象转化之后的 Native 端表示:
context1[@"xklog"] = testBlock;
NSLog(@"%@",[context1.globalObject toObject]);
上述代码打印结果:
2021-12-10 17:02:21.336441+0800 UPHybridDemo[1982:82899] {
xklog = "<__NSGlobalBlock__: 0x10f3edbe8>";
}
因为使用的是 JSCore 框架,而不是在浏览器中使用,所以 window 对象并没有宿主所提供的接口和功能,所以当前的 context 只有一个自定义的属性;
这里可以在 JS 端也打印看下:
image.png3. JSValue需要注意的是,在WKWebView 中只能通过 userContentController 来添加原生方法,且不是直接添加到 window 上的;如果想要进一步验证,感觉可以使用 UIWebView 获取 Context 来试一试;
略;
4. JSExport遵循 JSExport 协议的 OBJC 对象可以印射到 JS 中。
JS 继承的本质是在构造函数中重写原型链,并将子类的属性添加到这个原型链中:
function mammal (){}
mammal.prototype.commonness = function(){
alert('哺乳动物都用肺呼吸');
};
function Person() {}
Person.prototype = new mammal();//原型链的生成,Person的实例也可以访问commonness属性了
Person.prototype.name = 'tony stark';
Person.prototype.age = 48;
Person.prototype.job = 'Iron Man';
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
person1.commonness(); // d出'哺乳动物都用肺呼吸'
person1.sayName(); // 'tony stark'
所以,遵循 JSExport 协议的对象会根据继承关系生成对应的原型链;
四、Hybrid交互 1. Native 调用 JS 时的作用域问题虽然对象或者类可以在 Native 和 JS 之间进行转换,但是向 JS 中插入原生调用并不是将原生代码转化成了 JS 代码,而只是做了一个类似于打上一个标记之类的 *** 作:
image.png从上文第 2 点的 JSContext 全局对象打印结果也可以看出,Native 的回调在 JS 端保留的只是一个 Block 地址,本质上是一个函数地址,指向代码所在的位置。
这里的 [native code] 并不是指原生代码,而是函数中的 JS 代码过多或者不容易展示,直接使用 [native code] 表示,native 表示 JS;
所以,JS 里面虽然在 window 中添加了全局对象,但是 Native 的代码不会被转化成 JS 代码,只是一个指向。真正执行时,通过这个指向去寻找 Natie Code 的地址并执行。如果是 WKWebView,则还涉及到进程间的通信:
image.png虽然 Native 代码不能被转成 JS 代码,但是 Native 可以通过 JS 注入将字符串转成对应的 JS 代码:
[self.webview evaluateJavaScript:@"(function (){var a = 10;var b = 20; return a+b;})();" completionHandler:^(NSNumber _Nullable result, NSError * _Nullable error) {
NSLog(@"%@",result);
}];
即:
Native 可以为 JS 环境添加代码,但是 JS 只能引用(指向) Native 环境中的代码;所以,在设计 JS 调用原生方法之后的回调时,可能会考虑到直接将 JS 中的回调转化成字符串,JS 调用原生完成后,原生直接使用 evaluateJavaScript
来调用对应的 callBack 方法;
第一种,直接使用 webkit 自带的 postMessage 来完成转换:
function func01(){
const person = {
firstName: "John",
lastName: "Doe",
age: 50,
eyeColor: "blue",
callBack: function () {
console.log("native call");
},
};
window.webkit.messageHandlers.nativeMethod.postMessage(person);
}
这种方式会报错:
克隆算法其原因是因为 postMessage 传递的参数会被自动转换成 Native 类型,但是这个转换不支持 function 和 Error :
结构化了龙算法所以只能使用字符串:
image.png此时使用 Native 进行调用时,就会出现作用域改变的问题:
image.png总结:
Native 注入 JS 并调用,其作用域永远是在全局,即和 Window 统一作用域。可以添加
javascript:
前缀,其意义是以一个 script 来执行 JS,但是其作用域仍然是 window;
所以如果是为了实现 JS 回调,不能这么做。因为如果 function 内部访问了 function 外部变量时,Native 注入 JS 并调用会改变 function 的作用域链,导致变量无法访问;
6. JS 中的作用域链作用域链寻找变量的本质是:
如果没有声明变量,则优先到创建 fn 函数的那个作用域中取,无论 fn 函数将在哪里调用;例子:
var x = 10;
function fn() {
console.log(x);
}
function show(f) {
var x = 20;
(function() {
f(); //10,而不是20
})();
}
show(fn);
再看一个例子:
var a = 10;
function fn() {
var b = 20;
function bar() {
console.log(a + b); //30
}
return bar;
}
var x = fn(),
b = 200;
x(); //bar()
2. callback 的设计原则
如上文所讲,如果将 function 字符串化之后传给 Native ,Native 通过注入 JS 调用,最终相当于在全局作用域下声明了一个 function 并执行,function 及其内部变量的作用域发生变化。如果 function 只是访问入参那还好,如果访问 function 外的上级作用域,那么此时该变量就会找不到,因为回去全局搜索变量;
所以,回调设计的原则:
Native 负责透传 callback 信息给 H5,H5 负责调用 callback 函数;针对这个原则,有两种方案:
函数名;callbackId;第一种方案比较脑残:
Native 中直接拼出这样的字符串:'func(param1,param2)' 丢到 H5; 感觉这就是个脑残代码,而且参数你怎么传?
第二种方案:
H5 端 Map 方式存储 callbackId : callBackFunc ;JS 端需要为原生提供一个回调相关的函数;H5 调用原生时将 CallBackId 一起给到 Native ,Native 调用完毕调用 JS 中的回调函数,并将 CallBackId 传回给 H5;H5 通过 callbackId 获取 CallBackFunc 进行调用;这样不会改变函数的作用域和作用域链;
3. WebViewJavaScriptBridge 原理WebViewJavaScriptBridge 的本质是使用 iframe 标签触发 WKWebView 的拦截请求,所以也适用于单页面(SAP):
image.png image.png 4. WKWebView 的封装的思考WKWebView 中的 UserContentController 本质上是 Apple 对 JSCore 的封装:
image.png只不过,uerContentController 通过 [userCC addScriptMessageHandler:self name:@"nativeMehtod"];
被添加到了 window.webkit.messageHandlers
这个对象中:
早期的 UIWebView 可以通过注入 JS 的方式获取到 window 对象,而 window 对象几乎就等于 JSCore 框架中的 JSContext 对象。
WKWebView 封装的目的是为了限制客户端在 WebView 上对 JSCore 的使用,所以在 WKWebView 上并不提供获取 JSContext 的接口。无法获取到 JSContext 对象也就无法获取到 JSVituralMachine 对象,也就无法无法改变 WKWebView 内部的 JS 环境。
另外一个原因可能是 WKWebView 涉及到多进程问题。直接使用 JSCore 框架时,JSVirtualMachine 和 JSContext 和 WorkThread 都是出于同一进程。而 WKWebView 采用多进程的方式实现,所以提供另外一个进程的 JSContext 并支持修改,这可能会导致代码过度复杂虎或不可控;
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)