INSTRUMENTS調(diào)試工具的使用(七十六) —— 解決內(nèi)存和性能問題簡單示例(一)

版本記錄

版本號 時間
V1.0 2019.10.11 星期五

前言

我們在做app的時候讯柔,不是做完功能就結(jié)束了钓觉,很多時候是需要進行檢查和優(yōu)化的衔蹲,而xcode自帶了一個很好的檢查工具,可以檢測內(nèi)存泄漏衰粹。還可以查看哪一個方法比較耗時锣光。還可以檢測離屏渲染等等,隨后的幾篇我們就說一下這個工具的使用铝耻。感興趣的可以看這幾篇。
1.INSTRUMENTS調(diào)試工具的使用(一)
2.INSTRUMENTS調(diào)試工具的使用(二)
3.INSTRUMENTS調(diào)試工具的使用(三)
4.INSTRUMENTS調(diào)試工具的使用(四)
5.INSTRUMENTS調(diào)試工具的使用(五)
6.INSTRUMENTS調(diào)試工具的使用(六)
7.INSTRUMENTS調(diào)試工具的使用(七)
8.INSTRUMENTS調(diào)試工具的使用(八)
9.INSTRUMENTS調(diào)試工具的使用(九)
10. INSTRUMENTS調(diào)試工具的使用(十)
11. INSTRUMENTS調(diào)試工具的使用(十一) —— 簡介(一)
12. INSTRUMENTS調(diào)試工具的使用(十二) —— 通常任務(wù)之啟動Instruments(一)
13. INSTRUMENTS調(diào)試工具的使用(十三) —— 通常任務(wù)之簡單了解Instruments(二)
14. INSTRUMENTS調(diào)試工具的使用(十四) —— 通常任務(wù)之創(chuàng)建蹬刷、保存和打開跟蹤文檔(三)
15. INSTRUMENTS調(diào)試工具的使用(十五) —— 通常任務(wù)之指定目標(biāo)應(yīng)用和設(shè)備(四)
16. INSTRUMENTS調(diào)試工具的使用(十六) —— 通常任務(wù)之訪問和使用個別儀器(五)
17. INSTRUMENTS調(diào)試工具的使用(十七) —— 通常任務(wù)之記錄瓢捉、暫停和停止跟蹤(六)
18. INSTRUMENTS調(diào)試工具的使用(十八) —— 導(dǎo)航收集的數(shù)據(jù)之關(guān)于數(shù)據(jù)分析(一)
19. INSTRUMENTS調(diào)試工具的使用(十九) —— 導(dǎo)航收集的數(shù)據(jù)之導(dǎo)航時間軸窗格(二)
20. INSTRUMENTS調(diào)試工具的使用(二十) —— 導(dǎo)航收集的數(shù)據(jù)之導(dǎo)航詳細面板(三)
21. INSTRUMENTS調(diào)試工具的使用(二十一) —— 導(dǎo)航收集的數(shù)據(jù)之將數(shù)據(jù)映射到源代碼(四)
22. INSTRUMENTS調(diào)試工具的使用(二十二) —— 導(dǎo)航收集的數(shù)據(jù)之查看您應(yīng)用的源代碼(五)
23. INSTRUMENTS調(diào)試工具的使用(二十三) —— 分析你App的性能之測量CPU使用情況(一)
24. INSTRUMENTS調(diào)試工具的使用(二十四) —— 分析你App的性能之測量圖形性能(二)
25. INSTRUMENTS調(diào)試工具的使用(二十五) —— 分析你App的性能之監(jiān)視網(wǎng)絡(luò)和文件I / O(三)
26. INSTRUMENTS調(diào)試工具的使用(二十六) —— 分析你App的內(nèi)存使用之關(guān)于內(nèi)存分析(一)
27. INSTRUMENTS調(diào)試工具的使用(二十七) —— 分析你App的內(nèi)存使用之檢測內(nèi)存使用(二)
28. INSTRUMENTS調(diào)試工具的使用(二十八) —— 分析你App的內(nèi)存使用之找到廢棄的內(nèi)存(三)
29. INSTRUMENTS調(diào)試工具的使用(二十九) —— 分析你App的內(nèi)存使用之找到內(nèi)存泄露(四)
30. INSTRUMENTS調(diào)試工具的使用(三十) —— 分析你App的內(nèi)存使用之找到僵尸對象(五)
31. INSTRUMENTS調(diào)試工具的使用(三十一) —— 分析你App的能源之測量能源影響(一)
32. INSTRUMENTS調(diào)試工具的使用(三十二) —— 高級任務(wù)之導(dǎo)出和導(dǎo)入跟蹤數(shù)據(jù)(一)
33. INSTRUMENTS調(diào)試工具的使用(三十三) —— 高級任務(wù)之創(chuàng)建自定義Instruments(二)
34. INSTRUMENTS調(diào)試工具的使用(三十四) —— 分析模板和工具之分析模板(一)
35. INSTRUMENTS調(diào)試工具的使用(三十五) —— 分析模板和工具之Activity Monitor工具(二)
36. INSTRUMENTS調(diào)試工具的使用(三十六) —— 分析模板和工具之Allocations工具(三)
37. INSTRUMENTS調(diào)試工具的使用(三十七) —— 分析模板和工具之藍牙開關(guān)日志工具(四)
38. INSTRUMENTS調(diào)試工具的使用(三十八) —— 分析模板和工具之Carbon Events工具(五)
39. INSTRUMENTS調(diào)試工具的使用(三十九) —— 分析模板和工具之Cocoa Events工具(六)
40. INSTRUMENTS調(diào)試工具的使用(四十) —— 分析模板和工具之Connections工具(七)
41. INSTRUMENTS調(diào)試工具的使用(四十一) —— 分析模板和工具之Core Animation工具(八)
42. INSTRUMENTS調(diào)試工具的使用(四十二) —— 分析模板和工具之Core Data Cache Misses工具(九)
43. INSTRUMENTS調(diào)試工具的使用(四十三) —— 分析模板和工具之Core Data Faults工具(十)
44. INSTRUMENTS調(diào)試工具的使用(四十四) —— 分析模板和工具之Core Data Fetches工具(十一)
45. INSTRUMENTS調(diào)試工具的使用(四十五) —— 分析模板和工具之Core Data Saves工具(十二)
46. INSTRUMENTS調(diào)試工具的使用(四十六) —— 分析模板和工具之Counters工具(十三)
47. INSTRUMENTS調(diào)試工具的使用(四十七) —— 分析模板和工具之CPU Activity Log工具(十四)
48. INSTRUMENTS調(diào)試工具的使用(四十八) —— 分析模板和工具之Directory I/O工具(十五)
49. INSTRUMENTS調(diào)試工具的使用(四十九) —— 分析模板和工具之Dispatch工具(十六)
50. INSTRUMENTS調(diào)試工具的使用(五十) —— 分析模板和工具之Display Brightness Log工具(十七)
51. INSTRUMENTS調(diào)試工具的使用(五十一) —— 分析模板和工具之Displayed Surfaces工具(十八)
52. INSTRUMENTS調(diào)試工具的使用(五十二) —— 分析模板和工具之Energy Usage Log工具(十九)
53. INSTRUMENTS調(diào)試工具的使用(五十三) —— 分析模板和工具之GPS On/Off Log工具(二十)
54. INSTRUMENTS調(diào)試工具的使用(五十四) —— 分析模板和工具之GPU Driver工具(二十一)
55. INSTRUMENTS調(diào)試工具的使用(五十五) —— 分析模板和工具之GPU Hardware工具(二十二)
56. INSTRUMENTS調(diào)試工具的使用(五十六) —— 分析模板和工具之Graphics Driver Activity工具(二十三)
57. INSTRUMENTS調(diào)試工具的使用(五十七) —— 分析模板和工具之I/O Activity工具(二十四)
58. INSTRUMENTS調(diào)試工具的使用(五十八) —— 分析模板和工具之Leaks工具(二十五)
59. INSTRUMENTS調(diào)試工具的使用(五十九) —— 分析模板和工具之Location Energy Model工具(二十六)
60. INSTRUMENTS調(diào)試工具的使用(六十) —— 分析模板和工具之Metal Application工具(二十七)
61. INSTRUMENTS調(diào)試工具的使用(六十一) —— 分析模板和工具之Network Activity Log工具(二十八)
62. INSTRUMENTS調(diào)試工具的使用(六十二) —— 分析模板和工具之OpenGL ES Analyzer工具(二十九)
63. INSTRUMENTS調(diào)試工具的使用(六十三) —— 分析模板和工具之Sampler工具(三十)
64. INSTRUMENTS調(diào)試工具的使用(六十四) —— 分析模板和工具之Scheduling工具(三十一)
65. INSTRUMENTS調(diào)試工具的使用(六十五) —— 分析模板和工具之Sleep/Wake Log工具(三十二)
66. INSTRUMENTS調(diào)試工具的使用(六十六) —— 分析模板和工具之Spin Monitor工具(三十三)
67. INSTRUMENTS調(diào)試工具的使用(六十七) —— 分析模板和工具之System Calls工具(三十四)
68. INSTRUMENTS調(diào)試工具的使用(六十八) —— 分析模板和工具之Time Profiler工具(三十五)
69. INSTRUMENTS調(diào)試工具的使用(六十九) —— 分析模板和工具之VM Operations工具(三十六)
70. INSTRUMENTS調(diào)試工具的使用(七十) —— 分析模板和工具之VM Tracker工具(三十七)
71. INSTRUMENTS調(diào)試工具的使用(七十一) —— 分析模板和工具之Wi-Fi On/Off Log工具(三十八)
72. INSTRUMENTS調(diào)試工具的使用(七十二) —— 分析模板和工具之廢棄的模板和工具(三十九)
73. INSTRUMENTS調(diào)試工具的使用(七十三) —— 偏好設(shè)置和菜單之偏好設(shè)置(一)
74. INSTRUMENTS調(diào)試工具的使用(七十四) —— 偏好設(shè)置和菜單之菜單和鍵盤快捷鍵(二)
75. INSTRUMENTS調(diào)試工具的使用(七十五) —— 相關(guān)資源(一)

開始

題外話:徐S希望你未來幸福,我想你也會自己放在自己心里办成,十一加上請假十幾天瘦了七八斤泡态,希望未來可期~~

首先看主要內(nèi)容

主要內(nèi)容:了解如何使用Instruments來捕獲和修復(fù)應(yīng)用中的內(nèi)存問題和性能bug,以使其更快迂卢,響應(yīng)速度更快某弦。翻譯來自地址

然后看下寫作環(huán)境

Swift 5, iOS 13, Xcode 11

除了通過添加功能來改進其應(yīng)用程序之外而克,所有優(yōu)秀的應(yīng)用程序開發(fā)人員還應(yīng)該做一件事:Instrument代碼靶壮!

Instrument教程將向您展示如何使用Xcode隨附的Instrument工具的最重要功能。它使您可以檢查代碼中是否存在性能問題员萍,內(nèi)存問題腾降,引用循環(huán)和其他問題。

可以了碎绎,準備好進入迷人的Instrument世界螃壤!

在本Instrument教程中抗果,您不會從頭開始創(chuàng)建應(yīng)用程序。相反奸晴,打開下載好的完整的示例項目冤馏。您的任務(wù)就是瀏覽應(yīng)用程序,并以Instrument為指導(dǎo)進行改進寄啼,就像您自己開發(fā)應(yīng)用程序一樣逮光!

在Xcode中打開啟動項目。

此示例應(yīng)用程序使用Flickr API搜索圖像辕录。要使用API??睦霎,您需要一個API密鑰。對于演示項目走诞,您可以在Flickr的網(wǎng)站上生成示例密鑰副女。只需在Flickr API Explorer 中執(zhí)行任何搜索,然后從底部的URL復(fù)制API密鑰即可蚣旱。它一直沿“&api_key =”到下一個“&”碑幅。

例如,如果URL為:

http://api.flickr.com/services/rest/?method=flickr.photos.search
&api_key=ff417a50b95180cb0c7e3b68a8749fba
&format=rest&api_sig=f24f4e98063a9b8ecc8b522b238d5e2f

那么API key就是ff417a50b95180cb0c7e3b68a8749fba

打開FlickrAPI.swift并將現(xiàn)有的API key值替換為新值塞绿。

請注意沟涨,API密鑰每天都會更改,因此有時您需要重新生成一個新密鑰异吻。 只要密鑰無效裹赴,該應(yīng)用程序都會提醒您。

Build并運行诀浪,執(zhí)行搜索棋返,單擊結(jié)果,您將看到類似以下的內(nèi)容:

玩該應(yīng)用程序并查看其基本功能雷猪。 您可能會認為睛竣,一旦UI看起來很棒,該應(yīng)用就可以提交商店了求摇。 但是射沟,您將看到使用Instruments可以為您的應(yīng)用添加的價值。

本教程的其余部分將向您展示如何查找和修復(fù)應(yīng)用程序中仍然存在的問題与境。 您將看到Instruments如何使調(diào)試問題大為簡化验夯!


Time for Profiling

您要查看的第一個instrument是時間分析器(Time Profiler)。 每隔一定的時間間隔嚷辅,Instruments將暫停程序的執(zhí)行簿姨,并在每個正在運行的線程上進行堆棧跟蹤。 您可以將其視為單擊Xcode調(diào)試器中的暫停按鈕。

這是Time Profiler的預(yù)覽:

此屏幕顯示 Call Tree扁位。調(diào)用樹顯示了在應(yīng)用程序中執(zhí)行各種方法所花費的時間准潭。每行是程序執(zhí)行路徑遵循的不同方法。儀器通過計算分析器在每種方法中停止的次數(shù)來估算每種方法所花費的時間域仇。

例如刑然,如果您以1毫秒的間隔進行100個采樣,并且在10個采樣中的某個特殊方法出現(xiàn)在堆棧的頂部暇务,則可以推斷出該方法花費了該應(yīng)用程序大約10%的總執(zhí)行時間(10毫秒)泼掠。這是一個粗略的近似值,但是就是這個原理并起作用垦细!

注意:通常择镇,您應(yīng)該始終在實際設(shè)備上而不是模擬器上配置應(yīng)用程序。 iOS模擬器具有Mac的全部功能括改,而設(shè)備將具有移動硬件的所有限制腻豌。您的應(yīng)用似乎可以在模擬器中正常運行,但是一旦在真實設(shè)備上運行嘱能,您可能會發(fā)現(xiàn)性能問題吝梅。

因此,事不宜遲惹骂,該花些時間進行檢測了苏携!

1. Instrumenting

在Xcode的菜單欄中,選擇Product ? Profile或按Command-I对粪。 這將構(gòu)建應(yīng)用程序并啟動Instruments右冻。 您會看到一個選擇窗口,如下所示:

這些是Instruments隨附的所有不同模板著拭。

選擇Time Profiler儀器国旷,然后單擊Choose。這將打開一個新的Instruments文檔茫死。單擊左上角的紅色錄制按鈕開始錄制并啟動該應(yīng)用程序。 macOS可能會要求您輸入密碼以授權(quán)Instruments分析其他進程履羞。別擔(dān)心峦萎,可以在這里安全提供!

Instruments窗口中忆首,您可以看到時間在向上計數(shù)爱榔,并且在屏幕中心的圖形上方有一個小箭頭從左向右移動。這表明該應(yīng)用程序正在運行糙及。

現(xiàn)在,開始使用該應(yīng)用程序。搜索一些圖像晃听,然后向下鉆取一個或多個搜索結(jié)果。您可能已經(jīng)注意到版姑,進入搜索結(jié)果的過程非常緩慢,并且滾動瀏覽搜索結(jié)果列表也非常令人討厭迟郎。這是一個非常笨拙的應(yīng)用剥险!

好吧,你很幸運宪肖。您即將著手修復(fù)它表制!但是,您首先需要快速了解一下Instruments中的內(nèi)容控乾。

首先么介,確保工具欄右側(cè)的視圖選擇器選擇了兩個選項,如下所示:

這樣可以確保所有面板均打開蜕衡。 現(xiàn)在壤短,研究下面的屏幕截圖以及下面每個部分的說明:

  • 1) Recording controls:紅色的record按鈕停止并啟動當(dāng)前正在測試的應(yīng)用。暫停按鈕可暫停應(yīng)用程序的當(dāng)前執(zhí)行衷咽。
  • 2) Run timer:計時器計算配置文件應(yīng)用已運行多長時間以及已運行多少次鸽扁。單擊停止按鈕,然后重新啟動應(yīng)用程序镶骗。您會看到現(xiàn)在顯示Run 2 of 2桶现。
  • 3) Instrument track:對于您選擇的Time Profiler模板,只有一個instrument鼎姊,因此只有一個軌道骡和。您將在本教程的后面部分詳細了解圖形的詳細信息。
  • 4) Detail panel:顯示有關(guān)您正在使用的特定instrument的主要信息相寇。在這種情況下慰于,它顯示了“最熱”的方法,即使用最多CPU時間的方法唤衫。在詳細信息面板的頂部婆赠,單擊Profile,然后選擇Samples佳励。在這里您可以查看每個采樣休里。點擊一些采樣;您會看到捕獲的堆棧跟蹤顯示在右側(cè)的擴展詳細信息(Extended Detail)檢查器中赃承。完成后妙黍,切換回Profile
  • 5) Inspectors panel:有兩個檢查器 - Extended DetailRun Info - 您將稍后了解更多信息瞧剖。

Drilling Deep

執(zhí)行圖像搜索拭嫁,然后深入搜索結(jié)果可免。 我個人喜歡搜索“狗”,但請選擇您想要的任何東西-您可能是其中喜歡貓的人之一做粤!

現(xiàn)在浇借,上下滾動列表幾次,以便在Time Profiler中獲得大量數(shù)據(jù)驮宴。 您應(yīng)該注意到屏幕中間的數(shù)字在變化逮刨,并且圖形在填充。這表明您的應(yīng)用程序正在使用CPU周期堵泽。

為了幫助查明問題修己,您將設(shè)置一些選項。 單擊Stop迎罗,然后在詳細信息面板下方睬愤,單擊Call Tree。 在出現(xiàn)的彈出窗口中纹安,選擇Separate by Thread, Invert Call TreeHide System Libraries尤辱。 它看起來像這樣:

以下是每個選項對左側(cè)表格中顯示的數(shù)據(jù)的處理方式:

  • Separate by State:此選項按應(yīng)用程序的生命周期狀態(tài)對結(jié)果進行分組,是一種檢查應(yīng)用程序正在進行的工作量和時間的有用方法厢岂。
  • Separate by Thread:分別看每個線程光督。這使您能夠了解哪些線程導(dǎo)致最大的CPU使用量。
  • Invert Call Tree:使用此選項塔粒,堆棧跟蹤將首先顯示最近的幀结借。
  • Hide System Libraries:選擇此選項時,僅顯示您自己的應(yīng)用程序中的符號卒茬。選擇此選項通常很有用船老,因為通常您只關(guān)心CPU在自己的代碼中花費的時間,而對于系統(tǒng)庫正在使用多少CPU卻不那么關(guān)心圃酵!
  • Flatten Recursion:此選項顯示遞歸函數(shù)柳畔,這些函數(shù)調(diào)用自己,每個堆棧跟蹤中只有一個條目郭赐,而不是多次薪韩。
  • Top Functions:啟用此功能可使Instruments將在函數(shù)中花費的總時間視為直接在該函數(shù)中花費的時間之和,以及在該函數(shù)調(diào)用的函數(shù)中花費的時間捌锭。因此躬存,如果函數(shù)A調(diào)用B,則Instruments將A的時間報告為A所花費的時間加上B中所花費的時間舀锨。這非常有用,因為它使您每次下降到調(diào)用堆棧時都選擇最大的時間數(shù)字宛逗,將其清零使用最耗??時的方法坎匿。

掃描結(jié)果以識別Weight列中哪些行具有最高百分比。請注意,帶有Main Thread的行正在消耗大量的CPU周期替蔬。通過單擊文本左側(cè)的小箭頭來展開該行告私,然后向下看,直到看到您自己的方法之一承桥,并標(biāo)有“person”符號驻粟。盡管某些值可能會略有不同,但是條目的順序應(yīng)類似于下表:

好吧凶异,那看起來當(dāng)然不好蜀撑。 該應(yīng)用程序花費大量時間使用將“ tonal”濾鏡應(yīng)用于縮略圖的方法。 表格加載和滾動是用戶界面中最笨拙的部分剩彬,而表格單元不斷更新時酷麦,就沒那么流暢感了。

要了解有關(guān)該方法中發(fā)生的事情的更多信息喉恋,請雙擊表中的該行沃饶。 這樣做將顯示以下視圖:

applyTonalFilter()是擴展中添加到UIImage的方法,并且在應(yīng)用圖像濾鏡之后花費大量時間調(diào)用創(chuàng)建CGImage輸出的方法轻黑。

您實際上沒有什么可以加快的速度糊肤。 創(chuàng)建圖像是一個密集的過程,需要花費很長時間氓鄙。 嘗試返回查看應(yīng)用程序在哪里調(diào)用applyTonalFilter()馆揉。 單擊代碼視圖頂部的痕跡中的Root以返回上一屏幕:

現(xiàn)在,單擊表頂部applyTonalFilter行左側(cè)的小箭頭玖详。 這將顯示applyTonalFilter的調(diào)用者-您可能還需要展開下一行把介。 在對Swift進行性能分析時,有時在Call Tree中會有重復(fù)的行蟋座,并以@objc為前綴拗踢。 您對第一行帶有“person”圖標(biāo)(表示該行屬于您的應(yīng)用目標(biāo))感興趣:

在這種情況下,該行引用結(jié)果收集視圖的(_:cellForItemAt :)向臀。雙擊該行以查看項目中的關(guān)聯(lián)代碼巢墅。

現(xiàn)在您可以看到問題所在了∪颍看一下第70行:應(yīng)用tonal濾波器的方法執(zhí)行需要很長時間君纫,您可以直接從collectionView(_:cellForItemAt :)調(diào)用它。這將在每次請求濾鏡圖像時阻塞主線程芹彬,并因此阻塞整個UI蓄髓。

1. Offloading the Work

為解決此問題,您將采取兩個步驟:首先舒帮,使用DispatchQueue.global()会喝。async將圖像過濾卸載到后臺線程中陡叠。然后,在生成每個圖像后對其進行緩存肢执。入門項目中包含一個小型的簡單圖像緩存類-易記的名稱ImageCache-該類僅將圖像存儲在內(nèi)存中并使用給定的鍵檢索它們枉阵。

現(xiàn)在,您可以切換到Xcode预茄,并在Instruments中手動找到要查看的源文件兴溜。但是,在Instruments中有一個方便的Open in Xcode按鈕耻陕。在代碼上方的面板中找到它拙徽,然后單擊它:

Xcode將在正確的位置打開。

現(xiàn)在淮蜈,在collectionView(_:cellForItemAt :)中斋攀,將對loadThumbnail(for:completion :)的調(diào)用替換為以下內(nèi)容:

ImageCache.shared.loadThumbnail(for: flickrPhoto) { result in
  switch result {
  case .success(let image):
    if cell.flickrPhoto == flickrPhoto {
      if flickrPhoto.isFavourite {
        cell.imageView.image = image
      } else {
        // 1
        if let cachedImage = 
          ImageCache.shared.image(forKey: "\(flickrPhoto.id)-filtered") {
          cell.imageView.image = cachedImage
        } else {
          // 2
          DispatchQueue.global().async {
            if let filteredImage = image.applyTonalFilter() {
              ImageCache.shared.set(filteredImage, 
                                    forKey: "\(flickrPhoto.id)-filtered")
            
              DispatchQueue.main.async {
                cell.imageView.image = filteredImage
              }
            }
          }
        }
      }
    }
  case .failure(let error):
    print("Error: \(error)")
  }
}

該代碼的第一部分與之前相同,并從網(wǎng)絡(luò)上加載Flickr照片的縮略圖梧田。 如果照片是您最愛的淳蔼,則單元格將顯示縮略圖而無需修改。 但是裁眯,如果照片不是您的最愛鹉梨,則會應(yīng)用tonal濾鏡。

這是您更改內(nèi)容的地方:

  • 1) 檢查圖像緩存中是否存在針對該照片的濾鏡圖像穿稳。 如果是存皂,那就太好了; 顯示該圖像逢艘。
  • 2) 如果不是旦袋,請分派將tonal濾鏡應(yīng)用到后臺隊列的調(diào)用。 這允許UI在濾鏡運行時保持響應(yīng)它改。 濾鏡完成后疤孕,將圖像保存在緩存中并更新主隊列上的圖像視圖。

那是經(jīng)過濾鏡的圖像央拖,但是仍然有原始的Flickr縮略圖需要解決祭阀。 打開Cache.swift并找到loadThumbnail(for:completion :)。 將其替換為以下內(nèi)容:

func loadThumbnail(for photo: FlickrPhoto,
                   completion: @escaping FlickrAPI.FetchImageCompletion) {
  if let image = ImageCache.shared.image(forKey: photo.id) {
    completion(Result.success(image))
  } else {
    FlickrAPI.loadImage(for: photo, withSize: "m") { result in
      if case .success(let image) = result {
        ImageCache.shared.set(image, forKey: photo.id)
      }
      completion(result)
    }
  }
}

這與您處理已濾鏡圖像的方式非常相似鲜戒。 如果高速緩存中已經(jīng)存在圖像专控,則您可以立即與高速緩存的圖像一起調(diào)用completion閉包。 否則遏餐,您可以從Flickr加載圖像并將其存儲在緩存中伦腐。

Command-I再次在Instruments中運行該應(yīng)用程序。 請注意失都,這次Xcode不會詢問您要使用哪種Instruments蔗牡。 這是因為您仍然為應(yīng)用程序打開了一個窗口颖系,并且Instruments假設(shè)您想使用相同的選項再次運行。

再進行幾次搜索辩越。 用戶界面就不那么笨拙不流暢了! 該應(yīng)用程序現(xiàn)在在后臺應(yīng)用圖像濾鏡并緩存結(jié)果信粮,因此圖像僅需濾鏡一次黔攒。 您會在Call Tree中看到許多dispatch_worker_threads。 這些正在處理應(yīng)用圖像濾鏡的繁重工作强缘。

看起來很棒督惰!


Allocations, Allocations and Allocations

那么,您接下來要查找什么bug旅掂?

項目中隱藏著一些您可能不知道的東西赏胚。您可能聽說過內(nèi)存泄漏。但是您可能不知道的是實際上存在兩種泄漏:

  • 1) True memory leaks - 真實內(nèi)存泄漏:當(dāng)對象不再被任何東西引用但仍被分配時發(fā)生商虐。這意味著該內(nèi)存將永遠無法重復(fù)使用觉阅。

即使借助Swift和ARC幫助管理內(nèi)存,最常見的內(nèi)存泄漏類型仍然是retain cyclestrong reference cycle秘车。當(dāng)兩個對象相互之間擁有強引用時典勇,就會發(fā)生這種情況,從而使每個對象都不會釋放另一個對象叮趴。結(jié)果割笙,他們的內(nèi)存永遠不會釋放。

  • 2) Unbounded memory growth - 無限的內(nèi)存增長:在連續(xù)分配內(nèi)存且從未被釋放過的情況下發(fā)生眯亦。如果持續(xù)發(fā)生這種情況伤溉,則會耗盡內(nèi)存。在iOS上妻率,這意味著系統(tǒng)將終止您的應(yīng)用乱顾。

現(xiàn)在,您將探索Allocations工具舌涨。該工具為您提供有關(guān)應(yīng)用程序創(chuàng)建的所有對象以及支持它們的內(nèi)存的詳細信息糯耍。它還顯示每個對象的retain counts

1. Instrumenting Allocations

要以新的instruments配置文件重新開始囊嘉,請退出instruments應(yīng)用温技。不必擔(dān)心保存此特定運行。現(xiàn)在扭粱,在Xcode中按Command-I舵鳞,從列表中選擇Allocations,然后單擊Choose琢蛤。

片刻之后蜓堕,您會看到Allocations工具抛虏。 它對您應(yīng)該很熟悉,因為它看起來很像Time Profiler套才。

單擊左上角的Record按鈕以運行該應(yīng)用程序迂猴。 這次您會注意到兩條軌道。 在本教程中背伴,您只需要關(guān)注All Heap and Anonymous VM沸毁。

在應(yīng)用程序上運行Allocations工具后,可以在應(yīng)用程序中進行五次不同的搜索傻寂,但仍不深入查詢結(jié)果息尺。 確保搜索得到一些結(jié)果。 現(xiàn)在疾掰,讓應(yīng)用程序等待幾秒鐘搂誉。

您應(yīng)該已經(jīng)注意到All Heap and Anonymous VM軌道中的圖形一直在上升。 這告訴您您的應(yīng)用正在分配內(nèi)存静檬。 正是這一功能將引導(dǎo)您找到無限的內(nèi)存增長炭懊。


Generation Analysis

您要執(zhí)行的是世代分析(generation analysis)。 為此巴柿,請單擊詳細信息面板底部的Mark Generation的按鈕:

單擊它凛虽,您將看到一條紅旗出現(xiàn)在軌道中,如下所示:

世代分析的目的是多次執(zhí)行一個動作广恢,并查看內(nèi)存是否以無限的方式增長凯旋。 打開搜索結(jié)果,等待幾秒鐘以加載圖像钉迷,然后返回主頁至非。 再次標(biāo)記一代。 重復(fù)執(zhí)行此操作以進行不同的搜索糠聪。

檢查了幾次搜索后荒椭,Instruments將如下所示:

此時,您應(yīng)該變得疑惑了舰蟆。請注意趣惠,您每次進行深入的搜索時,藍色圖形都會隨著上升身害。好吧味悄,那當(dāng)然不是很好。但是塌鸯,等等侍瑟,內(nèi)存警告呢?你知道這些,對吧涨颜?

1. Simulating a Memory Warning

內(nèi)存警告是iOS告知應(yīng)用程序內(nèi)存出現(xiàn)問題的方式费韭,您需要清除一些內(nèi)存。

這種增長可能不僅取決于您的應(yīng)用庭瑰。這可能是UIKit保留內(nèi)存的原因星持。讓系統(tǒng)框架和您的應(yīng)用有機會先清除它們的內(nèi)存。

通過選擇Instruments菜單欄中的Instrument ? Simulate Memory Warning或從模擬器菜單欄中選擇Hardware ? Simulate Memory Warning來模擬內(nèi)存警告弹灭。您會注意到钉汗,內(nèi)存使用量有所下降一點點,甚至根本沒有下降鲤屡。當(dāng)然不會回到應(yīng)該的位置。因此福侈,某處仍然存在無限的內(nèi)存增長酒来。

您在檢查搜索的每個迭代之后都標(biāo)記了一個世代,這樣您就可以看到每個世代之間分配了什么內(nèi)存肪凛。在詳細信息面板中查看堰汉,您會看到很多代。

在每一代中伟墙,您將看到標(biāo)記該代時已分配并仍駐留的所有對象翘鸭。自從上一代被標(biāo)記以來,后代將只包含對象戳葵。

查看Growth列就乓,您肯定會發(fā)現(xiàn)某處確實有增長。打開其中的幾代拱烁,您將看到以下內(nèi)容:

哇生蚁,好多對象! 從哪里開始戏自?

簡單邦投。 單擊Growth標(biāo)題以按大小排序。 確保最占內(nèi)存的對象在頂部擅笔。 在每一代的頂部附近志衣,您都會注意到一列標(biāo)有VM:CoreImage的行,聽起來確實很熟悉猛们! 單擊VM:CoreImage左側(cè)的箭頭以顯示與此項目關(guān)聯(lián)的內(nèi)存地址念脯。 選擇第一個內(nèi)存地址以在右側(cè)面板上的Extended Detail檢查器中顯示關(guān)聯(lián)的堆棧跟蹤:

此堆棧跟蹤顯示了創(chuàng)建此特定對象的時間。 灰色的堆棧跟蹤部分位于系統(tǒng)庫中阅懦。 黑色部分在您的應(yīng)用代碼中和二。 嗯,看起來有些熟悉:一些黑色條目顯示了您的老朋友collectionView(_:cellForItemAt :)耳胎。 雙擊其中任何一項惯吕; 儀器將在其上下文中顯示代碼惕它。

看一下該方法。 它在第79行調(diào)用set(_:forKey :)废登。請記住淹魄,此方法會緩存圖片,以便應(yīng)用中再次使用該圖片堡距。 聽起來確實是個問題甲锡!

再次,單擊Open in Xcode以跳回Xcode羽戒。 打開Cache.swift并查看set(_:forKey :)的實現(xiàn):

func set(_ image: UIImage, forKey key: String) {
  images[key] = image
}

這會將圖像添加到字典缤沦,并在Flickr照片的照片ID作為鍵。 但是易稠,您會發(fā)現(xiàn)該圖片從未從該詞典中清除缸废!

那就是無限內(nèi)存增長的來源。 一切都按預(yù)期進行驶社,但是該應(yīng)用程序永遠不會從緩存中刪除內(nèi)容-它只會添加內(nèi)容企量!

要解決此問題,您需要做的就是讓ImageCache偵聽UIApplication觸發(fā)的內(nèi)存警告通知亡电。 當(dāng)ImageCache收到此消息時届巩,它必須清除其緩存。

要使ImageCache監(jiān)聽通知份乒,請打開Cache.swift并將以下初始化程序添加到該類:

init() {
  NotificationCenter.default.addObserver(
    forName: UIApplication.didReceiveMemoryWarningNotification,
    object: nil,
    queue: .main) { [weak self] notification in
      self?.images.removeAll(keepingCapacity: false)
  }
}

這為UIApplicationDidReceiveMemoryWarningNotification注冊了一個觀察者恕汇,以執(zhí)行清除圖像images的閉包。

該代碼所需要做的就是刪除緩存中的所有對象冒嫡。 這樣可以確保不再保留任何圖像并將它們釋放拇勃。

要測試此修復(fù),請再次啟動儀器孝凌,然后重復(fù)之前執(zhí)行的步驟方咆。 最后不要忘了模擬內(nèi)存警告。

注意:為了確保您使用的是最新代碼蟀架,請確保您從Xcode啟動并觸發(fā)構(gòu)建瓣赂,而不僅僅是點擊Instruments中的紅色按鈕。 您可能還需要先構(gòu)建并運行片拍,然后再進行性能分析煌集。 如果您只是進行性能分析,有時Xcode似乎不會將模擬器中的應(yīng)用程序版本更新為最新版本捌省。

這次苫纤,世代分析應(yīng)如下所示:

出現(xiàn)內(nèi)存警告后,您會注意到內(nèi)存使用率下降了【砭校總體而言喊废,內(nèi)存仍在增長,但遠未達到以前栗弟。

仍然有一定增長的原因?qū)嶋H上是由于系統(tǒng)庫污筷,您對此無能為力≌Ш眨看來系統(tǒng)庫并未釋放它們的所有內(nèi)存瓣蛀,這可能是設(shè)計使然,也可能是bug雷厂。您在應(yīng)用中可以做的就是釋放盡可能多的內(nèi)存惋增,而您已經(jīng)做到了!

做得好改鲫!修補了另一個問題器腋!您尚未解決的第一類泄漏問題。


Strong Reference Cycles

如前所述钩杰,當(dāng)兩個對象彼此保持強引用時,就會發(fā)生強引用循環(huán)诊县,從而防止兩個對象都被釋放讲弄。您可以使用Allocations工具以其他方式檢測這些引用。

關(guān)閉儀器并返回到Xcode依痊。再次選擇Product ? Profile避除,然后選擇Allocations模板。

這次胸嘁,您將不再使用世代分析瓶摆。 取而代之的是,您將查看在內(nèi)存中駐留的不同類型的對象的數(shù)量性宏。 單擊Record按鈕開始運行群井。 您應(yīng)該已經(jīng)看到大量對象填充了詳細信息面板-太多了! 為了縮小感興趣對象的范圍毫胜,請在左下角的字段中輸入Instruments作為過濾器书斜。

儀器中需要注意的兩列是#Persistent#Transient#Persistent列保留內(nèi)存中當(dāng)前存在的每種類型的對象數(shù)酵使。#Transient顯示了已存在但已釋放的對象數(shù)荐吉。Persistent對象正在耗盡內(nèi)存;transient對象不是口渔。

1. Finding Persistent Objects

您應(yīng)該看到有一個ViewController的持久實例样屠。這很有意義,因為這是您當(dāng)前正在查看的屏幕。該應(yīng)用的AppDelegate實例也是如此痪欲。

回到應(yīng)用程序悦穿!執(zhí)行搜索并深入研究結(jié)果。請注意勤揩,Instruments中現(xiàn)在顯示了一堆額外的對象:SearchResultsViewControllerImageCache等咧党。ViewController實例仍是持久性的,因為其導(dǎo)航控制器需要它陨亡。

現(xiàn)在點擊應(yīng)用程序中的后退按鈕傍衡。這會將SearchResultsViewController從導(dǎo)航堆棧中彈出,因此應(yīng)將其釋放负蠕。但是在Allocations摘要中蛙埂,它仍顯示# Persistent計數(shù)為1!為什么還在那里遮糖?

嘗試執(zhí)行另外兩個搜索绣的,然后在每個搜索之后點擊“后退”按鈕∠劢啵現(xiàn)在有三個SearchResultsViewControllers?!這些視圖控制器在內(nèi)存中保持著的事實意味著某些東西一直在強引用它們袜炕】安荆看來您有很強的參考周期硅堆!

在這種情況下虑凛,您的主要線索不僅是持久化SearchResultsViewController的持久化窟蓝,而是所有SearchResultsCollectionViewCells挠阁。引用循環(huán)可能介于這兩類之間乱灵。

幸運的是踢故,Xcode 8中引入的Visual Memory Debugger是一個簡潔的工具文黎,可以幫助您進一步診斷內(nèi)存泄漏和引用循環(huán)。Visual Memory Debugger不是Xcode儀器套件的一部分殿较,但它是一個非常有用的工具耸峭,值得在本教程中講述。來自Allocations工具和Visual Memory Debugger的交叉使用是一項強大的技術(shù)淋纲,可以使您的調(diào)試工作流更加有效劳闹。


Getting Visual

退出Instruments

在啟動Visual Memory Debugger之前,請像下面這樣在Xcode scheme編輯器中啟用Malloc Stack日志記錄:Option-單擊窗口頂部(stop按鈕旁邊)的InstrumentsTutorial scheme洽瞬。在出現(xiàn)的彈出窗口中玷或,單擊Run部分,然后切換到Diagnostics選項卡片任。選中顯示Malloc Stack的框偏友,然后選擇Live Allocations Only選項,然后單擊Close对供。

直接從Xcode啟動應(yīng)用程序位他。 與以前一樣氛濒,至少執(zhí)行三個搜索以累積一些數(shù)據(jù)。

現(xiàn)在鹅髓,像這樣激活Visual Memory Debugger

  • 1) 切換到Debug導(dǎo)航器舞竿。
  • 2) 單擊此圖標(biāo),然后從彈出窗口中選擇View Memory Graph Hierarchy窿冯。
  • 3) 單擊SearchResultsCollectionViewCell的條目骗奖。
  • 4) 您可以單擊圖形上的任何對象,以在檢查器窗格中查看詳細信息醒串。
  • 5) 您可以在此區(qū)域中查看詳細信息执桌。在此處切換到Memory inspector

Visual Memory Debugger會暫停您的應(yīng)用程序芜赌,并以直觀的方式顯示內(nèi)存中的對象及其之間的引用仰挣。

如上面的屏幕快照中突出顯示的,Visual Memory Debugger顯示以下信息:

  • Heap contents 堆內(nèi)容(Debug窗格):這將顯示您暫停應(yīng)用程序時在內(nèi)存中分配的所有類型和實例的列表缠沈。單擊一種類型將展開該行膘壶,以向您顯示該類型在內(nèi)存中的單獨實例。
  • Memory graph 內(nèi)存圖(主窗口):主窗口以可視方式表示內(nèi)存中的對象洲愤。對象之間的箭頭表示它們之間的引用(強關(guān)系和弱關(guān)系)颓芭。
  • Memory inspector內(nèi)存檢查器(Utilities窗格):包括類名稱和層次結(jié)構(gòu)以及引用是強引用還是弱引用等詳細信息。

請注意柬赐,Debug導(dǎo)航器中的某些行旁邊如何帶有括號畜伐。括號中的數(shù)字表示內(nèi)存中有多少個特定類型的實例。在上面的屏幕截圖中躺率,您可以看到經(jīng)過幾次搜索后,Visual Memory Debugger會確認您在Allocations工具中看到的結(jié)果万矾。換句話說悼吱,從20到(如果您滾動到搜索結(jié)果的末尾)的任何位置,每個SearchResultsViewController實例的60SearchResultsCollectionViewCell實例都將保留在內(nèi)存中良狈。

使用該行左側(cè)的箭頭展開該類型后添,并顯示內(nèi)存中的每個SearchResultsViewController實例。單擊單個實例將在主窗口中顯示該實例及其任何引用薪丁。

注意箭頭指向SearchResultsViewController實例遇西。 似乎有一些Swift closure contexts實例引用了相同的視圖控制器實例。 看起來有點懷疑严嗜,不是嗎粱檀? 細看。 選擇箭頭之一漫玄,以在Utilities窗格中顯示有關(guān)這些閉包實例之一與SearchResultsViewController之間的引用的更多信息茄蚯。

Memory Inspector中压彭,您可以看到Swift closure contextSearchResultsViewController之間的引用很強。 如果您在SearchResultsCollectionViewCellSwift closure context之間選擇引用渗常,您還將看到它也標(biāo)記為strong壮不。 您還可以看到閉包的名稱是heartToggleHandler。哈皱碘! SearchResultsCollectionViewCell聲明了這一點询一!

在主窗口中選擇SearchResultsCollectionViewCell的實例,以在檢查器窗格上顯示更多信息癌椿。

在回溯中健蕊,您可以看到單元實例已在collectionView(_:cellForItemAt :)中初始化。 當(dāng)您將鼠標(biāo)懸停在回溯中的這一行上時如失,將出現(xiàn)一個小箭頭绊诲。 點擊箭頭將帶您進入Xcode的代碼編輯器中的此方法。

collectionView(_:cellForItemAt :)中褪贵,找到設(shè)置每個單元格的heartToggleHandler屬性的位置掂之。 您將看到以下代碼行:

cell.heartToggleHandler = { isStarred in
  self.collectionView.reloadItems(at: [indexPath])
}

當(dāng)用戶在collection view cell中單擊“心形”按鈕時,將執(zhí)行此閉包脆丁。這是強引用所在世舰,但是除非您之前遇到過,否則很難找到槽卫。但是多虧了Visual Memory Debugger跟压,您才能夠一直追蹤到這段代碼!

閉包單元格使用self引用SearchResultsViewController歼培,這會創(chuàng)建一個強引用震蒋。閉包捕捉self。 Swift實際上會強迫您在閉包中顯式使用self一詞躲庄,而在引用當(dāng)前對象的方法和屬性時查剖,通常可以將其刪除噪窘。這可以幫助您更加了解要捕獲的事實笋庄。 SearchResultsViewController通過其collection view還對cell有很強的引用。

1. Break That Cycle

為了打破強引用倔监,您可以將capture list定義為閉包定義的一部分直砂。capture list可用于將閉包捕獲的實例聲明為weakunowned

  • Weak:在將來捕獲的引用可能為nil時使用。如果它所引用的對象被釋放浩习,則該引用將變?yōu)?code>nil静暂。因此,它們是可選類型谱秽。
  • Unowned:當(dāng)閉包及其引用的對象始終具有相同的生命周期并在同一時間被釋放時籍嘹,使用此屬性闪盔。無主引用永遠不會為nil

要解決這個強引用辱士,請像這樣將捕獲列表添加到heartToggleHandler

cell.heartToggleHandler = { [weak self] isStarred in
  self?.collectionView.reloadItems(at: [indexPath])
}

self聲明為weak表示意味著泪掀,即使集合視圖單元格擁有對它的引用,也可以將SearchResultsViewController釋放颂碘,因為它們現(xiàn)在只是弱引用异赫。 銷毀SearchResultsViewController將會銷毀其collection view,進而銷毀cell头岔。

在Xcode中塔拳,再次按Command-IInstruments中構(gòu)建并運行該應(yīng)用程序。

像以前一樣峡竣,使用Allocations工具在Instruments中再次查看該應(yīng)用靠抑。 切記將結(jié)果向下過濾,以僅顯示入門項目中的類适掰。 執(zhí)行搜索颂碧,導(dǎo)航至結(jié)果,然后再次返回类浪。 向后導(dǎo)航時载城,您應(yīng)該看到SearchResultsViewController及其cell已被釋放。 它們顯示瞬時transient實例费就,而不是persistent實例了诉瓦。

循環(huán)打破了!

后記

本篇主要講述了解決內(nèi)存和性能問題簡單示例力细,感興趣的給個贊或者關(guān)注~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末睬澡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子眠蚂,更是在濱河造成了極大的恐慌煞聪,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件河狐,死亡現(xiàn)場離奇詭異,居然都是意外死亡瑟捣,警方通過查閱死者的電腦和手機馋艺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來迈套,“玉大人捐祠,你說我怎么就攤上這事∩@睿” “怎么了踱蛀?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵窿给,是天一觀的道長。 經(jīng)常有香客問我率拒,道長崩泡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任猬膨,我火速辦了婚禮角撞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘勃痴。我一直安慰自己谒所,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布沛申。 她就那樣靜靜地躺著劣领,像睡著了一般。 火紅的嫁衣襯著肌膚如雪铁材。 梳的紋絲不亂的頭發(fā)上尖淘,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音衫贬,去河邊找鬼德澈。 笑死,一個胖子當(dāng)著我的面吹牛固惯,可吹牛的內(nèi)容都是我干的梆造。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼葬毫,長吁一口氣:“原來是場噩夢啊……” “哼镇辉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起贴捡,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤忽肛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后烂斋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屹逛,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年汛骂,在試婚紗的時候發(fā)現(xiàn)自己被綠了罕模。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡帘瞭,死狀恐怖淑掌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蝶念,我是刑警寧澤抛腕,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布芋绸,位于F島的核電站,受9級特大地震影響担敌,放射性物質(zhì)發(fā)生泄漏摔敛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一柄错、第九天 我趴在偏房一處隱蔽的房頂上張望舷夺。 院中可真熱鬧,春花似錦售貌、人聲如沸给猾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽敢伸。三九已至,卻和暖如春恒削,著一層夾襖步出監(jiān)牢的瞬間池颈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工钓丰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留躯砰,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓携丁,卻偏偏與公主長得像琢歇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子梦鉴,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容