Unity优化总结(持续更新)
工欲善其事,必先利其器。優先利用性能分析工具快速找出性能瓶頸,從瓶頸入手分析性能問題產生原因,可以事半功倍。
盡量減少占用的內存(資源體積)和CPU(計算量),首先著重減少總量才能更好的進行后續細節的優化。總量降低后,性能依舊有問題,那么可以考慮時間空間轉換的手段。
一般情況下,GPU比CPU富余,內存比CPU富余,磁盤比內存富余,分線程比主進程富余。所以一般都是GPU換CPU,內存換CPU,磁盤換內存,利用多線程分擔主進程的壓力。
比如利用GPU Instance可以減少CPU的壓力。利用對象池緩存,可以省略加載資源、實例化、銷毀實例、卸載資源的步驟,可以明顯降低CPU的消耗。
利用Loading進度條按需加載資源,可以減少內存峰值,大量節約內存。利用分線程進行計算,可以分擔主進程的壓力。
?
根據自己實踐優化總結,這個持續更新,遇到新的東西會記錄起來
設置相關
1.垂直同步
關閉垂直同步ProjectSetting-> Quality??VSync Count選成Don't Sync
2.幀率設置
設置最高幀率Application.targetFrameRate
3.物理更新
可以設置Fixed timestep減少物理更新
4.分辨率:
適當降低分辨率
很多2k分辨率手機,我們可以動態設置為1080,尤其對那些屏幕分辨率高,cpu,gpu性能差的手機提升明顯
5.碰撞層檢測
ProjectSetting-> physics里面有個layer collision matrix,根據項目實際情況勾選可生效的層碰撞
6.增量gc
這個是在2019.4版本有,就在playersetting設置,這個gc就會很平緩,不會突然卡一下那種
紋理相關(3d)
1.紋理格式
效果可以的情況下,android使用etc,etc2,ios使用pvrtc
如果效果達很差也不能用rgba32,雖然可以使用rgb16,但是遇到漸變的時候會出問題,這時可以嘗試astc這種格式(我項目在用,拋棄了低端機類似5s這些)
2.紋理尺寸:
在移動設備上的貼圖最大要控制在1024和512大小,可少量使用2048大小的貼圖,以1024、512大小貼圖為主。
角色的紋理需要比較精細,可以把頭發,衣服,皮膚,武器分離適合的尺寸紋理
可以使用這個工具查看紋理“場景檢查工具”
3.關閉read/write功能
一般沒用到換裝功能基本不會用到這個,而且內存會大一倍
4.小物件的紋理合并,并使用同一個材質球
5.Mipmap
Mipmap 會增加游戲包體的大小和占用一定量的內存,但在游戲中Mipmap的渲染可以減少顯存帶寬,降低渲染壓力,隨著相機的推遠貼圖會隨之切換成低像素的體貼,從而節省資源開支
5.texture streaming
streaming流加載貼圖mipmap,意思就是用到哪一級mipmap只加載這一級的,上面說到使用mipmap會增加內存,也有團隊為了減少內存,從而關閉了mipmap。這就會導致另一個問題,美術的貼圖非常精細,在UI中近距離觀察效果很好,可是一旦攝像機拉遠,就會出現“噪點”很難看。 所以mipmap一定要打開。所以unity2018.2中提供了streaming流加載貼圖mipmap,意思就是用到哪一級mipmap只加載這一級的,其他級的mipmap不加載具體可以看下面鏈接mipmap優化
用法:需要動態加載mipmap紋理勾選texture streaming,根據需求動態設置內存大小就ok
6.設置最高質量mipmap
QualitySettings.masterTextureLimit
對于低端機來說可以降低采樣率,對于那種ui和場景結合,然后場景模糊的可以設置,這樣大大提交性能
//0表示正常尺寸?//1表示降低1/2//2表示降低1/4?//3表示降低1/8//4表示降低1/16
QualitySettings.masterTextureLimit = 4;
7.減少使用透明貼圖
透明貼圖對gpu消耗大,
具體原理可以看看移動gpu原理性能分析和建議都有,說得很詳細,建議看看
https://zhuanlan.zhihu.com/p/112120206
角色場景模型mesh
1.減面
對場景模型減面優化是最常見的優化操作。主要是去掉對模型造型沒有影響的面,用盡可能少的面數表達清楚模型的結構和造型。比如:物件非關節點及物件背面、內部不會看見的面刪掉,這里提供一個"場景檢查工具"
2.合并模型(小范圍內才能使用)
合并同一小范圍內的非交互類的靜態小物件,同時合并小物件的貼圖。這樣可以減少DRAW CALL的數量。一組大小形狀不同的石頭,如果同一個物體,然后距離很遠,然后合并就沒什么意義,可能負優化
3.LOD
建筑和復雜的物件用LOD模型和遠處剔除來減少同屏面數。地形的LOD系統也可以對地形的面數做很大的優化。
4.模型的重復利用
相同的多個物件在unity內復制使用,復制的多個物體在引擎計算上算一個物體。但也不可復制太多個,太多會對內存帶來很大壓力。相同的物件太多,建議把幾個合并成一組做為一個Object,多做幾組,再進行復制。參考②。
5.地形優化
如果是用unity自帶的地形工具制作的地形,可以用T4M插件轉化成T4M格式地形,設置一個頂點值轉化后可以對地形優化很多。T4M也可以設置lod模型。
6.網格碰撞
盡可能不用mesh collider,要用的時候可以讓美術簡化一個mesh專做碰撞,而不是再原有基礎上加meshcollider
7.關閉模型read/write
粒子系統通常需要動態地修改其粒子的頂點屬性,所以用到粒子系統的mesh不能關閉read/write
場景中需要動態合并網格需要read/write
角色中需要換裝合并網格需要read/write
8.分場景加載
例如:主界面場景很大可以切割多個場景,通過add方式加載場景,不用的時候卸載
場景相關
1.批處理
Unity提供了三種批次合并的方法,分別是Static Batching,GPU Instancing和Dynamic Batching。它們的原理分別如下:
Static Batching,將靜態物體集合成一個大號vbo提交,但是只對要渲染的物體提交其IBO。這么做不是沒有代價。比如說,四個物體要靜態批次合并前三個物體每個頂點只需要位置,第一套uv坐標信息,法線信息,而第四個物體除了以上信息,還多出來切線信息,則這個VBO會在每個頂點都包括所有的四套信息,毫無疑問組合這個VBO是要對CPU和顯存有額外開銷的。要求每一次Static Batching使用同樣的material,但是對mesh不要求相同。
Dynamic Batching將物體動態組裝成一個個稍大的vbo+ibo提交。這個過程不要求使用同樣的mesh,但是也一樣要求同樣的材質。但是,由于每一幀CPU都要將每個物體的頂點從模型坐標空間變換到組裝后的模型的坐標空間,這樣做會帶來一定的計算壓力。所以對于Unity引擎,一個批次的動態物體頂點數是有限制的。
GPU Instancing是只提交一個物體的mesh,但是將多個使用同種mesh和material的物體的差異化信息(包括位置,縮放,旋轉,shader上面的參數等。shader參數不包括紋理)組合成一個PIA提交。在GPU側,通過讀取每個物體的PIA數據,對同一個mesh進行各種變換后繪制。這種方式相比static和dynamic節約顯存,又相比dynamic節約CPU開銷。但是相比這兩種批次合并方案,會略微給GPU帶來一定的計算壓力。但這種壓力通常可以忽略不計。限制是必須相同材質相同物體,但是不同物體的材質上的參數可以不同。
所以Unity默認策略是優先static,其次gpu instancing,最后dynamic。當然如果頂點數過于巨大(比如渲染它幾千顆使用同種mesh的樹),那么gpu instancing或許比static batching是一個更加合適的方案。
靜態批處理和動態批處理
2.GPU Instancing
提高圖形性能的另一個好辦法是使用GPU Instancing。GPU Instancing的最大優勢是可以減少內存使用和CPU開銷。當使用GPU Instancing時,不需要打開批處理,GPU Instancing的目的是一個網格可以與一系列附加參數一起被推送到GPU。要利用GPU Instancing,您必須使用相同的材質,且可以傳遞額外的參數到著色器,如顏色,浮點數等。
適用于不是靜態,沒有進行批處理的對象
例如:場景里面有很多箱子,可以被打爆,這種情況可以使用
3.光源“Important”個數
建議1個,一般為方向光。“Important”個數應該越小越少。個數越多,drawcall越多。
4.Pixel Light數目
建議1-2個。
5.場景烘焙
效果可以情況下光照圖盡可能小,場景不接受實時光照,實時光照只給主角
6.盡可能地使用prefab
2018版本開始有prefab嵌套的功能,做出prefab防止資源冗余問題,也可以降低內存帶寬的負擔
7.提高場景資源復用率
動態加載,場景里面所有東西都保存預設,把場景數據保存配置表,加載的時候都是讀取配置動態加載預設,唯一好處就是可以實現不同場景公用預設
整個場景打包,把公用的東西都設置標簽例如:fbx,材質球
8.場景加載
當前一個場景還未釋放的時候,切換到新的場景。這時候由于兩個內存疊加很容易達到內存峰值。解決方案是,在屏幕中間遮蓋一個Loading場景。在舊的釋放完,并且新的初始化結束后,隱藏Loading場景,使之有效的避開內存大量疊加超過峰值
流程:舊場景->空場景(loading場景)->新場景
粒子特效
1.屏幕上的最大粒子數
建議小于200個粒子。
2.每個粒子發射器發射的最大粒子數
建議不超過50個。
3.粒子大小
粒子的size應該盡可能地小。因為Unity的粒子系統的shader無論是alpha test還是alpha blending都是一筆不小的開銷。同時,對于非常小的粒子,建議粒子紋理去掉alpha通道。
4.不要開啟粒子的碰撞功能
5.粒子特效shader用mobile,特殊需求只能加shader
6.特效顯示規則
一個特效里面分開很多部分,根據性能設置來顯示部分特效“相關鏈接”
7.貼圖和shader選用
能使用Additive和黑色背景的特效圖(不透明),取代alpha blend+透明貼圖
8.關于粒子制作優化
如果單個粒子其實完全可以不用使用粒子腳本,如果只是簡單旋轉可以通過腳本進行實現
如果一個特效需要十幾張不同的貼圖,這就要看看是否真的合理
特效性能是個重災區
這些很考驗特效制作人的功力
動畫優化
1.動畫壓縮格式
Optimal對性能最好,但隨著設備的提升,Keyframe Reduction和Optimal的加載效率提升已不十分明顯
Optimal壓縮方式可能會降低動畫的視覺質量,因此,是否最終選擇Optimal壓縮模式,還需根據最終視覺效果的接受程度來決定。
2.默認不開啟Resample Curves選項
Unity官方給的建議是開啟,但是開啟會增大內存,所以在動畫沒有問題的情況下建議關閉該選項。
3.減少幀信息的精度
將幀信息精度減少為小數點后四位,網上大多數文章也是給出的是小數點后三位,但是優化到小數點后三位導致我們游戲一些角色的披風抖動時會穿幫,所以精度優化到了小數點后四位。??
4.優化未改變的Rotation、Position序列幀和剔除Scale序列幀
如果需要用到Scale變化就在骨骼節點上加關鍵字區分。Position、Rotation有很多骨骼節點其實并未發生變化,但是由于我們Animation.Compression這兩個優化值設置的比較小,所以這里我們手動優化掉沒有變化的Position、Rotation序列幀,只留頭尾兩幀,保證Position和Rotation初始值正確,這么處理一下會降不少內存
相關工具參考
音頻優化
1.格式使用
音效、語音:ogg
背景音樂:mp3
2.導入器設置
ForceToMono:根據項目實際對音樂的效果要求而定,一般語音、音效勾選,長音樂看情況;
CompressionFormat:Vorbis
SampleRateSetting:語音、音效選OverrideSampleRate的22050KHz就足夠了,保留初始音質可選PreserveSampleRate
LoadType: 對于內存壓力大的項目,首選Streaming
相關鏈接
UI優化
1.drawcall優化
drawcall不是越少越好,如果ui很多,但是drawcall很少,那就是占用很多顯存寬帶,也會導致發熱
一般都是處理應該合并但是沒有合并的ui
優化方向,詳細查看這個鏈接
- 空的image和透明的image(有自帶sprite調顏色透明)都改掉換成EmptyRaycast
-
盡可能不用mask或者mask2d(有些必須用的沒辦法),這個會引起drawcall無法合并
-
Ui排版
-
資源適當冗余可以減少drawcall
-
圖集整理
-
一些復雜的ui適當加canvas
2.ui重建
什么引起uirebuild,可以理解為整個ui就是一個網格,凡是引起網格改變就要rebuild
例如ui移動位置,大小改變,文字改變這些都會引起rebuild
根據重建的頻率來處理,尤其戰斗中的小地圖,飄字,血條,都是網格重建最多的
這些可以單獨抽出一個canvas
3.RayCast
屏蔽沒必要的raycast也是性能消耗
4.循環滾動列表
這個參考循環列表加個回調就可以滿足大部分需求
參考案例
5.字體相關
字體盡量使用一種,其他特殊字體可以使用圖片字
圖片字生成工具
6.ui動畫優化
用shader實現效果代替animation和dotween,因為dotween和aniamtion都是會引起網格重建,例如旋轉,進度條
具體鏈接
7.盡可能少圖集帶進戰斗里面,戰斗中的圖集盡量保持兩張左右(戰斗ui和頭像)
8.大圖脫離圖集使用rawimage(背景圖,尺寸偏大的圖片)
9.圖集盡可能在1024*1024范圍內,盡可能使用九宮格
? ?正常按照1920*1080切圖,放到圖集里面,整到1024*1024一般問題不大
10.關于冗余圖或者圖集放的不規范可以使用工具來進行優化
圖集工具
11.圖集關掉mipmap和read/write功能,這兩個都會增大內存
12.圖集導入設置相關
?鏈接
13.運行時動態圖集
運行的時候,把需要用到的sprite合成一張圖集,這樣可以減少寬帶,降低drawcall
演示demo
14.ui顯示隱藏
1、如果該UI界面開啟的頻率很低,可考慮直接通過Instantiate/Destroy來進行切換;
2、如果該UI界面使用較為頻繁,可嘗試通過Active/Deactive來代替Instantiate/Destroy操作,從而降低UI切換時的性能開銷;
3、如果該UI界面使用非常頻繁,則可嘗試直接改變UI界面位置的方式來移進/移出相機視域體,從而來極大提升UI界面的切換效率。
15.ui粒子
使用ui粒子避免層級問題導致不同ui得穿插
ui粒子鏈接
序列化優化
1.使用protobuf來處理配置表
相對xml,json,lua來說序列化速度快,可以看看這個工具excel轉protobuf工具
2.使用性能更好的Json
NewtonJson庫的GC量以及耗時最低
效果優化
1.屏幕效果,后處理優化?鏈接
2.陰影優化,鏡面反射優化 鏈接
3.下雨天效果優化 鏈接
4.特效扭曲效果? 鏈接
5.深度圖優化(景深之類的) 鏈接
材質球和shader優化
1.pass
減少不必要的pass,單個shader最好不要超過3個
2.減少standard shader
這個shader的變體是個噩夢,占內存,可以考慮根據美術需求寫surface shader
如果要使用,只能官網下載對應版本standard,修改一下shader,要關掉沒用的shader_feature,比如:_PARALLAXMAP、SHADOWS_SOFT、DIRLIGHTMAP_COMBINED DIRLIGHTMAP_SEPARATE、_DETAIL_MULX2、_ALPHAPREMULTIPLY_ON;另外要去掉多余的pass
3.surface shader
注意關掉不用的功能,比如:noshadow、noambient、novertexlights、nolightmap、nodynlightmap、nodirlightmap、nofog、nometa、noforwardadd等
4.shader變量類型選擇
用fixed、half代替float,建立shader統一類型(fixed效率是float的4倍,half是float的2倍)
5.添加宏開關
shader_feature、multi_compile,并將宏開關
如果某些效果真機丟失,其實就是變體丟失,建議使用multi_compile,這個會把變體全部打進去
性能設置的時候可以根據配置開啟
6.材質設置屬性優化
使用屬性塊代替直接修改屬性 相關鏈接
7.材質球設置參數會導致材質球無法合并渲染
例如:有個物件爆炸,加入爆炸面片都是單獨使用單個相同材質球,這時候如果單個面片執行材質球參數變化會導致全部實例化一個新材質球,導致材質球無法合并
解決方法:實例化一個材質球賦值給所有面片,材質球參數變化只處理實例化的材質球,這樣子不會引起材質球不合并問題
8.粒子shader多個pass無法合并
粒子特效上多個pass盡量少用
內存泄漏
資源內存泄漏都是可以找到源頭,堆內存泄漏是比較難查,下面是堆內存泄漏的經驗
堆內存泄漏
代碼優化
1.在Update里面一直new
例子:每次update都new list(keys)來遍歷定時器和移除定時器
因為foreach下不能移除字典數據,所以用這種new keys
//錯誤寫法 public class ErrorInvoke {Dictionary<string, InvokeData> dic = new Dictionary<string, InvokeData>();void Update(){List<string> list = new List<string>(dic.Keys);for(int i=0;i<list.Count;i++){//滿足情況就移除字典}} }解決方案:用列表來存儲相關定時器,update只遍歷列表,字典存儲列表的引用就好
//修改之后寫法 public class FixInvoke {Dictionary<string, object> dic = new Dictionary<string, object>();List<InvokeData> list = new List<InvokeData>();void Update(){for(int i=list.Count;i-->0;){//滿足條件移除列表移除字典}} }2.太多裝箱和拆箱
這個東西不用說太多就是目標類型->object裝箱,object->目標類型拆箱
這個用的最多可能就是事件系統,我發送事件里面可以有多個參數,這些參數類型不定,然后到最后消息回調里面進行類型轉換,頻繁的裝箱拆箱會引起內存問題,可以使用避免裝箱拆箱的事件系統
我同事寫的事件事件系統全部用parma object[],因為戰斗和ui之間的通信通過事件來,如果一直update來發送事件,裝箱拆箱很頻繁,我是不建議這么寫事件系統
可以參考這個事件系統https://blog.csdn.net/SnoopyNa2Co3/article/details/84971510
3.遍歷列表移除
很多情況我們遍歷列表來更新數據,如果數據已經沒用的情況要從list移除
很多情況的寫法是這樣的,把移除的存起來,遍歷完再移除
如果數據沒有先后順序問題的情況可以下面優化
存起來移除的代碼
public class Temp1 {List<datat> list = new List<datat>();List<datat> removeList = new List<datat>();void Update(){datat temp;for (int i=0;i<list.Count;i++){temp = list[i];if(temp.Remove){removeList.Add(temp);}}if(removeList.Count > 0){for(int i=0;i<removeList.Count;i++){list.Remove(removeList[i]);}removeList.Clear();}} }下面是優化過的代碼
public class Temp2 {List<datat> list = new List<datat>();void Update(){datat temp;for (int i = list.Count; i-->0 ;){temp = list[i];if (temp.Remove){list.RemoveAt(i);}}} }4.關于反射的使用
反射有什么好處,其實就是少寫一些代碼,方便一點
就例如:后端返回的協議號和對應的proto,來進行序列化數據
1.游戲初始化的時候進行綁定
2.就是用工具進行綁定代碼的生成
反射著東西能不用盡量不用
5.頻繁GetComponent
一般來說初始化的時候才會GetComponent,如果在update里面出現GetComponent或者頻繁,這代碼肯定有問題
尤其UI上面出現這種基本都是要改。拒絕沒必要的性能消耗
6.統一update和lateupdate
就是游戲里面只有掛MonoBehaviour腳本,里面的update和lateupdate驅動整個游戲
update里面遍歷100次比100個MonoBehaviour腳本 update效率更高
7.盡量減少MonoBehaviour使用
正常游戲邏輯都能代替MonoBehaviour,除非一些用到一些特殊接口,例如onaniamtorik之類的
8.對于不太重要的列表數據,需要update處理,可以用分幀處理
9.關于回調delegate,使用完立刻設null,防止某些寫法導致內存泄漏
10.Unity tag對比
if(other.tag == a.tag)改為other.CompareTag(a.tag).因為other.tag為產生180B的GC Allow.
11.對象緩存
我們可以把一些必要的對象緩存起來,在Unity中,類似于GameObject.Find , GetComponent,transform,camera.Main這類的函數,會產生較大的消耗
12.對象池(一般項目都會有)
13.Struct 與 Class 選擇
Struct 在棧中不產生 GC,class 在堆中,會產生 GC。對 Struct 的結點修改時,修改完以后記得重新賦值。因為 Struct 賦值是 copy而不是引用,修改完以后,以前的不生效。
?堆棧的空間有限,對于大量的邏輯的對象,創建類要比創建結構好一些。
結構表示輕量對象,并且結構的成本較低,適合處理大量短暫的對象。
在表現抽象和多級別的對象層次時,類是最好的選擇。
大多數情況下該類型只是一些數據時,結構是最佳的選擇。
14.string的優化
stringbuilder用不習慣,推薦使用zstring零gc,寫法簡單
傳送口https://github.com/871041532/zstring
15.注意log的消耗
debug打印cpu消耗很高,注意加宏或者自己封裝一下打印模塊
16.應盡量為類或函數聲明為sealed
IL2CPP就sealed的類或函數會有優化,變虛函數調用為直接函數調用。詳見《IL2CPP OPTIMIZATIONS: DEVIRTUALIZATION》。
17.減少Dictionary的冗余訪問
我們常習慣編寫這樣的代碼:
if(myDictionary.Contains(oneKey)) {MyValue myValue = myDictionary[oneKey];// ... }但其可減少冗余的哈希次數,優化為:
MyValue myValue; if(myDictionary.TryGetValue(oneKey, out myValue)) {// ... }18.應減少UnityEngine.Object的null比較
因為Unity overwrite掉了Object.Equals(),《CUSTOM == OPERATOR, SHOULD WE KEEP IT?》也說過unityEngineObject==null事實上和GetComponent()的消耗類似,都涉及到Engine層面的機制調用,所以UnityEngine.Object的null比較,都會有少許的性能消耗
19.關于實現思路問題
代碼實現千千萬萬種,必須多想想,就怕頭腦一熱就寫代碼,需要冷靜分析思考
下面給個真實存在的反例,有些代碼不看不知道,一看嚇一跳
主要功能用管理器管理所有角色部位陰影
看代碼就知道又什么問題
1.多了沒必要的字符串Contains判斷
2.直接update來執行unityaction(用于不同角色陰影位置更新,功能可能不一樣)(unityaction性能一般)
改法
管理器只管理陰影面片取出和回收,每個角色都有一個陰影控制器,自己管理自己的東西(可以重載功能)
這樣子代碼清晰,而且管理方便,這有點類似ecs思想,不過沒有狠徹底
public void RemoveShadow(string roleName){List<string> toDelete = new List<string>();foreach(KeyValuePair<string,GameObject> pair in shadows){if(pair.Key.Contains(roleName)){if (null != pair.Value){pair.Value.SetActive2(false);notUseShadows.Add(pair.Value);}toDelete.Add(pair.Key);}}for(int i = 0; i < toDelete.Count; i++){shadows.Remove(toDelete[i]);}updateActions.Remove(roleName);}public void Update(){foreach(UnityAction action in updateActions.Values){action();}}20.使用多線程,線程以及unity交互
可以減少不是非常必要的邏輯在主線程運行
關于線程和unity交互可以看這個鏈接https://blog.csdn.net/SnoopyNa2Co3/article/details/108546696
21.關于委托Delegate, Action,UnityAction
使用Delegate, Action性能比unityaction快4-5倍,除了UGUI里的EventSystem都是基于unityaction實現的,使用之外,其他地方沒有使用的必要
22.關于Input.touches
每次在對其調用時,都會new一個數組touches,從而造成一定的堆內存分配
Allocates temporary variables分配臨時變量
避免Input.touches的頻繁使用以防造成堆內存的額外占用
23.關于Linq相關函數的調用
Linq在執行過程中會產生一些臨時變量,而且會用到委托(lambda 表達式)。如果使用委托作為條件的判定方法,時間開銷就會很高,并且會造成一定的堆內存分配。所以在一般的Unity游戲項目開發中不推薦使用Linq相關的函數
24.使用更高效的異步/等待UniTask
Unity中使用協同程序通常是解決某些問題的好方法,但它也帶來了一些缺陷:
-
沒有返回值,在處理需要返回值時候,非常容易陷入回調地獄
-
協調程序使錯誤處理變得困難
所以在當前的項目中使用了UniTask來代替Unity coroutine使用。
UniTask解決了C#Task異步不能執行某些API的問題,將C# async/awake異步編程模型完美的帶入了Unity中(需要Unity2018.3/C#7.0+),在項目中的實踐,UniTask帶來了簡潔高效的體驗。
https://github.com/Cysharp/UniTask
?
總結
以上是生活随笔為你收集整理的Unity优化总结(持续更新)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 发票扫描识别技术的应用
- 下一篇: php会话到期提醒功能,php – 用