夢織網站短視頻seo推廣
內存(Memory)
unity 內存部分也是優(yōu)化過程中非常重要的一個環(huán)節(jié),也會影像渲染過程中的同步等待與帶寬問題。因此內存的優(yōu)化也可能會給我們渲染開銷帶來精簡,今天我們先來了解unity中的內存與使用到的內存工具。
Unity中的內存
- 托管內存:主要是指使用托管堆或者垃圾收集器自動分配和管理的內存也包括腳本堆棧與虛擬機內存。
- C#非托管內存:可以在C#與Unity Collection名字空間和包結合使用,不使用垃圾收集器管理的內存部分。如果使用數(shù)據(jù)結構,不建議使用system下的collection的數(shù)據(jù)結構,而是要使用unity collection下的數(shù)據(jù)結構進行開發(fā)。
- Native 內存:Unity 用于運行引擎的C++內存。
性能分析工具
下面我們聊一聊unity引擎提供了哪兒些內存方面的工具
- Unity Profiler下的Memory標簽:這里顯示了unity當下內存使用的追蹤狀態(tài),包括各類內存的分配和使用情況,以及當前unity下分配的對象與資源占用的內存情況。2021以后版本,profiler不再提供對象抓取快照功能了。而是使用memory profiler 直接抓取內存快照了。
- Memory Profiler:我們可以通過它來抓取內存快照,也可以對比兩個內存快照下unity對象與資源的差異。通過Tree Map來查看內存分配的視圖。通過Object and Allocation標簽查看具體對象內存快照,并可以通過鏈接直接找到原工程中對應的資源和對象,通過fragmentation標簽可以查看內存片段,unity2022以后 memory profiler變得更加簡潔了,針對native內存甚至可以查看到具體是分配到哪兒個allocators中的。
- Memory Settings中的設置:大家現(xiàn)在可以在權衡時間和空間維度上的性能指標做更精準的設置了。
- UPR中的內存快照功能:主要是針對移動設備,當你使用UPR做性能調試時會經常用到,它可以脫離unity編輯器,在運行時抓取內存信息與對象分配信息,并可以做到多幀對比。具體操作請參閱UPR手冊,后期我們可能會單獨做寫一個帖子,純翻譯供大家閱讀。
- mac或者ios上我們可以選擇xcode提供的instrument下的allocation工具。
- android上可以使用android相關的系統(tǒng)命令或者android studio profiler工具。
Profiler-Memory
Total Memory Breakdown(總內存分解)
- ManagedHeap:托管堆,重點監(jiān)控對象,不要讓它超過20MB,否則可能會有性能問題!
- Graphics & Graphics Driver:驅動程序在紋理、渲染目標、著色器和網格數(shù)據(jù)上使用的估計內存量。
- Audio:音效及聲音文件,重點優(yōu)化對象,播放時長較長的音樂文件需要進行壓縮成.mp3或.ogg格式,時長較短的音效文件可以使用.wav 或.aiff格式。
- Video:視頻系統(tǒng)的估計內存使用量。
- Other:顯示Unity跟蹤的本機內存,但不在特定計數(shù)器下報告。
- Profiler:探查器功能從系統(tǒng)中使用和保留的內存。
Objects Status(對象狀態(tài),顯示通常占用大量內存的資源類型(紋理,網格,材質,動畫剪輯)的對象實例數(shù)量,以及它們在內存中的累積大小(資源,游戲對象,場景對象))
-
Texture2D: 2D貼圖及紋理。重點優(yōu)化對象,有以下幾點可以優(yōu)化:
1.許多貼圖采用的Format格式是ARGB 32 bit所以保真度很高但占用的內存也很大。在不失真的前提下,適當壓縮貼圖,使用ARGB 16 bit就會減少一倍,如果繼續(xù)Android采用RGBA Compressed ETC2 8 bits(iOS采用RGBA Compressed PVRTC 4 bits),又可以再減少一倍。把不需要透貼但有alpha通道的貼圖,全都轉換格式Android:RGB Compressed ETC 4 bits,iOS:RGB Compressed PVRTC 4 bits。
2.當加載一個新的Prefab或貼圖,不及時回收,它就會永駐在內存中,就算切換場景也不會銷毀。應該確定物體不再使用或長時間不使用就先把物體制空(null),然后調用Resources.UnloadUnusedAssets(),才能真正釋放內存。
3.有大量空白的圖集貼圖,可以用TexturePacker等工具進行優(yōu)化或考慮合并到其他圖集中。
4.存在空白的或者純色的貼圖,可以用顏色節(jié)點代替
- Mesh:場景中使用的網格模型,注意網格模型的點面數(shù),能合并的mesh盡量合并。
- Materials:加載的材質和它們使用的內存的總數(shù)。
- AnimationClip: 加載的動畫和它們使用的內存的總數(shù)。
Assets(已加載資產的總數(shù))
- Game Objects:游戲對象總數(shù)
- Scene Objects:這個數(shù)字包括游戲對象的數(shù)量,加上組件的總數(shù),以及場景中所有不屬于資產的東西。
- GC allocated in frame:顯示選定幀中托管分配的數(shù)量及其總大小(以字節(jié)為單位)。
Memory Profiler
主要用來查看托管內存和本機內存的詳細分配情況。它通過捕獲、檢查、比對內存快照的方式來檢測內存泄漏和內存碎片。本篇文章中使用的版本是0.7.1版本。
安裝
add PackageManager- >Add By Name- >輸入com.unity.memoryprofiler
?
查看
Windows - > Analysis - > Memory Profiler
Memory Profiler界面,可以鏈接真機檢測,也可以在Editor檢測。
點擊Capture? New Snapshot截取保存當下幀的內容
點擊上圖中3號的位置Snap來查看詳細的內容
通過觀察,我們能看到上圖的數(shù)據(jù)與Profiler中的Memory的數(shù)據(jù)是一致的
單幀檢測
一般去看工程內的資源, 去檢查占用內存特別大的游戲對象。
Memory Breakdowns界面可以查看unity內的具體游戲對象,也同樣可以進行篩選
TreeMap界面進行檢查, 這里已經分好類, 同時可以根據(jù)Size的大小進行排序,查看內存占用較大的游戲對象進行優(yōu)化處理。
Fragmentation 頁簽進行查看, 點擊對應的地址塊,下方可顯示詳細信息。
1.該視圖會將內存數(shù)據(jù)可視化成虛擬內存布局。如下圖所示:
?2.每一行都會顯示一個內存塊和起始地址標簽。當起始地址標簽上面有黑色背景時,就表明該起始地址就是內存塊的開始部分,并且與之前的內存塊之間存在不連續(xù)性;否則,就表明該起始地址就是內存塊的一部分。如下圖所示:
3.既可以通過單擊起始地址標簽來選擇關聯(lián)的內存塊;也可以通過單擊鼠標拖動的方式來選擇感興趣的內存塊;甚至可以通過單擊內存塊中的虛擬內存來選擇該內存塊。
4.當選擇內存塊時,就會在Filters面板中將相關的虛擬內存按照指定的列表類型(區(qū)域列表-Regions list、分配列表-Allocations list、對象列表-Objects list)進行展示詳細信息。
區(qū)域列表類型展示信息如下圖所示:
分配列表類型展示信息如下圖所示:
還有對象列表類型展示信息,操作同上。
Objects and Allocations頁面可查看詳細的對比內容,可以進行篩選
篩選方式:Select Table View
篩選之后就可以進行詳細分析了,可以通過Type,Size,?Referenced By等標簽查看對應的游戲對象。
也可以鼠標右鍵點擊下圖1或者2來對類型和名字進行具體篩選。
兩幀對比檢測
一般使用兩幀率對比用于檢測內存泄漏。
在要對比的節(jié)點分別進行Capture? New Snapshot截取, 點擊Compare Snapshots進行對比,在分別點擊兩個Snap,進行對比。
Summary頁簽可看匯總的對比內容:
Objects and Allocations頁面可查看詳細的對比內容,可以進行篩選
篩選方式:Select Table View來查看以下幾種類型數(shù)據(jù):
- [Diff] Raw Data:從原始數(shù)據(jù)列表中選擇一項原始數(shù)據(jù)(Root Reference、Native Allocation、Native Object等)進行查看。
- [Diff] All Managed Objects:查看所有的托管對象(IL2CPP、Mono)。
- [Diff] All Native Objects:查看所有繼承自Unity.Object類型的本機對象。
- [Diff] All Objects:查看所有本機對象和托管對象。
- [Diff] Alloc:從分配列表中選擇一項分配數(shù)據(jù)(ByNativeObject、ByRoot、ByMemRegion)進行查看。
篩選之后就可以進行詳細分析了,可以通過Type,Size,?Referenced By等標簽查看對應的游戲對象。
也可以鼠標右鍵來對類型和名字進行具體篩選。
總結
MemoryProfiler 是一個非常好用的檢查內存問題的工具,以下問題都可以通過該工具進行排查
- 查找有問題的游戲資源,例如:Mesh和貼圖非常大的美術資源
- 內存泄漏問題
檢測內存占用:可以使用Unity Memory Profiler來檢測托管內存和本機內存的占用情況。檢測流程如下所示:
- 首先打開Unity Memory Profiler窗口;然后打開想要檢查的內存快照;最后在主視圖區(qū)域以樹形視圖的方式來顯示內存快照中深度內存數(shù)據(jù)。
- 查看樹形視圖中不同的對象類別。
- 單擊樹形視圖中某一個對象類別,此時會展開該對象類別中所有的對象以及在主視圖區(qū)域下方以對象表格的方式來顯示該對象類別中所有的對象。
- 單擊對象類別中某一個對象或者單擊對象表格中某一個對象,進而可以在對象表格中查看該對象的具體信息。
- 首先將對象表格中所有的對象按照從高到低的順序進行排序;然后優(yōu)先從紋理、著色器變體、預分配緩沖區(qū)這三種對象來制定好減少內存的目標。
檢測內存泄漏:可以使用Unity Memory Profiler來檢測托管內存和本機內存的泄漏情況。如下所示:
1.出現(xiàn)內存泄漏的危害如下所示:
- 應用程序可能因為GC遍歷對象時間變長的原因而出現(xiàn)卡頓現(xiàn)象。
- 應用程序可能因為可用內存空間不足的原因而出現(xiàn)閃退現(xiàn)象。
2.出現(xiàn)內存泄漏的原因如下所示:
- 對于自動垃圾回收而言,對象的引用計數(shù)不為0。
- 對于被動垃圾回收而言,對象沒有被代碼手動釋放。
3.查找并修復場景卸載后發(fā)生的內存泄漏:流程如下所示:
- 使用Unity Memory Profiler來設置捕獲目標。
- 首先在捕獲目標上加載一個空場景;然后在該場景上拍攝一張內存快照。
- 首先在捕獲目標上加載一個要檢測內存泄漏的場景;然后在該場景上執(zhí)行業(yè)務模塊;最后將該場景卸載(調用Resources.UnloadUnusedAssets函數(shù))掉或者切換到一個空場景。
- 在捕獲目標上再拍攝一張內存快照。
- 為了避免處理內存快照文件和捕獲目標之間競爭系統(tǒng)資源,建議此時關閉掉捕獲目標。
- 首先在工作臺區(qū)域打開第一張和第二張內存快照文件;然后單擊Diff按鈕來對兩個打開的內存快照進行差異比對;最后將差異比對生成的數(shù)據(jù)顯示在主視圖區(qū)域中。
- 首先在主視圖區(qū)域中選擇Diff表格屬性;然后選擇Group排序規(guī)則來將相同值(Deleted、New、Same)的對象合并在一個組內;最后查看數(shù)值為New的分組,如果存在對象是在第二張內存快照中的話,就表明該對象的內存泄漏了。
4.查找并修復小的連續(xù)分配可能造成的內存泄漏:流程如下所示:
- 使用Unity Memory Profiler來設置捕獲目標。
- 首先在捕獲目標上加載一個要檢測內存泄漏的場景;然后在該場景上拍攝第一張內存快照。
- 首先播放要檢測內存泄漏的場景;接著在該場景上拍攝第二張內存快照;然后繼續(xù)播放該場景;最后在該場景上拍攝第三張內存快照。
- 為了避免處理內存快照文件和捕獲目標之間競爭系統(tǒng)資源,建議此時關閉掉捕獲目標。
- 首先在工作臺區(qū)域打開拍攝的第二張和第三張內存快照文件;然后單擊Diff按鈕來對兩個打開的內存快照進行差異比對;最后將差異比對生成的數(shù)據(jù)顯示在主視圖區(qū)域中。
- 首先在主視圖區(qū)域中選擇Diff表格屬性;然后選擇Group排序規(guī)則來將相同值(Deleted、New、Same)的對象合并在一個組內。
- 首先在主視圖中選擇Owned Size表格屬性;然后選擇Group和Sort Descending排序規(guī)則來將相同值的對象合并在一個組內,并按照從大到小的順序來排列組。
- 查看較大內存分配組中的對象是否同時存在于Same組和New組中,記錄好滿足條件的對象。
- 首先在工作臺區(qū)域打開拍攝的第一張和第二張內存快照文件;接著單擊Diff按鈕來對兩個打開的內存快照進行差異比對;然后將差異比對生成的數(shù)據(jù)顯示在主視圖區(qū)域中;最后執(zhí)行4.6 ~ 4.8步驟,進而了解系統(tǒng)內潛在的內存泄漏。
元數(shù)據(jù):如下所示:
1.元數(shù)據(jù)類型為MetaData,包含的字段如下所示:
- content:包含項目名稱和捕獲目標為Unity Editor時的腳本版本。
- platform:應用程序對應的目標平臺。
- screenshot:針對捕獲目標截取的屏幕截圖(像素大小小于480x240)。
2.首先在捕獲目標上拍攝內存快照時就會生成元數(shù)據(jù);然后該元數(shù)據(jù)會自動添加到內存快照中;最后開發(fā)人員可以通過元數(shù)據(jù)來更好地了解內存快照的內容。
3.拍攝內存快照的方式如下所示:
- 當項目中有安裝Unity Memory Profiler時,此時就可以在工具欄區(qū)域中點擊Capture控件來針對捕獲目標來拍攝一張內存快照。
- 在代碼中通過MemoryProfiler.TakeSnapshot/TakeTempSnapshot函數(shù)來針對捕獲目標拍攝一張內存快照。在調用該函數(shù)時,可以設置包含內存快照文件路徑字符串和是否拍攝成功布爾值兩個參數(shù)的結束回調函數(shù)。
4.生成元數(shù)據(jù)的方式如下所示:
- 當項目中沒有安裝Unity Memory Profiler時,此時可以首先給MemoryProfiler.createMetaData委托注冊一個監(jiān)聽函數(shù);然后在該監(jiān)聽函數(shù)中設置元數(shù)據(jù)。
- 當項目中有安裝Unity Memory Profiler時,此時就會生成默認的元數(shù)據(jù)。
- 當項目中有安裝Unity Memory Profiler時,此時就可以首先創(chuàng)建一個繼承自MetadataCollect類型的元數(shù)據(jù)收集類型;然后在該類型里面重寫CollectMetadata函數(shù);最后在該函數(shù)中設置元數(shù)據(jù)。
項目中可能遇到的問題
首先要明確一點,在Editor中運行時,“Unity”大是正常的,因為在Editor中運行項目時,引擎包含了所有的資源占用的內存(除了部分紋理和Mesh是在GFX中),同時自身會進行很多的輔助操作來記錄各種游戲運行信息。一般來說,在查看游戲運行時的真實消耗內存,我們均是推薦直接在發(fā)布游戲上通過Profiler進行查看,在Editor中運行游戲所看到的內存是要大很多的。
1.Device.Present:
- GPU的presentdevice確實非常耗時,一般出現(xiàn)在使用了非常復雜的shader.
- GPU運行的非???#xff0c;而由于Vsync的原因,使得它需要等待較長的時間.
- 同樣是Vsync的原因,但其他線程非常耗時,所以導致該等待時間很長,比如:過量AssetBundle加載時容易出現(xiàn)該問題.
- Shader.CreateGPUProgram:Shader在runtime階段(非預加載)會出現(xiàn)卡頓(華為K3V2芯片).
- StackTraceUtility.PostprocessStacktrace()和StackTraceUtility.ExtractStackTrace(): 一般是由Debug.Log或類似API造成,游戲發(fā)布后需將Debug API進行屏蔽。
2.Overhead:
- 一般情況為Vsync所致.
- 通常出現(xiàn)在Android設備上.
3.GC.Collect:
原因:
- 代碼分配內存過量(惡性的)
- 一定時間間隔由系統(tǒng)調用(良性的).
占用時間:
- 與現(xiàn)有Garbage size相關
- 與剩余內存使用顆粒相關(比如場景物件過多,利用率低的情況下,GC釋放后需要做內存重排)
4.GarbageCollectAssetsProfile:
- 引擎在執(zhí)行UnloadUnusedAssets操作(該操作是比較耗時的,建議在切場景的時候進行)。
- 盡可能地避免使用Unity內建GUI,避免GUI.Repaint過渡GCAllow.
- if(other.tag == a.tag)改為other.CompareTag(a.tag).因為other.tag為產生180B的GC Allow.
- 少用foreach,因為每次foreach為產生一個enumerator(約16B的內存分配),盡量改為for.
- Lambda表達式,使用不當會產生內存泄漏.
5.盡量少用LINQ:
- 部分功能無法在某些平臺使用.
- 會分配大量GC Allow.
6.控制StartCoroutine的次數(shù):
- 開啟一個Coroutine(協(xié)程),至少分配37B的內存.
- Coroutine類的實例 -> 21B.
- Enumerator -> 16B.
7.使用StringBuilder替代字符串直接連接.
8.緩存組件:
- 每次GetComponent均會分配一定的GC Allow.
- 每次Object.name都會分配39B的堆內存.
9.ManagedHeap.UsedSize是項目邏輯代碼在運行時申請的堆內存,該選項只能通過優(yōu)化代碼來進行降低。 優(yōu)化方法一般如下:
- 盡可能地復用變量,減少new的次數(shù);
- 使用StringBuilder代替String連接,使用for代替foreach;
- 對于局部變量或非常駐變量,盡可能使用Struct來代替Class。
ManagedHeap.UsedSize過大,一方面可能會影響一次GC的耗時;另一方面也可能反映出腳本中不合理的GC Alloc。
10.有些小伙伴會發(fā)現(xiàn)System.ExecutableAndDlls占內存巨大,且一直在增長,是怎么回事?
System.ExecutableAndDlls該項顯示的是執(zhí)行文件和所調用的庫(物理、渲染、IO等系統(tǒng)庫)的總和。開發(fā)團隊不用太擔心該選項的數(shù)值,因為很多應用均在共用這些庫,并且它對于真實項目的內存壓力非常小,幾乎沒有影響,而且OS也不會因為該內存而殺掉游戲或應用。
11.凡是在Unity Profiler中能看到的資源就會保留在內存中。對于這種資源,在切換場景時調一下UnloadUnusedAssets API就可以釋放。
12.Profiler.BeginSample統(tǒng)計到的數(shù)據(jù)與直接看Memory下的不一樣,前者比后者的數(shù)據(jù)更大,這怎么理解?
這種情況確實也是經常會遇到的。一幀中分配如此高的內存是會觸發(fā)GC.Collect的,而Mono中顯示的數(shù)值則是GC之后的Mono內存數(shù)值。
13.正常情況下游戲如果一直玩下去,Mono是不是會一直增加? 比如頻繁打開一個界面,界面里有腳本會不斷創(chuàng)建一些東西 ,那么Mono是否會不斷增加?對性能上會不會造成影響呢?
在除開啟IL2CPP功能的應用中,Mono 確實是不會下降,但并不應該一直上升。
創(chuàng)建出來的東西,如果被引用在一個容器里,或者被某些腳本的變量引用,那么這部分堆內存就釋放不掉;但如果沒有被任何容器或者變量引用(比如,臨時拼一個 String),那么這部分堆內存會在 GC 的時候釋放(釋放是指變?yōu)榭臻e的堆內存,堆內存的總量是不會下降的)。
對于后者,頻繁地 new 對象雖然不會一直增加堆內存,但是會加速 GC 調用的頻率,所以同樣是需要盡量避免的。
14:我想請教一下,下圖這個函數(shù)中,每次我都申請了一個List temp = list();在這里存放6KB的數(shù)據(jù),但是如果不做GC處理,這6KB是否就一直累加,直到做GC處理了才會釋放掉,是這樣么?如果調用次數(shù)很多,每次都調用一點點,也會推高內存占用嗎?
是的,這個6KB堆內存會隨著Update的執(zhí)行一直分配內存,所累積的堆內存會在GC觸發(fā)時進行銷毀。一般來說,研發(fā)團隊需要盡可能避免在高頻次調用函數(shù)中進行堆內存的分配。
15:在進行內存優(yōu)化時,Unity Profiler給出的數(shù)據(jù)和Android系統(tǒng)(adb dumpsys meminfo,已經考慮memtrack的影響 )的數(shù)據(jù)差距較大(已經分析了Profiler自身的內存占用),如何分析這部分差異,比如包括對顯存消耗進行準確統(tǒng)計,OS消耗的統(tǒng)計等等?
內存差異較大是正常的,一般來說,Profiler統(tǒng)計的內存較為一致,而Android系統(tǒng)通過ADB反饋的PSS、Private Dirty等值則是差別很大。這主要是因為芯片和OS的不同而導致。具體的Android內存,建議直接查看Google Android OS的相關文檔。
Unity Profiler反饋的則是引擎的真實物理使用內存,一般我們都建議通過Profiler來查看內存是否存在冗余、泄露等問題。
16:已經預加載怪物,然后顯示怪物 PSS上升,并且在隱藏怪物后并沒有下降,這是什么原因導致?顯存上去了嗎?
僅僅隱藏怪物的話,內存是不會下降的。因為隱藏只是改變了GameObject的狀態(tài),并沒有對內存中的Object和資源進行移除。同時,即使是提前加載了怪物,也依然可能存在以上問題,因為某些資源是在顯示的時候,才會傳輸一份到GPU的,比如Mesh。一般情況下,顯存都不會即刻降低,這個是由Graphics Driver來管理的。建議可以看Profiler是否增長,如果Profiler沒有問題而PSS持續(xù)增長,就有可能發(fā)生了內存泄露。
對于這個問題,建議查看《性能優(yōu)化,進無止境---內存篇(下)》加深理解。
17:對于Handheld.PlayFullScreenMovie 這個Unity播放開場動畫的API,會有內存問題嗎?比如我的mp4動畫有20MB,那么這個動畫會撐高mono堆內存嗎?
Android上PlayFullScreenMovie 的實現(xiàn)實際上是通過Android原生的接口直接播放的,播放過程中Unity也是停止更新的,因此這部分的內存理論上并不會記錄在 Unity 中,同樣也不影響Mono。
18:Texture占用內存總是雙倍,這個是我們自己的問題,還是Unity引擎的機制?
出現(xiàn)這種情況的原因有兩種:一種是你在真機運行時開啟了Read&Write。另一種可能是Unity的Bug,目前的Unity 5.2.3 release note如下 :
(735644) -?OpenGL: Fixed texture memory usage reporting in profiler, was twice the actual size for most textures.
開發(fā)者需要關注下自己的開發(fā)版本,5.2.3以前類似情況的項目可以參考一下。
19:如果腳本引用了GameObject,那轉換場景的時候腳本和GameObject都沒了,還會產生堆內存的嗎?
如果腳本是MonoBehaviour,而且在切換場景后所掛的Game Object被釋放了,那么這個腳本對象所引用的堆內存就會在GC的時候被釋放。 但有一種例外,如果是通過Static變量引用的堆內存,那么依然是釋放不掉的,除非手動解開引用,比如變量置Null,數(shù)組Clear等等。
移動平臺內存經驗數(shù)據(jù)參考
Textures:80M-160M
Mesh:50M-70M
Render Textures:50M-80M
AnimationClips:30M-60M
Audio:10M-20M
Cubemap:0-50M
Font:5M-15M
Shader:20M-40M
System.xxx總和:15M-30M
AssetBundle:0-10M
其他各類對象單項:0-10M ,數(shù)量小于10000
ReservedMono:<100M
ReservedGFX:<300M
ReservedTotal:<650M
這些指標的上下限分別代表了在移動設備上的高低配數(shù)據(jù)的差異,其中Render Texture會根據(jù)目標設備的分辨率的不同會有差異變化。這里給出的是1080P分辨率下的經驗數(shù)據(jù)指標。一些下限為零的指標為不使用此功能,可能沒有這方面的開銷數(shù)據(jù),如果各個指標都在上述范圍內,不優(yōu)化也沒有問題。
移動平臺其他經驗數(shù)據(jù)參考
DrawCall:300-600
SetPassCall:80-120
Triangles Count:60W-100W
Material Count:200-400
建議你的游戲相關指標也控制在此范圍內,當然數(shù)據(jù)僅供參考。
在我的文章里你可能會看到重復的內容,原因是我的文章很多都是各路大神的心得,會有重復的,我沒有刪除,我覺得重復的多代表重要。
今天是2024年12月16日
重復一段毒雞湯來勉勵我和你
你的對手在看書
你的仇人在磨刀
你的閨蜜在減肥
隔壁的老王在練腰
而你在干嘛?