作者:閑魚技術-國有
為涿鹿等地區(qū)用戶提供了全套網(wǎng)頁設計制作服務,及涿鹿網(wǎng)站建設行業(yè)解決方案。主營業(yè)務為成都網(wǎng)站設計、成都網(wǎng)站建設、涿鹿網(wǎng)站設計,以傳統(tǒng)方式定制建設網(wǎng)站,并提供域名空間備案等一條龍服務,秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務。我們深信只要達到每一位用戶的要求,就會得到認可,從而選擇與我們長期合作。這樣,我們也可以走得更遠!
國有,閑魚架構團隊負責人。在7月13號落幕的2019年Archsummit峰會上就近一年來閑魚在FlutterFaaS一體化項目上的 探索 和實踐進行了分享。
隨著無線,IoT的發(fā)展,5G的到來,移動研發(fā)越發(fā)向多端化發(fā)展。傳統(tǒng)的基于Native+Web+服務端的開發(fā)方式,研發(fā)效率低下,顯然已經(jīng)無法適應發(fā)展需要。
我們希望 探索 閑魚這樣規(guī)模的獨立APP的高效研發(fā)架構。主要思路是圍繞Flutter解決多端問題,并使Flutter與FaaS等無服務容能力打通,形成云端一體化的研發(fā)能力,支持一云多端的發(fā)展需要。在某些場景已經(jīng)取得效果,希望分享過程中的思考,與大家交流。
閑魚選擇Flutter主要是出于高性能的考慮。Flutter高性能主要來源于2個原因:
更多比較:
沒有銀彈的解決方案,F(xiàn)lutter與RN各有優(yōu)點。如何選擇因素很多,關鍵看如何取舍,舉個例子:
云端技術棧的打通,是減少協(xié)同的不錯的解法。以往前端+Node.js的一體化方案大家應該不會陌生,然而如果端側使用了Flutter,那云側Dart自然是第一選擇。
FaaS的本質是運行在云端,那Dart適合用在云/Server上嗎?
Dart語言早于Flutter,在最初的設計上,Dart就可以用于Web、Server。Dart具備一些服務端語言的特點:
閑魚首先嘗試將Dart作為普通的Server,替代傳統(tǒng)的Java Server,然后再將Dart容器嵌入到FaaS容器中。建立Dart Server能力是第一步,也是主要的工作量所在。
閑魚在Dart Server方面的建設思路:
開發(fā)期:
運行期:
上述內(nèi)容實現(xiàn)了FlutterDart FaaS的技術棧的統(tǒng)一,但僅技術棧統(tǒng)一還遠遠不夠,端、云的同學仍然無法真正互補和一體化打通,原因在于還有更多深入問題需要考慮:
面向這些問題,閑魚的解法思路:
案例一,一體化在資源均衡方面的體現(xiàn)。在近期的一個項目中,云端一體化使原本2個月的項目時間,減少了20天。
案例二,一體化在業(yè)務閉環(huán)方面的體現(xiàn)。負責增長的一位開發(fā)同學,專注在增長業(yè)務上,在合適的情況下為合適的人投放合適的內(nèi)容,以此帶來用戶的增長和活躍效果。一體化的方式下,可以統(tǒng)一云、端的切面,業(yè)務研發(fā)不再受云、端的限制。
一體化是建設高效研發(fā)框架的方向,并不是所有場景都需要一體化的開發(fā),但一體化的Flutter、FaaS等技術組件,可以獨立使用,也會帶來效率提升,并且與原有的開發(fā)模式兼容。從一體化的思路去建設,可以使整體架構體系更加一致,也有機會做一體的架構沉淀。
未來閑魚希望在一體化上做更多嘗試和深入 探索 ,包括一體化工具、一體化業(yè)務平臺、數(shù)據(jù)化智能化等方向。
BinaryMessenger是Platform端與Flutter端通信的工具,其通信使用的消息格式為二進制格式數(shù)據(jù)。當我們初始化一個Channel,并向該Channel注冊處理消息的Handler時,實際上會生成一個與之對應的BinaryMessageHandler,并以channel name為key,注冊到BinaryMessenger中。當Flutter端發(fā)送消息到BinaryMessenger時,BinaryMessenger會根據(jù)其入?yún)hannel找到對應的BinaryMessageHandler,并交由其處理。
Binarymessenger在Android端是一個接口,其具體實現(xiàn)為FlutterNativeView。而其在iOS端是一個協(xié)議,名稱為FlutterBinaryMessenger,F(xiàn)lutterViewController遵循了它。
參考閑魚技術出品
深入理解Flutter Platform Channel
在Native側,創(chuàng)建一個methodChannel通道,用于調(diào)用flutter側方法,或者flutter側調(diào)用Native側方法,并提供callback。
iOS側:
關鍵詞:
channelName:channel唯一標識,Native側和flutter側保持名稱一致。
binaryMessenger:channel Context。
handle: typedef void (^FlutterMethodCallHandler)(FlutterMethodCall* call, FlutterResult result);
FlutterMethodCall:包含 method (方法名)和 arguments (參數(shù))的對象,管理方法對象
FlutterResult: typedef void (^FlutterResult)(id _Nullable result);
Android側:
關鍵詞:
binaryMessenger:傳入flutter Context,及FlutterNativeView。
flutter側:
關鍵詞:
Future、async:異步操作套裝
Future-官方文檔
setState:觸發(fā)重繪當前節(jié)點,以更新UI。
前端的發(fā)展太快了,前端框架和技術的發(fā)展也層出不窮,還包括不同智能設備的出現(xiàn),對前端開發(fā)同學來說是個很大的跳轉,簡單列舉下:
這樣就滋生了一些問題,比如我要開發(fā)一個通用的頁面,兼容不同的端側和 小程序 ,顯然目前是做不到的,我們只能開發(fā)多套頁面去適配不同的場景,這樣的話成本就太高了。
很多同學都在嘗試解決這個問題,也催生了類似taro這樣的多端統(tǒng)一開發(fā)框架,這是一個好的解決方案,但是比較被動,缺乏一定的擴展性。
這篇文章我們要探討的是,看能不能換個角度去解決這個問題,提升開發(fā)效率。
ViewModel
當我們在開發(fā)一個頁面的時候,不管用的是哪一種框架,通常都會抽象出一層viewmodel層,它主要有2個作用
從上圖中我們可以看出,viewmodel是一段獨立的通用代碼邏輯,起到了承前啟后的作用。它和view層關系更加緊密,因此通常會放在前端測。
既然viewmodel是獨立的,那我們能不能把它放在后端呢?這樣一個最大的好處就是viewmodel可以進行復用,不需要在重復編寫,而且只需要改動一個viewmodel,就可以全量生效。
似乎是一個很美好的想法,但是這部分代碼由誰去開發(fā)呢,總不可能寄希望于后端同學吧,當然只能是我們自己,也感謝于serverless架構的出現(xiàn),讓這件事情變成了可能。
有些同學可能會問,既然viewmodel后移了,那view呢?后續(xù)會考慮結合我們的ui2code技術,那真的就比較完美了。
什么是serverless
架構上,我們可以把serverless分為FaaS和BaaS。
FaaS是用于創(chuàng)建、運行、管理函數(shù)服務的計算平臺,它支持多種開發(fā)語言,比如java、nodejs、dart等,這有利于不同端側的開發(fā)同學介入開發(fā)。FaaS是基于事件驅動的思想,只有當一個函數(shù)被事件觸發(fā)時才會占用服務器資源執(zhí)行,不然都是無需占用服務器資源的。
BaaS提供了用于函數(shù)調(diào)用的第三方基礎服務,比如身份校驗、日志、數(shù)據(jù)庫等,它是由服務商直接提供,開發(fā)者無需關系實現(xiàn),直接調(diào)用即可。
業(yè)務落地
我們是通過gaia平臺開發(fā)后端接口,gaia可以理解為上文提到的FaaS平臺。
日常開發(fā)中有這樣一個需求,下面是這個需求的一個頁面。
因為這個頁面上的數(shù)據(jù)比較多,先把它切分成一個個小的模塊,后臺返回數(shù)據(jù)的時候也根據(jù)模塊來返回數(shù)據(jù)。
我們是根據(jù)viewmodel來設計接口,首先肯定有一個首屏數(shù)據(jù)接口;然后是頁面上的交互,比如切換卡片、切換芝麻信用按鈕,切換會引起頁面數(shù)據(jù)變化,我們可以統(tǒng)一封裝一個頁面更新的接口;最后是一個開通的接口。
后端接口
前后端交互最重要的數(shù)據(jù)結構的設計,我們省略了中間的業(yè)務邏輯處理,看下接口的數(shù)據(jù)結構。
首屏接口返回的數(shù)據(jù)主要有幾個特征:
更新接口的返回數(shù)據(jù)結構和首屏接口類似,但是入?yún)⒂兴煌饕?個字段:
前端處理
從后端返回的數(shù)據(jù)可以看到,數(shù)據(jù)是及其詳細的,無需我們做任何的業(yè)務邏輯處理,直接映射到頁面即可。這樣,前端已經(jīng)變成了很薄的一層數(shù)據(jù),沒有任務的業(yè)務邏輯處理,變的很簡單,當需要遷移到其他端時,只需要遷移視圖層即可。當有任何的業(yè)務變動時,只需要修改后端的接口,就能生效。
收益與總結
通過具體的實踐,我們發(fā)現(xiàn),對于前端開發(fā)同學來說,變的簡單了,開發(fā)效率有很大的提升,前端同學甚至都不需要去理解具體的業(yè)務邏輯,就能完成頁面的開發(fā)。而且,提取的viewmodel可以復用到不同的端側,設置還包括native端。我們還可以將viewmodel拆分成更小粒度的viewmodel,方便在不同的頁面接口中進行復用。我們有同學還在FaaS側基于redux的思想封裝了一個通用的狀態(tài)管理框架,規(guī)范了前后端的交互。
后面, 還有一些問題待我們?nèi)ソ鉀Q,比如開發(fā)成本、viewmodel的邏輯拆分、具體接口問題定位等。
閑魚團隊是Flutter+Dart FaaS前后端一體化新技術的行業(yè)領軍者,就是現(xiàn)在! 客戶端/服務端java/架構/前端/質量工程師 面向 社會 招聘,base杭州阿里巴巴西溪園區(qū),一起做有創(chuàng)想空間的社區(qū)產(chǎn)品、做深度頂級的開源項目,一起拓展技術邊界成就極致!
*投喂簡歷給小閑魚→ guicai.gxy@alibaba-inc .com
開源項目、峰會直擊、關鍵洞察、深度解讀
請認準 閑魚技術
文/陳爐軍
整理/LiveVideoStack
大家好,我是阿里巴巴閑魚事業(yè)部的陳爐軍,本次分享的主題是Flutter浪潮下的音視頻研發(fā)探索,主要內(nèi)容是針對閑魚APP在當下流行的跨平臺框架Flutter的大規(guī)模實踐,介紹其在音視頻領域碰到的一些困難以及解決方案。
分享內(nèi)容主要分為四個方面,首先會對Flutter有一個簡單介紹以及選擇Flutter作為跨平臺框架的原因,其次會介紹Flutter中與音視頻關系非常大的外接紋理概念,以及對它做出的一些優(yōu)化。之后會對閑魚在音視頻實踐過程中碰到的一些Flutter問題提出了一些解決方案——TPM音視頻框架。最后是閑魚Flutter多媒體開源組件的介紹。
Flutter
Flutter是一個跨平臺框架,以往的做法是將音頻、視頻和網(wǎng)絡這些模塊都下沉到C++層或者ARM層,在其上封裝成一個音視頻的SDK,供UI層的PC、iOS和Android調(diào)用。
而Flutter做為一個UI層的跨平臺框架,顧名思義就是在UI層也實現(xiàn)了一個跨平臺開發(fā)。可以預想的是未Flutter發(fā)展的好的話,會逐漸變?yōu)橐粋€從底層到UI層的一個全鏈路的跨平臺開發(fā),技術人員分別負責SDK和UI層的開發(fā)。
在Flutter之前已經(jīng)有很多跨平臺UI解決方案,那為什么選擇Flutter呢?
我們主要考慮性能和跨平臺的能力。
以往的跨平臺方案比如Weex,ReactNative,Cordova等等因為架構的原因無法滿足性能要求,尤其是在音視頻這種性能要求幾乎苛刻的場景。
而諸如Xamarin等,雖然性能可以和原生App一致,但是大部分邏輯還是需要分平臺實現(xiàn)。
我們可以看一下,為什么Flutter可以實現(xiàn)高性能:
原生的native組件渲染以IOS為例,蘋果的UIKit通過調(diào)用平臺自己的繪制框架QuaztCore來實現(xiàn)UI的繪制,圖形繪制也是調(diào)用底層的API,比如OpenGL、Metal等。
而Flutter也是和原生API邏輯一致,也是通過調(diào)用底層的繪制框架層SKIA實現(xiàn)UI層。這樣相當于Flutter他自己實現(xiàn)了一套UI框架,提供了一種性能超越原生API的跨平臺可能性。
但是我們說一個框架最終性能怎樣,其實取決于設計者和開發(fā)者。至于現(xiàn)在到底是一個什么狀況:
在閑魚的實踐中,我們發(fā)現(xiàn)在正常的開發(fā)沒有特意的去優(yōu)化UI代碼的情況下,在一些低端機上,F(xiàn)lutter界面的流暢性是比Native界面要好的。
雖然現(xiàn)在閑魚某些場景下會有卡頓閃退等情況,但是這是一個新事物發(fā)展過程中的必然問題,我們相信未來性能肯定不會成為限制Flutter發(fā)展的瓶頸的。
在閑魚實踐Flutter的過程中,混合棧和音視頻是其中比較難解決的兩個問題,混合棧是指一個APP在Flutter過程中不可能一口氣將所有業(yè)務全部重寫為Flutter,所以這是一個逐步迭代的過程,這期間原生native界面與Flutter界面共存的狀態(tài)就稱之為混合棧。閑魚在混合棧上也有一些比較好的輸出,例如FlutterBoost。
外接紋理
在講音視頻之前需要簡要介紹一下外接紋理的概念,我們將它稱之為是Flutter和Frame之間的橋梁。
Flutter渲染一幀屏幕數(shù)據(jù)首先要做的是,GPU發(fā)出的VC信號在Flutter的UI線程,通過AOT編譯的機器碼結合當前Dart Runtime,生成Layer Tree UI樹,Layer Tree上每一個葉子節(jié)點都代表了當前屏幕上所需要渲染的每一個元素,包含了這些元素渲染所需要的內(nèi)容。將Layer Tree拋給GPU線程,在GPU線程內(nèi)調(diào)用Skia去完成整個UI的渲染過程。Layer Tree中有PictureLayer和TextureLayer兩個比較重要的節(jié)點。PictureLayer主要負責屏幕圖片的渲染,F(xiàn)lutter內(nèi)部實現(xiàn)了一套圖片解碼邏輯,在IO線程將圖片讀取或者從網(wǎng)絡上拉取之后,通過解碼能夠在IO線程上加載出紋理,交給GPU線程將圖片渲染到屏幕上。但是由于音視頻場景下系統(tǒng)API太過繁多,業(yè)務場景過于復雜。Flutter沒有一套邏輯去實現(xiàn)跨平臺的音視頻組件,所以說Flutter提出了一種讓第三方開發(fā)者來實現(xiàn)音視頻組件的方式,而這些音視頻組件的視頻渲染出口,就是TextureLayer。
在整個Layer Tree渲染的過程中,TextureLayer的數(shù)據(jù)紋理需要由外部第三方開發(fā)者來指定,可以把視頻數(shù)據(jù)和播放器數(shù)據(jù)送到TextureLayer里,由Flutter將這些數(shù)據(jù)渲染出來。
TextureLayer渲染過程:首先判斷Layer是否已經(jīng)初始化,如果沒有就創(chuàng)建一個Texture,然后將Texture Attach到一個SufaceTexture上。
這個SufaceTexture是音視頻的native代碼可以獲取到的對象,通過這個對象創(chuàng)建的Suface,我們可以將視頻數(shù)據(jù)、攝像頭數(shù)據(jù)解碼放到Suface中,然后Flutter端通過監(jiān)聽SufaceTexture的數(shù)據(jù)更新就可以順利把剛才創(chuàng)建的數(shù)據(jù)更新到它的紋理中,然后再將紋理交給SKIA渲染到屏幕上。
然而我們?nèi)绻枰肍lutter實現(xiàn)美顏,濾鏡,人臉貼圖等等功能,就需要將視頻數(shù)據(jù)讀取出來,更新到紋理中,再將GPU紋理經(jīng)過美顏濾鏡處理后生成一個處理后的紋理。按Flutter提供的現(xiàn)有能力,必須先將紋理中的數(shù)據(jù)從GPU讀出到CPU中,生成Bitmap后再寫入Surface中,這樣在Flutter中才能順利的更新到視頻數(shù)據(jù),這樣做對系統(tǒng)性能的消耗很大。
通過對Flutter渲染過程分析,我們知道Flutter底層需要渲染的數(shù)據(jù)就是GPU紋理,而我們經(jīng)過美顏濾鏡處理完成以后的結果也是GPU紋理,如果可以將它直接交給Flutter渲染,那就可以避免GPU-CPU-GPU這樣的無用循環(huán)。這樣的方法是可行的,但是需要一個條件,就是OpenGL上下文共享。
OpenGL
在說上下文之前,得提到一個和上線文息息相關的概念:線程。
Flutter引擎啟動后會啟動四個線程:
第一個線程是UI線程,這是Flutter自己定義的UI線程,主要負責GPU發(fā)出的VSync信號時候用當前Dart編譯的機器碼和當前運行環(huán)境創(chuàng)建出Layer Tree。
還有就是IO線程和GPU線程。和大部分OpenGL處理解決方案中一樣,F(xiàn)lutter也采取一個線程責資源加載,一部分負責資源渲染這種思路。
兩個線程之間紋理共享有兩種方式。一種是EGLImage(IOS是 CVOpenGLESTextureCache)。一種是OpenGL Share Context。Flutter通過Share Context來實現(xiàn)紋理共享,將IO線程的Context和GPU線程的Context進行Share,放到同一個Share Group下面,這樣兩個線程下資源是互相可見可以共享的。
Platform線程是主線程,F(xiàn)lutter中有一個很奇怪的設定,GPU線程和主線程共用一個Context。并且在主線程也有很多OpenGL 操作。
這樣的設計會給音視頻開發(fā)帶來很多問題,后面會詳細說。
音視頻端美顏處理完成的OpenGL紋理能夠讓Flutter直接使用的條件就是Flutter的上下文需要和平臺音視頻相關的OpenGL上下文處在一個Share Group下面。
由于Flutter主線程的Context就是GPU的Context,所以在音視頻端主線程中有一些OpenGL操作的話,很有可能使Flutter整個OpenGL被破壞掉。所以需要將所有的OpenGL操作都限制在子線程中。
通過上述這兩個條件的處理,我們就可以在沒有增加GPU消耗的前提下實現(xiàn)美顏和濾鏡等等功能。
TPM
在經(jīng)過demo驗證之后,我們將這個方案應用到閑魚音視頻組件中,但改造過程中發(fā)現(xiàn)了一些問題。
上圖是攝像頭采集數(shù)據(jù)轉換為紋理的一段代碼,其中有兩個操作:首先是切進程,將后面的OpenGL操作都切到cameraQueue中。然后是設置一次上下文。然后這種限制條件或者說是潛規(guī)則往往在開發(fā)過程中容易被忽略的。而這個條件一旦忽略后果就是出現(xiàn)一些莫名其妙的詭異問題極難排查。因此我們就希望能抽象出一套框架,由框架本身實現(xiàn)線程的切換、上下文和模塊生命周期等的管理,開發(fā)者接入框架以后只需要安心實現(xiàn)自己的算法,而不需要關心這些潛規(guī)則還有其他一些重復的邏輯操作。
在引入Flutter之前閑魚的音視頻架構與大部分音視頻邏輯一樣采用分層架構:
1:底層是一些獨立模塊
2:SDK層是對底層模塊的封裝
3:最上層是UI層。
引入Flutter之后,通過分析各個模塊的使用場景,我們可以得出一個假設或者說是抽象:音視頻應用在終端上可以歸納為視頻幀解碼之后視頻數(shù)據(jù)幀在各個模塊之間流動的過程,基于這種假設去做Flutter音視頻框架的抽象。
咸魚Flutter多媒體開源組件
整個Flutter音視頻框架抽象分為管線和數(shù)據(jù)的抽象、模塊的抽象、線程統(tǒng)一管理和上下文同一管理四部分。
管線,其實就是視頻幀流動的管道。數(shù)據(jù),音視頻中涉及到的數(shù)據(jù)包括紋理、Bit Map以及時間戳等。結合現(xiàn)有的應用場景我們定義了管線流通數(shù)據(jù)以Texture為主數(shù)據(jù),同時可以選擇性的添加Bit Map等作為輔助數(shù)據(jù)。這樣的數(shù)據(jù)定義方式,避免重復的創(chuàng)建和銷毀紋理帶來的性能開銷以及多線程訪問紋理帶來的一些問題。也滿足一些特殊模塊對特殊數(shù)據(jù)的需求。同時也設計了紋理池來管理管線中的紋理數(shù)據(jù)。
模塊:如果把管線和數(shù)據(jù)比喻成血管和血液,那框架音視頻的場景就可以比喻成器官,我們根據(jù)模塊所在管線的位置抽象出采集、處理和輸出三個基類。這三個基類里實現(xiàn)了剛才說的線程切換,上下文切換,格式轉換等等共同邏輯,各個功能模塊通過集成自這些基類,可以避免很多重復勞動。
線程:每一個模塊初始化的時候,初始化函數(shù)就會去線程管理的模塊去獲取自己的線程,線程管理模塊可以決定給初始化函數(shù)分配新的線程或者已經(jīng)分配過其他模塊的線程。
這樣有三個好處:
一是可以根據(jù)需要去決定一個線程可以掛載多少模塊,做到線程間的負載均衡。第二,多線程并發(fā)式能夠保證模塊內(nèi)的OpenGL操作是在當前線程內(nèi)而不會跑到主線程去,徹底避免Flutter的OpenGL 環(huán)境被破壞。第三,多線程并行可以充分利用CPU多核架構,提升處理速度。
從Flutter端修改Flutter引擎將Context取出后,根據(jù)Context創(chuàng)建上下文的統(tǒng)一管理模塊,每一個模塊在初始化的時候會獲取它的線程,獲取之后會調(diào)用上下文管理模塊獲取自己的上下文。這樣可以保證每一個模塊的上下文都是與Flutter的上下文進行Share的,每個模塊之間資源都是共享可見的,F(xiàn)lutter和音視頻native之間也是互相共享可見的。
基于上述框架如果要實現(xiàn)一個簡單的場景,比如畫面實時預覽和濾鏡處理功能,
1:需要選擇功能模塊,功能模塊包括攝像頭模塊、濾鏡處理模塊和Flutter畫面渲染模塊,
2:需要配置模塊參數(shù),比如采集分辨率、濾鏡參數(shù)和前后攝像頭設置等,
3:在創(chuàng)建視頻管線后使用已配置的參數(shù)創(chuàng)建模塊
4:最后管線搭載模塊,開啟管線就可以實現(xiàn)這樣簡單的功能。
上圖為整個功能實現(xiàn)的代碼和結構圖。
結合上述音視頻框架,閑魚實現(xiàn)了Flutter多媒體開源組件。
組要包含四個基本組件分別是:
1:視頻圖像拍攝組件
2:播放器組件
3:視頻圖像編輯組件
4:相冊選擇組件
現(xiàn)在這些組件正在走內(nèi)部開源流程。預計9月份,相冊和播放器會實現(xiàn)開源。
后續(xù)展望和規(guī)劃
1:實現(xiàn)開頭所說的從底層SDK到UI的全鏈路的跨端開發(fā)。目前底層框架層和模塊層都是各個平臺各自實現(xiàn),反而是Flutter的UI端進行了跨平臺的統(tǒng)一,所以后續(xù)會將底層也按照音視頻常用做法把邏輯下沉到C++層,盡可能的實現(xiàn)全鏈路跨平臺。
2:第二部分內(nèi)容為開源共建,閑魚開源的內(nèi)容不僅包括拍攝、編輯組件,還包括了很多底層模塊,希望有開發(fā)者在基于Flutter開發(fā)音視頻應用時可以充分利用閑魚開源出的音視頻模塊能力,搭建APP框架,開發(fā)者只要去負責實現(xiàn)特殊需求模塊就可以,盡可能的減少重復勞動。
文章題目:閑魚flutter,閑魚flutter開源
網(wǎng)站URL:http://vcdvsql.cn/article38/dsiessp.html
成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供建站公司、用戶體驗、關鍵詞優(yōu)化、面包屑導航、網(wǎng)站改版、網(wǎng)站內(nèi)鏈
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉載內(nèi)容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)