
每次來公司面試的人,一般都會問最基本的兩個問題,一個是自定義View的繪制流程及事件分發(fā),第二個就是性能優(yōu)化內存泄漏如何處理?第一個問題基本上都能說個大概,第二個問題其實很多工作好幾年的都不一定能回答的比較讓人滿意。這里整理下基本的內存泄漏及解決辦法。使用的是LeakCannary來進行檢測。
你能從本文了解到如下知識:1. 什么是內存泄漏 2. 內存泄漏的分類及影響 3.常見的內存泄漏及解決辦法 4.文章總結
[toc]
什么是內存泄漏?
內存泄漏也稱作"存儲滲漏",用動態(tài)存儲分配函數(shù)動態(tài)開辟的空間,在使用完畢后未釋放,結果導致一直占據(jù)該內存單元。直到程序結束。(其實說白了就是該內存空間使用完畢之后未回收)即所謂內存泄漏。再形象點比喻就像家里的水龍頭沒有擰緊,漏水了。
內存泄漏的分類及影響?
分類:常發(fā)性內存泄漏,偶發(fā)性內存泄漏,一次性內存泄漏,隱式內存泄漏。
危害:內存泄漏造成的影響其實是內存泄漏的堆積,這將會消耗系統(tǒng)所有的內存。所以一個內存泄漏危害并不大,因為不會堆積,而隱式內存泄漏危害性則非常大,因為較之于常發(fā)性和偶發(fā)性內存泄漏它更難被檢測到。
常見的內存泄漏及解決辦法:
1. 單例造成的內存泄漏:
第一種情況:
public class LoginActivity extends Activity {
public static LoginActivity instance;
@Override
protected void onCreate(Bundle savedInstanceState) {
……
instance = this;
}
}
在其他地方引用LoginActivity.instance會造成檢測如下的:
這種情況我們可以通過使用弱引用的方法來優(yōu)化,修改如下:
public class LoginActivity extends Activity {
public static WeakReference<LoginActivity> instance;
@Override
protected void onCreate(Bundle savedInstanceState) {
……
instance = new WeakReference<LoginActivity>(this);
}
}
單例造成的內存泄漏第二種情況(在網(wǎng)上找到的實例及圖片):
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
檢測結果如下:
解決辦法,使用Application的context替代Activity的context,修改后的diam如下:
public class LoginManager {
private static LoginManager mInstance;
private Context mContext;
private LoginManager(Context context) {
this.mContext = context.getApplicationContext();
}
public static LoginManager getInstance(Context context) {
if (mInstance == null) {
synchronized (LoginManager.class) {
if (mInstance == null) {
mInstance = new LoginManager(context);
}
}
}
return mInstance;
}
public void dealData() {
}
}
2. 接口實現(xiàn)引用造成的內存泄漏。
不知道這樣實現(xiàn)代碼的多不多?
public class MyApplication extends LitePalApplication{
……
UnReadMsgListener unReadMsgListener;
public void setUnReadMsgListener(UnReadMsgListener unReadMsgListener){
this.unReadMsgListener = unReadMsgListener;//在其他頁面進行接口實現(xiàn)
}
……
}
造成的內存泄漏分析圖如下:
原因分析:在其他頁面進行setUnReadMsgListener操作,MyApplication將明顯持有對此接口的引用,此接口被Activity實現(xiàn),所以MyApplication一直持有Activity的引用。
在盡量不修改原代碼的情況下,解決辦法如下:
UnReadMsgListener unReadMsgListener;
WeakReference<UnReadMsgListener> mListenerWeakReference;
public void setUnReadMsgListener(UnReadMsgListener unReadMsgListener){
mListenerWeakReference = new WeakReference<UnReadMsgListener>(unReadMsgListener);
this.unReadMsgListener = mListenerWeakReference.get();
}
3. 使用ViewVideo造成的內存泄漏(MediaPlayer.mSubtitleController):
有時候為了快速開發(fā),經常會在xml中使用VideoView去快速集成播放一個視頻,這樣做就會內存泄漏。檢測結果如下:
從LeakCanary分析結果得出,是由于VideoView持有對Activity的Context的引用造成的。因為我們將VideoView寫在XMl中,所以默認是應用當前頁面的Context的。
解決辦法:
第一種:將VideoView在代碼中實現(xiàn):
VideoView mVideoView = new VideoView(MyApplication.getContext());
//添加到父容器
……
第二種:重寫當前Activity頁面的attachBaseContext方法:
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(new ContextWrapper(newBase)
{
@Override
public Object getSystemService(String name)
{
if (Context.AUDIO_SERVICE.equals(name))
return getApplicationContext().getSystemService(name);
return super.getSystemService(name);
}
});
}
4. MediaPlayer源碼存在的內存泄漏問題:
這個問題是緊接上一個內存泄漏,上面的處理方式基本能解決VideoView給我們帶來的內存泄漏問題。這里我們來深入了解下為什么使用VideoView會造成內存泄漏??碝ediaPlayer的源碼我們可以得知:在系統(tǒng)的MediaPlayer的release過程中就mSubtitleController 資源未做處理,幸運的是在reset中進行此資源的處理,所以我們在使用MeidPlayer播放視頻后進行資源釋放時再release時進行下MediaPlayer的reset操作。下面我們看下MediaPlayer的源碼:
//MediaPlayer系統(tǒng)源碼
……
public void release() {
baseRelease();
stayAwake(false);
updateSurfaceScreenOn();
mOnPreparedListener = null;
mOnBufferingUpdateListener = null;
mOnCompletionListener = null;
mOnSeekCompleteListener = null;
mOnErrorListener = null;
mOnInfoListener = null;
mOnVideoSizeChangedListener = null;
mOnTimedTextListener = null;
if (mTimeProvider != null) {
mTimeProvider.close();
mTimeProvider = null;
}
mOnSubtitleDataListener = null;
_release();
}
……
public void reset() {
mSelectedSubtitleTrackIndex = -1;
synchronized(mOpenSubtitleSources) {
for (final InputStream is: mOpenSubtitleSources) {
try {
is.close();
} catch (IOException e) {
}
}
mOpenSubtitleSources.clear();
}
if (mSubtitleController != null) {//這里有對mSubtitleController進行處理操作
mSubtitleController.reset();
}
if (mTimeProvider != null) {
mTimeProvider.close();
mTimeProvider = null;
}
stayAwake(false);
_reset();
// make sure none of the listeners get called anymore
if (mEventHandler != null) {
mEventHandler.removeCallbacksAndMessages(null);
}
synchronized (mIndexTrackPairs) {
mIndexTrackPairs.clear();
mInbandTrackIndices.clear();
};
}
如果上面的描述不夠詳細,你可以參考stackoverflow
解決辦法上面有提到過,如下:
沒錯,我就是截圖過來滴。
5. Handler使用造成的內存泄漏(MessageQueue.mMessage)
Handler 的使用造成的內存泄漏問題應該說是最為常見了,我們看一下下面代碼:
public class BaseActivity extends AppCompatActivity {
......
private Handler baseHandler = new Handler();
@Override
protected void onResume() {
baseHandler.postDelayed(new Runnable() {
@Override
public void run() {
heartBeat();
if (!isPause) {
baseHandler.postDelayed(this, 60 * 1000);
}
}
}, 100);
}
}
檢測到泄漏結果如下:
原因分析:由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的。因此這種實現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導致無法正確釋放。
解決辦法:在 Activity 中避免使用非靜態(tài)內部類,比如上面我們將 Handler 聲明為靜態(tài)的,則其存活期跟 Activity 的生命周期就無關了。同時通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進去,最后當我們Activity銷毀后,Looper線程的消息隊列中可能會存在待處理的消息,所以我們在Activity的OnDestroy中移除消息隊列 MessageQueue 中的消息。修改后代碼如下:
public class BaseActivity extends AppCompatActivity {
......
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {//這里切記要判空
// ...
}
}
}
private final MyHandler baseHandler= new MyHandler(this);
@Override
protected void onResume() {
baseHandler.postDelayed(new Runnable() {
@Override
public void run() {
heartBeat();
if (!isPause) {
baseHandler.postDelayed(this, 60 * 1000);
}
}
}, 100);
}
@Override
protected void onDestroy() {
if (baseHandler != null) {
baseHandler.removeCallbacks(null);
baseHandler = null;
}
}
}
當然這里簡單說一下軟引用和弱引用的使用:記住兩點即可:
第一點:如果只是想避免OutOfMemory異常的發(fā)生,則可以使用軟引用。如果對于應用的性能更在意,想盡快回收一些占用內存比較大的對象,則可以使用弱引用。
第二點:可以根據(jù)對象是否經常使用來判斷選擇軟引用還是弱引用。如果該對象可能會經常使用的,就盡量用軟引用。如果該對象不被使用的可能性更大些,就可以用弱引用。
6. 匿名內部類造成的內存泄漏
在異步操作過程中,我們經常會這樣做:
public class WelcomeActivity extends Activity {
......
public void sel(){
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(10000);
//do something ... 里面持有對當前activity的引用
}
}).start();
}
}
檢測到的內存泄漏結果如下:
原因分析:在Activity結束時,若線程內依舊還有任務未完成,則會發(fā)生內存泄漏。上面的Runnable是一個內部類,因此對當前的Activity存在一個隱式引用(文章開頭有提到,威脅最大的一種引用)。
解決思路:不使用匿名內部類,通過靜態(tài)內部類來實現(xiàn),使用弱應用來持有Activity的引用。
解決后的代碼:
public class WelcomeActivity extends Activity {
......
public void sel(){
new Thread(new splashhandler()).start();
}
static class splashhandler implements Runnable {
public void run() {
SystemClock.sleep(10000);
WelcomeActivity welcomeActivity = welcomeActivityWeakReference.get();
if(welcomeActivity != null) //注意判空
//do something ... 里面持有對當前activity的引用
}
}
匿名內部類被異步線程所持有的時候,我們一定要特別小心,如果么有進行任何處理措施,極容易出現(xiàn)內存泄漏的情況。下面我們再分析一種使用AsyncTask過程中造成的內存泄漏處理情況:
public class MainActivity extends Activity {
public void sel(){
new AsyncTask<void, void="">() {
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(10 * 1000);
//do something ... 里面持有對當前activity的引用
return null;
}
}.execute();
}
}
原因分析和上面是一樣的,Activity結束了,異步任務還未處理完。
解決辦法:使用軟引用,并在Activity的onDestroy里調用AsyncTask.cancel()方法。
public class MainActivity extends Activity {
private WeakReference<context> weakReference;
AsyncTask asyncTask;
public void sel(){
asyncTask = new AsyncTask<>() {
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(10 * 1000);
//do something ... 里面持有對當前activity的引用
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
MainActivity activity = (MainActivity) weakReference.get();
if (activity != null) {
//...
}
}
};
asyncTask.execute();
}
@Override
protected void onDestroy() {
asyncTask.cancel();
}
}
關于異步線程內存泄漏的原理,推薦看下這篇文章深入分析 ThreadLocal 內存泄漏問題
7. 集合的內存泄露問題
通常我們會添加一些對象的引用到集合中,當我們不需要用到該集合對象時,我們需要及時將該集合清空掉,如果不清空,將導致這個集合會越來越大。如果集合是靜態(tài)的話,那情況將會更嚴重,因為聲明為static的生命周期和整個app進程的生命周期一致。
下面代碼
public class MyApplication extends LitePalApplication {
private static Map<String, Activity> destoryMap = new HashMap<>();
public void registerActivity(String activityName,Activity act) {
if (allActivities == null) {
destoryMap.put(activityName, activity);
}
}
public void unregisterActivity(String activityName) {
destoryMap.remove(activityName);
}
}
檢測結果如下圖:
HashMap的value對應的Activity對象未釋放,這里解決辦法我們可以使用上面多次提到過的使用弱應用去處理HashMap的Value值,當然這是在最小修改的前提下進行。如果需要對Activity進行管理,這里建議不要使用HashMap,可以使用HashSet去做,同樣最好存放的是弱應用對象,而且集合列表最好不要使用static修飾。修改后的代碼如下(使用HashMap或者HashSet存儲對象時,最好覆蓋hashCode()和equal()方法):
public class MyApplication extends LitePalApplication {
……
private Set<WeakReference<Activity>> destoryMap ;
public void registerActivity(Activity act) {
if (allActivities == null) {
allActivities = new HashSet<WeakReference<Activity>>();
}
allActivities.add(new WeakReference<Activity>(act));
}
public void unregisterActivity(Activity act) {
if (allActivities != null) {
allActivities.remove(new WeakReference<>(act));
}
}
}
當然,我們在使用集合時,應該注意不要使用staic去修飾。其次就是使用完集合之后需要將其致空。如果是如下寫法也會出現(xiàn)內存泄漏:
public void sel(){
Vector vector = new Vector(10);
for (int i = 0; i < 100; i++) {
Object o = new Object();
vector.add(o);
o = null;
}
}
我們將對象置空其集合還會持有對該對象的引用,為此我們應該在不使用Vector的時侯將vector 置null。這種情況比較常見的就是我們在Recyclerview的適配器中的運用,我們在當前活動頁面銷毀的時候應該將其對應的所有集合都清空。
8.資源對象沒關閉造成的內存泄漏
在開發(fā)過程中我們經常會使用到BraodcastReceiver,ContentObserver,InputStream,Cursor,Stream,Bitmap等資源。切記在資源不再使用的時候將其釋放,關閉掉。大多數(shù)頻發(fā)的OOM出現(xiàn)絕大部分是因為圖片資源未回收。在圖片資源使用完后可以通過recycler方法來進行處理:
if(!mBitmap.isRecycled){
mBitmap.recycle();
mBitmap = null;
}
當然,廣播的注銷,內容觀察者的注銷,輸入輸出流的關閉,cursor的關閉這些就不一一列舉了。只要在使用的時候多留意下這些都不是問題滴。
總結
對于內存泄漏問題,記住以下幾點:
1、對于生命周期比Activity長的對象如果需要應該使用ApplicationContext,在需要使用Context參數(shù)的時候先考慮Application.Context.
2、在引用組件Activity,F(xiàn)ragment時,優(yōu)先考慮使用弱引用。
3、在使用異步操作時注意Activity銷毀時,需要清空任務列表,如果有使用集合,將集合清空并置空,釋放相應的資源。
4、內部類持有外部類的引用盡量修改成靜態(tài)內部類中使用弱引用持有外部類的引用。
5、 留意活動的生命周期,在使用單例,靜態(tài)對象,全局性集合的時候應該特別注意置空。
文章中部分代碼純手打,可能有個別單詞誤差,如果有誤差,還請各位看官理解,如果能留言指出就十分感謝了。
等等,最后:盜用大牛的一句話,技術無罪,我是aserbao,微信公眾號aserbao,微博同名。隨時歡迎撩(學習交流)。