股票学习网

股票行情网,股票入门,和讯股票,股票查询 - - 股票学习网!

pbx指标(pbx指标源码)

2023-04-11 08:09分类:黑马捕捉 阅读:

英国《泰晤士报》4月10日的报道称,有人在乌克兰东部的卢甘斯克地区拍到了星光防空导弹将俄罗斯的米-28N击落的画面。乌克兰的这些星光导弹来自于英国的紧急援助,弥补了大型雷达在超低空的探测盲区,与高射炮、弹炮系统及其他近程防空导弹系统共同构成了其低空防御体系。

俄军直升机被星光高速导弹击落这一事件,向世人展现了被称为全球飞得最快的近程防空导弹的星光导弹的作战效能,那么,星光导弹的性能究竟如何?星光导弹在国际上处于什么地位?本文将为读者们揭开星光导弹的神秘面纱。

一、星光导弹的性能究竟如何?

星光导弹源于上世纪80年代初期,当时的英国总参谋部认为高速导弹系统最能满足其防空的需要,因此提出研发一款全新的高速导弹系统以取代现有的肩射导弹,编号为GSR 3979的性能需求书列出了英国总参谋部对该导弹系统的“三发齐射、三种平台”指标,前者顾名思义,后者即车载发射器、三联装依托式发射器、单发肩扛式发射器。

1984年,英国国防部把新型高速导弹系统的研发合同授予英国航宇公司与肖特导弹系统公司,最终肖特公司的实验型号以其优异的性能赢得竞标,得到了3.65亿英镑的合同,并于1986年11月签署了进一步研发与制造合同。

1997年9月,星光导弹正式入役英军以替换标枪防空导弹。这款名为星光的新型导弹有四大亮点

第一个亮点是其动力源于二级火箭推进带来的速度。一级发动机能将导弹推出发射筒,二级发动机在导弹出筒5米后点火,这使其作战高度能够达到5千米,射程则能够达到7公里以上,在离筒400米后速度就将达到3.5马赫。

星光导弹的第二个亮点是其配备三枚钨合金材质的“飞镖”子弹头。三枚子弹头有效提高这款导弹击中目标的概率,这三枚子弹头在释放后彼此保持一米左右的间距,据称能共同确定目标二维矩阵的中心点,依托数据链调整自身位置进行协同攻击。

此外,每枚子弹头旋转的前部有两片鸭翼,旋转的后部有四片矩形尾翼,这些舵面能够使其完成复杂转向,具备较高的机动性。在导弹刚刚离筒时,导弹后端外壳会以每秒10转的速度开始自转,四片矩形尾翼将被弹簧牵拉展开,弹道呈抛物线式。

而在离筒5米后,导弹将点燃二级发动机进行加速,同时根据弹载计算机的数据对目标进行迎击,此时外壳的转速将提升到每秒35转。离筒400米后,导弹速度将接近3.5马赫。在接近目标时,三个子弹头将被释放,尖锐细长的弹头有效减少了阻力,保留了较高的动能。

在重达900克的子弹头穿入目标内部后,其撞击延迟引信将引爆弹体内部的450克PBX-98高爆炸药,释放大量钨合金外壳预制破片。1999年9月,英国展示了这款导弹对FV432装甲人员输送车的攻击效果,体现出其高速下的钨合金弹头击穿步兵战车薄弱装甲的能力。

星光导弹的第三个亮点是其激光制导方式。这型导弹配备了激光制导技术,能够不受红外热源的干扰,操作手在光学瞄具中检测到目标后可以直接发射,只需使用激光照射装置对目标进行持续照射,弹载计算机便将根据投射到目标上的两道激光提供的垂直扫描和水平扫描数据构建出一个二维矩阵,从而计算出相对位置对目标进行持续追踪。

星光导弹的生产制造商泰雷兹空防公司称这两道激光的强度能够保证目标无法检测到其存在,从而保证袭击的突然性。另外,与传统的红外制导不同,激光制导不易被热源所干扰,即便是俄军直升机配备的新型维捷布斯克机载自卫系统的主动激光照射也难以对其进行干扰。

俄军的维捷布斯克机载自卫系统中,目前已有六个部分投入使用,分别是L150雷达告警接收机、L140激光告警接收机、L370-2导弹逼近告警传感器、UV-26诱饵弹发射器、L370-5激光定向干扰机和L370-1综合控制单元这六个部分。此外,研发中的L370-3S有源干扰站和有源拖曳电子诱饵仍未投入使用。

维捷布斯克机载自卫系统的L370-2导弹逼近告警传感器在检测到来袭导弹的紫外信号后将发出告警,并将实时测角信息传给L370-1综合控制单元,L370-1则控制UV-26诱饵弹发射器发射诱饵弹干扰来袭导弹的红外导引头,同时控制L370-5激光定向干扰机按照L370-2提供的测角信息对目标进行激光致盲。

可见,维捷布斯克机载自卫系统的性能比较优秀,但乌军的星光导弹凭借4马赫的高速,会极大压缩其反应时间,并且乌军还能使用战术降低这款机载自卫系统对导弹的拦截率。

例如,在近日曝光的一则乌军击落俄罗斯直升机的视频中,可以看到隐蔽在掩体后的乌军操作手在视距内对直升机发射了一枚肩扛式防空导弹,在导弹接近直升机前都未对直升机进行照射,以免触发其机载激光告警系统,而在导弹即将与直升机擦肩而过时,乌军操作手迅速照射锁定直升机,一举将其击落。

星光导弹的性能究竟如何?答案是十分强悍,拥有上述的高速度、多弹头、抗干扰三大亮点。当然,除了这三大亮点以外,星光导弹同样有一些难以忽视的缺陷,例如其更换弹药时间极长、缺乏近炸引信导致其容错率低、激光制导导致其缺乏“发射后不管”能力等,但总的来说,这款导弹仍然是世界先进水平的便携式防空导弹。。

二、星光导弹在国际上处于什么地位?

在介绍星光导弹在国际上的地位之前,需要先对便携式防空导弹的发展史进行梳理,以引出国际上的三代便携式防空导弹。

1959年,美国陆军、美国海军陆战队与通用动力公司签订了FIM-43红眼导弹研制合同。同年,苏联也开始研制便携式防空导弹。只不过,美国的红眼导弹使用搭载电池氟利昂气体制冷组件的制冷式红外导引头,而苏联则采用了以硫化铅探测器为基础的非致冷式导引头。值得一提的是,两国的便携式防空导弹仍有共同点,例如都采用弹体旋转方式进行操纵,且研制进度都比较拖沓。

首批装备红眼的作战部队直到1968年才形成作战能力,而苏联的便携式防空导弹研制方则从托列波夫设计局改为了科罗姆纳机器制造设计局,该局直到1965年才将箭-2研制完成,但其比红眼略逊一筹。当然,这两型导弹都具备一定的缺陷,箭-2的红外导引头不能过滤热源,而红眼的射击角度不大而且无敌我识别器。

为解决箭-2存在的问题,苏联在该型导弹上改用了可过滤假目标的致冷式导引头,并对战斗部进行重新设计以提高杀伤力,改进后的箭-2M导弹于1971年服役,缩小了其与红眼导弹在性能上的差距。

六七十年代,各国都已启动第二代便携式防空导弹的研制计划,英国的肖特公司研制的吹管导弹于1965年进行了首次试射,于1974年投入生产,于1975年入列服役。美国于1966年提出研发一款替代红眼的全向攻击红外导弹,这款导弹最初名为红眼-2,后于1972年改为FIM-92毒刺。苏联则在部署箭2M导弹后开始研制箭-3导弹,计划采用新型致冷式导引头,具备在偏离弹轴40度的范围内锁定目标的能力。

美国的采用红外导引热追踪的FIM-92A毒刺导弹于1979年投产,改用红外/紫外双波段导引热追踪的FIM-92B毒刺导弹于1983年投入量产,两者均于1987年停产,取而代之的是1987年量产的抗干扰能力更强的FIM-92C毒刺导弹。

苏联的箭-3导弹于1978年开始替换箭-2M导弹,采用二级固体发动机。导弹离筒55米后点燃一级发动机将导弹加速到超音速,在一级装药燃尽后,二级发动机点火使导弹保持1.24马赫的速度,虽然其平均速度低于箭-2M,但射程却增加了约15%,作战高度也有所提高。

对于苏联而言,该国与毒刺及其改进型对应的第二代便携式防空导弹除了箭-3,还有1981年服役的针-M和1983年服役的针,这一时期便携式防空导弹的性能提升主要在于拓展作战高度的上限和下限、提高速度和射程。

值得一提的是,苏联早期便携式防空导弹均被我国仿制,越南战争期间北越给我国提供了箭-2和箭-2M并请求我国仿制并对其供应,这催生了我国的红缨-5和红缨-5A。1980年代中期,扎伊尔(现民主刚果)从安盟(安哥拉革命政党)与安哥拉政府武装部队交战的战利品中获得苏联箭-3导弹,并向我国提供,使我国成功仿制出红缨-5B以及后续车载的红缨-5C。

说了这么多,接下来回归正题,目前国际上将便携式防空导弹划为三代:采用非制冷式红外导引头的第一代便携式防空导弹只能尾追攻击喷气式飞机,采用制冷式红外导引头的第二代便携式防空导弹在能够对喷气式飞机等目标进行全向攻击的同时,还具备较强的抗干扰能力,而采用新型导引头的第三代便携式防空导弹则具备更强的抗干扰能力和打击能力。

星光导弹在国际上处于什么地位?从划代的角度来看,英国的星光导弹与我国的前卫-2和红缨-6、美国的毒刺改进型、俄罗斯的针-S和柳树、法国的西北风同属第三代便携式防空导弹,整体已经属于世界先进水平。

星光导弹在整体属于世界先进水平的同时,个别领域还处于第三代便携式防空导弹中的领先地位,结合前文来看,这款导弹不会受到红外/无线电干扰,不会被反辐射导弹压制,具备打击低空高速固定翼战机的能力,杀伤范围和命中率均较大,4马赫的速度压缩了敌方目标对其的反应时间。

综上所述,从纸面数据来看,星光导弹是一款性能十分优越的第三代便携式防空导弹,整体性能属于世界先进水平,个别领域还能占据领先地位。俄军米-28N直升机被星光高速导弹击落的事件,则进一步向世人展现了这款被称为全球飞得最快的近程防空导弹的作战效能。

结 语

英国等西欧国家虽然整体军力已经难以与中美俄相媲美,但其科研能力仍然不容小觑,在个别领域仍然能够研发出性能居世界前列的产品,欧洲联合研制的流星空空导弹是如此,英国的星光便捷式防空导弹也是如此。

在西欧诸国军事实力日渐式微的今天,星光导弹无疑是大不列颠科研实力的一大体现,如果英国能够保持当下的科研能力,也许仍能在未来的国际中占据一席之地。

一场突如其来的疫情,让数以亿计的白领只能居家办公,云视频会议系统突然成为最重要的办公软件。腾讯会议在 2019 年 12 月 25 日正式上线后,短短两个月时间内积累千万日活。除了时机,腾讯会议产品又为什么能脱颖而出呢?产品力是个不得不提的因素,腾讯多媒体实验室高级研究员王晓海在【腾讯技术开放日·云视频会议专场】中,对腾讯会议在复杂网络情况下如何保证高清音质进行了分享。

VoIP 和 PSTN 的前世今生

PSTN(Public Switch Telephone Network 公共交换电话网) 从贝尔发明电话起就已经存在了,通过这张网连接全世界所有的电话机,发展到今天的 PSTN 骨干网基本上都已经数字化了,但是在一些集团电话或者偏远地区,PBX(Private Branch Exchange 用户交换机)接入的可能还是一些模拟电信号接入的传统电话。PSTN 可以通俗理解成传统电话和者蜂窝电话 (Public Land Mobile Network 公共路基移动网) 的总和

VoIP 是基于 IP(Internet Protocol)的语音传输,可以理解为经过互联网传输的通话,也有部分通过电信运营商的传统电话网络进行传输。VoIP 网络通话是电脑或者移动终端之间的互通,比较典型的 QQ 或者微信,以及苹果设备之间的 FaceTime。VoIP 比较便宜,这是因为 VoIP 不过是一种互联网应用,那么这个流量用户来看视频,还是用来做语音视频通话,实际上资费是一样的。

那么为什么 VoIP 服务有些要收钱,有些却免费?这是因为 VoIP 服务不仅能够沟通 VoIP 用户,还可以和电话用户通话,比如使用传统固话 PSTN,以及无线手机蜂窝网络 (PLMN)2,3,4,5G 的用户,对于这部分通话,VoIP 服务商必须要给固话网络运营商以及无线通讯运营商支付通话费用,这部分的收回就会转到 VoIP 用户头上,而网络 VoIP 用户之间的通话可以是免费的。

有好多 PSTN 网络或者集团电话网络,它本身是有质量保证的。但是 VoIP 电话,一般是走公网的,它发出去或者接到的最后一公里电路是没有保障的,同时因为各种原因会丢包或者抖动,会让端到端通话质量受损。我们所关注的工作重点,就是图一右侧密密麻麻的这些内容,实际集中在 QoS,也就是 Quality of Service(服务质量),包含网络传输的一系列服务需求,比如带宽、延时、抖动、丢包等

VoIP 的发展进化史

Webex1995 年诞生,是业界最早的一款 VoIP 产品。到了 1999 年 GIPS 诞生,它为业界提供了广泛的引擎,对整个 VoIP 影响巨大,在 2003、2004 年,GIPS 向 Skype 和 Webex,以及 QQ 提供了它的 GIPS 音频引擎,2011 年 GIPS 被谷歌收购,该项目开始开源,即为大家所熟知的 WebRtc 开源项目。

2011 年这个时间点很重要,因为 WebRtc 的开源,促使业界诸多音视频通讯领域的头部玩家开始躁动,同年 Skype 被微软收购,ZOOM 创立,它的创始人就是 Webex 出来的。2011 年腾讯也开始自研音频引擎,腾讯在国内召集了一批音频及通信领域的从业者开发了第一代引擎 TRAE(Tencent Realtime Audio Engine),并且同年腾讯把自研的 TRAE 引擎上线换掉 GIPS,TRAE 音频引擎正式作为 QQ 音频引擎为几亿 QQ 用户服务。

2014 年腾讯“新一代语音视频通信引擎 Sharp 系统”获得公司技术突破金奖,Skype 在国际长途通话市场市占率达到 40%,总通话量达到 2000 多亿分钟。2015 年腾讯“音视频融合通讯项目”获得公司技术突破金奖,腾讯从 2016 年开始的向外界提供了 OpenSDK 能力,对内对外服务了众多音视频通话类的产品,其中 2017 年获得腾讯内部产品最高奖—名品堂的“全民 K 歌”也是使用 OpenSDK 的基础音视频处理及通讯能力,这种互联互通的能力为后来的发展奠定了坚实基础。

其实腾讯的音视频引擎又可以分为三个小代际。第一代就是 QQ 用的,2011 年 TRAE 引擎,第二代是 2016 年开始向外界开放的 OpenSDK 引擎,到 2017 年腾讯开发了 XCast 引擎,这算作第三代,在最新的 XCast 引擎之上,诞生出今天的“腾讯会议”。

2019 年 12 月 25 号腾讯会议上线,2020 年 3 月 6 日腾讯会议已经登顶 App Store 免费软件 NO.1,到今天不过两个多月,3 月份日活达一千万,成绩还是比较难得的。ZOOM 人数日活突破一千万是 2014 年 6 月份,当时的 ZOOM 是用了 3 年时间。

VoIP 音频系统的主要构成

VoIP 从发送端到接收端大概有哪些模块呢?我今天着重讲 网络的流量控制、拥塞控制,包括丢包、抗网络抖动的一些逻辑,以及他们怎么样融合才能更好提升服务质量。核心是 QoS 的概念

QoS(Quality of Services)概念当时在 IETF(The Internet Engineering Task Force 国际互联网工程任务组)提出的时候,只专注于纯网络范畴的指标,比如丢包、延迟、抖动、带宽等。进入新世纪以后,行业对 VoIP 或者融合通信的理解有变化,进入宽带时代以后对指标会有更高期许和更高要求,比如说音频采集,本来采集信源不好,再经过压缩、传输、解码,可能最终效果不好。如果从 QoE(Quality of Experience)环节来看,端到端不限于采集模拟接口出来的声音,甚至包括人的讲话环境和听音环境影响,用户感受到的音频质量,是整个体系反馈出来的诊断。

QoS 没有太多秘密,无非就是抗丢包,抗抖动,网络拥塞控制等等,ARC(Adaptive Rate Control)可以看做一个中央指挥官,它只能去指挥整合这些方式、手段,最终保证音频质量是良好的改动。所以说到这块,QoS 一般套路就类似于一个全家桶,无非这六种模块的合理有机组合,接下来会对这几块深入讲一下

腾讯会议是如何保证服务质量的?

1、腾讯会议的“流控”:ARC

先看 ARC,ARC 在腾讯会议的概念就是“流控”,流控能干什么?

是三个大的层面,首先它是一个配置系统,无论双人或多人通话,系统所需要的基础配置参数,还有在什么场景下需要什么样的参数。通过这个云端的参数配置及开关配置,系统拥有了基本的云端可控手段。

然后动态控制是中间的这块,流控是把源端到目标端的传输行为,发出来数据想让对方解码,会存在动态的能力交换的要求。此外,系统如果发生了抖动,或者丢包的情况,我们会动态的去控制相应的模块去处理这些情况。所以能力交换,或者动态下发的能力,实际上是动态控制的第二层次水平

最高层的能力,是聪明灵活自适应的能力,就是对 ARC 的指挥能力的更进一步的要求,丢包的时候怎样去抗丢包,抖动的时候怎么样去抗抖动,去动态控制,但是这些抗丢包、抗抖动的方法有可能会占用过多的网络带宽、或者以牺牲端到端延时为代价的、于是当网络发现了比如带宽不足,或者网络用户本身终端连接路由器质量有问题,甚至出现网络趋于拥塞的情况,这种情况怎么去把它及时挽救回来,这个配置是对 ARC 更高层次的要求,涉及到网络拥塞控制(Congestion Control)这个核心命题上来了。

2、“流控”在腾讯内部的演进过程

一开始是都写死的参数,比如解码器参数、音频前处理 3A 开关等、抗丢包和抗抖动参数也基本是固定的,效果肯定不好。后来我们 在系统增加了流控策略,根据客户端动态上报,动态去算 QoS 的参数下发。进化到多人通话以后,特别带宽预测是比较大的挑战,比如上行应该怎么算,下行是接收多交流又该怎么算,因为发送行为不一样,原来那种用一个算法对不同流进行预测,肯定不能用。

后来,我们还做了服务器混音。首先可以减少下行用户的流量,其次多路混音成一路,也可以对融合通信发挥基础作用。在对外提供 OpenSDK 的时代,因为对外用户的需求很不一样,因为应用场景的差别,有的用户需要不通类型 Codec,有的用户需要关掉 3A 处理,有的用户不需要那么大流量,有的用户更加在乎质量而不在乎流量,所以云端的 Spear 可配置参数的流控策略可以满足我们企业内部应用,包括外部用户的差异化需求。

腾讯会议对于拥塞控制的做法

接下来我们看比较核心的拥塞控制(Congestion Control)。其实拥塞控制在实时 RTC(Real Time Communication)音视频通讯领域应用中是必不可少的模块,WebRtc 在开源以后分别向社区贡献了 GCC1 和 GCC2 版本,当然这块不是说 Linux 系统下编译器的那个 GCC。

GCC1(Google Congestion Control ver.1)是一个基于接收端的算法,根据每家系统的软件架构的不同,它部署的位置也不一样。GCC1 核心算法是通过实时监控端到端延时的变化量(Jitter),从而判断当前这个网络是否趋于达到网络拥塞的情况。

我们首先看端到端延时这个基础概念,端到端延时由三部分组成:一个是传输延时,跟数据包大小及链路宽有关;第二个是队列延时,即数据包在路由器的队列中通过的时长;第三个传播延时,一般传播延时跟传输介质有关。

实际上在 GCC1 或者 GCC2 里面,它真正进入系统、进入计算的这个变量不是端到端延时,而是其变化量即 Jitter;Jitter=(TR(i)- TR(i-1))- (TS(i)- TS(i-1)) 包括前后两个数据包的接收时间戳之差再减去前后两个包发送时间戳之差,算法输入的是一个 Jitter,GCC1 通过 Kalman 自适应滤波器去除噪点,通过网络传输也好,通过整个链路传输过程也好,实际上这种延时的变化值 Jitter 是一种符合高斯分布的特征,当我们把噪点去掉以后就可以得出它的 Jitter 趋势。GCC1 算法将滤波后的 Jitter 值与动态阈值进行相应的状态判断,从而得出当前的网络是否趋于拥塞或者处于正常,与此同时还通过实时接收到的流量和预测算法给出当前一个合理的带宽预测值。

后来 GCC2 又更新了,是基于发端的,它的数据处理源还是 Jitter,就刚才说那个 Jitter,它是一个什么概念呢?自变量就是 Jitter,应变量是什么呢?应变量是它的历史平均值。所以它对自变量和应变量做了一个最小二乘法的一元线性回归,相当于去观察当前的 Jitter 值相比较历史平均值有怎样的发展趋势,被称作 TrendLine 算法。GCC 算法它在发送端也好还是在接收端也好,带来了工作上的灵活性,而 GCC1 中绝对值的阈值比较变成了 GCC2 中趋势线的判断,算法的准确性上有提高。而从及时性上来说,我们在 QQ 时代使用 GCC1 算法是,SDK 的架构还是有私有协议的,比如说反馈机制都是基于两秒的机制,在最新重构的第三代 xCast SDK 上上,完全兼容了标准协议,RTP 算法核心有准确度的提升,反馈上 RTCP 时机和及时性也有提升,所以“腾讯会议”相关的算法控制会远远老一代的 SDK 产品更加及时和准确。

FEC 如何把丢失的数据包恢复?

FEC(Forward Error Correction)实际上没有太多新意,这块无非就是利用其基本的特性。比如分组划分,接收端恢复不受数据包顺序影响等特征。举个例子:如果是分组是 4,那么在网络传输过程中任意丢掉一个,在接收端任意收到任何顺序的 4 个属于本分组的数据包,那就可以把丢失的包恢复。

那么它到底是怎么工作的呢?FEC 目前一般采用了 Reed Solomon 算法,Reed Solomon 算法的核心思想包含三个部分:

  1. 利用范德蒙德(Vandermonde)矩阵 F,通过 FEC 控制参数生成冗余矩阵。冗余矩阵的上半部分是一个单位矩阵,分组数据矩阵和这部分计算完以后还是原来的数据,接下来这部分数据则是实际用来产生冗余数据的矩阵。图示相当于 4+2 的原始数据生成 2 个冗余数据,ENCODING 就是这样范德蒙德矩阵与原始数据相乘,分组的原始数据相当于数据源,根据 FEC 编码参数额外生成的数据包就是冗余数据。
  2. 利用高斯消元法(Gaussian elimination)恢复损坏的数据,即算冗余矩阵的逆矩阵。使用逆矩阵与接收端凑齐分组的数据矩阵进行行列式乘法计算,从而恢复丢失的数据包。
  3. 为了方便计算机处理,所有的运算是在伽罗华域(Galios)的基础上进行。伽罗华域的特点是大小为 n 的集合,其上的任何四则运算的结果仍在集合中。伽罗华域上的加减法实际上等同于异或 Xor 运算,伽罗华域上的乘除法则通过查表计算非常快。

比如,传输过程中它可能会丢掉,比如 4+2 是 6 个包,任何顺序的 2 个包,还剩下 4 个包,就会去计算它的逆矩阵,这个逆矩阵去乘以接收端收到的任何顺序的,但是数量上凑够分组长度 4 个的数据包,通过矩阵算法可以恢复丢失的数据包。

从原理来讲很简单的,我们今天要讲 FEC,它在实际落地过程中还有一些技巧。比如在算法实际落地的时候,怎么样去评价 FEC 算法的效果,首先会有量化数据,比如基于一个统计算法,FEC 的恢复率是多少?加不加常态 FEC?多少倍的冗余公式去加这个 FEC?最终的效果什么样的?这些都需要强大的基于大盘数据的分析和 ABTest 运维能力才能逐步获取到的最佳值。比如,在一开始的版本,在没有加常态的 FEC 下,动态 FEC 恢复其实不到 90%。到再加常态 FEC,FEC 恢复率可以爬升至 95%,网络经常有小的丢包,那么指望系统、流控或者任何反馈机制,实际上不靠谱的,宁可去添加一些常态的 FEC 冗余。此外,对于实际的网络,突发的丢包是经常发生的,FEC 参数的设定也有融入控制论的相关思想,除了动态计算和下发 FEC 参数,还要考虑参数在一段时间的有效性,比如我们对 FEC 参数增加的缓降控制窗口逻辑,又进一步将 FEC 恢复率提升至最高的 99% 左右。

右上角是大盘的数据,可以发现 FEC 整体会有明显攀升,这里就是常态 FEC 的一个效果。另外对于在这里的分组长度的确定,分组要兼顾整个系统延迟,以及你的系统规格要兼顾怎样的边界和指标,如果不通过大数据运营,你永远不知道分组多少是合适的。通过前面讲的大数据 ABTest 运营方式把数据放在真实用户的全网进行验证,找到合适分组、冗余倍率公式、控制相关的策略。下面这两张图就可以看到最终的结果,看似还是很不错的,FEC 恢复率从 95% 恢复到高的接近 99%。

网络是有突发丢包的,可能时不时的来一下,或者丢包前后有一些持续的小丢包。FEC 控制策略上的拖尾时间窗口的方式,可以 Cover 住这一类连续丢包。

如何做音频 ARQ 抗抖动处理

ARQ 也是一个比较成熟的技术,我们在做的过程中也是踩过一些坑的,所以今天也非常愿意跟大家分享。

如果这块做不好的话,实际上会有副作用,宁可不要这功能。在 QQ 时代,一个典型例子是应用层有个逻辑,在基于 RTT 小于多少毫秒的基础情况下,给音频数据包进行重传,主观上认为只要是把丢包重新传回来,这个效果肯定是好的,至于底层的 TRAE 音频引擎怎么处理,实际上不太关心。但这个是不够的,这就是今天讲的红箭头 5 和 6 比较细节的地方,重传算法主要关注的是对于缺失的数据包重传间隔以及最大重传次数,但是数据包重传回来的时候,这个包怎么合理利用它。同时,播放器则是按照时间轴不停播放的,数据没有来,是否还不停地要呢?这块有一个正反馈和负反馈的过程。

另外如果仅仅是重传数据包,没有记录或者管理数据包从第一次到最后重传了多少次,这个包重传成功总共消耗了多少时间,这个环节是非常有价值的,包括许多开源算法包括 WebRtc 这一块的逻辑也是没有做的。通过对重传数据包所消耗的时间管理,可以精细化的去控制接下来我们会讲的 Jitter Buffer 之前的配合,比如我们有了这个重传消耗时长,我们就可以知道让 Jitter Buffer 未来的一段时间等待多久,另外对于已经解码成功的数据随着时间轴实时的在播放,如果时间轴播放到了某些缺失的数据包应该出现的地方,某些数据包再重传回来也不要了,这时候也要及时去更新重传列表,这也是重传效率的问题。

怎么样去精细化升级算法,做了两方面的工作。一般重传两个关键因素,一个是重传次数,再一个是重传间隔。重传间隔首先不能小于 RTT,一般都是 1 点几倍率的 RTT 时间间隔要一次包,在一个 RTT 时间去等它,如果不来再去要。然后还会考虑一个基于:“截断二进制指数退避“的算法概念。比如说 20%,理论上重传几次就够了,30%、40%、50% 甚至 80% 重传几次,如果超过这个次数的上限,再结合比如说带宽预测状态或者 RTT 值的情况,及时中止这种行为,这种行为可以避免系统本身因为算法本身不合理造成的流量雪崩。

音频的抗抖动处理

接下来就是抗抖动。抖动怎么理解呢?有些网络情况下不太容易发生,在网络拥塞 Congestion Control 那块,我们在介绍 GCC1 和 GCC 算法的时候解释了 Jitter 的计算方法,以及它出现的一些原因。在使用 Wifi 网络的情况下经常有五六百毫秒抖动非常正常,所以如果对抗网络抖动也是一个非常关键的功能。从 GIPS 开源之前,NetEQ(Net equalizer)就被作为引擎内部的王牌特性,被用来对抗各种情况网络抗延时控制,以及抗击抖动的最佳算法实践。它开源以后,大家基于此的技术进行追踪、优化都是受益匪浅的。

看左上角这个网络真实来包的情况,右下角则是期望通过抗抖处理送给解码器均匀的数据包,理想的包是这样顺序且均匀间隔的数据包,但现实总是不美好的,来的包总是非常不均匀,甚至几百毫秒没有数据,然后接下来突发几秒数据一起到来。这个 Jitter Buffer 的作用就是,尽量去维持合适的数据包水位,这个水位也最终会影响到整个系统的端到端延时,水位太低或者太高都是不行的,太高的话我们及时把这个降下来,太低我们要及时把它调整到认为合理的一个位置。合理的水位可以对抗网络突发,播放端则因为 Jitter Buffer 能够保持合理水位,拥有稳定持续的数据源用来播放,这对于用户最终听到完整和高质量的声音都是非常有帮助的

Jitter Buffer 通过监测什么变量去看抖动行为呢?刚才我们在网络拥塞控制那张讲的 Jitter 的计算,需要发送端时间戳和接收端时间戳去计算。而这里它只是通过相邻两个包到达时间的间隔,最终这个 IAT(Inter Arrival Time)的概念去表征这个时间间隔,他表示端到端前后两个数据包到达的时间间隔,这个 IAT 时间间隔归一化为包个数,对一定时间区间内的 IAT 数据做一个概率分布,它是满足正态分布的趋势,我们取它是满足 95% 的概率分布作为置信区间,选取其中的最大 IAT 来估算当前网络的大延时。

刚才讲的网络延时估计与跟踪,相当于它在对网络包进行合理缓存,保证数据的完整性。那么怎么样把已经收到的数据包延时压下来,或者让数据包水位不够的时候,把这个时间轴拉长?其实这里面也是一个 Waveform Similarty 的算法,我们叫 Wsola,它兼顾 PCM 数据在幅度上,频率上、以及相位上的延续性算法,效果还是不错的。这个算法是一种基于拓展压缩语音长度,是一个变长不变调的技术,它是基于 Pitch 基音周期进行相似波形叠加的一个算法。

音频编解码器的抗丢包能力

Codec 编解码器专题一般会去讲解码器的历史或音质对比,我们今天从网络抗性的角度重点是看它的带内抗丢包能力,或者 Codec 层面能够给抗丢包带来一些什么收益

Codec 层面不能指望完全无损的把二进制包恢复,那它的优势就哪里?

它的优势在于可以节省包头,不管是以前的私有协议还是现在的 RTP 协议,用于传输的 IP,UDP 协议字段是节省不了的,信道编码的方法比如带外 FEC,或者重传 ARQ 都是对完整数据包进行恢复或者请求重传,这里面的数据包头占用了许多流量。而所以在 Codec 中的带内 FEC 里面的实现算法里面,一般来说它可以携带 t-1 帧的数据,而这个 t-1 帧的数据包可以用一个低码率的参数进行编码,在接收端收到这个携带 t-1 帧的数据包,则可以解码重建出来 t-1 这一帧质量稍逊一点的数据。

讲到这里就是大家也有个疑问,比如说 silk 也是 t-1,然后它的抗丢包特性大概 15%,而最最新版本的 Opus1.3.1 大家可以听一下不同丢包率场景下他的表现,Opus 在内它为什么最后 30% 呢?

这个图就是刚才说的全家桶算法里面使用的抗丢包算法基本都包括在里面了,我们所使用的一些方法在这个合集里面只是占其中一小部分。PLC 就是 Packet Loss Concealment,丢包恢复算法它的内涵还是比较丰富的。画一个树状图,把整个合集集中起来发现它有两大阵营,一个是基于发端的,一个是基于收端的。基于发端的有被动式和主动式,重传类的就是主动式,前向纠错的就是被动式

至于重传为什么给它送到发端?以我的理解,在接收端不管是 Ack, Go-N Ack 或者是 NACK 方式,都是接收端的反馈,真正要发包还是在发送端的,所以还是基于发端。

基于发端的另外一个大类就是 FEC。前面讲的 FEC 工程实践和原理就是所里德—罗门算法,这种算法还有很多类似的,比如喷泉码,比如 RLNC。这个 RLNC 有个比较好的特性,可以支持重建码,比如在网络比较好的情况下,我现在收听上百人千人,针对不同的下行用户,再根据下行信道的参数进行重新编码,不至于像用喷泉、RS 或异或只能保持原状。另外,其中信源相关的 FEC 就是上一页讲的 Codec 层面的带内 FEC。

基于接收端有很多方法,比如插入式方法,比如在接收端,那么插入静音、白噪音,或者干脆把前面一个包复制一下,就不管它的相关性和衔接,这样效果不太好。而且它的补偿效果也就是 20 毫秒顶天了,在衔接不好的情况下还容易产生金属音。

另外一大类就是内插式的方法,比如波形相似法,还有基音周期复制法,还有就是波形伸缩法。到这块,所有的方法就能很好地解决幅度连续、频率连续、相位连续的特征,所以它的效果总体来说还是不错的。

另外基于接收端的,它有一类再生补偿方法。比如时域数据传输本身挺大的,如果在变换域的话,它可能只需要一些变换域的参数,在接收端进行恢复。

再接下来还有一些比较偏门的技术,比如基于传统的语音增强,比如自适应滤波器的一些方法,进行语音重建的方法,这里不说了。前面做选择的方案也仅仅是使用比较多的方法,通过有机融合把它更好的控制起来,最终达到好的效果。

现在回答一下,刚才上一页为什么 Silk15%、Opus 达到 30%。这是一系列基于接收端的技术,不断升级、不断演进、不断优化的效果,T-1 只是一个工程化的思想,做 T-2 可不可以,如果不再考虑算法因素 T-3 行不行?

于是就引出来实验室目前正在重点发力的基于机器学习的方法来做 PLC,用上下文分析的方法,这就是目前的一个效果,大家可以看到这块有四个语音的时域图。左边这个图丢失了 100 毫秒数据,100 毫秒看似非常少,它是个什么概念呢,按照正常语速大概一个字是 150 毫秒,100 毫秒基本上大半个字丢了。我们通过机器学习 PLC 的种方法把它给恢复出来,而且效果还不错。

腾讯会议为什么能取得疫情期间高速增长?

最后,疫情期间腾讯会议为什么能在短短两个多月时间,外部用户可以从 0 做到 1000 万?这是有一定的有必然性的,首先“腾讯会议”产品是一个全平台的应用,PC、手机、传统电话、Pad、网页、小程序还有专业视频设备都可以接入的,这种互联互通的能力本身就给广大用户带来了价值。今天官微公布,腾讯会议 API 向全网开放了,国内外开发者都可以接入腾讯会议的 API,去把这个市场做大。

另外也要归功于腾讯会议的海量用户群体,10 亿微信用户、8 亿 QQ 用户,还有 250 万企业微信用户,基本上覆盖了所有的中国用户。我记得张小龙有一句话,七个价值观,其中之一就是说:“让需求自然生长”。在疫情期间,“腾讯会议”的极速扩张就是一个自然生长的过程。为了助力疫情期间人与人减少接触,全免费让大家使用体验,这件事情符合实际需求,就是它短期内爆发的又一个原因。

腾讯云做 ToB 业务之后,它给腾讯内外部的各种产品提供了强大的支撑力,遍布全球 130 多个国家,1300 多个的加速节点,专网接入,音视频会议最低延时达到 80 毫秒,而且动态扩容能力非常强。值得一提的是,疫情期间我们发现有“腾讯会议”用户量高峰的规律变化,许多人从早上六点开始工作,然后 6 点半一拨,7 点一拨高峰,后来发现是各地的医生护士在线沟通进度,向大家说一声辛苦了。

QA

Q: Opus 能达到 30% 的抗性的结论是怎么得到的?请问音频编码带内如何,包括和带外如何结合进行优化?

A:对于网络抗性或弱网的抗性,为了总量保证音频质量,我们提供了一揽子结合方案。比如 Opus 的带内抗性,它是从工程化角度做的一个概念,是发端数据包内编码携带前一帧质量稍差的一帧压缩数据,并且结合接收端的不断升级的 PLC 算法。这个 Opus 带内抗性是编解码器原生提供的抗丢包能力,通过专业的思伯伦设备测试在 30% 丢包率的场景下可以达到 3.0 分以上的效果,这是一个客观的数据。

第二个问题是个好问题,就像刚才讲的,怎么样把各个手段优点结合发挥出来。有一句俗话说,甘蔗没有两头甜,我们就要做到怎么让它两头都甜,而且还要在系统里配合良好,有机运转。

我举个例子,FEC 的算法落地,在照顾正常网络的情况下,同时还能自适应去 Cover 突发小丢包网络,我们会做一些假定,比如就认为在通话过程一定会有突发状况、各种各样的丢包。那么我们会预设一部分的带外 FEC,带外的优点和缺点也是很明确的,最大缺点就是费流量。Codec 技术发展到今天得到了长足进步,我们就可以用带内 FEC 做这方面的抗丢包。

至于重传这块怎么结合?首先要有对这个产品的定位,比如腾讯会议它的定位是实时交流产品,一定要保延时,同时应对复杂网络,各种各样的复杂网络。

怎么做到低延时还抗大丢包,带外 FEC 的最大特点就是说费流量,但是它可以延时做得非常低,它的延时受限于分组延时,重传的话效率非常高,但又受到网络 RTT 的影响。

我概括成一句,正常网络用带内去加常态,在系统边界上限要求抗超大丢包而且还要考虑延时的时候,使用带外 FEC 算法那,但是对于带外 FEC 的使用要配合控制精准的网络拥塞算法,即做到能收能放。此外,重传 ARQ 对于的突发丢包,会有比较好的效果,另外对于综合抗丢包能力也是一种很好的补充。通过这三种有机结合,基本上会有比较好的结果。

Q: 关于语音激励是怎么实现的?现在的架构是 SFU 还是 MCU,是自源的还是开源的?

A:我们目前是自研的方案,是混合的,SFU 和 MCU 对于语音能力来说,我们的系统在混音服务器是可以根据用户的需要可配置的,我们可以做到 Server 端全混音,也可以客户端混音。当然既然支持融合通信,不然我们多路数据输出一路给 PSTN,一定是经过 MCU 的。

语音激励是怎么实现的,实际上是 MCU 本身的一个基础的语音选路能力,选路信息再由 MCU 同步到客户端就 OK 了。

Q:FEC 码的信息量与音频信息量的比例是怎样的?

A: 这块还是关于系统边界的问题,一般是说产品的规格。从 QQ 技术交流一直到 2019 年、2020 年, 21 年的积淀,在不同时代基于当时的网络情况和市场需求,当时的定位,产品规格都是不一样的。

现在问一个具体的信息量比的具体数字,它的意义不是太大,这跟产品规格也有关系。如果有一个强大的 ARC 去统筹、去控制,结合今天讲的全家桶工具,那么做任何类型的实时音视频类产品,只要定好规则,基本上就是可以实现,而且做得效果还不错。

讲师简介

王晓海,腾讯多媒体实验室高级研究员。王晓海于 2011 年加入腾讯,曾参与开发 QQ 音频 TRAE 引擎、OpenSDK 引擎、GME 游戏语音引擎、以及腾讯会议 XCast 引擎。加入腾讯前,王晓海于 2006 年进入中科院软件所嵌入式软件开发中心 (Casky 中科开元),拥有丰富的嵌入式多媒体开发、音频编解码、音效处理算法及优化经验。2011 年加入腾讯后,参与开发了腾讯第一代自研音频引擎 TRAE(Tencent Realtime Audio Engine),深耕音频 + 网络算法方向,负责实时音频 SDK 中的音频流控、音频网络抗性、网络拥塞控制、以及相关功能设计和开发工作。

前言最近高德地图APP完成了一次启动优化专项,超预期将双端启动的耗时都降低了65%以上,iOS在iPhone7上速度达到了400毫秒以内。就像产品们用后说的,快到不习惯。算一下每天为用户省下的时间,还是蛮有成就感的,本文做个小结。

 

 

(文中配图均为多才多艺的技术哥哥手绘)

启动阶段性能多维度分析

要优化,首先要做到的是对启动阶段的各个性能纬度做分析,包括主线程耗时、CPU、内存、I/O、网络。这样才能更加全面的掌握启动阶段的开销,找出不合理的方法调用。

启动越快,更多的方法调用就应该做成按需执行,将启动压力分摊,只留下那些启动后方法都会依赖的方法和库的初始化,比如网络库、Crash库等。而剩下那些需要预加载的功能可以放到启动阶段后再执行。

启动有哪几种类型,有哪些阶段呢?

启动类型分为:

  • Cold:APP重启后启动,不在内存里也没有进程存在。
  • Warm:APP最近结束后再启动,有部分在内存但没有进程存在。
  • Resume:APP没结束,只是暂停,全在内存中,进程也存在。

分析阶段一般都是针对Cold类型进行分析,目的就是要让测试环境稳定。为了稳定测试环境,有时还需要找些稳定的机型,对于iOS来说iPhone7性能中等,稳定性也不错就很适合,Android的Vivo系列也相对稳定,华为和小米系列数据波动就比较大。

除了机型外,控制测试机温度也很重要,一旦温度过高系统还会降频执行,影响测试数据。有时候还会设置飞行模式采用Mock网络请求的方式来减少不稳定的网络影响测试数据。最好是重启后退iCloud账号,放置一段时间再测,更加准确些。

了解启动阶段的目的就是聚焦范围,从用户体验上来确定哪个阶段要快,以便能够让用户可视和响应用户操作的时间更快。

简单来说iOS启动分为加载Mach-O和运行时初始化过程,加载Mach-O会先判断加载的文件是不是Mach-O,通过文件第一个字节,也叫魔数来判断,当是下面四种时可以判定是Mach-O文件:

  • 0xfeedface对应的loader.h里的宏是MH_MAGIC
  • 0xfeedfact宏是MH_MAGIC_64
  • NXSwapInt(MH_MAGIC)宏MH_GIGAM
  • NXSwapInt(MH_MAGIC_64)宏MH_GIGAM_64

Mach-O主要分为:

  • 中间对象文件(MH_OBJECT)
  • 可执行二进制(MH_EXECUTE)
  • VM 共享库文件(MH_FVMLIB)
  • Crash 产生的Core文件(MH_CORE)
  • preload(MH_PRELOAD)
  • 动态共享库(MH_DYLIB)
  • 动态链接器(MH_DYLINKER)
  • 静态链接文件(MH_DYLIB_STUB)符号文件和调试信息(MH_DSYM)这几种。

确定是Mach-O后,内核会fork一个进程,execve开始加载。检查Mach-O Header。随后加载dyld和程序到Load Command地址空间。通过 dyld_stub_binder开始执行dyld,dyld会进行rebase、binding、lazy binding、导出符号,也可以通过DYLD_INSERT_LIBRARIES进行hook。

dyld_stub_binder给偏移量到dyld解释特殊字节码Segment中,也就是真实地址,把真实地址写入到la_symbol_ptr里,跳转时通过stub的jump指令跳转到真实地址。dyld加载所有依赖库,将动态库导出的trie结构符号执行符号绑定,也就是non lazybinding,绑定解析其他模块功能和数据引用过程,就是导入符号。

Trie也叫数字树或前缀树,是一种搜索树。查找复杂度O(m),m是字符串的长度。和散列表相比,散列最差复杂度是O(N),一般都是 O(1),用 O(m)时间评估 hash。散列缺点是会分配一大块内存,内容越多所占内存越大。Trie不仅查找快,插入和删除都很快,适合存储预测性文本或自动完成词典。

为了进一步优化所占空间,可以将Trie这种树形的确定性有限自动机压缩成确定性非循环有限状态自动体(DAFSA),其空间小,做法是会压缩相同分支。

对于更大内容,还可以做更进一步的优化,比如使用字母缩减的实现技术,把原来的字符串重新解释为较长的字符串;使用单链式列表,节点设计为由符号、子节点、下一个节点来表示;将字母表数组存储为代表ASCII字母表的256位的位图。

尽管Trie对于性能会做很多优化,但是符号过多依然会增加性能消耗,对于动态库导出的符号不宜太多,尽量保持公共符号少,私有符号集丰富。这样维护起来也方便,版本兼容性也好,还能优化动态加载程序到进程的时间。

然后执行attribute的constructor函数。举个例子:

#include <stdio.h> __attribute__((constructor)) static void prepare() { printf("%s\n", "prepare"); } __attribute__((destructor)) static void end() { printf("%s\n", "end"); } void showHeader() { printf("%s\n", "header"); }

运行结果:

ming@mingdeMacBook-Pro macho_demo % ./main "hi" prepare hi end

运行时初始化过程分为:

  • 加载类扩展。
  • 加载C++静态对象。
  • 调用+load函数。
  • 执行main函数。
  • Application初始化,到applicationDidFinishLaunchingWithOptions执行完。
  • 初始化帧渲染,到viewDidAppear执行完,用户可见可操作。

 

也就是说对启动阶段的分析以viewDidAppear为截止。这次优化之前已经对Application初始化之前做过优化,效果并不明显,没有本质的提高,所以这次主要针对Application初始化到viewDidAppear这个阶段各个性能多纬度进行分析。

工具的选择其实目前看来是很多的,Apple提供的System Trace会提供全面系统的行为,可以显示底层系统线程和内存调度情况,分析锁、线程、内存、系统调用等问题。总的来说,通过System Trace能清楚知道每时每刻APP对系统资源的使用情况。

System Trace能查看线程的状态,可以了解高优线程使用相对于CPU数量是否合理,可以看到线程在执行、挂起、上下文切换、被打断还是被抢占的情况。虚拟内存使用产生的耗时也能看到,比如分配物理内存,内存解压缩,无缓存时进行缓存的耗时等。甚至是发热情况也能看到。

System Trace还提供手动打点进行信息显式,在你的代码中导入sys/kdebug_signpost.h后,配对kdebug_signpost_start和kdebug_signpost_end就可以了。这两个方法有五个参数,第一个是id,最后一个是颜色,中间都是预留字段。

Xcode11开始XCTest还提供了测量性能的Api。苹果在2019年WWDC启动优化专题:

https://developer.apple.com/videos/play/wwdc2019/423/

也介绍了Instruments里的最新模板App launch如何分析启动性能。但是要想达到对启动数据进行留存取均值、Diff、过滤、关联分析等自动化操作,App launch目前还没法做到。

下面针对主线程耗时、CPU、网络、内存、I/O 等多维度进行分析:

 

  • 主线程耗时

多个纬度性能分析中最重要、最终用户体感到的是主线程耗时分析。对主线程方法耗时可以直接使用Massier,这是everettjf开发的一个Objective-C方法跟踪工具:

https://everettjf.github.io/2019/05/06/messier/

生成trace json进行分析,或者参看这个代码

GCDFetchFeed/SMCallTraceCore.c at master · ming1016/GCDFetchFeed · GitHub

https://github.com/ming1016/GCDFetchFeed/blob/master/GCDFetchFeed/GCDFetchFeed/Lib/SMLagMonitor/SMCallTraceCore.c

自己手动hook objc_msgSend生成一份Objective-C方法耗时数据进行分析。还有种插桩方式,可以解析IR(加快编译速度),然后在每个方法前后插入耗时统计函数。

文章后面我会着重介绍如何开发工具进一步分析这份数据,以达到监控启动阶段方法耗时的目的。

hook所有的方法调用,对详细分析时很有用,不过对于整个启动时间影响很大,要想获取启动每个阶段更准确的时间消耗还需要依赖手动埋点。

为了更好的分析启动耗时问题,手动埋点也会埋的越来越多,也会影响启动时间精确度,特别是当团队很多,模块很多时,问题会突出。但是每个团队在排查启动耗时往往只会关注自己或相关某几个模块的分析,基于此,可以把不同模块埋点分组,灵活组合,这样就可以照顾到多种需求了。

  • CPU

为什么分析启动慢除了分析主线程方法耗时外,还要分析其它纬度的性能呢?

我们先看看启动慢的表现,启动慢意味着界面响应慢、网络慢(数据量大、请求数多)、CPU超负荷降频(并行任务多、运算多),可以看出影响启动的因素很多,还需要全面考虑。

对于CPU来说,WWDC的

What’s New in Energy Debugging - WWDC 2018 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2018/228/

介绍了用Energy Log来查CPU耗电,当前台三分钟或后台一分钟CPU线程连续占用80%以上就判定为耗电,同时记录耗电线程堆栈供分析。还有一个MetrickKit专门用来收集电源和性能统计数据,每24小时就会对收集的数据进行汇总上报,Mattt在NShipster网站上也发了篇文章专门进行介绍:

https://nshipster.com/metrickit/

那么,CPU的详细使用情况如何获取呢?也就是说哪个方法用了多少CPU。

有好几种获取详细CPU使用情况的方法。线程是计算机资源调度和分配的基本单位。CPU使用情况会提现到线程这样的基本单位上。task_theads的act_list数组包含所有线程,使用thread_info的接口可以返回线程的基本信息,这些信息定义在thread_basic_info_t结构体中。这个结构体内的信息包含了线程运行时间、运行状态以及调度优先级,其中也包含了CPU使用信息cpu_usage。

获取方式参看:

objective c - Get detailed iOS CPU usage with different states - Stack Overflow

https://stackoverflow.com/questions/43866416/get-detailed-ios-cpu-usage-with-different-states

GT GitHub - Tencent/GT

https://github.com/Tencent/GT

也有获取CPU的代码。

整体CPU占用率可以通过host_statistics函数取到host_cpu_load_info,其中cpu_ticks数组是CPU运行的时钟脉冲数量。通过cpu_ticks数组里的状态,可以分别获取CPU_STATE_USER、CPU_STATE_NICE、CPU_STATE_SYSTEM这三个表示使用中的状态,除以整体CPU就可以取到CPU的占比。

通过NSProcessInfo的activeProcessorCount还可以得到CPU的核数。线上数据分析时会发现相同机型和系统的手机,性能表现却截然不同,这是由于手机过热或者电池损耗过大后系统降低了CPU频率所致。

所以,如果取得CPU频率后也可以针对那些降频的手机来进行针对性的优化,以保证流畅体验。获取方式可以参考:

https://github.com/zenny-chen/CPU-Dasher-for-iOS

  • 内存

要想获取APP真实的内存使用情况可以参看WebKit的源码:

https://github.com/WebKit/webkit/blob/52bc6f0a96a062cb0eb76e9a81497183dc87c268/Source/WTF/wtf/cocoa/MemoryFootprintCocoa.cpp

JetSam会判断APP使用内存情况,超出阈值就会杀死APP,JetSam获取阈值的代码在这里:

https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/bsd/kern/kern_memorystatus.c

整个设备物理内存大小可以通过NSProcessInfo的physicalMemory来获取。

  • 网络

对于网络监控可以使用Fishhook这样的工具Hook网络底层库CFNetwork。网络的情况比较复杂,所以需要定些和时间相关的关键的指标,指标如下:

  • DNS时间
  • SSL时间
  • 首包时间
  • 响应时间

有了这些指标才能够有助于更好的分析网络问题。启动阶段的网络请求是非常多的,所以HTTP的性能是非常要注意的。以下是WWDC网络相关的Session:

Your App and Next Generation Networks - WWDC 2015 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2015/719/

Networking with NSURLSession - WWDC 2015 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2015/711/

Networking for the Modern Internet - WWDC 2016 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2016/714/

Advances in Networking, Part 1 - WWDC 2017 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2017/707/

Advances in Networking, Part 2 - WWDC 2017 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2017/709/

Optimizing Your App for Today’s Internet - WWDC 2018 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2018/714/

  • I/O

对于I/O可以使用

Frida • A world-class dynamic instrumentation framework | Inject JavaScript to explore native apps on Windows, macOS, GNU/Linux, iOS, Android, and QNX

https://www.frida.re/

这种动态二进制插桩技术,在程序运行时去插入自定义代码获取I/O的耗时和处理的数据大小等数据。Frida还能够在其它平台使用。

关于多维度分析更多的资料可以看看历届WWDC的介绍。下面我列下16年来 WWDC关于启动优化的Session,每场都很精彩。

Using Time Profiler in Instruments - WWDC 2016 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2016/418/

Optimizing I/O for Performance and Battery Life - WWDC 2016 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2016/719/

Optimizing App Startup Time - WWDC 2016 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2016/406/

App Startup Time: Past, Present, and Future - WWDC 2017 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2017/413/

Practical Approaches to Great App Performance - WWDC 2018 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2018/407/

Optimizing App Launch - WWDC 2019 - Videos - Apple Developer

https://developer.apple.com/videos/play/wwdc2019/423/

延后任务管理

 

经过前面所说的对主线程耗时方法和各个纬度性能分析后,对于那些分析出来没必要在启动阶段执行的方法,可以做成按需或延后执行。

任务延后的处理不能粗犷的一口气在启动完成后在主线程一起执行,那样用户仅仅只是看到了页面,依然没法响应操作。那该怎么做呢?套路一般是这样,创建四个队列,分别是:

  • 异步串行队列
  • 异步并行队列
  • 闲时主线程串行队列
  • 闲时异步串行队列

有依赖关系的任务可以放到异步串行队列中执行。异步并行队列可以分组执行,比如使用dispatch_group,然后对每组任务数量进行限制,避免CPU、线程和内存瞬时激增影响主线程用户操作,定义有限数量的串行队列,每个串行队列做特定的事情,这样也能够避免性能消耗短时间突然暴涨引起无法响应用户操作。使用dispatch_semaphore_t在信号量阻塞主队列时容易出现优先级反转,需要减少使用,确保QoS传播。可以用dispatch group替代,性能一样,功能不差。异步编程可以直接GCD接口来写,也可以使用阿里的协程框架

coobjc GitHub - alibaba/coobjc

https://github.com/alibaba/coobjc

闲时队列实现方式是监听主线程runloop状态,在kCFRunLoopBeforeWaiting时开始执行闲时队列里的任务,在kCFRunLoopAfterWaiting时停止。

优化后如何保持?

攻易守难,就像刚到新团队时将包大小减少了48兆,但是一年多一直能够守住,除了决心还需要有手段。对于启动优化来说,将各个性能纬度通过监控的方式盯住是必要的,但是发现问题后快速、便捷的定位到问题还是需要找些突破口。我的思路是将启动阶段方法耗时多的按照时间线一条一条排出来,每条包括方法名、方法层级、所属类、所属模块、维护人。考虑到便捷性,最好还能方便的查看方法代码内容。

接下来我通过开发一个工具,详细介绍下怎么实现这样的效果。

  • 解析json

如前面所说在输出一份Chrome trace规范的方法耗时json后,先要解析这份数据。这份json数据类似下面的样子:

{"name":"[SMVeilweaa]upVeilState:","cat":"catname","ph":"B","pid":2381,"tid":0,"ts":21}, {"name":"[SMVeilweaa]tatLaunchState:","cat":"catname","ph":"B","pid":2381,"tid":0,"ts":4557}, {"name":"[SMVeilweaa]tatTimeStamp:state:","cat":"catname","ph":"B","pid":2381,"tid":0,"ts":4686}, {"name":"[SMVeilweaa]tatTimeStamp:state:","cat":"catname","ph":"E","pid":2381,"tid":0,"ts":4727}, {"name":"[SMVeilweaa]tatLaunchState:","cat":"catname","ph":"E","pid":2381,"tid":0,"ts":5732}, {"name":"[SMVeilweaa]upVeilState:","cat":"catname","ph":"E","pid":2381,"tid":0,"ts":5815}, …

通过Chrome的Trace-Viewer可以生成一个火焰图。其中name字段包含了类、方法和参数的信息,cat字段可以加入其它性能数据,ph为B表示方法开始,为E表示方法结束,ts字段表示。

很多工程在启动阶段会执行大量方法,很多方法耗时很少,可以过滤那些小于10毫秒的方法,让分析更加聚焦。

耗时的高低也做了颜色的区分。外部耗时指的是子方法以外系统或没源码的三方方法的耗时,规则是父方法调用的耗时减去其子方法总耗时。

目前为止通过过滤耗时少的方法调用,可以更容易发现问题方法。但是,有些方法单次执行耗时不多,但是会执行很多次,累加耗时会大,这样的情况也需要体现在展示页面里。另外外部耗时高时或者碰到自己不了解的方法时,是需要到工程源码里去搜索对应的方法源码进行分析的,有的方法名很通用时还需要花大量时间去过滤无用信息。

因此接下来还需要做两件事情,首先累加方法调用次数和耗时,体现在展示页面中,另一个是从工程中获取方法源码能够在展示页面中进行点击显示。

完整思路如下图:

  • 展示方法源码

在页面上展示源码需要先解析.xcworkspace文件,通过.xcworkspace文件取到工程里所有的.xcodeproj文件。分析.xcodeproj文件取到所有.m和.mm源码文件路径,解析源码,取到方法的源码内容进行展示。

解析.xcworkspace

开.xcworkspace,可以看到这个包内主要文件是contents.xcworkspacedata。内容是一个xml:

<?xml version="1.0" encoding="UTF-8"?> <Workspace version = "1.0"> <FileRef location = "group:GCDFetchFeed.xcodeproj"> </FileRef> <FileRef location = "group:Pods/Pods.xcodeproj"> </FileRef> </Workspace>

解析.xcodeproj

通过XML的解析可以获取FileRef节点内容,xcodeproj的文件路径就在FileRef节点的location属性里。每个xcodeproj文件里会有project工程的源码文件。为了能够获取方法的源码进行展示,那么就先要取出所有project工程里包含的源文件的路径。

xcodeproj的文件内容看起来大概是下面的样子。

其实内容还有很多,需要一个个解析出来。

考虑到xcodeproj里的注释很多,也都很有用,因此会多设计些结构来保存值和注释。思路是根据XcodeprojNode的类型来判断下一级是key value结构还是array结构。如果XcodeprojNode的类型是dicStart表示下级是key value结构。如果类型是arrStart就是array结构。当碰到类型是dicEnd,同时和最初dicStart是同级时,递归下一级树结构。而arrEnd不用递归,xcodeproj里的array只有值类型的数据。

有了基本节点树结构以后就可以设计xcodeproj里各个section的结构。主要有以下的section:

  • PBXBuildFile:文件,最终会关联到PBXFileReference。
  • PBXContainerItemProxy:部署的元素。
  • PBXFileReference:各类文件,有源码、资源、库等文件。
  • PBXFrameworksBuildPhase:用于framework的构建。
  • PBXGroup:文件夹,可嵌套,里面包含了文件与文件夹的关系。
  • PBXNativeTarget:Target的设置。
  • PBXProject:Project的设置,有编译工程所需信息。
  • PBXResourcesBuildPhase:编译资源文件,有xib、storyboard、plist以及图片等资源文件。
  • PBXSourcesBuildPhase:编译源文件(.m)。
  • PBXTargetDependency:Taget的依赖。
  • PBXVariantGroup:.storyboard文件。
  • XCBuildConfiguration:Xcode编译配置,对应Xcode的Build Setting面板内容。
  • XCConfigurationList:构建配置相关,包含项目文件和target文件。

得到section结构Xcodeproj后,就可以开始分析所有源文件的路径了。根据前面列出的section的说明,PBXGroup包含了所有文件夹和文件的关系,Xcodeproj的pbxGroup字段的key是文件夹,值是文件集合,因此可以设计一个结构体XcodeprojSourceNode用来存储文件夹和文件关系。

接下来需要取得完整的文件路径。通过recusiveFatherPaths函数获取文件夹路径。这里需要注意的是需要处理 ../ 这种文件夹路径符。

解析.m .mm文件

对Objective-C解析可以参考LLVM,这里只需要找到每个方法对应的源码,所以自己也可以实现。分词前先看看LLVM是怎么定义token的。定义文件在这里:

https://opensource.apple.com/source/lldb/lldb-69/llvm/tools/clang/include/clang/Basic/TokenKinds.def

根据这个定义我设计了token的结构体,主体部分如下:

// 切割符号 [](){}.&=*+-<>~!/%^|?:;,#@ public enum OCTK { case unknown // 不是 token case eof // 文件结束 case eod // 行结束 case codeCompletion // Code completion marker case cxxDefaultargEnd // C++ default argument end marker case comment // 注释 case identifier // 比如 abcde123 case numericConstant(OCTkNumericConstant) // 整型、浮点 0x123,解释计算时用,分析代码时可不用 case charConstant // ‘a’ case stringLiteral // “foo” case wideStringLiteral // L”foo” case angleStringLiteral // <foo> 待处理需要考虑作为小于符号的问题 // 标准定义部分 // 标点符号 case punctuators(OCTkPunctuators) // 关键字 case keyword(OCTKKeyword) // @关键字 case atKeyword(OCTKAtKeyword) }

完整的定义在这里:

MethodTraceAnalyze/ParseOCTokensDefine.swift

https://github.com/ming1016/MethodTraceAnalyze/blob/master/MethodTraceAnalyze/OC/ParseOCTokensDefine.swift

分词过程可以参看LLVM的实现:

clang: lib/Lex/Lexer.cpp Source File

http://clang.llvm.org/doxygen/Lexer_8cpp_source.html

我在处理分词时主要是按照分隔符一一对应处理,针对代码注释和字符串进行了特殊处理,一个注释一个token,一个完整字符串一个token。我分词实现代码:

MethodTraceAnalyze/ParseOCTokens.swift

https://github.com/ming1016/MethodTraceAnalyze/blob/master/MethodTraceAnalyze/OC/ParseOCTokens.swift

由于只要取到类名和方法里的源码,所以语法分析时,只需要对类定义和方法定义做解析就可以,语法树中节点设计:

// OC 语法树节点 public struct OCNode { public var type: OCNodeType public var subNodes: [OCNode] public var identifier: String // 标识 public var lineRange: (Int,Int) // 行范围 public var source: String // 对应代码 } // 节点类型 public enum OCNodeType { case `default` case root case `import` case `class` case method }

其中lineRange记录了方法所在文件的行范围,这样就能够从文件中取出代码,并记录在source字段中。

解析语法树需要先定义好解析过程的不同状态:

private enum RState { case normal case eod // 换行 case methodStart // 方法开始 case methodReturnEnd // 方法返回类型结束 case methodNameEnd // 方法名结束 case methodParamStart // 方法参数开始 case methodContentStart // 方法内容开始 case methodParamTypeStart // 方法参数类型开始 case methodParamTypeEnd // 方法参数类型结束 case methodParamEnd // 方法参数结束 case methodParamNameEnd // 方法参数名结束 case at // @ case atImplementation // @implementation case normalBlock // oc方法外部的 block {},用于 c 方法 }

完整解析出方法所属类、方法行范围的代码在这里:

MethodTraceAnalyze/ParseOCNodes.swift

https://github.com/ming1016/MethodTraceAnalyze/blob/master/MethodTraceAnalyze/OC/ParseOCNodes.swift

解析.m和.mm文件,一个一个串行解的话,对于大工程,每次解的速度很难接受,所以采用并行方式去读取解析多个文件。经过测试,发现每组在60个以上时能够最大利用我机器(2.5 GHz双核Intel Core i7)的CPU,内存占用只有60M,一万多.m文件的工程大概2分半能解完。

使用的是dispatch group的wait,保证并行的一组完成再进入下一组。

现在有了每个方法对应的源码,接下来就可以和前面trace的方法对应上。页面展示只需要写段js就能够控制点击时展示对应方法的源码。

页面展示

在进行HTML页面展示前,需要将代码里的换行和空格替换成HTML里的对应的和 。

let allNodes = ParseOC.ocNodes(workspacePath: “/Users/ming/Downloads/GCDFetchFeed/GCDFetchFeed/GCDFetchFeed.xcworkspace”) var sourceDic = [String:String]() for aNode in allNodes { sourceDic[aNode.identifier] = aNode.source.replacingOccurrences(of: “\n”, with: “</br>”).replacingOccurrences(of: “ “, with: “ ”) }

用p标签作为源码展示的标签,方法执行顺序的编号加方法名作为p标签的id,然后用display: none; 将p标签隐藏。方法名用a标签,click属性执行一段js代码,当a标签点击时能够显示方法对应的代码。这段js代码如下:

function sourceShowHidden(sourceIdName) { var sourceCode = document.getElementById(sourceIdName); sourceCode.style.display = “block”; }

最终效果如下图:

将动态分析和静态分析进行了结合,后面可以通过不同版本进行对比,发现哪些方法的代码实现改变了,能展示在页面上。还可以进一步静态分析出哪些方法会调用到I/O函数、起新线程、新队列等,然后展示到页面上,方便分析。

读到最后,可以看到这个方法分析工具并没有用任何一个轮子,其实有些是可以使用现有轮子的,比如json、xml、xcodeproj、Objective-C语法分析等,之所以没有用是因为不同轮子使用的语言和技术区别较大,当格式更新时如果使用的单个轮子没有更新会影响整个工具。开发这个工具主要工作是在解析上,所以使用自有解析技术也能够让所做的功能更聚焦,不做没用的功能,减少代码维护量,所要解析格式更新后,也能够自主去更新解析方式。更重要的一点是可以亲手接触下这些格式的语法设计。

结语

本文小结了启动优化的技术手段,总的来说,对启动进行优化的决心的重要程度是远大于技术手段的,决定着是否能够优化的更多。技术手段有很多,我觉得手段的好坏区别只是在效率上,最差的情况全用手动一个个去查耗时也是能够解题的。

建筑架构和软件架构有很多共同点,但是建筑师在学习期间会观察数以千计的建筑,并研究大师们对这些建筑的评论。相比之下,大多数软件开发设计人员只熟悉少数大型程序——通常是他们自己编写的程序——而很少研究历史上伟大的程序。结果,他们重复彼此的错误,而不是在彼此成功的基础上再接再厉。

本文为《The Architecture of Open Source Applications》的学习笔记,四十多个著名开源软件的作者深度剖析他们的作品,讲述系统如架构如何设计,为什么这么设计,以及从中获得的经验教训。

Asterisk 1是基于GPLv2协议发布的一款开源电话应用平台。简单地说,这是一个服务端程序,用于处理电话的拨出、接入以及自定义流程。Asterisk得名于Unix通配符:*,该项目的宗旨是能做所有与电话相关的事情。如今的Asterisk已经支持一系列用于接拨电话的技术。这些技术包括诸多VoIP(Voice over IP,语音IP)协议,与传统电话网络的模/数连接性,以及PSTN(Public Swithed Telephone Network,公共交换电话网络)。

 

1.1 关键架构概念

本节讨论一些跟Asterisk每一部分息息相关的概念。这些思想是Asterisk架构的基础。

1.1.1 通道

通道表示Asterisk系统与某电话端点的一条连接(如图1)。一路电话呼叫接入了Asterisk系统,就用通道表示这一连接。在Asterisk代码中,通道是数据结构ast_channel的实例。图中这个呼叫场景可以视为呼叫方与某一系统应用(比如语音信箱)的交互。

 

图1.1 一个通道表示一条呼叫线路

 

1.1.2 通道桥接

更熟悉的一个呼叫场景是两个电话之间的连接:一个人使用电话A呼叫另一个使用电话B的人。在此场景下,连接到Asterisk系统的有两个电话终端,因而分配了两个通道(如图1.2)。

 

图1.2 两个通道表示两条呼叫线路

 

如上图连接的通道称之为通道桥接。为了实现在通道间传输媒体的目的而把通道连接起来,称为通道桥接。然而,在电话呼叫过程中也可能有视频流或文本流。即使有多种类型的媒体流,也是由Asterisk系统中负责连接两端的通道独立处理。

有两种方法可以完成两个通道的桥接:通用桥接和本地桥接。通用桥接时,无论通道使用什么技术都能够工作,它上层通过Asterisk抽象通道接口传输所有的音频和信号。尽管这是一种最灵活的桥接方式,却是最低效的,因为完成桥接必须有多层抽象。图1.2描述的就是通用桥接。

本地桥接是面向特定技术的通道连接方式。如果连接到Asterisk的两个通道使用相同的媒体传输技术,则势必有一种比通过抽象层更为高效的连接方式,因为抽象层是为使用不同技术的通道之间连接而准备的。例如,如果使用相同专用硬件连接的电话网络,则可以在硬件上直接桥接,这样根本不必通过应用程序向上流动,只有呼叫信号流经Asterisk,通话的媒体流直接连接,高效的多。

在桥接两个通道的时候,系统通过比较两通道的连接技术来决定使用通用桥接还是本地桥接。如果两通道都标识出支持相同的连接方式,那么就用本地桥接;反之使用通用方式。图1.3描述的是本地桥接的一个实例,呼叫信号流经Asterisk传递,媒体流建立直接连接。

 

图1.3 本地桥接实例

 

1.1.3 帧

在呼叫过程中,Asterisk帧来通信使用帧,帧是数据结构ast_frame的实例。帧可以是媒体帧,也可以是信号帧。在一个基本的呼叫过程中,媒体帧的流包含音频,是通过系统传输的。信号帧则用于发送呼叫事件相关的消息,如按下数字键,挂起电话,挂断电话等。

可用的帧类型是静态定义的。帧由一个编码类型和一个子类型表示。完整列表可在源码文件include.asterisk/frame.h中找到。下面举几个例子:

  • VOICE:这类帧携带一部分音频流。
  • VIDEO:这类帧携带一部分视频流。
  • MODEM:这类帧数据部分使用的编码,如用于通过IP协议发送传真的T.38编码;其主要用途就是处理传真。对于这类帧的处理,保证原始数据完好无损是很重要的,这样另一端才能被成功解码。AUDIO帧不一样,因为转码到其他音频编解码器的时候,牺牲音频质量以节省带宽是可接受的做法。
  • CONTROL:这类帧表示呼叫信号,用于指示呼叫信号事件,包括电话接通,挂断、挂起等等。
  • DTMF_BEGIN:开始的数字键。当呼叫者按下电话上的DTMF键时,发送此帧(DTMF 代表双音多频,当按下电话上的某个键时在电话音频中发送的音调)
  • DTMF_END:结束的数字键。当呼叫者停止按电话上的DTMF键时,发送此帧。

1.2 Asterisk组件抽象

Asterisk是一款高度模块化的软件。其内核程序可由源码树上的main/目录的源码构建而成。但是内核程序本身作用不大,其主要作用是模块注册。系统还有一些代码负责连接所有抽象接口,使电话呼叫工作起来。这些接口的具体实现是由一些可载入模块在运行时完成注册的。

默认状态下,出于简便性的考虑,当主程序启动时,Asterisk会在文件系统上一个预先指定的目录下找到所有模块,并加载。还有一个可更改的配置文件,可具体指定加载哪些模块及其加载顺序。如果一个模块可从网络接受连接,但实际并不需要用它,那么最好还是不要加载它。

1.2.1 通道驱动

通道驱动接口是Asterisk提供的最复杂且最重要的接口。Asterisk通道API提供了电话协议抽象层,使得其它所有Asterisk特性能够独立于所使用的电话协议而工作。此组件负责在Asterisk通道抽象层与其实现的电话技术细节层面进行转换。

Asterisk通道驱动接口定义称为ast_channel_tech接口。它定义了通道驱动必须实现的一组方法。通道驱动须实现的第一个方法是ast_channel工厂方法,也是ast_channel_tech中的requester方法。当Asterisk为一个接入或拨出的电话呼叫建立通道后,该通道类型对应的ast_channel_tech实现方法负责对ast_channel进行实例化和初始化。

ast_channel实例创建完成后,有一个对其创建者ast_channel_tech的引用,因为还有其他一些针对具体技术的操作需要处理,这些操作必须由*ast_channel*执行,但是实际的处理就需要由ast_channel_tech的适当方法来完成。图1.2表示Asterisk中的两个通道,在图1.4中进行拓展,表示两个桥接通道,以及图中对应的通道技术实现。

 

图1.4 通道技术层和抽象通道层

 

ast_channel_tech中最重要的方法有:

  • requester:回调函数,用于请求某个通道驱动实例化一个ast_channel对象,并针对其类型进行初始化。
  • call:回调函数,用于从端点(由ast_channel表示)发起一个拨出呼叫。
  • answer:Asterisk决定对接入的呼叫进行应答时调用。
  • hangup:系统决定应该挂断呼叫时调用。调用后,通道驱动以基于某种协议的方式通知端点:呼叫结束。
  • indicate:呼叫接通后,有可能产生许多其它的事件,需要给端点发信号。例如,如果电话被挂起,此回调函数将被调用,以通知此事件。通知呼叫挂起事件的方法可以是基于协议的,也可能只是由通道驱动简单发起播放挂起音乐的操作。
  • send_digit_begin:调用此函数的作用是指示数字按键(DTMF)的开始发送至设备。
  • send_digit_end:调用此函数的作用是指示数字按键(DTMF)的结束发送至设备。
  • read:此函数由Asterisk内核调用,用于从端点读回*ast_frame*。*ast_frame*是1.1.3节中提到Asterisk的一个用于封装媒体(如音频或视频)以及触发事件的帧。
  • write:此函数用于向设备发送*ast_frame*。通道驱动取得数据,按照其实现的电话协议做适当的打包,并传送至端点。
  • bridge:针对通道类型的本地桥接回调函数。如前所述,通道驱动使用本地桥接,可以为两个同类型通道实现更高效的桥接方法,而没必要让这两个通道的信号和媒体流都通过额外的抽象层。从性能原因考虑,这是极为重要的。

呼叫结束后,Asterisk内核中负责抽象通道处理的代码调用*ast_channel_tech*的hangup回调函数,销毁*ast_channel*对象。

1.2.2 拨号计划

Asterisk管理员使用Asterisk拨号计划(存于
/etc/asterisk/extensions.conf文件)来设置呼叫路由表。拨号计划是由一系列被称为扩展规则的呼叫规则组成。当有一个电话呼叫接入,系统用被叫号码在拨号计划中查找扩展规则,用以处理本次呼叫。扩展规则包括一组拨号计划应用,由通道执行。拨号计划中可用于执行的应用由一个应用注册表维护,在运行期间,模块被加载填充至注册表。

Asterisk内置近200个应用。应用定义非常松散,并可任意使用系统内部API与通道交互。有些应用执行单个任务,如回放(用于向呼叫方回放一个音频文件);还有一些应用则复杂得多,要执行大量操作,如语音信箱。

你可以集成诸多使用Asterisk拨号计划的应用,用于自定义呼叫处理。如果你需要对内置拨号计划语言的能力做些自定义扩展,系统也有脚本接口,允许使用任意编程语言做自定义呼叫处理。即使通过另一编程语言使用这些脚本接口,也需要调用拨号计划应用来实现与通道交互。

举例说明之前,我们先看一个Asterisk拨号计划的语法,此拨号计划用于处理对号码1234的呼叫。共有3个拨号程序被调用:首先,应答呼叫;其次,回放音频文件;最后,挂断呼叫。

; Define the rules for what happens when someone dials 1234. ; exten => 1234,1,Answer() same => n,Playback(demo-congrats) same => n,Hangup()

关键字exten用于定义扩展。在exten一行的右侧,1234的意思是我们为呼叫1234定义了一组处理规则;紧接着,1的意思是此号码被拨叫后的第一个处理步骤;最后,Answer指示系统应答此呼叫。下面两行都以关键字same起始,是为最后一个扩展(此例指1234)指定的规则。n是下一步(next)的简写;该行的最后一项指定了采取的动作。

下面是一个Asterisk拨号计划的应用示例。此例做的事情是应答接入的一个呼叫,向呼叫方播放蜂鸣音,然后从呼叫方读入最多4个数字,存入变量DIGITS,接着读入的数字重复播放给呼叫方,最后结束呼叫。

exten => 5678,1,Answer() same => n,Read(DIGITS,beep,4) same => n,SayDigits(${DIGITS}) same => n,Hangup()

如前所述,应用定义得非常松散--注册原型非常简单:

int (*execute)(struct ast_channel *chan, const char *args);

应用的实现可以使用/asterisk/目录下几乎所有的API。

1.2.3 拨号计划函数

大多数拨号计划应用接受字符串参数。其中有些值是硬编码,但在某些地方的行为需要有更多的动态处理,这时应使用变量。下面这个例子是一个拨号计划的代码片段,其作用是设置一个变量,并使用Verbose应用在Asterisk命令行界面上打印这个变量值。

exten => 1234,1,Set(MY_VARIABLE=foo) same => n,Verbose(MY_VARIABLE is ${MY_VARIABLE})

调用拨号计划函数的语法与应用相同。Asterisk模块可注册拨号计划函数,取得一些信息并返回给拨号计划;反之,函数也可以从拨号计划中取数据并有所动作。一个通用规则是:拨号计划可设置或获取通道的元数据,但不发任何信号,也不做任何媒体处理,这些工作留给拨号计划应用来做。

下面这个例子展示了拨号计划函数的用法。此函数首先向Asterisk命令行界面打印当前通道的CallerID,然后调用Set应用更改CallerID。此例中,VerboseSet是应用,CALLERID是函数。

exten => 1234,1,Verbose(The current CallerID is ${CALLERID(num)}) same => n,Set(CALLERID(num)=<256>555-1212)

CallerID信息存于数据结构*ast_channel*的实例中,不仅仅是一个变量,更多是一个用于信息存储的数据结构。拨号计划函数中的代码能够从这些数据结构中存取数据。

还有一个拨号计划函数的用法示例--在呼叫日志中添加自定义信息,即CDR(呼叫详细记录)。CDR函数允许获取呼叫详细记录信息以及添加自定义信息。

exten => 555,1,Verbose(Time this call started: ${CDR(start)}) same => n,Set(CDR(mycustomfield)=snickerdoodle)

1.2.4 编解码转换器

在VOIP领域有许多编解码器用于媒体编码及跨网络发送。多种技术选择为媒体质量、CPU消耗、带宽需求等方面提供了折中方案。Asterisk支持多种不同的编解码器,必要时能够在它们之间进行转码。

Asterisk设置完呼叫后,将会尝试使用公共媒体编解码器来沟通两个端点,这样就不需要转码。然而实际上这种情况不太可能发生。即使使用公共编解码器,也可能需要转码。比如,如果通过配置使Asterisk对流经系统的音频做信号处理(如增大或减小音量),就需要将音频转换为未压缩形式之后,才能执行信号处理。也可以通过配置使Asterisk做呼叫录音。如果配置的录音格式与呼叫的音频格式不同,也需要转码。

编解码的协商


用于协商媒体流将使用哪个编解码器的方法和连接到asterisk的通信技术密切相关。在某些情况下,例如在传统电话网络 (PSTN) 上的呼叫,可能不需要进行任何协商。然而,在其他情况下,尤其是使用 IP 协议时,会使用一种协商机制来根据能力和偏好需求就编解码器达成一致。

例如,对于SIP(最常用的VOIP协议),从高层视角来看,当呼叫送达Asterisk系统时,编解码器的协调执行如下:

  • 端点向Asterisk发送呼叫请求中包含其优先使用的编解码器列表。
  • Asterisk查询管理员提供的配置文件,配置文件中包含一个支持编解码器的列表,按优先级排序。随后Asterisk从配置文件的列表中选择优先级最高(基于配置文件中的优先级设置)、同时也包含在请求方所支持的列表中的编解码器,供应答使用。

对于更复杂的编解码,尤其是视频方面,Asterisk对此领域处理得还不够好。在过去十年里,编解码器协商需求变得更加复杂。我们还有更多的工作要做,才能更好的处理最新的视频编解码,才能使系统对视频的支持比现在更好。


 

1.3 线程

Asterisk是多线程应用程序,使用POSIX线程API来管理线程,并使用了相关服务,如加锁。所有与线程相关的调用的 Asterisk 代码都会被包装一层,这样调试会更方便。Asterisk的大多数线程可归类为网络监视线程或通道线程(有时亦称为PBX线程)。

1.3.1 网络监视线程

Asterisk的每个主要通道驱动程序中都存在网络监视线程,负责监视任何网络(IP网络,或PSTN,等等)连接,以及接入的呼叫或其它类型的请求。这类线程还要处理连接的建立和初始工作,如认证及拨号验证。呼叫建立完成后,监视线程将创建Asterisk通道(ast_channel)的一个实例,并启动一个通道线程在其生命周期的剩余时间内处理该呼叫。

1.3.2 通道线程

前面讨论过,通道是Asterisk的基本概念。通道有入站通道和出站通道之分。当有呼叫接入Asterisk系统时,就创建一个入站通道,执行拨号计划。Asterisk为每个入站通道创建一个线程来执行拨号计划。这类线程即被称为通道线程。

拨号计划应用一定是在通道线程的环境中执行。拨号计划函数亦如此。尽管也可能从诸如Asterisk CLI的异步接口读写拨号计划函数,但通常情况下,通道线程仍是ast_channel结构的拥有者,控制着其对象的生命周期。

1.4 呼叫场景

前两节介绍了Asterisk组件的重要接口以及线程执行模型。本节将分解一些常见的呼叫场景演示 Asterisk 组件如何协同工作来处理电话呼叫。

1.4.1 检查语音信箱

有这样一个呼叫场景的示例:呼叫接入电话系统,检查语音信箱。此场景涉及的第一个主要组件是通道驱动。通道驱动负责处理接入系统的电话呼叫请求,此动作发生在通道驱动的监视线程中。实现对系统的呼叫依赖于所使用的电话技术,因而可能会要求某种协商机制来设置呼叫。建立呼叫的另一个步骤是确定呼叫的预期目的地。这通常由呼叫者拨打的号码指定。但是,在某些特殊情况下,没有可用的特定号码,因为用于传递呼叫的技术不支持拨打指定的号码。

如果拨号计划(呼叫路由配置)为拨叫的号码定义了扩展,而通道驱动也查到了Asterisk配置有这样的扩展,系统将分配一个Asterisk通道对象(*ast_channel*),并创建一个通道线程。通道线程主要负责处理呼叫的余下动作。

 

图1.5 创建呼叫的时序图

 

通道线程的主循环用于处理拨号计划的执行,按照拨号扩展定义的规则执行。下面是一个扩展示例,用拨号计划的语法编写,存于extension.conf文件。有人拨叫**123*时,此扩展应答呼叫,并执行应用VoicemailMain。用户调用此应用就能检查邮箱里的信息。

exten => *123,1,Answer() same => n,VoicemailMain()

当通道线程执行应用Answer时,Asterisk就会应答接入的呼叫。应答呼叫除了一些通用的接听处理之外,还需调用关联ast_channel_tech结构中的回调来处理电话接听,这可能会涉及通过 IP 网络发送特殊数据包、将模拟线路连通等操作。

下一步就是由通道线程执行应用VoicemailMain(如图1.6)。此应用是由*app_voicemail*模块提供的。需要注意:虽然Voicemail代码处理大量呼叫交互,但它对用于将呼叫传送到 Asterisk 系统的技术一无所知,它只负责语音信箱相关的处理。Asterisk通道的抽象对语音邮件的实现隐藏了这些细节。

为呼叫方提供对语音信箱的访问涉及很多系统功能。然而,所有这些功能主要都实现为读写音频文件响应呼叫方的输入(主要是以数字按键的形式输入)。DTMF数字可以通过多种不同的方式发送给Asterisk系统。同样,这些实现细节都由通道驱动处理。当读入一个按键输入时,Asterisk将其转换为一个通用按键事件,传递给语音信箱代码。

我们讨论过,Asterisk重要接口之一是编解码转换器接口。编解码的实现对于这类呼叫场景而言非常重要。语音信箱代码向呼叫方回放一个音频文件时,Asterisk系统与呼叫方通信使用的音频格式不一定和该音频格式相同。如果需要音频转码,系统会生成一个转码路径,从源格式经一个或多个编解码转换器到目标格式。

 

图1.6 VoicemailMain的调用

 

在某一时刻,呼叫者完成与语音信箱的交互,挂断了呼叫。这时通道驱动检测到此动作的发生,并将其转换为Asterisk通道的一个通用信号事件。语音信箱代码接收到这一信号事件后退出,因为呼叫方挂断后就没有什么可执行的了。然后通道线程中的控制流程将返回到主循环,继续执行拨号计划。

1.4.2 呼叫桥接

Asterisk中还有一个很常用的呼叫场景叫做两通道间的呼叫桥接。此场景即一方电话通过系统呼叫另一方电话。呼叫的初始设置过程与前例相同。呼叫设置完毕,通道线程开始执行呼叫计划,之后的处理流程是不同的。

下面这个拨号计划是呼叫桥接的一个简单示例。如果系统使用了此扩展,当一方电话拨叫1234时,拨号计划执行应用Dial,这正是发起出站呼叫的主应用。

exten => 1234,1,Dial(SIP/bob)

Dial应用的参数SIP/bob的含义是,系统应发起一个出站呼叫,发送到设备SIP/bob。此参数的SIP部分指定了传送呼叫应使用SIP协议,bob部分由实现SIP协议的通道驱动*chan_sip*负责解释。假设此通道驱动有一个叫做bob的账户已经配置正确,那么它就知道如何将呼叫送达Bob的电话。

首先,应用Dial要求Asterisk内核根据SIP/bob标识符分配一个新的Asterisk通道。然后,内核请求SIP通道驱动执行针对所用技术的初始化操作。通道驱动也会发起出站呼叫过程。随着请求操作的继续执行,将会有事件传回给Asterisk内核,并由Dial应用接收。这些事件包括呼叫响应、目标忙、网络拥塞、呼叫被拒,或者其它很多可能的响应。理想情况下,呼叫会被应答。当两个通道都有应答时,通道桥接就开始了(如图1.7)。

 

图1.7 普通呼叫桥接的组件图

 

通道桥接过程中,音频和信号事件由一个通道不断传送至另一通道,直到发生某些导致桥接终止的事件,如一方呼叫挂断。图1.8所示的顺序图阐释了通过呼叫桥接传输音频帧的执行过程。

 

图1.8 桥接中处理音频帧的顺序图

 

呼叫完成时,挂断流程与前例很相似。主要不同之处在于此处涉及两个通道。通道线程结束之前,两个通道都要执行对应技术的挂断处理操作。

1.5 结论

迄今为止,Asterisk的架构已有十年以上的历史。然而,尽管这个行业在不断发展,Asterisk的一些东西,如通道的基本概念、使用拨号计划进行灵活的呼叫处理,仍然支持着复杂电话系统的开发。有一个领域Asterisk的架构还没有处理的太好,即如何使系统在多服务器间可伸缩。Asterisk开发社区正在开发一个叫做Asterisk SCF(可伸缩通信框架)的伙伴项目,目的就是解决可伸缩性的课题。未来几年,我们期待看到Asterisk以及Asterisk SCF继续称雄电话市场,包括更大型的系统项目。

脚注

  1. http://www.asterisk.org/
  2. DTMF表示多频双音,即按下一个电话键发送的呼叫音。

https://www.xusbuy.com

上一篇:遭遇绩优股 无缺 小说(主打绩优股)

下一篇:napster公司(浪胃仙创始人被捕)

相关推荐

返回顶部