maq
maq
  • 发布:2015-12-11 12:32
  • 更新:2015-12-16 10:50
  • 阅读:11228

多个 webview 里的 js 是多线程还是单线程执行的?【已解决】

分类:HTML5+

每个 webview 里面的 js 都是各自独立的上下文,那么从系统级来看,它们是在多线程分别执行的还是在一个单线程里调度执行的呢?

我观察到的现象是,一个 webview 调用 evalJS() 在另一个 webview 里执行 js,evalJS() 返回后马上调用 alert() 显示弹出框,结果好像另一个 webview 里面的 js 是在 alert() 对话框关掉后才开始执行的。这似乎说明所有 webview 里面的 js 都是在同一个线程里调度执行的。是这样吗?

2015-12-11 12:32 负责人:无 分享
已邀请:
maq

maq (作者)

到目前为止,我认为原帖的问题已经得到了比较确切的结论。帖子较长,为了便于后来者阅读,我这里简单总结一下。

结论:如果 app 中创建了多个 webview,那么所有 webview 中的 js 都是由同一个线程驱动执行的。

推论:如果其中一个 js 执行了具有阻塞能力的代码,那么其它所有 js 都将被阻塞而无法执行,直到阻塞解除。

潜在危害:如果 js 代码的执行过程被阻塞,可能产生哪些危害?我想到这些,可能会更多——

  • 用 js 实现的 UI 动画可能会卡顿
  • setInterval() 定时不准
  • 异步通信(比如 AJAX、websocket)无法及时处理响应结果

需留意的代码:哪些是具有阻塞能力的代码?我想到这些,可能会更多——

  • alert()
  • XMLHttpRequest 设置为 sync 方式时

HIH :)

chender

chender - 与人为善

对于同一个webview,js是单线程了,不同webview之间,js是多线程的;
你说的这个现象是由两个原因造成的
1、evalJS是异步的,所以a页面先alert
2、alert对于浏览器(多个webview)来说,是单线程的,且是阻塞的,所以a页面alert后,b页面的js逻辑也没法继续执行了

maq

maq (作者)

@chender

第一个原因,没错,是这样的。

第二个原因,似乎这正说明多个 webview 里的 js 是在同一个线程里跑的呀,否则 alert() 只能阻塞自己这个 webview 里面的 js,怎么会影响到另一个 webview 里面 js 的运行呢?

  • chender

    你看一下pc浏览器,一个选项卡alert后,整个浏览器就被阻塞住了,alert会锁住整个浏览器的进程;如果选项卡之间是单线程的话,浏览器就没办法同时加载多个选项卡中的页面了

    2015-12-11 13:05

maq

maq (作者)

回复 chender:

你看一下pc浏览器,一个选项卡alert后,整个浏览器就被阻塞住了,alert会锁住整个浏览器的进程;如果选项卡之间是单线程的话,浏览器就没办法同时加载多个选项卡中的页面了

PC 浏览器里 alert 看上去会阻塞整个浏览器,但实际上仅仅因为那是一个“模式对话框”,导致界面上无法点击切换到另一个页签窗口而已,其实其它页签里的 js 都是照样在跑的,只有本页面中的 js 会被阻塞。

cnt = 0;  
setInterval(function() {  
    console.log(cnt ++);  
}, 1000);

我在 Chrome 浏览器两个页签里分别执行了上面的代码,然后在其中一个页签里执行 alert(),测试结果跟我前面的说法一致。

  • chender

    我怎么刚才试了一下,一个页面alert,其他页面就没打日志了。。。。

    2015-12-11 14:31

  • maq (作者)

    你是在 PC 上测试的么?是 Chrome 浏览器么?

    2015-12-11 17:34

maq

maq (作者)

而且,事实上,Chrome 浏览器的多个页签之间甚至不是“多线程”的,其实它们是“多进程”的,这一点从 Windows 的任务管理器能看出来。 ^_^

  • chender

    多个web页面在内存上是隔离开的,所以显示出来是多个进程,但是应该没办法从任务管理器里指定关掉某一个选项卡

    2015-12-11 14:39

  • maq (作者)

    从任务管理里面是不能“关掉”一个选项卡,但你可以杀掉这个进程,界面上看到的是对应的页面显示“喔唷,崩溃啦!”。只有这一个页面会受影响,所以……

    2015-12-11 17:33

chender

chender - 与人为善

并且如果你正在加载一个页面的时候,你在另外一个页面alert,那么之前那个页面在alert被点掉之前就会一直加载,应该是被阻塞了

  • maq (作者)

    好像你描述的这个场景不太容易准确重现,而且仅从界面上看到的现象未必是进程/线程被阻塞,可能会有其它原因。先不要管 PC 上到底会不会阻塞了,这个歪楼有点远了。还是回到原始问题吧,我现在最关心的是 plus 里面多个 webview 的 js 到底是单线程还是多线程 :)

    2015-12-11 17:39

maq

maq (作者)

回复 chender:

好像你描述的这个场景不太容易准确重现,而且仅从界面上看到的现象未必是进程/线程被阻塞,可能会有其它原因。先不要管 PC 上到底会不会阻塞了,这个歪楼有点远了。还是回到原始问题吧,我现在最关心的是 plus 里面多个 webview 的 js 到底是单线程还是多线程 :)

maq

maq (作者)

我的 app 用预加载的方式打开了很多个 webview(这里有滥用的嫌疑,以后会考虑优化一些),里面各自都会有一些 js 在跑。如果所有 webview 里面的 js 都是在同一个线程中执行的,那么就会有一些问题必须要考虑了。

首先就是多核的性能无法发挥出来了,但这可能并不重要,毕竟那些 js 加在一起也未必能把一个核跑满。一个严重的问题是,有些处于【后台】的 js 是不能被阻塞的,比如通过 websocket 与服务器进行实时通信。如果某个页面不小心用个 alert() 什么的,麻烦就大了。

maq

maq (作者)

回复 DCloud_heavensoft:
@DCloud_IOS_XTY:
@DCloud_App_Array:
哪位大侠能出手,答疑解惑!

chender

chender - 与人为善

好吧,感觉楼主对单线程模型这个概念不是很清楚。
首先要明白单线程只是个相对的概念,只能说xxx对于xxx是单线程的,比如java是支持多线程的吧,但是swt、android里面对界面元素的操作都是单线程的(只能在主线程/ui线程里对界面元素进行,和界面元素不相关的操作可以随意启线程),相信其他语言在UI方面应该也是同样的机制;
js本身是没有单线程之说,只是浏览器js引擎通过单线程的方式去运行它罢了,现在很多浏览器都支持WebWorkers了,可以开后台线程处理任务了,当然这和我前的面描述并不冲突,当Webworkers需要访问界面的资源时(dom操作),也会变成单线程;
这样做可以让开发者不用去考虑资源(界面元素)的线程安全问题,编码起来会简单N倍;
还有就是"单线程不等于慢,多线程不等于快",这个要看应用场景,在处理UI的时候一般都是用EventLoop的方式,一个线程负责操作UI,一个线程负责UI线程和其他进程之间的通信;比如在浏览器中,js所在的线程是UI线程;和后台通信、以及界面渲染的线程位于浏览器中的其他进程中,这样对于js而言,他不用关心(当然也不用等待/阻塞)请求后台花了多少时间、加载这张图片花了多长时间,只专注于和UI相关的(内存级别的)逻辑操作,所以js的单线程并不是性能的瓶颈,它只需要花万分之一毫秒来执行xx.style.display="none"这句赋值的代码,但是浏览器的其他进程却需要花几毫秒甚至几十毫秒来处理相应的界面渲染动作,瓶颈在哪里,一目了然;
如果楼主有接触过NodeJs,应该就会有所感触;他是单线程的,但是可以在耗费极少资源的情况下处理很大流量的访问;
至于楼主所说的那个问题,要是某个webview中alert了一下,其他webview中的js代码肯定会阻塞的,因为alert是浏览器(非选项卡)的界面资源,弹出来后浏览器就被锁住了,这里的锁住应该不是楼主所倾向的那种“加蒙版,不让点”;
然后就是不同webveiw中的js是不是单线程的问题,这个你自己也说过,不同的webview处于不同的进程中,去判断他们是不是单线程本身就是不应该的,打个比喻,我给小王打电话,和你给小张打电话是单线的吗?如果我两共用一个电话,那肯定是单线程的;同理,当不同webview中涉及到共用的资源的时候,才会是单线的了;否则的话,就是多线程的;
个人见解,如果不对的地方,见谅;

  • maq (作者)

    主要的内容在回复里说了,这里用评论的方式再聊一些小话题 :)


    多线程的程序在访问共用资源的时候,要通过某种同步技术(信号量、临界区等)实现互斥,从而使得对共用资源的访问串行化。并不是多线程变成了单线程。

    2015-12-14 12:38

  • maq (作者)

    关于“单线程不等于慢,多线程不等于快”的论述,我完全赞同,例子也很生动 :)


    nodejs 是个相当不错的服务器端平台,虽然它是单线程的。它非常胜任 IO 密集型的业务,比如静态文件、数据库查询等。但对于计算密集型的业务就很为难了,比如有一个请求是要计算 PI 小数点后 200 位,在这个请求计算完毕之前,其它请求是无法被响应的。所以现在 nodejs 又发展出来 Cluster 模块,能够创建子进程来负担计算任务。

    2015-12-14 12:45

  • chender

    回复 maq:我明白你的意思,我说的多线程变单线程只是从效果上来来进行描述的,如果要从本质上来描述线程的话,意义不大,因为多线程线程本身就是个抽象的概念,都是通过cpu分时来实现的,要不然我们就只能说四核的系统只支持四个线程了;

    2015-12-14 13:04

maq

maq (作者)

回复 chender:

非常感谢你写了这么多来回复我的帖子,我很仔细地看了你写的内容,可以说,我对多线程的理解跟你基本上是一致的,除了一些具体的措辞。

我说的“不同的webview处于不同的进程中”,是指桌面端 Chrome 浏览器的多个页签(选项卡)之间的关系,并不是指 5+ 环境下的多个 webview,而后者正是我此贴所关注的核心问题所在。

用 HBuilder 开发的混合模式的 app,在 H5 所实现的应用层的下面,是一个支持 5+ Runtime 的基座程序。如果解剖这个基座程序,应该无外乎一个标准的原生 app 框架,比如在 Android 系统上,就应该有 android.app.Activity 的一个实现类。而负责运行这个主界面的,就是你说的“负责操作UI”的线程。那么后续的每个 webview 如何创建,是否由这个“主 UI 线程”统一处理,就是我现在所关注的问题。

alert() 之所以能够“锁住界面”,是因为它是一个模式对话框(Modal Dialog),它自己构建了一个“临时的消息循环”,从而使得主 UI 线程脱离了“主消息循环”,导致 UI 的其它部分无法接受处理。那么,如果那些 webview(以及其中的 js)也都是由同一个主消息循环来处理,那么自然也就失去了执行的机会(表现出来就是被阻塞了),我在原帖中描述的就是这种现象。

顺便再说一个稍微远一点的话题:其实 js 是有“单线程”的说法的,V8 引擎(WebKit 所采用的 js 引擎)就是 Thread-Unsafe 的,所有属于同一个 Isolate 的 js 只能由一个单线程来执行,而多个 Isolate(比如多个 webview 的情况),可以由多线程来分别处理,当然也可以由单线程(比如主 UI 线程)来处理,这个就看产品设计了。桌面版的 Chrome 是用多进程来处理的,那么 HBuilder 基座程序呢?……

chender

chender - 与人为善

这个和hbuilder的基座没关系,dcloud只是把webview包装了一下,底层还是取决于webkit的内核,个人感觉原理和pc中的chrome不会有实质性的差异;
语言本身没有多线程单线程一说,我要是足够厉害,我甚至可以自己写一个js解析器,只支持多线程,单线程跑久了直接死机;语言只是一个描述,就像一句“我爱你”,可以是煽情的表白,也可以是无奈的诀别;
android中webview的创建肯定是主线程来完成的,但是创建完后,webview中的代码逻辑,就和android的那个主线程没有任何关系了,所以 你的这句话我不是很明白:”那么后续的每个 webview 如何创建,是否由这个“主 UI 线程”统一处理,就是我现在所关注的问题。“
还有关于模态对话框的问题,其实就是锁,就是单线程,本质一样,为什么创建了一个临时的”消息队列循环“后,之前的消息队列循环就接收不到消息了?界面的每一个点击事件是一个资源,一个不可共享的资源,给了A队列就没法给B队列;就是这个不可共享的资源,让两个消息队列只能单线程执行,这句话表面上看起来可能和无聊、很显而易见,但其实单线程说到底就是这个意思;
再举个例子,java中的两个线程,同时调用一个带synchronized关键字的方法,那么这段方法的逻辑就是单线程的;所以,单线程只是个相对的概念;
当然你说V8用说js是单线程的,也仅仅是v8用单线程模型去解析js代码而已;
一个webview alert的时候,其他webview的js肯定会被阻塞的;或者说,其他webview若在执行耗时的操作,那当前webview会alert不出来,不信的话你可以试一下

chender

chender - 与人为善

个人觉得世界上没有真正的多线程,举个例子,现在有一个多核的cpu,cpu的运算是需要电(电子)的吧,在某一段,肯定只有一个cpu在进行运算,为什么?因为只要时间间隔足够短,短到只有一个电子(增量)通过电缆,这一个电子能让多块cpu都参与工作吗?哈哈。。这个想法有点天马行空;
多线程应该是用来描述某一个时间段的一种现象,没有瞬时性

maq

maq (作者)

回复 chender:

你一直在强调“单线程只是个相对的概念”,我不确定只是表达上的差异还是概念理解的差异,我认为,线程是个实实在在的概念。从程序员的观点,“线程”是用一个“数据结构(比如在 Windows 中)”或者是一个“对象实例(比如在 Java 中)”来管理的,一个线程就是一个“可执行的指令序列”,多线程就是“可以同时分别执行的多个指令序列”。当多个线程访问一个非线程安全(Thread-Unsafe)的共用资源时,要通过同步技术使访问变成串行化(比如 java 中的 synchronized 关键字就是一种同步技术)。

一个消息驱动的系统(Windows 和 Android 都是),主程序就是一个消息循环从消息队列里取出消息,取出一个执行一个,而对每个消息的处理执行就构成了整个程序的应用逻辑。当执行某一个消息的时候,其执行程序如果自己又构造了一个(临时的)消息循环,那么就只有等这个消息循环退出的时候,程序才能够回到主消息循环里。当你用 C 语言做基于 Windows SDK 的桌面应用的时候,这个消息循环的模型是非常清楚的,典型情况下一个消息循环不超过 10 行代码。而上面说的这一堆,都是在一个线程里执行的。程序可以创建另外一个线程,从而启动了另外一个代码序列的执行,那个线程可以有自己的消息循环(如果它要做 UI 处理的话),也可能只是一段计算代码,计算完成后线程结束。

【那么后续的每个 webview 如何创建,是否由这个“主 UI 线程”统一处理,就是我现在所关注的问题。】
如果 webview 的相关代码都是在主线程中执行的(也就是说,相关的 UI 事件消息都被发送到主消息循环所管理的队列中),那么它们就是单线程的,当其中一个 webview 出现 alert() 的时候,由于进入“临时消息循环”,所有的 webview 都无法接受处理,直到对话框关闭。从主帖所描述的现象看,HBuilder 的 5+ Runtime 很可能属于这种情况。

另一种实现方案,如果创建 webview 对象后,同时创建了一个新的线程,在一个新的消息循环中处理 webview 的相关功能(包括 UI 事件和 js 的执行),那么这些 webview 互相之间就没有什么关系了,一个 webview 里面的 alert() 也不会阻碍另外 webview 里面 js 的执行。

wenju

wenju - https://www.mescroll.com -- 精致的下拉刷新和上拉加载组件

高...高手...之间的对决 我等农民默默地膜拜`

chender

chender - 与人为善

我知道线程是实际”存在“的,他之所以存在是编程语言把他抽象了出来,并用类似Thread这样的对象去描述他;
比如在单核的操作系统(cpu)中,你用java程序开启了十个线程去处理任务,对于你的程序而言,你是多线程的,所以你可以说,我在只支持单线程的操作系统中运行了一个支持多线程的程序,这句话没有错,因为线程是相对的,上述两个线程的意思不尽相同;至于多核操作系统,我前面有打一个不是很恰当的比喻,说明多核操作系统的并行处理也是相对的;
另外,android中创建一个webview和创建一个普通的group是一样的,创建的时候是需要使用用到UI线程的,创建完毕后,webview中的事件队列,和andorid中的事件队列完全是两码事;因为webview只是一个引用,实际的页面(包括里面的事件队列)都是由系统中的浏览器内核去完成的,这也就是为什么webview alert的时候会阻塞住其他的webview中的js执行,而不会阻塞住android中其他界面中代码的执行;
所以你上面说的那两种可能的情况,其实都是不可能的;
说了这么多,你可以写测试代码试一下,alert肯定会阻塞其他webview中的js代码的;
至于各个webview中的普通代码是不是多线程的,这个很难考证,即使是单线程的也无所谓,反正js本身不可能有耗时的操作,说白了他就只能做比较和赋值,没有IO操作,至于界面的渲染和前后台IO,各个webview之间肯定是支持多线程的(当然浏览器内核本身肯定有一定的阀值,支持的线程数应该不会太大);

  • maq (作者)

    【只支持单线程的操作系统中运行了一个支持多线程的程序】这句话还真是不对的。一个单核的系统并不是一个“只支持单线程的系统”。“只支持单线程的系统”现在我只能想到 DOS,而那里真是无法运行多线程程序的。

    2015-12-14 17:52

  • maq (作者)

    【多核操作系统的并行处理也是相对的】,还真不是相对的,真的是并行的,在同一个 CPU 时钟周期内,两个核同时在执行自己的指令。即便你把颗粒度细分到“电子”这个层级,两个 CPU 内核也是各自有自己的电子在流动 ^_^

    2015-12-14 17:56

  • chender

    回复 maq:cpu是单线程的,他怎么支持多线程,除非你承认cpu的单线程是相对的,通过分时,可以变成多线程

    2015-12-14 18:03

  • chender

    回复 maq:只有一个电子的增量,两个核怎么会有各自的电子,当然你可以说是之前的电子,ok,那我重复之前的逻辑,每次都只进来一个电子,两个核还会一直都”有各自的电子在流动吗“

    2015-12-14 18:07

  • maq (作者)

    多核跟多线程是两个不同的概念。


    单核系统可以跑多线程,当然只能通过 CPU 分时。


    多核系统可以跑多线程,这里既有多核并行执行,也有 CPU 分时。


    多核系统也可以跑单线程,这个需要编译器支持并行计算(可以参考 OpenMP)。

    2015-12-14 18:30

chender

chender - 与人为善

5+runtime没有/很难在这方面做什么控制(甚至说几乎是透明的),所以你把5+runtime考虑进来的话,出发点错了

maq

maq (作者)

【你可以写测试代码试一下,alert肯定会阻塞其他webview中的js代码的】,正如我原帖中所描述的现象,在 5+ 环境中“好像”确实是这样的,所以我才发帖求证原因是否在于“所有 webview 都运行在一个线程里”。

如果答案是肯定的,那么这种阻塞就无法避免,只能写程序的时候注意不要使用 alert() 这种具有阻塞能力的语句;

如果答案是否定的,那么,找到“阻塞”的真正原因,也许就是可以避免的。

  • chender

    alert阻塞住其他webview中的js代码,不是因为js在不同webview中是单线程的,而是因为alert这个动作是单线程的;

    至于阻塞的真正原因,和5+,android都没任何关系,这涉及到浏览器的相关机制,目测是因为alert是一把锁,至少会阻塞住所有webview的ui线程(这个我有验证),所以想从上游进行避免是不可能的;

    2015-12-14 18:11

maq

maq (作者)

貌似我已经得到结论了:所有 webview 都是在同一个线程中执行的

下面的程序在 Android 模拟器中运行,在每个 webview 中都通过 Native.js 调用 java 接口检查 thread id,答案就出来了。

index.html

<!DOCTYPE html>  
<html>  
<head>  
<meta charset="utf-8">  
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />  
<title>main webview</title>  
<script src="js/mui.js"></script>  
<script type="text/javascript">  
mui.init({  
    subpages: [{  
        url: 'sub-webview.html',  
        id: 'sub-webview-1',  
        styles: {  
            top: '33%',  
            bottom: '33%'  
        }  
    }, {  
        url: 'sub-webview.html',  
        id: 'sub-webview-2',  
        styles: {  
            top: '66%',  
            bottom: 0  
        }  
    }]  
});  
mui.plusReady(function() {  
    var vid = plus.webview.currentWebview().id;  
    mui('#my_vid')[0].innerText = vid;  

    var Thread = plus.android.importClass('java.lang.Thread');  
    var tid = Thread.currentThread().getId();  
    mui('#my_tid')[0].innerText = tid;  
});  
</script>  
</head>  
<body>  
I'm '<span id="my_vid">-</span>'.  
<br>  
my thread id is '<span id="my_tid">-</span>'.  
</body>  
</html>

sub-webview.html

<!DOCTYPE html>  
<html>  
<head>  
<meta charset="utf-8">  
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />  
<title>sub webview</title>  
<script src="js/mui.js"></script>  
<script type="text/javascript">  
mui.init();  
mui.plusReady(function() {  
    var vid = plus.webview.currentWebview().id;  
    mui('#my_vid')[0].innerText = vid;  

    var Thread = plus.android.importClass('java.lang.Thread');  
    var tid = Thread.currentThread().getId();  
    mui('#my_tid')[0].innerText = tid;  
});  
</script>  
</head>  
<body>  
I'm '<span id="my_vid">-</span>'.  
<br>  
my thread id is '<span id="my_tid">-</span>'.  
</body>  
</html>

运行结果截屏

chender

chender - 与人为善

你这样验证不行,虽然答案有可能是正确的,因为你的几个webview的加载是有先后顺序的,所以获取线程Id的方法在执行上是有时间差的,即使没有时间差,也有可能是nativeJS的那套框架上存在锁,导致了只能单线程,因此每次id都是一样的;
我拿数据库连接池打个比喻,前端三台电脑都发请求到后台查询数据,在查询的时候我把数据库连接的id打印出来了,然后发现三次打印的id是一样的,难道这三台电脑也是同一个线程。。。其实这并不能说明什么问题,因为有很多原因会导致这样的现象,比如,三次请求有时间先后顺序,所以在获取连接的时候并处存在并发,于是每次都是取得的第一个连接,或者,三个请求虽然是(绝对的)同一时刻开始获取连接,但是由于连接池里只有一个可用的连接,所以这三个请求不得不依次排队获取这个连接,连接是同一个,id当然相同;
应为webview里的js代码的执行机制已经和5+,甚至应用本身没有直接关系,所以验证这个问题的时候,一定不要引入这两者,要让验证环境尽量干净;
你可以打开两个webview,在一个webview中调用一个同步的ajax请求,然后后台debug住,这个时候这个webview里的代码就阻塞住了(等同于一直在执行);
然后在另外一个webview中你每隔一段时间打印一下日志,看下日志会不会持续打印即可;

maq

maq (作者)

你说的有一定道理。如果 Native.js 对接口调用做了串行化处理,把来自多个线程的调用请求串行到一个固定的工作线程中去,的确会造成误判。

但我认为这种情况的可能性很小。试想你来设计实现这个 Native.js,这个接口本来就是要把系统底层的功能暴露给 js 来使用,有什么特别的理由要把来自不同线程的调用(假设真的是来自不同的线程)专门串行化到同一个工作线程里去呢?况且真要这么做的话,势必会产生线程间的同步等待,造成不必要的阻塞。

当然这些都只能是个人的理解和猜测。

至于你给出的验证方案,倒是个不错的思路,虽然我还没有实做,不妨预判一下:结果应该是另一个 webview 中打印日志的操作会被阻断。其实这正是我发此帖的初衷,我就是担心由于不小心使用了某种具有阻断能力的操作,比如 alert,比如你说的同步请求,导致程序功能失常。虽然这两种操作在正式产品中一般不会使用,但如果有必要的话,还是得多留意,也许有别的什么操作也具有此类的阻断能力。

Lee_Bus

Lee_Bus

高手啊!只能围观

maq

maq (作者)

嗯,实验结果,我的预判是正确的。

该问题目前已经被锁定, 无法添加新回复