本文主要介绍Angular中的黑科技之WebWorker Renderer,使用Worker线程渲染如何渲染页面?从源码的角度切入,带领带大家看个究竟。

先来做个对比

开发框架版本:Angular 4.x

项目地址:angular-webworker-renderer-demo

对比对象:传统的UI线程渲染和使用WebWorker线程渲染页面

对比方法:各执行1到1000的连乘,并循环20次,要求实时展示进度

运行结果
首先是传统的UI线程渲染效果:

其次时使用WebWorker线程渲染效果:

从动图中很明显可以看出,使用了WebWorker Renderer渲染的页面运行流畅,没有卡顿。

简单介绍下Web Woker

Web Workers是一种机制,通过它可以使一个脚本操作在与Web应用程序的主执行线程分离的后台线程中运行。这样做的优点是可以在单独的线程中执行繁琐的处理,让主(通常是UI)线程运行而不被阻塞/减慢。 —— Web Workers API from MDN

简单来说,在出现WebWoker之前,Web开发人员无法手动在浏览器中创建线程,而出现WebWoker之后,Web开发人员可以进入多线程开发Web项目了。

Web Worker的优势

下面根据YouTube视频(见参考)中的内容总结了下使用WebWorker的优势:

  • 运行过程中不会阻碍主线程(UI渲染线程)的运行,特别适合执行计算密集型的程序
  • WebWorker线程可跨窗口或frames(使用SharedWorker)
  • 使用WebWorker后能更优雅地执行测试过程(一些脱离可DOM操作的测试)
  • 兼容性(IE 10+)
  • 更高效地利用电量

对于最后一点的解释,应该先转化为另外一个问题,一些计算密集型的程序为什么不在服务端执行完毕后返回给前端?这在视频中也给出了解释,作者总结了一句话:It costs more to transmit a byte than to compute it,意思是传输一个byte比计算出一个byte的消耗更大。为什么呢?自己想吧

Web Worker可能的使用场景

那么真的有这么多应用场景吗?以下列举了几个场景:

  • 解析一个庞大的JSON结构
  • 图片/音频处理
  • 大规模数据可视化

仔细想一想,这样的场景还是很特殊的,可能在实际的应用中并不多见。那么,在目前的主流前端框架是否有利用到WebWorker的特性来帮助其提升性能呢?经过调研,发现很多还在探索阶段,比如在React框架中的探索,Parashuram在2016年发布了文章《Using Webworkers to make React faster》,文章是关于如何利用Webworker提升React的渲染速度,主要是把Virtual DOM的相关计算过程(如diff算法)放入WebWorker线程,从结果可以看出,在Benchmark的对比下,使用WebWorker的一方帧率有所提高,感兴趣的同学可以查看其演示示例和项目地址。这里忍不住要引用作者的一张图(如下图所示,纵轴是帧率,横轴是节点的个数),简要展示下React项目在使用WebWorker的情况下,性能的提升效果。


(图片来源:Using Webworkers to make React faster)

那么WebWorker已经面世这么了,浏览器支持也跟上了,为何其应用场景或者与主流框架的结合并没有很多见?我想可能与以下几点WebWorker的缺点相关:

  • 在Webworker线程中无法访问DOM节点
  • 无法与UI线程共享内存
  • 与UI线程通讯的信息需要序列化
  • 线程间通讯不可避免的并发问题

虽然如此,Angular背后的Google团队已经开始尝试打破这些限制,并已经在Angular 2.x中得进行了应用(WebWorker Renderer),虽然到了目前的Angular 4.x在源码中仍标识为@experimental,但相信其在将来会成为Angular框架的标配。接下来的文章内容,会分析到在Angular框架中Webworker Renderer是如何工作的,包括如下三个要点:

  • 通讯信息如何序列化与反序列化?内存数据如何共享?
  • 如何打破Webworker线程不能操作DOM节点的局限?
  • 如何处理并发?

希望你能带着这两个问题阅读完以下的篇幅。

先感受一下

图中显示的基本是整个UI线程与WebWorker线程通讯的过程,给你来个初步的影响,可以帮助你在阅读后续内容时有个整体观的把控,图中涉及的类、方法以及过程,在接下来的文章中会一一介绍到。

介绍几个基本的类

先来看看这个RenderStroe类,在Angular是被标识为@Injectable()的可注入类,其中_nextIndex是一个自增的索引号,通过allocateId函数递增分配。store和remove函数是对_lookupById和_lookupByObject两个Map类型的容器进行新增和删除操作,其中的传入的id参数作为唯一的索引号(通过allocateId函数分配而来)。最后deserialize和serialize方法分别是根据id取出内容和根据内容取出id。这意味中在RenderStore中序列化就是将对象转换成一个唯一数字,而相对应的反序列化就是将数字转换为一个对象。

这样一个很重要的RenderStore类就介绍完毕了,它承担了线程间数据信息通讯序列化/反序列化的重要工作。总的来说,就是将需要传输的内容对象与一个索引号对应起来,实现序列化和反序列化的过程。这个类会穿梭于整个工作流程,经常会注入到其他关键类中,是UI线程与WebWorker线程公用的类,两端共同维护同一个副本,间接到达线程间数据共享的目的。

通过这个RenderStore类,我们已经可以解决之前提出第一个问题,放张动图大家先消化消化。聪明的你可能会有以下几个疑问:

  • Object对象里存的到底是什么东西?
  • 难倒只能由WebWorker线程向UI线程单向地发送同步RenderStore数据的指令?

不慌,我们接下去讲。


这个Serializer类主要用于WebWoker线程与UI主线程之间通讯的时候,提供消息信息序列化和反序列化的操作,其实还是主要依赖于RenderStore提供的方法。

该类定义了序列化的类型,对于string,number,boolean类型,即PRIMITIVE类型,是不需要序列化/反序列化的。通过代码枚举得知,操作支持如下几种类型:

enum SerializerTypes {
    // RendererType2
    RENDERER_TYPE_2,// Primitive types,such as string,number,boolean
    PRIMITIVE,// An object stored in a RenderStore
    RENDER_STORE_OBJECT,}

具体做如下说明(其中只列举了序列化的过程):

  • PRIMITIVE类型(原始类型),serializer方法不做任何处理,直接返回;
  • Array类型,使用map方法对数组中的每一项再serializer,然后返回;
  • RENDER_STORE_OBJECT类型,通过RenderStore类中的serialize方法序列化后返回;
  • RENDERER_TYPE_2类型,通过调用_serializeRendererType2方法处理后返回;
  • RenderComponentType类型,通过调用_serializeRenderComponentType方法处理后返回;
  • LocationType类型,通过调用_serializeLocation方法处理后返回;

其中,RenderComponentType,RendererType2类型是@angular/core中定义的,两者都是Angular编译器中对DOM节点进行渲染处理时定义的类型,这里不多做阐述。LocationType类型是针对浏览器的路由操作(windows.locaion.*)进行的包装,包含href,protocol,host,hostname,port等,容易理解。

此外,serializeRendererType2serializeRenderComponentType方法体中也是根据序列化对象的结构再进行拆分对待,并继续调用serialize方法处理。比如_serializeRendererType2方法中是这样的:

private _serializeRendererType2(type: RendererType2): {[key: string]: any} {
    return {
      'id': type.id,'encapsulation': this.serialize(type.encapsulation),'styles': this.serialize(type.styles),'data': this.serialize(type.data),};
}

从代码中可以看出,通讯信息的序列化/反序列化过程其实就是主要针对string,number,boolean类型(PRIMITIVE类型)和RENDER_STORE_OBJECT类型在作处理,前者不需要序列化/反序列化,后者通过RenderStore提供的方法进行处理。

是时候回答下之前提出的问题:RenderStore中存的Object对象到底是哪些?RENDER_STORE_OBJECT类型是指哪些类型呢?

  • WebWorkerRenderer2类型,继承自Renderer2类(该类是Angular的核心类,用于操作DOM相关,这里就不啰嗦了)
  • WebWorkerRenderNode类型,该类有且只有一个类型为NamedEventEmitter的成员变量events

于是,不得不提到NamedEventEmitter类,这个类维护了一个Map类型的容器_listener,存储了事件名称和对应的方法,并提供新增(listen)、删除(unliten)以及触发事件的方法(dispatchEvent)。

由此可见,事件的定义、维护和触发在整个线程间通讯过程中至关重要。

再说说与通讯相关的类

根据官方远源码介绍,MessageBus类是一个低级别的API,是一个抽象类,主要用于UI主线程与WebWorker线程的通信相关。而双方的通信是基于通道(channel),通道的两端分别是MessageBusSink(信息流出)和MessageBusSource(信息流入),后续会细说到。类中提及的Zone是Angular的魔法,由于对与本文内容的理解不受影响,因此不做过多阐述,如感兴趣请自行查看。

首先是来列举下Angular中定义的三种通道的类型,三种通道负责不同的工作,分为渲染、事件和路由。

// DOM渲染通道
export declare const RENDERER_2_CHANNEL = "v2.ng-Renderer";
// DOM事件通道
export declare const EVENT_2_CHANNEL = "v2.ng-Events";
// 路由通道
export declare const ROUTER_CHANNEL = "ng-Router";

接下来具体讲下PostMessageBus类,作为MessageBus抽象类的一个实现,类结构如下图所示。

该类的两个公共成员变量分别是source(PostMessageBusSource类型,是MessageBusSource类的一实现类)和sink(PostMessageBusSink类型,是MessageBusSink的实现类),可以解释为水源和水槽。可以这么理解,信息好比是水,可以通过水槽流出,也可以流入到水源中

类中的initChannel方法对这通道进行初始化,其中有2个的关键点:1)每个通道的实例最多只能有三个不同的通道类型;2)Channel通道信息初始化时候包含了一个EventEmitter类的实例对象,在Sink通道初始化的时候还会对其进行了订阅操作,触发后会执行相应的sendMessage操作,这个发送信息的方法的实现主要是通过该类的构造函数中传入,后面会有所介绍。

另外需要介绍一下PostMessageBusSource类,该类在构造函数中会对Worker对象通过addEventListener方法监听message事件,这个过程能监听信息接收的事件,并且做相应的信息处理的操作,即通过EventEmitter类的emit方法来触发相应的订阅事件。

首先介绍一下WebWorkerRendererFactory2类,从类名中可以解释为WebWorker渲染工厂,在Angular中也被标为@Injectable()类型,其构造函数中依赖ClientMessagebrokerFactory,MessageBus,Serializer,RenderStore类的注入,并对其初始化,如下:

this._messagebroker = messagebrokerFactory.createMessagebroker(RENDERER_2_CHANNEL);
bus.initChannel(EVENT_2_CHANNEL);
const source = bus.from(EVENT_2_CHANNEL);
source.subscribe({next: (message: any) => this._dispatchEvent(message)});

从构造函数中能了解到,主要依赖注入类型的作用,首先通过ClientMessagebrokerFactory创建了通道为RENDERER_2_CHANNEL的代理人,虽然还未具体解释ClientMessagebroker类的作用,但从类命名中就可以了解到它的作用就是作为与UI线程通讯的中间代理人,在该类中负责向UI线程传输DOM节点渲染的工作,这个会在后续会详细介绍。另外,通过自身的MessageBus创建了EVENT_2_CHANNEL通道,并且对信息源做了subscribe的订阅操作,即当UI线程DOM事件触发时,该MessageBus的Source会接收到信息,并触发相应的_dispatchEvent函数操作,在WebWorker层中做相应的处理。

WebWorkerRendererFactory2对应的就是WebWorkerRenderer2类,该类从类结构中就可以看出包含了各种对DOM节点的操作函数,基本覆盖原生JS的DOM操作函数。特别注意,该类里面的操作函数并不是真正地操作DOM节点,而是在WebWorker线程中的模拟,最后还是以消息的形式发送给UI主线程中调用Renderer2进行操作,后续会讲到。举例说一下其中的createElement函数:

createElement(name: string,namespace?: string): any {
  const node = this._rendererFactory.allocateNode();
  this.callUIWithRenderer('createElement',[
    new FnArg(name),new FnArg(namespace),new FnArg(node,SerializerTypes.RENDER_STORE_OBJECT),]);
  return node;
}

其函数体中的_rendererFactory成员变量是一个WebWorkerRendererFactory2类的实例(在构造函数中传入),调用了allocateNode方法,创建一个包含事件处理的类,再通过RenderStore生成唯一Id,并存储。然后调用callUIWithRenderer函数,如下:

private callUIWithRenderer(fnName: string,fnArgs: FnArg[] = []) {
  // always pass the renderer as the first arg
  this._rendererFactory.callUI(fnName,[this.asFnArg,...fnArgs]);
}

其函数体中的_rendererFactory成员变量是一个WebWorkerRendererFactory2类,调用了callUI方法处理DOM相关的操作函数,包含fnName(方法名)和fnArgns(参数),其中asFnArg是一个默认参数(FnArg类的一个实例),并指定了序列化的类型,因此会被存入RenderStore,定义如下:

private asFnArg = new FnArg(this,SerializerTypes.RENDER_STORE_OBJECT);

那么callUI方法是怎么处理的呢?我们来看一下:

callUI(fnName: string,fnArgs: FnArg[]) {
  const args = new UiArguments(fnName,fnArgs);
  this._messagebroker.runOnService(args,null);
}

从函数体中,可以看出调用了ClientMessagebroker的runOnService方法,通过该方法向UI线程发送渲染指令(通过Sink的emit方法)并处理反馈信息,这里先做个简单的介绍,后续会详细介绍。

小小地总结下,WebWorkerRenderer2类定义了在WebWorker线程中模拟操作DOM节点的方法,并且发出指令向UI线程发送信息。

WebWorkerRenderer2类对应的是MessageBasedRenderer2类,前者在WebWorker线程中工作,后者在UI线程中工作。同样的,是MessageBasedRenderer2类中也定义了丰富的DOM操作方法(与WebWorkerRenderer2类对应),这些方法才是真正意义上操作DOM的方法,通过调用Renderer2中的相关方法。同时,DOM的事件触发后会通过MessageBus的Sink发送给WebWorker线程中的WebWorkerRendererFactory2类做处理。

WebWorkerRenderer2类既然有个ClientMessagebroker类来作中间代理人,负责传递信息,那么,MessageBasedRenderer2类也需要一个代理人来接头,这就是ServiceMessagebrokerFactory类。它负责注册WebWorkerRenderer2类中DOM操作函数,并接收从ClientMessagebroker传过来的渲染指令,然后触发对应的方法,执行结束后,在反馈给ClientMessagebroker代理人(成功还是失败)。

枯燥无聊的先前知基本都说到了,接下来该干正事了。

统观全局

我们看图说话。

图中只介绍了两个通道的(RENDERER_2_CHANNEL通道和EVENT_2_CHANNEL通道)的通讯流程,还差一个ROUTER_CHANNEL通道没有提及,其通过过程和RENDERER_2_CHANNEL通道类似。

先来介绍一下初始化的部分(图中的蓝色部分),从左到右来介绍。首先左侧MessageBasedRenderer2类初始化的时候创建ServiceMessagebroker类型作为自己的通讯代理人,同时初始化两条通道,创建PostMessageBusSink和Source为通讯做准备(其中事件通道中只用到了Sink),并对Source订阅事件。右侧的WebWorker线程中的初始化操作类似,用到的类不同而已。

在启动时(图中的黄色部分),MessageBasedRenderer2会在start函数中调用registerMethod方法去向ServiceMessagebroker注册DOM操作相关的方法,存储在_methods中,等待触发。

接下来,就是正式运行渲染环节了,避免错乱,更新下图,如下:

图中显示了3种颜色,代表了3个线程间通讯的过程,从绿色的开始说。

在WebWorker线程中,Angular引擎首先会根据页面布局拆分为细化的DOM节点操作,并执行渲染操作。通过callUI调用broker中的runOnService方法,并存储在_pending容器(存的是什么东西?)中,同时使用Sink向UI线程的Source发送消息,包含id、方法名和参数。UI线程的代理人接收到信息后,触发相应的订阅事件_handleMessage方法,该方法就去_mehtods中找MessageBassedRenderer2在启动时候注册的对应方法并执行,具体过程就是调用Renderer2中相应的DOM操作方法。

等DOM操作结束,就进入了蓝色标示的过程,其实是个反馈的过程。通过Sink向WebWorker线程发送消息,消息内容如下:

{
 'type': 'result','value': this._serializer.serialize(result,type),'id': id,}

其中type类型是‘result’(其实还可能是'error',本文未指出),WebWorker线程收到消息后,会触发在初始化时候定义的订阅事件,执行_handleMessage方法,操作_pengding容器,根据id获取对应条目执行(执行的是什么?)并且删除,这样这个蓝色过程就结束了。

那么_pending容器里面存的是什么?执行的又是什么?从字面理解是应该是用于存储'待解决'的事务,这也回答了,怎么处理线程间并发这个问题?在此解答一下,先上相关代码:

interface PromiseCompleter {
  resolve: (result: any) => void;
  reject: (err: any) => void;
}
// ...
let completer: PromiseCompleter = undefined !;
let promise = new Promise((resolve,reject) => { completer = {resolve,reject}; });
let id = this._generateMessageId(args.method);
// 存储
this._pending.set(id,completer); 
// catch和then
promise.catch((err) => {...});
promise = promise.then((v: any) => this._serializer ? this._serializer.deserialize(v,returnType) : v); 
// ... 反馈时
if (message.type === 'result') {
    this._pending.get(id) !.resolve(message.value);
} else {
    this._pending.get(id) !.reject(message.value);
}
this._pending.delete(id);
// ...

_pending容器里面存了id和与之对应的Promise对象的 {resolve,reject},并且预先定义好then方法(这里是做了反序列化操作,并没有其余操作),当WebWorker线程接收到type为'result' or 'error'的消息时,并对应执行resolve或者reject方法,以此释放Promise。相信你已经明白了,线程间并发问题就是用过Promise方法来完成同步。

最后来到紫色的过程,UI线程中DOM节点绑定的事件触发后,通过Sink通过事件通道向WebWorker线程的Source发送消息,WebWorker线程收到消息后,触发相应的订阅方法,这里不像渲染通道一样,有反馈过程。可能的原因时,与事件相关的方法(大部分是在对DOM节点操作),还是通过渲染通道(通过绿色和蓝色的过程)通知给UI线程。

这样整个WebWoker Renderer的线程间通讯的部分就介绍完毕了。

回顾下一开始提出的三个问题:

  • 通讯信息如何序列化与反序列化?内存数据如何共享? 答:通过RenderStore类。
  • 如何打破Webworker线程不能操作DOM节点的局限? 答:通过RENDERER_2_CHANNEL通道。
  • 如何处理并发?答:通过操作反馈与Promise机制。

还没完

我猜你一定想知道Angular中是怎么启动WebWorker线程并执行渲染操作?如何开启UI线程中的start方法?来来来,慢慢絮叨。

我们从如何将传统的Angular项目(基于platformbrowserDynamic的JIT项目或者基于platformbrowser的AOT项目)转换成基于platformWorkerAppDynamic的WebWorker项目,可以参考本文一开始提供的DEMO项目或者参考文章《Angular with Web Workers: Step by step》。基于现有的WebWorker项目,我们来讲解一下,启动过程。

首先会调用@angular/platform-webworker中的bootstrapWorkerUi方法启动一个WebWorkerLoader,传入的是一个WebPack打包输出的webworker.bundle.js(可在webpack.config.js输出),基于的文件就是一个platformWorkerAppDynamic的启动文件,其余的配置与传统项无异,由此可见改动成本还是比较小的。

主要还是来看一下bootstrapWorkerUi方法做了些什么?

export function bootstrapWorkerUi(
    workerScriptUri: string,customProviders: Provider[] = []): Promise<PlatformRef> {
  // For Now,just creates the worker ui platform...
  const platform = platformWorkerUi([
    {provide: WORKER_SCRIPT,useValue: workerScriptUri},...customProviders,]);
  return Promise.resolve(platform);
}

从代码中看出主要是创建一个platformWorkerUi对象,讲loader文件地址传入。

这么关键的一个platformWorkerUi我们简单来将想,该对象通过createPlatformFactory(Angular/core中的方法)创建,并传入一组Provider,包括MessageBasedRenderer2SerializerRenderStoreMessageBus等之前介绍过的注入类,

还有一个关键的WebWorkerInstance,申明如下:

@Injectable()
export class WebWorkerInstance {
  public worker: Worker;
  public bus: MessageBus;
  /** @internal */
  public init(worker: Worker,bus: MessageBus) {
    this.worker = worker;
    this.bus = bus;
  }
}

作为WebWorker的一个实例,包含Woker对象的实例,和MessageBus的实例,那么什么时候调用的init方法初始化呢?接着往下看,

Provider中还有一个关键init时需要的注入类,描述如下:

{
    provide: PLATFORM_INITIALIZER,useFactory: initWebWorkerRenderPlatform,multi: true,deps: [Injector]
}

里面使用了initWebWorkerRenderPlatform方法,提取梳理出关键的步骤:

const webWorker: Worker = new Worker(url);
const sink = new PostMessageBusSink(webWorker);
const source = new PostMessageBusSource(webWorker);
const bus = new PostMessageBus(sink,source);
WebWorkerInstance.init(webWorker,bus);

// initialize message services after the bus has been created
const services = injector.get(WORKER_UI_STARTABLE_MESSAGING_SERVICE);
// 这里的 WORKER_UI_STARTABLE_MESSAGING_SERVICE 在应用中归根到底其实就是调用的MessageBasedRenderer2类
zone.runGuarded(() => { services.forEach((svc: any) => { svc.start(); }); });

根据url路径创建Worder对象,该对应用于PostMessageBusSinkPostMessageBusSource对象初始化,比如在PostMessageBusSource初始化中会对Worker对象addEventListener监听'message'事件。然后使用sink和source实例化PostMessageBus类,再调用WebWorkerInstance对象的init方法。最后,将注入的MessageBasedRenderer2类自动调用start方法。

总结

说一下自己的感受,不要为了用WebWorker线程而用,还是要集合多方面因素来考虑,比如线程间通讯时间,开启线程的性能消耗等,毕竟WebWorker提出的初衷是为了那些计算密集型的操作,被Angular框架使用到渲染中,是一个有突破的创新,但目前并不能支持所有项目的转换,还不稳定(@experimental),请谨慎使用。但相信随着浏览器的发展,为了极致化用户体验,WebWorker的渲染势必会被各大主流前端框架考虑在内。

至此,Angular WebWorker Renderer的前前后后的源码解析就解密完了,肯定有诸多解析不到位的地方,欢迎留言吐槽。

知乎@charway

参考

Angular Platform-Webworker 源码
Angular Web Worker - Building Super Responsive UI
Using Web Workers for more responsive apps
Angular with Web Workers: Step by step

解密Angular WebWorker Renderer的更多相关文章

  1. three.js模拟实现太阳系行星体系功能

    这篇文章主要介绍了three.js模拟实现太阳系行星体系功能,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下

  2. HTML5页面无缝闪开的问题及解决方案

    这篇文章主要介绍了HTML5页面无缝闪开方案,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  3. ios – 仅在异步函数完成执行后运行代码

    所以,例如:如果问题是你不知道要调用什么函数,你可以配置你周围的函数/对象,这样有人可以给你一个函数,然后你在我上面说“调用函数”的地方调用你的函数.例如:

  4. ios – 如何使用Objective C类中的多个参数调用Swift函数?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. iOS:核心图像和多线程应用程序

    我试图以最有效的方式运行一些核心图像过滤器.试图避免内存警告和崩溃,这是我在渲染大图像时得到的.我正在看Apple的核心图像编程指南.关于多线程,它说:“每个线程必须创建自己的CIFilter对象.否则,你的应用程序可能会出现意外行为.”这是什么意思?我实际上是试图在后台线程上运行我的过滤器,所以我可以在主线程上运行HUD(见下文).这在coreImage的上下文中是否有意义?

  6. ios – 多个NSPersistentStoreCoordinator实例可以连接到同一个底层SQLite持久性存储吗?

    我读过的关于在多个线程上使用CoreData的所有内容都讨论了使用共享单个NSPersistentStoreCoordinator的多个NSManagedobjectContext实例.这是理解的,我已经使它在一个应用程序中工作,该应用程序在主线程上使用CoreData来支持UI,并且具有可能需要一段时间才能运行的后台获取操作.问题是NSPersistentStoreCoordinator会对基础

  7. ios – XCode断点应该只挂起当前线程

    我需要调试多线程错误.因此,为了获得生成崩溃的条件,我需要在代码中的特定点停止一个线程,并等待另一个线程到达第二个断点.我现在遇到的问题是,如果一个线程遇到断点,则所有其他线程都被挂起.有没有办法只停止一个线程,让其他线程运行,直到它们到达第二个断点?)其他更有趣的选择:当你点击第一个断点时,你可以进入控制台并写入这应该在该断点处暂停当前上下文中的线程一小时.然后在Xcode中恢复执行.

  8. ios – 在后台线程中写入Realm后,主线程看不到更新的数据

    >清除数据库.>进行API调用以获取新数据.>将从API检索到的数据写入后台线程中的数据库中.>从主线程上的数据库中读取数据并渲染UI.在步骤4中,数据应该是最新数据,但我们没有看到任何数据.解决方法具有runloops的线程上的Realm实例,例如主线程,updatetothelatestversionofthedataintheRealmfile,因为通知被发布到其线程的runloop.在后台

  9. iOS 7,用于断开调用的私有API CTCallDisconnect不起作用

    谢谢!

  10. ios – 为什么,将nil作为参数从Objc C发送到swift类初始化器,用新对象替换nil参数

    除非属性本身被声明为nonnull:

随机推荐

  1. Angular2 innerHtml删除样式

    我正在使用innerHtml并在我的cms中设置html,响应似乎没问题,如果我这样打印:{{poi.content}}它给了我正确的内容:``但是当我使用[innerHtml]=“poi.content”时,它会给我这个html:当我使用[innerHtml]时,有谁知道为什么它会剥离我的样式Angular2清理动态添加的HTML,样式,……

  2. 为Angular根组件/模块指定@Input()参数

    我有3个根组件,由根AppModule引导.你如何为其中一个组件指定@input()参数?也不由AppModalComponent获取:它是未定义的.据我所知,你不能将@input()传递给bootstraped组件.但您可以使用其他方法来做到这一点–将值作为属性传递.index.html:app.component.ts:

  3. angular-ui-bootstrap – 如何为angular ui-bootstrap tabs指令指定href参数

    我正在使用角度ui-bootstrap库,但我不知道如何为每个选项卡指定自定义href.在角度ui-bootstrap文档中,指定了一个可选参数select(),但我不知道如何使用它来自定义每个选项卡的链接另一种重新定义问题的方法是如何使用带有角度ui-bootstrap选项卡的路由我希望现在还不算太晚,但我今天遇到了同样的问题.你可以通过以下方式实现:1)在控制器中定义选项卡href:2)声明一个函数来改变控制器中的散列:3)使用以下标记:我不确定这是否是最好的方法,我很乐意听取别人的意见.

  4. 离子框架 – 标签内部的ng-click不起作用

    >为什么标签标签内的按钮不起作用?>但是标签外的按钮(登陆)工作正常,为什么?>请帮我解决这个问题.我需要在点击时做出回复按钮workingdemo解决方案就是不要为物品使用标签.而只是使用divHTML

  5. Angular 2:将值传递给路由数据解析

    我正在尝试编写一个DataResolver服务,允许Angular2路由器在初始化组件之前预加载数据.解析器需要调用不同的API端点来获取适合于正在加载的路由的数据.我正在构建一个通用解析器,而不是为我的许多组件中的每个组件设置一个解析器.因此,我想在路由定义中传递指向正确端点的自定义输入.例如,考虑以下路线:app.routes.ts在第一个实例中,解析器需要调用/path/to/resourc

  6. angularjs – 解释ngModel管道,解析器,格式化程序,viewChangeListeners和$watchers的顺序

    换句话说:如果在模型更新之前触发了“ng-change”,我可以理解,但是我很难理解在更新模型之后以及在完成填充更改之前触发函数绑定属性.如果您读到这里:祝贺并感谢您的耐心等待!

  7. 角度5模板形式检测形式有效性状态的变化

    为了拥有一个可以监听其包含的表单的有效性状态的变化的组件并执行某些组件的方法,是reactiveforms的方法吗?

  8. Angular 2 CSV文件下载

    我在springboot应用程序中有我的后端,从那里我返回一个.csv文件WheniamhittingtheURLinbrowsercsvfileisgettingdownloaded.现在我试图从我的角度2应用程序中点击此URL,代码是这样的:零件:服务:我正在下载文件,但它像ActuallyitshouldbeBook.csv请指导我缺少的东西.有一种解决方法,但您需要创建一个页面上的元

  9. angularjs – Angular UI-Grid:过滤后如何获取总项数

    提前致谢:)你应该避免使用jQuery并与API进行交互.首先需要在网格创建事件中保存对API的引用.您应该已经知道总行数.您可以使用以下命令获取可见/已过滤行数:要么您可以使用以下命令获取所选行的数量:

  10. angularjs – 迁移gulp进程以包含typescript

    或者我应该使用tsc作为我的主要构建工具,让它解决依赖关系,创建映射文件并制作捆绑包?

返回
顶部