摘要:導語智能手機發展到今天已經有十幾個年頭,手機的軟硬件都已經發生了翻天覆地的變化,特別是陣營,從一開始的一兩百到今天動輒,內存。恰好最近做了內存優化相關的工作,這里也對內存優化相關的知識做下總結。
導語
智能手機發展到今天已經有十幾個年頭,手機的軟硬件都已經發生了翻天覆地的變化,特別是Android陣營,從一開始的一兩百M到今天動輒4G,6G內存。然而大部分的開發者觀看下自己的異常上報系統,還是會發現各種內存問題仍然層出不窮,各種OOM為crash率貢獻不少。Android開發發展到今天也是已經比較成熟,各種新框架,新技術也是層出不窮,而內存優化一直都是Android開發過程一個不可避免的話題。 恰好最近做了內存優化相關的工作,這里也對Android內存優化相關的知識做下總結。
在開始文章之前推薦下公司同事翻譯整理版本《Android性能優化典范 - 第6季》,因為篇幅有限這里我對一些內容只做簡單總結,同時如果有不正確內容也麻煩幫忙指正。
本文將會對Android內存優化相關的知識進行總結以及最后案例分析(一二部分是理論知識總結,你也可以直接跳到第三部分看案例):
一、 Android內存分配回收機制
二 、Android常見內存問題和對應檢測,解決方式。
三、 JOOX內存優化案例
四 、總結
工欲善其事必先利其器,想要優化App的內存占用,那么還是需要先了解Android系統的內存分配和回收機制。
一 ,Android內存分配回收機制參考Android 操作系統的內存回收機制[1],這里簡單做下總結:
從宏觀角度上來看Android系統可以分為三個層次
1. Application Framework,
2. Dalvik 虛擬機
3. Linux內核。
這三個層次都有各自內存相關工作:
1. Application FrameworkAnroid基于進程中運行的組件及其狀態規定了默認的五個回收優先級:
Empty process(空進程)
Background process(后臺進程)
Service process(服務進程)
Visible process(可見進程)
Foreground process(前臺進程)
系統需要進行內存回收時最先回收空進程,然后是后臺進程,以此類推最后才會回收前臺進程(一般情況下前臺進程就是與用戶交互的進程了,如果連前臺進程都需要回收那么此時系統幾乎不可用了)。
由此也衍生了很多進程保活的方法(提高優先級,互相喚醒,native保活等等),出現了國內各種全家桶,甚至各種殺不死的進程。
Android中由ActivityManagerService 集中管理所有進程的內存資源分配。
2. Linux內核參考QCon大會上阿里巴巴的Android內存優化分享[2],這里最簡單的理解就是ActivityManagerService會對所有進程進行評分(存放在變量adj中),然后再講這個評分更新到內核,由內核去完成真正的內存回收(lowmemorykiller, Oom_killer)。這里只是大概的流程,中間過程還是很復雜的,有興趣的同學可以一起研究,代碼在系統源碼ActivityManagerService.java中。
3. Dalvik虛擬機Android進程的內存管理分析[3],對Android中進程內存的管理做了分析。
Android中有Native Heap和Dalvik Heap。Android的Native Heap言理論上可分配的空間取決了硬件RAM,而對于每個進程的Dalvik Heap都是有大小限制的,具體策略可以看看android dalvik heap 淺析[4]。
Android App為什么會OOM呢?其實就是申請的內存超過了Dalvik Heap的最大值。這里也誕生了一些比較”黑科技”的內存優化方案,比如將耗內存的操作放到Native層,或者使用分進程的方式突破每個進程的Dalvik Heap內存限制。
Android Dalvik Heap與原生Java一樣,將堆的內存空間分為三個區域,Young Generation,Old Generation, Permanent Generation。
最近分配的對象會存放在Young Generation區域,當這個對象在這個區域停留的時間達到一定程度,它會被移動到Old Generation,最后累積一定時間再移動到Permanent Generation區域。系統會根據內存中不同的內存數據類型分別執行不同的gc操作。
GC發生的時候,所有的線程都是會被暫停的。執行GC所占用的時間和它發生在哪一個Generation也有關系,Young Generation中的每次GC操作時間是最短的,Old Generation其次,Permanent Generation最長。
GC時會導致線程暫停,導致卡頓,Google在新版本的Android中優化了這個問題, 在ART中對GC過程做了優化揭秘 ART 細節 —— Garbage collection[5],據說內存分配的效率提高了10倍,GC的效率提高了2-3倍(可見原來效率有多低),不過主要還是優化中斷和阻塞的時間,頻繁的GC還是會導致卡頓。
上面就是Android系統內存分配和回收相關知識,回過頭來看,現在各種手機廠商鼓吹人工智能手機,號稱18個月不卡頓,越用越快,其實很大一部分Android系統的內存優化有關,無非就是利用一些比較成熟的基于統計,機器學習的算法定時清理數據,清理內存,甚至提前加載數據到內存。
二 ,Android常見內存問題和對應檢測,解決方式 1. 內存泄露不止Android程序員,內存泄露應該是大部分程序員都遇到過的問題,可以說大部分的內存問題都是內存泄露導致的,Android里也有一些很常見的內存泄露問題[6],這里簡單羅列下:
單例(主要原因還是因為一般情況下單例都是全局的,有時候會引用一些實際生命周期比較短的變量,導致其無法釋放)
靜態變量(同樣也是因為生命周期比較長)
Handler內存泄露[7]
匿名內部類(匿名內部類會引用外部類,導致無法釋放,比如各種回調)
資源使用完未關閉(BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap)
對Android內存泄露業界已經有很多優秀的組件其中LeakCanary最為知名(Square出品,Square可謂Android開源界中的業界良心,開源的項目包括okhttp, retrofit,otto, picasso, Android開發大神Jake Wharton就在Square),其原理是監控每個activity,在activity ondestory后,在后臺線程檢測引用,然后過一段時間進行gc,gc后如果引用還在,那么dump出內存堆棧,并解析進行可視化顯示。使用LeakCanary可以快速地檢測出Android中的內存泄露。
正常情況下,解決大部分內存泄露問題后,App穩定性應該會有很大提升,但是有時候App本身就是有一些比較耗內存的功能,比如直播,視頻播放,音樂播放,那么我們還有什么能做的可以降低內存使用,減少OOM呢?
2. 圖片分辨率相關分辨率適配問題。很多情況下圖片所占的內存在整個App內存占用中會占大部分。我們知道可以通過將圖片放到hdpi/xhdpi/xxhdpi等不同文件夾進行適配,通過xml android:background設置背景圖片,或者通過BitmapFactory.decodeResource()方法,圖片實際上默認情況下是會進行縮放的。在Java層實際調用的函數都是或者通過BitmapFactory里的decodeResourceStream函數
public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) { if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); }
decodeResource在解析時會對Bitmap根據當前設備屏幕像素密度densityDpi的值進行縮放適配操作,使得解析出來的Bitmap與當前設備的分辨率匹配,達到一個最佳的顯示效果,并且Bitmap的大小將比原始的大,可以參考下騰訊Bugly的詳細分析Android 開發繞不過的坑:你的 Bitmap 究竟占多大內存?。
關于Density、分辨率、-hdpi等res目錄之間的關系:
舉個例子,對于一張1280×720的圖片,如果放在xhdpi,那么xhdpi的設備拿到的大小還是1280×720而xxhpi的設備拿到的可能是1920×1080,這兩種情況在內存里的大小分別為:3.68M和8.29M,相差4.61M,在移動設備來說這幾M的差距還是很大的。
盡管現在已經有比較先進的圖片加載組件類似Glide,Facebook Freso, 或者老牌Universal-Image-Loader,但是有時就是需要手動拿到一個bitmap或者drawable,特別是在一些可能會頻繁調用的場景(比如ListView的getView),怎樣盡可能對bitmap進行復用呢?這里首先需要明確的是對同樣的圖片,要 盡可能復用,我們可以簡單自己用WeakReference做一個bitmap緩存池,也可以用類似圖片加載庫寫一個通用的bitmap緩存池,可以參考GlideBitmapPool[8]的實現。
我們也來看看系統是怎么做的,對于類似在xml里面直接通過android:background或者android:src設置的背景圖片,以ImageView為例,最終會調用Resource.java里的loadDrawable:
Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException { // Next, check preloaded drawables. These may contain unresolved theme // attributes. final ConstantState cs; if (isColorDrawable) { cs = sPreloadedColorDrawables.get(key); } else { cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key); } Drawable dr; if (cs != null) { dr = cs.newDrawable(this); } else if (isColorDrawable) { dr = new ColorDrawable(value.data); } else { dr = loadDrawableForCookie(value, id, null); } ... return dr; }
可以看到實際上系統也是有一份全局的緩存,sPreloadedDrawables, 對于不同的drawable,如果圖片時一樣的,那么最終只會有一份bitmap(享元模式),存放于BitmapState中,獲取drawable時,系統會從緩存中取出這個bitmap然后構造drawable。而通過BitmapFactory.decodeResource()則每次都會重新解碼返回bitmap。所以其實我們可以通過context.getResources().getDrawable再從drawable里獲取bitmap,從而復用bitmap,然而這里也有一些坑,比如我們獲取到的這份bitmap,假如我們執行了recycle之類的操作,但是假如在其他地方再使用它是那么就會有”Canvas: trying to use a recycled bitmap android.graphics.Bitmap”異常。
3. 圖片壓縮BitmapFactory 在解碼圖片時,可以帶一個Options,有一些比較有用的功能,比如:
inTargetDensity 表示要被畫出來時的目標像素密度
inSampleSize 這個值是一個int,當它小于1的時候,將會被當做1處理,如果大于1,那么就會按照比例(1 / inSampleSize)縮小bitmap的寬和高、降低分辨率,大于1時這個值將會被處置為2的倍數。例如,width=100,height=100,inSampleSize=2,那么就會將bitmap處理為,width=50,height=50,寬高降為1 / 2,像素數降為1 / 4
inJustDecodeBounds 字面意思就可以理解就是只解析圖片的邊界,有時如果只是為了獲取圖片的大小就可以用這個,而不必直接加載整張圖片。
inPreferredConfig 默認會使用ARGB_8888,在這個模式下一個像素點將會占用4個byte,而對一些沒有透明度要求或者圖片質量要求不高的圖片,可以使用RGB_565,一個像素只會占用2個byte,一下可以省下50%內存。
inPurgeable和inInputShareable 這兩個需要一起使用,BitmapFactory.java的源碼里面有注釋,大致意思是表示在系統內存不足時是否可以回收這個bitmap,有點類似軟引用,但是實際在5.0以后這兩個屬性已經被忽略,因為系統認為回收后再解碼實際會反而可能導致性能問題
inBitmap 官方推薦使用的參數,表示重復利用圖片內存,減少內存分配,在4.4以前只有相同大小的圖片內存區域可以復用,4.4以后只要原有的圖片比將要解碼的圖片大既可以復用了。
4. 緩存池大小現在很多圖片加載組件都不僅僅是使用軟引用或者弱引用了,實際上類似Glide 默認使用的事LruCache,因為軟引用 弱引用都比較難以控制,使用LruCache可以實現比較精細的控制,而默認緩存池設置太大了會導致浪費內存,設置小了又會導致圖片經常被回收,所以需要根據每個App的情況,以及設備的分辨率,內存計算出一個比較合理的初始值,可以參考Glide的做法。
5. 內存抖動什么是內存抖動呢?Android里內存抖動是指內存頻繁地分配和回收,而頻繁的gc會導致卡頓,嚴重時還會導致OOM。
一個很經典的案例是string拼接創建大量小的對象(比如在一些頻繁調用的地方打字符串拼接的log的時候), 見Android優化之String篇[9]。
而內存抖動為什么會引起OOM呢?
主要原因還是有因為大量小的對象頻繁創建,導致內存碎片,從而當需要分配內存時,雖然總體上還是有剩余內存可分配,而由于這些內存不連續,導致無法分配,系統直接就返回OOM了。
其他比如我們坐地鐵的時候,假設你沒帶公交卡去坐地鐵,地鐵的售票機就只支持5元,10元,而哪怕你這個時候身上有1萬張1塊的都沒用(是不是覺得很反人類..)。當然你可以去兌換5元,10元,而在Android系統里就沒那么幸運了,系統會直接拒絕為你分配內存,并扔一個OOM給你(有人說Android系統并不會對Heap中空閑內存區域做碎片整理,待驗證)。
常用數據結構優化,ArrayMap及SparseArray是android的系統API,是專門為移動設備而定制的。用于在一定情況下取代HashMap而達到節省內存的目的,具體性能見HashMap,ArrayMap,SparseArray源碼分析及性能對比[10],對于key為int的HashMap盡量使用SparceArray替代,大概可以省30%的內存,而對于其他類型,ArrayMap對內存的節省實際并不明顯,10%左右,但是數據量在1000以上時,查找速度可能會變慢。
枚舉,Android平臺上枚舉是比較爭議的,在較早的Android版本,使用枚舉會導致包過大,在個例子里面,使用枚舉甚至比直接使用int包的size大了10多倍 在stackoverflow上也有很多的討論, 大致意思是隨著虛擬機的優化,目前枚舉變量在Android平臺性能問題已經不大,而目前Android官方建議,使用枚舉變量還是需要謹慎,因為枚舉變量可能比直接用int多使用2倍的內存。
ListView復用,這個大家都知道,getView里盡量復用conertView,同時因為getView會頻繁調用,要避免頻繁地生成對象
謹慎使用多進程,現在很多App都不是單進程,為了保活,或者提高穩定性都會進行一些進程拆分,而實際上即使是空進程也會占用內存(1M左右),對于使用完的進程,服務都要及時進行回收。
盡量使用系統資源,系統組件,圖片甚至控件的id
減少view的層級,對于可以 延遲初始化的頁面,使用viewstub
數據相關:序列化數據使用protobuf可以比xml省30%內存,慎用shareprefercnce,因為對于同一個sp,會將整個xml文件載入內存,有時候為了讀一個配置,就會將幾百k的數據讀進內存,數據庫字段盡量精簡,只讀取所需字段。
dex優化,代碼優化,謹慎使用外部庫, 有人覺得代碼多少于內存沒有關系,實際會有那么點關系,現在稍微大一點的項目動輒就是百萬行代碼以上,多dex也是常態,不僅占用rom空間,實際上運行的時候需要加載dex也是會占用內存的(幾M),有時候為了使用一些庫里的某個功能函數就引入了整個龐大的庫,此時可以考慮抽取必要部分,開啟proguard優化代碼,使用Facebook redex使用優化dex(好像有不少坑)。
三 案例JOOX是IBG一個核心產品,2014年發布以來已經成為5個國家和地區排名第一的音樂App。東南亞是JOOX的主要發行地區,實際上這些地區還是有很多的低端機型,對App的進行內存優化勢在必行。
上面介紹了Android系統內存分配和回收機制,同時也列舉了常見的內存問題,但是當我們接到一個內存優化的任務時,我們應該從何開始?下面是一次內存優化的分享。
1. 首先是解決大部分內存泄露。不管目前App內存占用怎樣,理論上不需要的東西最好回收,避免浪費用戶內存,減少OOM。實際上自JOOX接入LeakCanary后,每個版本都會做內存泄露檢測,經過幾個版本的迭代,JOOX已經修復了幾十處內存泄露。
2. 通過MAT查看內存占用,優化占用內存較大的地方。JOOX修復了一系列內存泄露后,內存占用還是居高不下,只能通過MAT查看到底是哪里占用了內存。關于MAT的使用,網上教程無數,簡單推薦兩篇MAT使用教程[11],MAT - Memory Analyzer Tool 使用進階[12]。
點擊Android Studio這里可以dump當前的內存快照,因為直接通過Android Sutdio dump出來的hprof文件與標準hprof文件有些差異,我們需要手動進行轉換,利用sdk目錄/platform-tools/hprof-conv.exe可以直接進行轉換,用法:hprof-conv 原文件.hprof 新文件.hprof。只需要輸入原文件名還有目標文件名就可以進行轉換,轉換完就可以直接用MAT打開。
下面就是JOOX打開App,手動進行多次gc的hprof文件。
這里我們看的是Dominator Tree(即內存里占用內存最多的對象列表)。
Shallo Heap:對象本身占用內存的大小,不包含其引用的對象內存。
Retained Heap: Retained heap值的計算方式是將retained set中的所有對象大小疊加。或者說,由于X被釋放,導致其它所有被釋放對象(包括被遞歸釋放的)所占的heap大小。
第一眼看去 居然有3個8M的對象,加起來就是24M啊 這到底是什么鬼?
我們通過List objects->with incoming references查看(這里with incoming references表示查看誰引用了這個對象,with outgoing references表示這個對象引用了誰)
通過這個方式我們看到這三張圖分別是閃屏,App主背景,App抽屜背景。
這里其實有兩個問題:
這幾張圖原圖實際都是1280x720,而在1080p手機上實測這幾張圖都縮放到了1920x1080
閃屏頁面,其實這張圖在閃屏顯示過后應該可以回收,但是因為歷史原因(和JOOX的退出機制有關),這張圖被常駐在后臺,導致無謂的內存占用。
優化方式:我們通過將這三張圖從xhdpi挪動到xxhdpi(當然這里需要看下圖片顯示效果有沒很大的影響),以及在閃屏顯示過后回收閃屏圖片。
優化結果:
從原來的8.29x3=24.87M 到 3.68x2=7.36M 優化了17M(有沒一種萬馬奔騰的感覺。。可能有時費大力氣優化很多代碼也優化不了幾百K,所以很多情況下內存優化時優化圖片還是比較立竿見影的)。
同樣方式我們發現對于一些默認圖,實際要求的顯示要求并不高(圖片相對簡單,同時大部分情況下圖片加載會成功),比如下面這張banner的背景圖:
優化前1.6M左右,優化后700K左右。
同時我們也發現了默認圖片一個其他問題,因為歷史原因,我們使用的圖片加載庫,設置默認圖片的接口是需要一個bitmap,導致我們原來幾乎每個adapter都用BitmapFactory decode了一個bitmap,對同一張默認圖片,不但沒有復用,還保存了多份,不僅會造成內存浪費,而且導致滑動偶爾會卡頓。這里我們也對默認圖片使用全局的bitmap緩存池,App全局只要使用同一張bitmap,都復用了同一份。
另外對于從MAT里看到的圖片,有時候因為看不到在項目里面對應的ID,會比較難確認到底是哪一張圖,這里stackoverflow上有一種方法,直接用原始數據通過GIM還原這張圖片。
這里其實也看到JOOX比較吃虧一個地方,JOOX不少地方都是使用比較復雜的圖片,同時有些地方還需要模糊,動畫這些都是比較耗內存的操作,Material Design出來后,很多App都遵循MD設計進行改版,通常默認背景,默認圖片一般都是純色,不僅App看起來比較明亮輕快,實際上也省了很多的內存,對此,JOOX后面對低端機型做了對應的優化。
3. 我們也對Bugly上的OOM進行了分析,發現其實有些OOM是可以避免的。下面這個crash就是上面提到的在LsitView的adapter里不停創建bitmap,這個地方是我們的首頁banner位,理論上App一打開就會緩存這張默認背景圖片了,而實際在使用過一段時間后,才因為為了解碼這張背景圖而OOM, 改為用全局緩存解決。
下面這個就是傳說中的內存抖動
實際代碼如下,因為打Log而進行了字符串拼接,一旦這個函數被比較頻繁地調用,那么就很有可能會發生內存抖動。這里我們新版本已經改為使用stringbuilder進行優化。
還有一些比較奇怪的情況,這里是我們掃描歌曲文件頭的時候發生的,有些文件頭居然有幾百M大,導致一次申請了過大的內存,直接OOM,這里暫時也無法修復,直接catch住out of memory error。
4. 同時我們對一些邏輯代碼進行調整,比如我們的App主頁的第三個tab(Live tab)進行了數據延遲加載,和定時回收。這里因為這個頁面除了有大圖還有輪播banner,實際強引用的圖片會有多張,如果這個時候切到其他頁面進行聽歌等行為,這個頁面一直在后臺緩存,實際是很浪費耗內存的,同時為優化體驗,我們又不能直接通過設置主頁的viewpager的緩存頁數,因為這樣經常都會回收,導致影響體驗,所以我們在頁面不可見后過一段時間,清理掉adapter數據(只是清空adapter里的數據,實際從網絡加載回來的數據還在,這里只是為了去掉界面對圖片的引用),當頁面再次顯示時再用已經加載的數據顯示,即減少了很多情況下圖片的引用,也不影響體驗。
5. 最后我們也遇到一個比較奇葩的問題,在我們的Bugly上報上有這樣一條上報我們在stackoverflow上看到了相關的討論,大致意思是有些情況下比如息屏,或者一些省電模式下,頻繁地調System.gc()可能會因為內核狀態切換超時的異常。這個問題貌似沒有比較好的解決方法,只能是優化內存,盡量減少手動調用System.gc()
優化結果
我們通過啟動App后,切換到我的音樂界面,停留1分鐘,多次gc后,獲取App內存占用
優化前:
優化后:
多次試驗結果都差不多,這里只截取了其中一次,有28M的優化效果。
當然不同的場景內存占用不同,同時上面試驗結果是通過多次手動觸發gc穩定后的結果。對于使用其他第三方工具不手動gc的情況下,試驗結果可能會差異比較大。
對于上面提到的JOOX里各種圖片背景等問題,我們做了動態的優化,對不同的機型進行優化,對特別低端的機型設置為純色背景等方式,最終優化效果如下:
平均內存降低41M。
本次總結主要還是從圖片方面下手,還有一點邏輯優化,已經基本達到優化目標。
四 總結上面寫了很多,我們可以簡單總結,目前Andorid內存優化還是比較重要一個話題,我們可以通過各種內存泄露檢測組件,MAT查看內存占用,Memory Monitor跟蹤整個App的內存變化情況, Heap Viewer查看當前內存快照, Allocation Tracker追蹤內存對象的來源,以及利用崩潰上報平臺從多個方面對App內存進行監控和優化。上面只是列舉了一些常見的情況,當然每個App功能,邏輯,架構也都不一樣,造成內存問題也是不盡相同,掌握好工具的使用,發現問題所在,才能對癥下藥。
參考鏈接1.Android 操作系統的內存回收機制
https://www.ibm.com/developerworks/cn/opensource/os-cn-android-mmry-rcycl/2.阿里巴巴的Android內存優化分享
http://www.infoq.com/cn/presentations/android-memory-optimization3.Android進程的內存管理分析
http://blog.csdn.net/gemmem/article/details/89200394.android dalvik heap 淺析
http://blog.csdn.net/cqupt_chen/article/details/110681295.揭秘 ART 細節 —— Garbage collection
http://www.cnblogs.com/jinkeep/p/3818180.html6.Android性能優化之常見的內存泄漏
http://blog.csdn.net/u010687392/article/details/499094777.Android App 內存泄露之Handler
http://blog.csdn.net/zhuanglonghai/article/details/382330698.GlideBitmapPool
https://github.com/amitshekhariitbhu/GlideBitmapPool9.Android 性能優化之String篇
http://blog.csdn.net/vfush/article/details/5303843710.HashMap,ArrayMap,SparseArray源碼分析及性能對比
http://www.jianshu.com/p/7b9a1b38626511.MAT使用教程
http://blog.csdn.net/itomge/article/details/4871952712.MAT - Memory Analyzer Tool 使用進階
http://www.lightskystreet.com/2015/09/01/mat_usage/
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70566.html
摘要:不努力不奮斗,可能就會在基層一輩子止步不前。不過,只一句,如果你還在做這一行,還是一名程序猿媛,想走上坡路的你,也許我這到手的十幾家一線互聯網公司性能優化項目實戰可能會對你有所幫助。 ...
閱讀 3040·2023-04-26 00:49
閱讀 3719·2021-09-29 09:45
閱讀 964·2019-08-29 18:47
閱讀 2738·2019-08-29 18:37
閱讀 2723·2019-08-29 16:37
閱讀 3286·2019-08-29 13:24
閱讀 1773·2019-08-27 10:56
閱讀 2344·2019-08-26 11:42