Dropbox 如何解決Android App的內存泄漏問題?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"本文最初發佈於Dropbox技術博客,經原作者授權由InfoQ中文站翻譯並分享。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當應用程序爲對象分配內存,而對象不再被使用時卻沒有釋放,就會發生內存泄漏。隨着時間的推移,泄漏的內存會累積,導致應用程序性能變差,甚至崩潰。泄漏可能發生在任何程序和平臺上,但由於"},{"type":"link","attrs":{"href":"https:\/\/developer.android.com\/guide\/components\/activities\/activity-lifecycle?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"活動生命週期"}]},{"type":"text","text":"的複雜性,這種情況在Android應用中尤其普遍。最新的Android 模式,如"},{"type":"link","attrs":{"href":"https:\/\/developer.android.com\/topic\/libraries\/architecture\/viewmodel?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"ViewModel"}]},{"type":"text","text":"和"},{"type":"link","attrs":{"href":"https:\/\/developer.android.com\/reference\/androidx\/lifecycle\/LifecycleObserver?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"LifecycleObserver"}]},{"type":"text","text":"可以幫助避免內存泄漏,但如果你遵循舊的模式或不知道要注意什麼,很容易漏過錯誤。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"常見例子"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"引用長期運行的服務"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/74\/749fee0d5444617a4ac48681c9de01dc.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":10}}],"text":"Fragment引用了一個活動,而該活動引用一個長期運行的服務"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這種情況下,我們有一個標準設置,活動持有一個長期運行的服務的引用,然後是Fragment及其視圖持有活動的引用。例如,假設活動以某種方式創建了對其子Fragment的引用。然後,只要活動還在,Fragment也會繼續存在。那麼在Fragment的"},{"type":"codeinline","content":[{"type":"text","text":"onDestroy"}]},{"type":"text","text":"和活動的"},{"type":"codeinline","content":[{"type":"text","text":"onDestroy"}]},{"type":"text","text":"之間就發生了內存泄漏。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a5\/a56acbe29ef494e868422366bb2a4b86.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":10}}],"text":"該Fragment永遠不會再使用,但它會一直在內存中"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"長期運行的服務引用了Fragment視圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一方面,如果服務獲得了Fragment視圖的引用呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,視圖現在將在服務的整個持續時間內保持活動狀態。此外,因爲視圖持有對其父活動的引用,所以該活動現在也會泄漏。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/bf\/bf03b2c00b71243264003a106a657155.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":10}}],"text":"只要服務存在,FragmentView 和Activity 都會浪費內存"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"檢測內存泄漏"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,我們已經知道了內存泄漏是如何發生的。讓我們討論下如何檢測它們。顯然,第一步是檢查你的應用是否會因爲"},{"type":"codeinline","content":[{"type":"text","text":"OutOfMemoryError"}]},{"type":"text","text":"而崩潰。除非單個屏幕佔用的內存比手機可用內存還多,否則肯定在某個地方存在內存泄漏。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/0c\/0c927622532d4bbc0315aff0cc9ebb4a.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種方法只告訴你存在的問題,而不是根本原因。內存泄漏可能發生在任何地方,記錄的崩潰並不沒有指向泄漏,而是指向最終提示內存使用超過限制的屏幕。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可以檢查所有的麪包屑控件,看看它們是否有一些相似之處,但很可能罪魁禍首並不容易識別。讓我們研究下其他選項。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"LeakCanary"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/square.github.io\/leakcanary\/#:~:text=LeakCanary%20is%20a%20memory%20leak,developers%20dramatically%20reduce%20OutOfMemoryError%20crashes.?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"LeakCanary"}]},{"type":"text","text":"是目前最好的工具之一,它是一個用於Android的內存泄漏檢測庫。我們只需在構建中"},{"type":"link","attrs":{"href":"https:\/\/square.github.io\/leakcanary\/getting_started\/?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"添加一個build.gradle文件依賴項"}]},{"type":"text","text":"。下一次,我們安裝和運行我們的應用時,LeakCanary將與它一起運行。當我們在應用中導航時,LeakCanary會偶爾暫停以轉儲內存,並提供檢測到的泄漏痕跡。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個工具比我們之前的方法要好得多。但是這個過程仍然是手動的,每個開發人員只有他們個人遇到的內存泄漏的本地副本。我們可以做得更好!"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"LeakCanary和Bugsnag"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LeakCanary提供了一個非常方便的代碼配方("},{"type":"link","attrs":{"href":"https:\/\/square.github.io\/leakcanary\/recipes\/#uploading-to-bugsnag?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"code recipe"}]},{"type":"text","text":"),用於將發現的泄漏上傳到"},{"type":"link","attrs":{"href":"https:\/\/www.bugsnag.com\/?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"Bugsnag"}]},{"type":"text","text":"。我們可以跟蹤內存泄漏,就像我們在應用程序中跟蹤任何其他警告或崩潰。我們甚至可以更進一步,使用"},{"type":"link","attrs":{"href":"https:\/\/www.bugsnag.com\/integrations?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"Bugsnag Integration"}]},{"type":"text","text":"將其連接到項目管理軟件,如Jira,以獲得更好的可見性和問責制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/95\/95803f8dca7ba2cafb63a52d7fb79d1e.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":10}}],"text":"Bugsnag連接到Jira"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"LeakCanary和集成測試"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一種提高自動化的方法是將LeakCanary與CI測試連接起來。同樣,我們有一個"},{"type":"link","attrs":{"href":"https:\/\/square.github.io\/leakcanary\/recipes\/#running-leakcanary-in-instrumentation-tests?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"代碼配方"}]},{"type":"text","text":"。以下內容來自官方文件:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LeakCanary提供了一個專門用於在UI測試中檢測漏洞的構件,它提供了一個運行偵聽器,後者會等待測試結束,如果測試成功,它將查找留存的對象,在需要時觸發堆轉儲並執行分析。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意,LeakCanary會降低測試速度,因爲它每次都會在其偵聽的測試結束後轉儲堆。在我們的例子中,由於我們的"},{"type":"link","attrs":{"href":"https:\/\/dropbox.tech\/mobile\/revamping-the-android-testing-pipeline-at-dropbox?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"選擇性測試和分片設置"}]},{"type":"text","text":",額外增加的時間可以忽略不計。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最終,就像CI上的任何其他構建或測試失敗一樣,內存泄漏也會被暴露出來,並且漏洞跟蹤信息也被記錄了下來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在CI上運行LeakCanary幫助我們學到了更好的編碼模式,特別是涉及到新的庫時,在任何代碼進入生產環境前。例如,當我們使用"},{"type":"link","attrs":{"href":"https:\/\/github.com\/airbnb\/mavericks?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"MvRx"}]},{"type":"text","text":"測試時,它發現了這個漏洞:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"Test failed because application memory leaks were detected: ==================================== HEAP ANALYSIS RESULT ==================================== 4 APPLICATION LEAKS References underlined with \"~~~\" are likely causes. Learn more at https:\/\/squ.re\/leaks. 198449 bytes retained by leaking objects Signature: 6bf2ba80511dcb6ab9697257143e3071fca4 ┬─── \n│ GC Root: System class \n│ ├─ com.airbnb.mvrx.mocking.MockableMavericks class \n│ Leaking: NO (a class is never leaking) \n│ ↓ static MockableMavericks.mockStateHolder \n│ ~~~~~~~~~~~~~~~ \n├─ com.airbnb.mvrx.mocking.MockStateHolder instance \n│ Leaking: UNKNOWN \n│ ↓ MockStateHolder.delegateInfoMap \n│ ~~~~~~~~~~~~~~~ \n├─ java.util.LinkedHashMap instance \n│ Leaking: UNKNOWN \n│ ↓ LinkedHashMap.header \n│ ~~~~~~ \n├─ java.util.LinkedHashMap$LinkedEntry instance \n│ Leaking: UNKNOWN \n│ ↓ LinkedHashMap$LinkedEntry.prv \n│ ~~~ \n├─ java.util.LinkedHashMap$LinkedEntry instance \n│ Leaking: UNKNOWN \n│ ↓ LinkedHashMap$LinkedEntry.key \n│ ~~~ \n╰→ com.dropbox.product.android.dbapp.photos.ui.view.PhotosFragment instance \n Leaking: YES (ObjectWatcher was watching this because com.dropbox.product.android.dbapp.photos.ui.view.PhotosFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null) \n key = 391c9051-ad2c-4282-9279-d7df13d205c3 \n watchDurationMillis = 7304 \n retainedDurationMillis = 2304 198427 bytes retained by leaking objects \n Signature: d1c9f9707034dd15604d8f2e63ff3bf3ecb61f8\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實證明,在編寫測試時,我們沒有正確地清理測試。添加幾行代碼可以避免泄漏:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":" @After\n fun teardown() {\n scenario.close()\n val holder = MockableMavericks.mockStateHolder\n holder.clearAllMocks()\n }\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能會想:既然這種內存泄漏只發生在測試中,那麼修復它真的那麼重要嗎?好吧,那就看你了!與代碼檢查一樣,泄漏檢測可以告訴你什麼時候出現了"},{"type":"link","attrs":{"href":"https:\/\/en.wikipedia.org\/wiki\/Code_smell?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"代碼氣味"}]},{"type":"text","text":"或"},{"type":"link","attrs":{"href":"https:\/\/en.wikipedia.org\/wiki\/Software_design_pattern?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"糟糕的編碼模式"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它可以幫助工程師編寫更健壯的代碼——在本例中,我們知道了"},{"type":"codeinline","content":[{"type":"text","text":"clearAllMocks()"}]},{"type":"text","text":"。泄漏的嚴重程度,以及是否必須修復,都是工程師可以做出的決定。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於我們不想運行泄漏檢測的測試,我們編寫了一個簡單的註解:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.METHOD, ElementType.TYPE})\npublic @interface SkipLeakDetection {\n \/**\n * The reason why the test should skip leak detection.\n *\/\n String value();\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們的類重寫了LeakCanary的"},{"type":"codeinline","content":[{"type":"text","text":"FailOnLeakRunListener()"}]},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"override fun skipLeakDetectionReason(description: Description): String? {\n return when {\n description.getAnnotation(SkipLeakDetection::class.java) != null ->\n \"is annotated with @SkipLeakDetection\"\n description.testClass.isAnnotationPresent(SkipLeakDetection::class.java) ->\n \"class is annotated with @SkipLeakDetection\"\n else -> null\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單個測試或整個測試類可以使用這個註解跳過泄漏檢測。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"修復內存泄漏"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,我們討論了各種查找和暴露內存泄漏的方法。下面,我們討論一下如何真正理解和修復它們。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LeakCanary提供的泄漏跟蹤是診斷泄漏最有用的工具。本質上講,泄漏跟蹤打印出與泄漏對象關聯的引用鏈,並解釋爲什麼將其視爲泄漏。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於如何閱讀和使用泄漏跟蹤,LeakCanary有了很好的"},{"type":"link","attrs":{"href":"https:\/\/square.github.io\/leakcanary\/fundamentals-fixing-a-memory-leak\/?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"文檔"}]},{"type":"text","text":",這裏無需重複。取而代之,讓我們回顧一下我自己經常要處理的兩類內存泄漏。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"視圖"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們經常看到視圖被聲明爲類級變量:"},{"type":"codeinline","content":[{"type":"text","text":"private TextView myTextView"}]},{"type":"text","text":";或者,現在有更多的Android代碼正在用"},{"type":"link","attrs":{"href":"https:\/\/kotlinlang.org\/?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"Kotlin"}]},{"type":"text","text":"編寫:"},{"type":"codeinline","content":[{"type":"text","text":"private lateinit var myTextView: textview"}]},{"type":"text","text":"——非常常見,我們沒有意識到這些都可以導致內存泄漏。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除非在Fragment的"},{"type":"codeinline","content":[{"type":"text","text":"onDestroyView"}]},{"type":"text","text":"中消除對這些字段的引用,(對於"},{"type":"codeinline","content":[{"type":"text","text":"lateinit"}]},{"type":"text","text":"變量不能這麼做),否則對這些視圖的引用在Fragment的整個生命週期內都會存在,而不是像它們應該的那樣在Fragment視圖的生命週期內存在。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"導致內存泄漏的一個最簡單場景是:我們在FragmentA上。我們導航到FragmentB,現在FragmentA在棧裏。FragmentA沒有被銷燬,但是FragmentA的視圖被銷燬了。任何綁定到FragmentA生命週期的視圖現在已經不需要了,但都還保留在內存中。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在大多數情況下,這些泄漏很小,不會導致任何性能問題或崩潰。但是對於保存對象和數據、圖像、視圖\/數據綁定等的視圖,我們更有可能遇到麻煩。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以,如果可能的話,避免在類級變量中存儲視圖,或者確保在"},{"type":"codeinline","content":[{"type":"text","text":"onDestroyView"}]},{"type":"text","text":"中正確地清理它們。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"說到視圖\/數據綁定,Android的"},{"type":"link","attrs":{"href":"https:\/\/developer.android.com\/topic\/libraries\/view-binding?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"視圖綁定文檔"}]},{"type":"text","text":"明確地告訴我們:字段必須被清除以防止泄漏。他們提供的代碼片段建議我們做以下工作:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"private var _binding: ResultProfileBinding? = null\n\/\/ This property is only valid between onCreateView and\n\/\/ onDestroyView.\nprivate val binding get() = _binding!!\noverride fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?\n): View? {\n _binding = ResultProfileBinding.inflate(inflater, container, false)\n val view = binding.root\n return view\n}\noverride fun onDestroyView() {\n super.onDestroyView()\n _binding = null\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每個 Fragment 中都有很多樣板代碼(另外,避免使用"},{"type":"link","attrs":{"href":"https:\/\/stackoverflow.com\/questions\/34342413\/what-is-the-kotlin-double-bang-operator?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"!!"}]},{"type":"text","text":",因爲如果變量爲空,這會拋出"},{"type":"codeinline","content":[{"type":"text","text":"KotlinNullPointerException"}]},{"type":"text","text":"。使用顯式空處理來代替。)我們解決這個問題的方法是創建一個"},{"type":"codeinline","content":[{"type":"text","text":"ViewBindingHolder"}]},{"type":"text","text":"(和"},{"type":"codeinline","content":[{"type":"text","text":"DataBindingHolder"}]},{"type":"text","text":"),Fragment可以實現爲下面這樣:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"plain"},"content":[{"type":"text","text":"interface ViewBindingHolder {\n var binding: B?\n \/\/ Only valid between onCreateView and onDestroyView.\n fun requireBinding() = checkNotNull(binding)\n fun requireBinding(lambda: (B) -> Unit) {\n binding?.let {\n lambda(it)\n }}\n \/**\n * Make sure to use this with Fragment.viewLifecycleOwner\n *\/\n fun registerBinding(binding: B, lifecycleOwner: LifecycleOwner) {\n this.binding = binding\n lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {\n override fun onDestroy(owner: LifecycleOwner) {\n owner.lifecycle.removeObserver(this)\n [email protected] = null\n }\n })\n }\n}\ninterface DataBindingHolder : ViewBindingHolder\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這爲 Fragment 提供了一種簡單而乾淨的方式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"確保在需要綁定時提供綁定"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只有在綁定可用時才執行某些代碼"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自動在"},{"type":"codeinline","content":[{"type":"text","text":"onDestroyView"}]},{"type":"text","text":"上清除綁定"}]}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"暫時性泄漏"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些泄漏只會存在很短時間。特別是,我們遇到過一個由"},{"type":"codeinline","content":[{"type":"text","text":"EditTextView"}]},{"type":"text","text":"異步任務引起的泄漏。異步任務持續的時間恰好比 LeakCanary 的默認等待時間長,因此,即使內存很快就被正確地釋放了,也會報告一個泄漏。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你懷疑自己遇到了暫時性泄漏,一個很好的檢查方法是使用Android Studio的"},{"type":"link","attrs":{"href":"https:\/\/developer.android.com\/studio\/profile\/memory-profiler?fileGuid=dg5RuSiDPDkmicBU","title":"","type":null},"content":[{"type":"text","text":"內存分析器"}]},{"type":"text","text":"。一旦在分析器中啓動會話,就可以按步驟重現泄漏,但是在轉儲堆並檢查之前要等待更長時間。經過這段額外的時間後,泄漏可能就消失了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/65\/65a40b60466063341938623772fb30ae.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":10}}],"text":"Android Studio的內存分析器顯示了清理暫時性泄漏的效果"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"經常測試,儘早修復"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們希望,通過本文介紹,你能在自己的應用程序中跟蹤和解決內存泄漏!與許多Bug和其他問題一樣,最好是能經常測試,在糟糕的模式紮根代碼庫之前儘早修復。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作爲一名開發人員,你一定要記住,雖然內存泄漏並不總是會影響應用性能,但低端機型和手機內存小的用戶會感激你爲他們所做的工作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/dropbox.tech\/mobile\/detecting-memory-leaks-in-android-applications?fileGuid=dg5RuSiDPDkmicBU"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章