文由ScriptEcho平臺提供技術支持
項目地址:傳送門
本代碼旨在利用 p5.js JavaScript 庫在 Web 應用程序中創建動態網格線。此功能可用于各種場景,例如:
此代碼使用 p5.js 庫創建了一個動態網格線畫布,其中包含以下功能:
let jsUrls=['https://registry.npmmirror.com/p5/1.9.3/files/lib/p5.min.js']
await Promise.all(jsUrls.map((jsUrl)=> loadJavascript(jsUrl)))
此代碼使用 loadJavascript 函數異步加載 p5.js 庫。jsUrls 數組指定了 p5.js 庫的 URL。
const sketch=(s)=> {
s.setup=()=> {
s.createCanvas(720, 360)
s.background(0)
s.noStroke()
...
}
}
此代碼創建了一個 p5.js 草圖,它定義了畫布設置和繪制邏輯。s.setup 函數在畫布創建時執行,并負責設置畫布大小、背景顏色和禁用描邊。
let gridSize=35
for (let x=gridSize; x <=s.width - gridSize; x +=gridSize) {
for (let y=gridSize; y <=s.height - gridSize; y +=gridSize) {
s.noStroke()
s.fill(255)
s.rect(x - 1, y - 1, 3, 3)
s.stroke(255, 50)
s.line(x, y, s.width / 2, s.height / 2)
}
}
這些嵌套循環負責繪制網格線。內層循環繪制水平網格線,而外層循環繪制垂直網格線。每個網格線交點都放置一個 3x3 像素的白色方塊,并用一條從交點到畫布中心的 50% 不透明度的白色線連接。
new p5(sketch, 'container')
此代碼使用 new p5 函數實例化 p5.js 草圖,并將其附加到具有 ID 為 "container" 的 DOM 元素。
開發這段代碼的過程是一個有益的學習經歷。它展示了如何使用 p5.js 庫創建交互式圖形。
經驗與收獲:
未來拓展與優化:
獲取更多Echos
本文由ScriptEcho平臺提供技術支持
項目地址:傳送門
微信搜索ScriptEcho了解更多
章來自于https://mp.weixin.qq.com/s/9lX8VgMmtXRjFdf7zkOFMQ,@Big前端
提到瀏覽器不得不說Chrome,Chrome是Google發行的商業產品,而Chromium是一個開源版本的Chrome,兩者很像但是不完全一樣。
這里嘗試將自己的理解結合下方PPT用最直白的語言記錄最近了解到的瀏覽器的渲染原理知識,方便后續查閱。因為涉及到的知識點非常多且繁雜,如果有表述不到位的地方敬請諒解,錯別字/錯誤理解之類的歡迎聯系我修改。
為什么要做這件事?近幾年瀏覽器更新挺大的,Chrome/Chromium整體還在不斷演進,越來越頻繁。本文有一些自己的理解如果有誤歡迎聯系
PPT內容整體來自Chromium開發者,Steve Kobes的演講,PPT地址
https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y/edit#slide=id.ga884fe665f_64_1691
簡單的說瀏覽器作為應用,底層分別有content,Blink,V8,Skia等等,一層一層像套娃一樣一層引用一層。對比普通應用的項目來說就是不斷用第三方庫和組件來拼湊應用,Chrome也不例外
網頁的渲染可以表示為Content經過rendering最后呈現的過程,即Code -> 可交互的頁面
可以看到content就是WebContents對象,C++代碼的一個類。其代表的區域其實是標簽頁頁打開的部分(上圖紅色部分)。而瀏覽器主進程還包含有地址欄、導航按鈕、菜單、擴展,安全提示的小彈窗等等。
渲染進程render process是一個沙盒,基于安全考慮單獨渲染進程render process掛了不會引起整個瀏覽器掛
渲染進程render process包含Blink渲染排版引擎和Chromium compositor(上圖中綠色的CC簡寫)
content還表示網頁內容代碼,有HTML,CSS,JS,圖片等,還有video,canvas,WebAssembly,WebGL等都可能在content區域顯示或者運行
綜上,content就是網頁代碼最后運行的結果,瀏覽器開發者工具可以看到最后是一個經過處理后的HTML的結構。「而這個HTML在渲染流水線里是一個輸入」
寫過C/C++代碼的同學知道,我們必須使用操作系統提供的底層API去畫圖,操作系統底層又去調用驅動程序,驅動程序驅動硬件。
今天大多數平臺上都提供了“OpenGL”的標準化API。在Windows上有一個額外的DirectX轉換。這些庫提供諸如“紋理”和“著色器”之類的低級圖形基元,并允許執行類似“在這些坐標處繪制一個三角形到虛擬像素緩沖區”之類的底層操作。未來計劃用Vulkan替代Skia來做底層圖形化調用。
所以渲染流水線的整個過程就是將輸入的HTML、CSS、JS轉化為OpenGL調用,最后在屏幕上呈現像素
把渲染管道分成多個階段的話,可以看出來原來的content內容會被各個階段stage處理為中間數據,最后才呈現為畫面呈現出來。
HTML嵌套解析,解析時候解析為數據對象映射反應這里的嵌套模型
DOM(Document Object Model)是一棵樹,樹有父子,鄰居的關系,而且這棵樹是暴露API給JS調用,JS可以查詢和修改這棵樹。JS引擎V8通過bindings的系統將DOM包裝為DOM API供給Web開發者調用
如上圖的示例,自定義元素custom element有shadow tree。ShadowRoot的子元素其實被嵌入到slot元素里了,這跟各大前端框架的slot其實很像。
其實最后是在遍歷樹后合成視圖,也就是兩棵樹合并為一棵樹
style步驟依賴前置的DOM樹解析結果,選擇器是選擇DOM節點集合決定最后應用范圍,最后樣式生效是多個選擇器共同作用的結果,而且樣式間可能互相沖突導致沒有按照預期運行,關于選擇器的優先級感興趣的同學自行查閱
CSS解析器樣式表StyleSheet構建樣式規則。樣式表可能位于<style>元素、單獨加載的資源的css文件中,也可能由瀏覽器默認提供。樣式規則以各種方式編制索引以實現高效查找。
屬性類在構建時由Python腳本自動生成,以聲明方式定義了所有樣式屬性,如上圖右上側css_properties.json經過py腳本轉化為.cc文件。
樣式的重新計算(recalc)從活動樣式表中獲取所有解析的樣式規則,并計算每個DOM元素的每個樣式屬性的最終值。這些內容存儲在一個名為ComputedStyle的對象中,該對象只是樣式屬性到值的映射。可以看到其實每一個DOM節點都對應有一個ComputedStyle對象
在Chrome瀏覽器里的話,就是對應開發者工具的Computed樣式屬性這一欄。或者是通過getComputedStyle的JSAPI去獲取。
在DOM和Style計算好后開始進入布局Layout階段,比如將DIV解析為一個塊級的LayoutReat區域,用x+y+width+height來表示,布局就是為了計算x,y,width,height這些數據
默認情況下文檔按照順序排列下去形成了文檔流
文字和內聯元素則是左右浮動的,而且內聯元素會被行尾打斷(自動換行)。當然也有從右到左的語言,比如阿拉伯語和希伯來語
布局也包括字體的排列,因為布局需要考慮文本在那里進行換行,Layout使用名為HarfBuzz的開源文本庫來計算每個字形的大小和位置,這決定了文本的總寬度。字體成型必須考慮到排版特征,如字距調整letter-spacing和連字。
布局可以計算單個元素的多種邊界矩形。例如,當存在溢出時,Layout將同時計算邊界框和布局溢出。如果節點的溢出是可滾動的,Layout還會計算滾動邊界并為滾動條預留空間。最常見的可滾動DOM節點是文檔本身
表格元素或display:table的樣式需要更復雜的布局,這些元素或樣式指定將內容分成多列,或浮動對象漂浮在一邊,內容在其周圍流動,或者東亞語言的文本垂直排列,而不是水平排列。請注意DOM結構和ComputedStyle值(如“Float:Left”)是布局算法的輸入。「渲染流水線的每個階段都會使用到前面階段的結果」
通過遍歷DOM樹創建渲染樹LayoutTree,節點一一對應。布局樹中的節點實現布局算法。根據所需的布局行為,LayoutObject有不同的子類。比如LayoutBlockFlow就是塊級Flow的文檔節點。樣式更新階段也構建布局樹。
在樣式解析最后結束時需要構建布局樹LayoutTree,布局階段遍歷布局樹,對布局樹每個節點LayoutObject執行布局,計算幾何數據、換行符,滾動條等。
一般情況下一個DOM節點會有一個LayoutObject,但是有時候LayoutObject是沒有DOM節點與之對應的。
比如上圖,span標簽外部沒有section標簽嵌套,但是LayoutTree會自動創建LayoutBlock的匿名節點與之對應,再比如樣式有display:none的樣式,那么也不會創建對應的LayoutTree。
最后,如果是shadowTree的話,其LayoutObject節點可能會在不同的TreeScope里
如上圖所示,「LayoutNG」代表下一代的布局引擎,2020年布局引擎還在過渡階段,所以有中間形態,如上圖包含了LayoutObject和LayoutNGMixin混合節點。未來所有節點都會變成LayoutNG的layout object
NG節點的更新主要是因為之前的節點包含了輸入、輸出還有布局算法的信息,也就是說單個節點可以看到整棵樹的狀態(節點有可能需要獲取父節點的寬高數據,但是父節點正在遞歸子節點布局中,實際上還沒確定最后的布局)。
而新的NG節點對輸入和輸出做了明顯的區分,而且輸出是immutable的,可以緩存結果
布局結果指向描述物理幾何的片段樹,如圖一個NGLayoutResult對應幾個NGPhysicalFragment,對應右上角的幾個矩形圖形,如果NGLayoutResult沒變化則對應整塊都不會變化。
上圖的HTML代碼,會渲染如右下角的例子,對應的DOM樹如左側所示
DOM樹跟Layout樹很像,節點幾乎是一一對應的,但是注意這里anonymous匿名節點被創建出來,它只有一個塊級子元素。一個布局節點只能擁有塊級元素或者內聯元素其中之一
上圖的子元素前面兩個其實共享了匿名LayoutNGBlockFlow,也就是說有共同的父節點
在「fragment tree」里我們可以更好的看到文本換行后的繪制結果,以及每個fragment的位置和大小
paint階段只是創建繪制指令paint op,頁面還沒有東西,甚至直到GL調用之前頁面都是沒有呈現任何東西的狀態
繪制paint階段創建繪制指令列表paint ops list
繪制指令paint op可以理解為在某些坐標用什么顏色畫一個矩形類似的意思,
每個布局對象LayoutObejct可以有多個顯示項目,對應于其視覺外觀的不同部分,如背景、前景、輪廓等
正確的繪制順序非常重要,這樣當元素重疊時,它們才能正確堆疊。順序可以由樣式控制,而不是完全依靠DOM的先后順序
每個繪制階段「paint phase」都需單獨遍歷堆疊上下文staking context。
一個元素甚至可能部分位于另一個元素的前面,部分位于另一個元素的后面。這是因為繪制在多個階段中運行,每個繪制階段都對自己的子樹進行遍歷。
如上,一個樣式和DOM節點渲染出來的結果,包含了四個繪制指令paint ops。
文本繪制操作包含文本塊的繪制,其中包含每個字的字符和偏移量以及字體。如圖這些數據都是HarfBuzz計算后得到的
中文說的柵格化或者光柵化,本文取PS圖層右鍵的柵格化為譯文。熟悉PS的會知道矢量圖形柵格化后放大圖形會“糊”,但是不做柵格化處理直接放大矢量圖形則不會。原因就是柵格化后只記錄了單像素點的rgba值,放大后本來一個點數據要填滿N個點,圖像就”糊了“
把顯示列表里的繪制操作執行的過程,成為任務,也稱柵格化。比如PS里的合并圖層任務,主要區別就是本來矢量的圖任務后會變成位圖bitmap,后面再縮放就會模糊。
生成的位圖bitmap中的每個單元格都包含對單個像素的顏色和透明度進行編碼的位。這里用十六進制FFFFFFFF表示一個點的rgba值
任務還可以對頁面中嵌入的圖像資源進行解碼。繪制指令引用壓縮數據(JPEG、PNG等),任務調用適當的解碼器將其解壓縮。
GPU還可以運行生成位圖的命令(“加速柵格化”)。請注意,這些像素還沒有出現在屏幕上!
raster產生的位圖數據存儲在GPU內存中,通常是OpenGL紋理對象引用的GPU內存。
過去通常是存在內存里再傳給GPU,但是現代GPU可以直接運行著色器shader并在GPU上生成像素,這種情況稱為“加速柵格化”。但是兩個結果都是一致的,最終內存(主存或者GPU內存)里擁有位圖bitmap
GL調用即OpenGL調用,OpenGL意為"開放圖形庫",可以在不同操作系統、不同編程語言間適配2D,3D矢量圖的渲染。
raster通過名為Skia的庫發出GL調用。Skia提供了圍繞硬件的抽象層,如路徑和貝塞爾曲線,子像素抗鋸齒以及各種混合疊加模式。
Skia是開源的,由谷歌維護。跟隨Chrome一起發布,但位于單獨的代碼庫中。它也被其他產品使用,比如Android。Skia的GPU加速代碼路徑構建自己的繪制操作緩沖區,在柵格化結束時刷新。實際上發起GL調用的是Skia的后端,后面會說到
回想一下,渲染器進程是一個沙箱環境,因此它不能直接進行系統調用。繪制操作被運送到GPU進程進行任務處理。GPU進程可以發出實際的GL調用。除了獨立于渲染器沙箱之外,在GPU進程中隔離圖形化操作還可以保護我們免受不穩定或不安全的圖形驅動程序的影響。比如GPU進程崩了,瀏覽器可以重啟GPU進程
柵格化的繪制操作通過GPU命令緩沖區command buffer傳輸,渲染進程和GPU進程通過IPC通道發送。命令緩沖區command buffer最初是為序列化的GL圖形命令構建的,類似一個proxy。當前的“進程外”柵格化(即GPU)以不同的方式使用它們,更多是繪制操作的包裝器,就是命令緩沖區command buffer與底層圖形API無關
GPU進程中的GL函數指針通過動態查找操作系統底層共享的OpenGL庫進行初始化,Windows上用ANGLE做一個轉化步驟。
Angel是另一個由Google構建的庫;它的工作是將OpenGL轉換為DirectX,DirectX是微軟在Windows上用于加速圖形的API。調查發現Angle比Windows的OpenGL驅動程序運行更好。
至此擁有了整個pipeline,從DOM一直到內存中的像素,牢記渲染不是靜態的,也不是執行一次就完成了,瀏覽器會話期間發生的任何事情都會動態更改渲染的過程。并且整個pipeline從頭開始運行是非常昂貴的,盡可能希望能減少不必要的工作以提高效率
低于60幀每秒的動畫和滾動看起來會非常卡,渲染器生成動畫幀,每個幀都是內容在特定時間點狀態的完整呈現,多個幀連起來就是看到的動畫,其實動畫只要達到60幀每秒那么看起來就會是連貫的。新的設備甚至要求90或120甚至更高的幀率。
如果在1/60秒內,約16.66ms還不能渲染完一幀畫面,那么畫面看起來就是斷斷續續很卡的樣子
為了提高性能,很簡單的想到了盡可能復用上一階段處理的結果,對于渲染來說既重復使用以前幀的輸出
大塊區域的繪制和柵格化是非常昂貴的,比如在滾動的時候,視口內所有像素都變化了,這個過程稱為重繪repaint
渲染進程的主線程的任何事情都會跟JS競爭(互斥關系),意味著其實JS也會阻塞渲染主線程其他任務的執行
將頁面分解為不同的層便于柵格化raster對不同的層單獨處理,在渲染進程主線程構建層后commit到合成線程compositor thread去,合成線程compositor thread會對每一層進行單獨繪制
我們可以在瀏覽器開發工具的Layer看到當前頁面的分層,分層的目的是可以對單獨的層進行變換transform和柵格化raster
試想一下如果有123三層,其中1,2兩層沒變化,第3層旋轉了,那么只要對第三層每幀進行變換就可以得到每一幀的輸出,計算量大大減少
「所以分層的目的是為了減少計算加速渲染效率,在渲染進程合成器線程執行則是為了不影響渲染主線程的任務執行」
圖中的impl*即渲染進程的合成線程,因為歷史原因在代碼里都是這樣表示,后面所有表示合成線程都用impl表示
分層的作用在有動畫時候可以顯著提升性能,如圖所示BBB文本一層的變換不會影響其他層
動畫是層的移動,頁面滾動是層的移動和裁剪,放大縮小也是層的縮放
當滾動事件沒有觸發JS邏輯時候,即使渲染進程主線程很繁忙,但是瀏覽器進程發出的頁面滾動事件的處理也不會受到影響,因為渲染進程的合成線程compositor thread可以單獨處理滾動事件
當然如果滾動觸發了JS的邏輯,那么合成線程必須轉發事件到主線程去,滾動事件會進入主線程任務隊列等待處理
正常情況下一個LayoutView會創建一個PaintLayer,對應一個cc(Chromium Compositor) Layer。
但是某些樣式屬性也會導致對應的LayoutObject單獨成層,比如transform屬性就類似創建新層的“觸發器”一樣,瀏覽器遇到這個屬性就會單獨創建新層,cc(Chromium Compositor) Layer沒有父子關系,是一個平級的列表,但是還是保留LayerTree的名稱
滾動容器創建特殊的多個層,比如元素加了overflow:scroll的滾動屬性,那么合成的時候會有5個層,其中4個層都是滾動條scrollbar的層,這些層合并起來稱為CompositedLayerMapping
合成透明滾動條會禁用子像素抗鋸齒,如上圖左下角所示。而且判斷是否合成滾動條也有判斷邏輯,在安卓和ChromeOS上可以合成所有的滾動條
如上圖,合成任務包含構建層樹的過程。在布局layout之后,繪制paint任務之前,這個過程也可以稱為「分層和合成任務」,每一層layer都是獨立繪制的,一些屬性節點單獨為層,比如will-change,3D屬性transform之類
渲染進程合成線程繪制的時候,合成線程里的合成器可以將各種屬性應用于其繪制的圖層,如變換矩陣,裁剪,滾動偏移,透明度。這些數據儲存在屬性樹里,可以將這些理解為圖層的屬性(過去也是這么干的)。后面為了解耦這些屬性,讓它們可以脫離層單獨使用,需要引入prepaint的過程
預繪制prepaint階段遍歷并構建屬性樹
CAP是composite after paint的縮寫,它的目標是將屬性和層解耦。即在paint階段只需要paint的信息,而不需要知道層的任何信息,因為這時層還沒有構建
在過去,變換、裁剪、效果滾動等信息等存儲在層本身上,但CAP要求層的屬性解耦。未來,層layer的合成會在繪制后進行
在繪制paint階段完成后,即繪制指令準備完成后,會進入渲染進程合成線程commit階段
commit會拷貝層和屬性樹生成副本,這里合成線程的commit會阻塞主線程直到commit完成
「注意:渲染進程合成線程拿到的是layer副本,用LayerImpl表示」
整個網頁是非常大的,向下延伸理論上可以無限長(比如新聞類網站的無限滾動)。
柵格化是繪制之后的步驟,柵格化會將繪制指令轉化為位圖bitmap。試想一下如果在繪制完整個圖層之后再柵格化整個圖層,則成本會很大,但如果只柵格化部分圖層的可見部分成本則會小很多。
這里tiling是平鋪的意思,類似裝修時候鋪地板用大塊瓷磚平鋪,頁面顯示的做法類似。
根據視口viewport所在位置的不同,渲染進程合成器線程會選擇靠近視口的圖塊tiles進行渲染,將最后選擇渲染的圖塊傳遞給GPU柵格化線程池里的單個柵格化線程執行柵格化,最后得到柵格化好后的tile圖塊。圖塊大小根據不同設備的分辨率有不同的大小,比如256*256或512*512
「圖塊tiles是柵格化任務的單位,柵格化就是將一塊塊的tiles轉化為位圖bitmap」
在柵格化所有的圖塊tiles完成后,渲染進程的合成器線程會生成draw quads命令。
quad類似于在屏幕上特定位置繪制圖塊tile的指令,draw quads就是繪制圖塊們的意思。
此時的quad是層樹layer tree在拿屬性樹經過一堆變換后的最終結果,每個quad都引用圖塊tile在GPU內存里的柵格化輸出結果。
多個DrawQuad最后被包裝在「CompositorFrame」里(簡單理解就是一排要鋪上去的瓷磚 :-),這是渲染進程最后的輸出,包含有渲染進程生成的動畫幀,會被傳遞給GPU進程。
「注意執行到這里還只是數據,這里屏幕還沒有像素呈現」
在準備圖塊tiles進行柵格化和draw兩個階段渲染進程的合成線程都會參與,但是渲染進程主線程里的layer數據還在不斷commit過來。實際上合成線程具有兩個樹的拷貝副本
這里pending tree 和 active tree都是層列表和屬性樹的結合,不是真的樹結構,基于習慣沿用樹的叫法
GPU進程的顯示合成器display compositor會將多個進程最后的「CompositorFrame」進行合并顯示,前面說過「CompositorFrame」是每個進程最后的輸出,包裹了DrawQuad列表。
可以看到這里也有瀏覽器主進程的「CompositorFrame」,導航欄,收藏夾,前進后退這些Content外的渲染是瀏覽器主進程控制的。瀏覽器主進程有自己合成器為瀏覽器UI生成動畫幀,比如標簽條和地址欄的動畫。
界面可以嵌入其他界面。瀏覽器嵌入渲染器,渲染器可以嵌入其他渲染器用于跨源iframe(也稱為站點隔離,“進程外iframe”或OOPIF)。同源網頁,比如iframe和一個標簽頁可能共用一個渲染進程,而跨源網頁則一定是多個渲染進程。
地址:https://www.chromium.org/developers/design-documents/oop-iframes
顯示合成器display compositor在GPU進程中的Viz線程上運行。Viz取Visuals視覺效果的意思。
顯示合成器display compositor同步傳入的幀,了解嵌入界面之間的依賴關系,做界面聚合。
Viz線程除了做界面聚合還發起圖形調用,最后屏幕上顯示compositor frame的quad。Viz線程是雙緩沖的,分為前置緩沖區和后置緩沖區,這里將數據處理后序列化放到后置緩沖區
舊模式是GPU主線程解碼器真正發起GL調用,新模式中是交給Skia庫。Skia繪制到一個異步顯示列表里,會一起傳遞到GPU主線程。GPU主線程的Skia后端發起真正的GL調用。
分離GL調用通過第三方的Skia或者未來準備使用的Vulkan實現與OpenGL解耦
在大多數平臺上,顯示合成器display compositor的輸出是雙緩沖的,即包含前后兩個緩沖區。圖塊繪制到后臺緩沖區,Viz發出命令交換前后緩沖區使其可見
也就是說屏幕顯示器這一幀的畫面,是每HZ從前置緩沖區讀取后在屏幕顯示的,后置緩沖區在馬不停歇的繪制,通過前后緩沖區的交換實現新一幀畫面的呈現。在OS X上,使用CoreAnimation做了一些稍微不同的事情
顯卡的作用?負責將數據寫到后緩沖區,寫完后前后緩沖區互換。通常情況下顯卡的更新頻率和顯示器的刷新頻率是一致的,如果不一致則會發現視覺上的卡頓。大多數設備屏幕的更新頻率是60次/秒,這也就意味著正常情況下要實現流暢的動畫效果,渲染引擎需要每秒更新60張圖片到顯卡的后緩沖區
至此瀏覽器完成了它的任務,底層驅動通過調用硬件完成繪制。最后,我們的像素出現在屏幕上
回顧一下整個渲染流水線的過程,從渲染主線程獲取Web內容,構建DOM樹,解析樣式,更新布局,layer分層后合成,生成屬性樹,創建繪制指令列表。
再到渲染進程合成線程收到渲染主線程commit過來的帶有繪制指令和屬性樹的layer,將layer分塊為圖塊,使用Skia對圖塊進行柵格化,拷貝pending tree到active tree,生成draw quads命令,將quad發送給GPU的Viz線程,最后像素顯示到屏幕上。
大多數階段是在渲染器進程里執行的,但是raster和display則在GPU進程中執行。
核心渲染階段DOM,style,layout,paint是在渲染進程主線程的Blink進行的,但是滾動和縮放等交互事件在渲染主線程繁忙時可以在渲染進程合成線程里執行
頁面的滾動等交互會進入渲染進程合成線程compositor thread里處理,這也是渲染進程主線程繁忙時交互也不卡的原因
者: 蜀中亮子
轉發鏈接:https://mp.weixin.qq.com/s/ugrBaCIWYzGn8nuStp7ohw
*請認真填寫需求信息,我們會在24小時內與您取得聯系。