波尔多住宅建筑方案cad:Android 開發者必會的內存泄漏指南 [復制鏈接]

2019-10-8 14:56
Growup 閱讀:354 評論:0 贊:0
Tag:  內存泄漏

恋恋波尔多 www.luaogj.com.cn 我們都知道,寫一個 Android 的應用很容易,但是要寫一個高性能的應用可就不容易了。以我的個人經驗來說,在 App 的開發過程中,主要的精力都會放在新功能、新???、新組件的開發上。

開發過程中,看得見的 UI 比看不見的性能更能吸引我們的目光。所以我強制自己將“優化應用程序(如內存泄漏)”的優先級提高,并養成習慣。

長期以來,不關注性能,帶來了很多的技術債。經過一年多的努力調整, 比起一年前,我有很多的心得體會。

對于很多開發者來說,內存泄漏都是一個老大難的問題。關于處理內存泄漏,你有可能會覺得太難,又或是太費時,又或者是覺得完全沒有意義。但我要告訴你的是,事實并非如此。當你開始處理這些問題的時候,你會發現,這感覺超級棒。

在本篇文章中,我會以盡可能簡單的方式講解這些問題,即使你是一個初學者,也可以學習到如何構建一個高質量、高性能的應用。

垃圾回收

Java 是一個非常強大的語言。在 Android 中,我們幾乎不會像 C / C++ 那樣,手動分配和釋放內存。因為 Java 會自動清理內存。

讓我們來思考一個問題,如果 Java 內建的垃圾回收系統可以在我們不需要的時候自動回收內存,那我們為什么還需要關心內存呢?是因為垃圾回收機制不夠完善嗎?

當然不是,Java 垃圾回收機制當然是完善的。垃圾回收機制是可以正常工作,但是,如果我們的應用程序出現 Bug, 導致垃圾回收器不能正常檢查出不需要的內存塊,就會導致問題。

總體來說,是我們自己的代碼錯誤導致垃圾回收不可用。不可否認,垃圾回收機制是 Java 最偉大的設計之一。

關于垃圾回收器

在處理內存問題之前,你需要了解垃圾回收器的工作原理。它的概念非常簡單,但在它背后,有著極其復雜的邏輯。但是你也別擔心,我們也只關心一些簡單的概念。

火速收藏!Android 開發者必會的內存泄漏指南

如圖所示,Android 或者 Java 應用程序都有一個起點,從對象的初始化,并且調用方法。我們可以認為,這個點就是圖中的 "GC Roots"。有一些對象引用被 GC Roots 直接持有,剩下的則由它們自己創建并持有。

如此,整個引用鏈構成了內存樹。垃圾回收器從 GC roots 開始,直接或間接的鏈接著其它的對象。當整個遍歷結束,還有一些不能被訪問到的對象,就是變成了垃圾,這些對象就會被垃圾回收器回收。

內存泄漏

到現在,你已經知道了垃圾回收的概念,也知道了垃圾回收在 Android 中是如何管理內存的。下面,我們將深入研究一下內存泄漏。

簡單來說,內存泄漏是指你的對象已經使用結束,但是它卻不能被釋放掉。每個對象在完成它自己的生命周期過后,都需要從內存中清理出來。但是如果一個對象被另一個對象直接或間接的持有,垃圾回收器不能檢查出它已經使用結束。朋友們,這樣子就導致了內存泄漏。

值得慶幸的是,我們并不需要太擔心所有的內存泄漏,因為并不是所有的內存泄漏都是有害的。有一些內存泄漏是無關痛癢(只泄漏幾 KB 的內存),并且,在 Android Framwork 層也會有一些內存泄漏,但是你并不需要去修復,因為它們對 App 的性能影響微乎其微,你可以忽略。

但是有一些會引起 App 崩潰, 或者嚴重卡頓。這些都是需要你時刻注意的。

為什么要解決內存泄漏?

沒有人會想使用一個又慢又占內存的應用。如果使用一段時間就會崩潰,你的用戶也會“崩?!鋇?,如果長時間出現這樣子的問題,你的用戶會毫不猶豫的卸載掉你的應用,并且再也不會使用它。

火速收藏!Android 開發者必會的內存泄漏指南

如果你的應用中存在內存泄漏,垃圾回收器不能回收不使用的內存,隨著用戶使用時間的增長,內存的占用會越來越多。如此下去,當系統不能在給它分配更多內存的時候,就會導致 OutOfMemoryError, 然后應用程序會崩潰掉。

垃圾回收有利有弊,垃圾回收是一龐大的系統,在應用中,盡可能少的讓垃圾回收器運行,這樣對應用體驗會更好。

隨著你的應用使用的堆內存逐漸增加,Short GC 就會觸發,來保證立即清理無用對象。現在這些快速清理內存的 GC 運行在不同的線程中,這些 GC 不會導致你的應用變慢。

但是如果你的應用中存在嚴重的內存泄漏,Short GC 沒有辦法回收內存,并且占用內存持續增加,這將會導致 Larger GC 被觸發。它會將整個應用程序掛起,阻塞大概 50~100ms,這會導致應用程序變慢并且有可能不能使用。

修復內存泄漏,減少對 App 的影響,給用戶提供更好的體驗。

如何發現內存泄漏?

現在,你已經認識到,你需要修復隱藏在你 App 中的內存泄漏。但是,我們如何才能找到它們呢?

Android Studio 為我們提供了一個非常強大的工具:Monitors。

通過它,你能看到網絡、CPU、GPU、內存的使用情況。

在調試運行 App 的時候,要密切關注內存監視器。內存泄漏的第一個現象就是,在使用的過程中,內存一直增加,不能減少,即使你把 APP 退到后臺也不能釋放。內存分配監視器能夠清楚的看到不同對象所占用的內存,可以清楚的知道哪個對象占用內存較多,需要處理。

但是,它本身還不夠,它需要你指定時間,然后轉存出對應的內存堆。這是一個很無趣的工作。

幸運的是,我們現在已經有更好的方式來實現。LeakCanary, 一個和 App 一起運行的庫,它會在內存泄漏的時候,轉存出內存信息,然后給我們發送一個通知并給我們一個有用的棧信息。

常見的內存泄漏

從我的經驗來看,有很多相似且經常出現內存泄漏的問題,你在你每天的開發中,都有可能會遇到它們。一但你清楚了它們發生的時間、地點、原因 ,你就可以很輕松的修復它們。

  • 未取消的 Listener

很多時候,你在 Activity/Fragment 中注冊了一個 Listener, 但是忘記取消注冊了。如果你的運氣不好,它很可能會引起一個嚴重的內存泄漏問題。一般來說,這些 Listener 的 注冊與取消注冊是同步出現的,在你使用的時候需要注冊,在不使用的時候需要取消注冊。

舉個例子,當我們的應用程序需要獲取定位的時候,需要使用 LocationManager,你會從系統服務中拿到它,并且給其設置一個地理位置更新的回調:

private void registerLocationUpdats {
mManager = (LocationManager) getSystemService(
Context.LOCATION_SERVICE);
mManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
TimeUnit.MINUTES.toMillis(1),
100,
this);
}

在代碼中,可以看出來,使用了 Activity 自己來實現了地理位置更新的回調。LocationManager 會持有這個回調的引用。當你退出了這個頁面,Android 系統會調用 onDestory ,但是垃圾回收器并不能清理掉它,因為 LocationManager 持有它的強引用。

當然,解決方案也很簡單,就是在 onDestory 方法中,取消注冊就可以了。

@Override
public voidonDestroy {
super.onDestroy;
if (mManager != ) {
mManager.removeUpdates(this);
}
}
  • 內部類

內部類在 Java 和 Android 開發中經常用到,非常簡單,但是如果使用不當,也會造成嚴重的內存泄漏。讓我們先來看一個簡單的例子:

public class BadActivity extends Activity {
private TextView mMessageView;
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_bad_activity);
mMessageView = (TextView) findViewById(R.id.messageView);
new LongRunningTask.execute;
}
private class LongRunningTask extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... params) {
// 做一些耗時操作
return "Am finally done!";
}
@Override
protected voidonPostExecute(String result) {
mMessageView.setText(result);
}
}
}

這是一個很簡單的 Activity 頁面,在頁面啟動的時候,在后臺啟動了一個耗時的任務(比如說,復雜的數據庫查詢或者是很慢的網絡)。等到任務執行結束,把拿到的結果顯示到頁面上??雌鵠?,這樣做并沒有問題。事實上,非靜態的內部類會隱式的持有外部類的引用(在這里,就是 Activity)。如果在耗時任務執行完之前,你旋轉屏幕或者退出這個頁面,垃圾回收器就不能從內存中清理掉 Activity 的實例。這個簡單的問題會導致很嚴重的內存泄漏問題。

當然,解決方案也非常地簡單,如下:

public class GoodActivity extends Activity {
private AsyncTask mLongRunningTask;
private TextView mMessageView;
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_good_activity);
mMessageView = (TextView) findViewById(R.id.messageView);
mLongRunningTask = new LongRunningTask(mMessageView).execute;
}
@Override
protected voidonDestroy {
super.onDestroy;
mLongRunningTask.cancel(true);
}
private static class LongRunningTask extends AsyncTask<Void, Void, String> {
private final WeakReference<TextView> messageViewReference;
publicLongRunningTask(TextView messageView) {
this.messageViewReference = new WeakReference<>(messageView);
}
@Override
protected String doInBackground(Void... params) {
String message = ;
if (!isCancelled) {
message = "I am finally done!";
}
return message;
}
@Override
protected voidonPostExecute(String result) {
TextView view = messageViewReference.get;
if (view != ) {
view.setText(result);
}
}
}
}

正如你看到的代碼,首先我將非靜態內部類改成了靜態內部類,這樣它就不會持有外部類的引用了。當然,使用靜態的內部類,非靜態的變量就不能訪問了。所以我們需要將 TextView 通過構造方法把它傳過去。

在這里,我強烈推薦使用 WeakReference ,它能更好的避免引起內存泄漏。你應該去學習 Java 中關于不同引用類型的知識:

//javarevisited.blogspot.in/2014/03/difference-between-weakreference-vs-softreference-phantom-strong-reference-java.html

  • 匿名內部類

匿名內部類也是在開發過程中經常使用到的一個東西,它的定義和使用都非常的簡潔。但以我的經驗來看,匿名內部類造成了大量的內存泄漏的問題。

匿名內部類與非靜態內部類相似,造成內部類的原因也和上面說的一樣。你有可能在好多地方都使用了匿名內部類,如果使用不當,會嚴重影響 App 的性能。

public class MoviesActivity extends Activity {
private TextView mNoOfMoviesThisWeek;
@Override
protected voidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_movies_activity);
mNoOfMoviesThisWeek = (TextView) findViewById(R.id.no_of_movies_text_view);
MoviesRepository repository = ((MoviesApp) getApplication).getRepository;
repository.getMoviesThisWeek
.enqueue(new Callback<List<Movie>> {

@Override
public voidonResponse(Call<List<Movie>> call,
Response<List<Movie>> response) {
int numberOfMovies = response.body.size;
mNoOfMoviesThisWeek.setText("No of movies this week: " + String.valueOf(numberOfMovies));
}
@Override
public voidonFailure(Call<List<Movie>> call, Throwable t) {
// Oops.
}
});
}
}

上面的例子中,我使用一個常用的網絡庫 Retrofit 發送了一個網絡請求,然后在 TextView 中顯示返回的結果。很明顯,那個 Callback 對象持有 Activity 的引用。如果現在網絡很慢,在網絡響應回來之前,頁面旋轉或者關閉,就會導致 Activity 泄漏。

我強烈建議,在需要的時候,盡量使用靜態的內部類,而非匿名內部類。當然,我的意思不是不在使用匿名內部類,如果你需要使用匿名內部類,你需要注意引起內存泄漏的問題,保證不會出現問題。

  • Bitmaps

在應用中,你看到的所有圖片都是 Bitmap 對象,包含了所有的像素數據。現在這些 Bitmap 數據非常的大,一個處理不好,就會引起 OOM, 造成 APP 崩潰。在 APP 中使用的圖片資源生成的 Bitmap 會由系統進行管理,但是如果你需要自己處理 Bitmap ,要記住,使用完過后要調用 bitmap.recycle 來釋放資源。

在處理 Bitmap 時,需要將一張大的圖縮放變小過后,在使用,多重用同一個圖片數據。Google 官方有一個關于處理 Bitmap 內存的文檔:

https://developer.android.com/training/displaying-bitmaps/manage-memory.html

  • Contexts

另一個是關于 Context 的濫用引起的內存泄漏。Activity / Application / Service 都是繼承自 Context 并實現它們自己的功能,但是你也需要搞清楚它們之間的區別,什么是 activity 級別的 Context,什么是 application 級別的 Context,根據項目需求的場景去選擇使用哪一個 Context 。錯誤地使用 Activity Context,導致引用不能被釋放,就會引起內存泄漏。

結語

現在,你知道了什么是垃圾回收器,什么是內存泄漏,內存泄漏給你帶來的影響。你也知道如何檢測和修復內存泄漏。

從現在開始,構建高質量/高性能的應用。處理內存泄漏不僅能讓你的應用有更好的用戶體驗,也能讓你成為更好的開發者。


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反?。[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 恋恋波尔多 )

彩九游戏 派派玩久能赚钱吗 孙红雷代言贪玩蓝月能赚钱 1元打印相片赚钱 手游游戏主播赚钱吗 男的越丑更会赚钱吗 投资电影院能赚钱吗 苹果下载游戏赚钱是真的吗 加盟轻食明月包子赚钱吧 5sing可以赚钱 gta5古柯碱工厂赚钱买哪个 收卡怎么赚钱吗 gpk钱龙捕鱼怎么调自动射击 我想开店什么最赚钱 赚钱套路吧 在家做寿司赚钱吗