每次來(lái)公司面試的人芒篷,一般都會(huì)問(wèn)最基本的兩個(gè)問(wèn)題私植,一個(gè)是自定義View的繪制流程及事件分發(fā),第二個(gè)就是性能優(yōu)化內(nèi)存泄漏如何處理沸手?第一個(gè)問(wèn)題基本上都能說(shuō)個(gè)大概,第二個(gè)問(wèn)題其實(shí)很多工作好幾年的都不一定能回答的比較讓人滿意注簿。這里整理下基本的內(nèi)存泄漏及解決辦法契吉。使用的是LeakCannary來(lái)進(jìn)行檢測(cè)。
你能從本文了解到如下知識(shí):1. 什么是內(nèi)存泄漏 2. 內(nèi)存泄漏的分類及影響 3.常見(jiàn)的內(nèi)存泄漏及解決辦法 4.文章總結(jié)
[toc]
什么是內(nèi)存泄漏诡渴?
內(nèi)存泄漏也稱作"存儲(chǔ)滲漏"捐晶,用動(dòng)態(tài)存儲(chǔ)分配函數(shù)動(dòng)態(tài)開(kāi)辟的空間,在使用完畢后未釋放妄辩,結(jié)果導(dǎo)致一直占據(jù)該內(nèi)存單元惑灵。直到程序結(jié)束。(其實(shí)說(shuō)白了就是該內(nèi)存空間使用完畢之后未回收)即所謂內(nèi)存泄漏眼耀。再形象點(diǎn)比喻就像家里的水龍頭沒(méi)有擰緊英支,漏水了。
內(nèi)存泄漏的分類及影響哮伟?
分類:常發(fā)性內(nèi)存泄漏干花,偶發(fā)性內(nèi)存泄漏妄帘,一次性內(nèi)存泄漏,隱式內(nèi)存泄漏池凄。
危害:內(nèi)存泄漏造成的影響其實(shí)是內(nèi)存泄漏的堆積弧轧,這將會(huì)消耗系統(tǒng)所有的內(nèi)存薇组。所以一個(gè)內(nèi)存泄漏危害并不大漾橙,因?yàn)椴粫?huì)堆積稚配,而隱式內(nèi)存泄漏危害性則非常大,因?yàn)檩^之于常發(fā)性和偶發(fā)性內(nèi)存泄漏它更難被檢測(cè)到柏副。
常見(jiàn)的內(nèi)存泄漏及解決辦法:
1. 單例造成的內(nèi)存泄漏:
第一種情況:
public class LoginActivity extends Activity {
public static LoginActivity instance;
@Override
protected void onCreate(Bundle savedInstanceState) {
……
instance = this;
}
}
在其他地方引用LoginActivity.instance會(huì)造成檢測(cè)如下的:
這種情況我們可以通過(guò)使用弱引用的方法來(lái)優(yōu)化,修改如下:
public class LoginActivity extends Activity {
public static WeakReference<LoginActivity> instance;
@Override
protected void onCreate(Bundle savedInstanceState) {
……
instance = new WeakReference<LoginActivity>(this);
}
}
單例造成的內(nèi)存泄漏第二種情況(在網(wǎng)上找到的實(shí)例及圖片):
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;
}
檢測(cè)結(jié)果如下:
解決辦法蚣录,使用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. 接口實(shí)現(xiàn)引用造成的內(nèi)存泄漏割择。
不知道這樣實(shí)現(xiàn)代碼的多不多?
public class MyApplication extends LitePalApplication{
……
UnReadMsgListener unReadMsgListener;
public void setUnReadMsgListener(UnReadMsgListener unReadMsgListener){
this.unReadMsgListener = unReadMsgListener;//在其他頁(yè)面進(jìn)行接口實(shí)現(xiàn)
}
……
}
造成的內(nèi)存泄漏分析圖如下:
原因分析:在其他頁(yè)面進(jìn)行setUnReadMsgListener操作萎河,MyApplication將明顯持有對(duì)此接口的引用荔泳,此接口被Activity實(shí)現(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造成的內(nèi)存泄漏(MediaPlayer.mSubtitleController):
有時(shí)候?yàn)榱丝焖匍_(kāi)發(fā),經(jīng)常會(huì)在xml中使用VideoView去快速集成播放一個(gè)視頻擎椰,這樣做就會(huì)內(nèi)存泄漏支子。檢測(cè)結(jié)果如下:
從LeakCanary分析結(jié)果得出,是由于VideoView持有對(duì)Activity的Context的引用造成的达舒。因?yàn)槲覀儗ideoView寫(xiě)在XMl中值朋,所以默認(rèn)是應(yīng)用當(dāng)前頁(yè)面的Context的。
解決辦法:
第一種:將VideoView在代碼中實(shí)現(xiàn):
VideoView mVideoView = new VideoView(MyApplication.getContext());
//添加到父容器
……
第二種:重寫(xiě)當(dāng)前Activity頁(yè)面的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源碼存在的內(nèi)存泄漏問(wèn)題:
這個(gè)問(wèn)題是緊接上一個(gè)內(nèi)存泄漏巩搏,上面的處理方式基本能解決VideoView給我們帶來(lái)的內(nèi)存泄漏問(wèn)題昨登。這里我們來(lái)深入了解下為什么使用VideoView會(huì)造成內(nèi)存泄漏」岬祝看MediaPlayer的源碼我們可以得知:在系統(tǒng)的MediaPlayer的release過(guò)程中就mSubtitleController 資源未做處理丰辣,幸運(yùn)的是在reset中進(jìn)行此資源的處理,所以我們?cè)谑褂肕eidPlayer播放視頻后進(jìn)行資源釋放時(shí)再release時(shí)進(jìn)行下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) {//這里有對(duì)mSubtitleController進(jìn)行處理操作
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();
};
}
如果上面的描述不夠詳細(xì)笙什,你可以參考stackoverflow
解決辦法上面有提到過(guò),如下:
沒(méi)錯(cuò)睦擂,我就是截圖過(guò)來(lái)滴得湘。
5. Handler使用造成的內(nèi)存泄漏(MessageQueue.mMessage)
Handler 的使用造成的內(nèi)存泄漏問(wèn)題應(yīng)該說(shuō)是最為常見(jiàn)了,我們看一下下面代碼:
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);
}
}
檢測(cè)到泄漏結(jié)果如下:
原因分析:由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的顿仇。因此這種實(shí)現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致淘正,故很容易導(dǎo)致無(wú)法正確釋放摆马。
解決辦法:在 Activity 中避免使用非靜態(tài)內(nèi)部類,比如上面我們將 Handler 聲明為靜態(tài)的鸿吆,則其存活期跟 Activity 的生命周期就無(wú)關(guān)了囤采。同時(shí)通過(guò)弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進(jìn)去惩淳,最后當(dāng)我們Activity銷毀后蕉毯,Looper線程的消息隊(duì)列中可能會(huì)存在待處理的消息,所以我們?cè)贏ctivity的OnDestroy中移除消息隊(duì)列 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;
}
}
}
當(dāng)然這里簡(jiǎn)單說(shuō)一下軟引用和弱引用的使用:記住兩點(diǎn)即可:
第一點(diǎn):如果只是想避免OutOfMemory異常的發(fā)生代虾,則可以使用軟引用。如果對(duì)于應(yīng)用的性能更在意激蹲,想盡快回收一些占用內(nèi)存比較大的對(duì)象棉磨,則可以使用弱引用。
第二點(diǎn):可以根據(jù)對(duì)象是否經(jīng)常使用來(lái)判斷選擇軟引用還是弱引用学辱。如果該對(duì)象可能會(huì)經(jīng)常使用的乘瓤,就盡量用軟引用。如果該對(duì)象不被使用的可能性更大些策泣,就可以用弱引用衙傀。
6. 匿名內(nèi)部類造成的內(nèi)存泄漏
在異步操作過(guò)程中,我們經(jīng)常會(huì)這樣做:
public class WelcomeActivity extends Activity {
......
public void sel(){
new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(10000);
//do something ... 里面持有對(duì)當(dāng)前activity的引用
}
}).start();
}
}
檢測(cè)到的內(nèi)存泄漏結(jié)果如下:
原因分析:在Activity結(jié)束時(shí)萨咕,若線程內(nèi)依舊還有任務(wù)未完成统抬,則會(huì)發(fā)生內(nèi)存泄漏。上面的Runnable是一個(gè)內(nèi)部類任洞,因此對(duì)當(dāng)前的Activity存在一個(gè)隱式引用(文章開(kāi)頭有提到蓄喇,威脅最大的一種引用)。
解決思路:不使用匿名內(nèi)部類交掏,通過(guò)靜態(tài)內(nèi)部類來(lái)實(shí)現(xiàn)妆偏,使用弱應(yīng)用來(lái)持有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 ... 里面持有對(duì)當(dāng)前activity的引用
}
}
匿名內(nèi)部類被異步線程所持有的時(shí)候盅弛,我們一定要特別小心钱骂,如果么有進(jìn)行任何處理措施,極容易出現(xiàn)內(nèi)存泄漏的情況挪鹏。下面我們?cè)俜治鲆环N使用AsyncTask過(guò)程中造成的內(nèi)存泄漏處理情況:
public class MainActivity extends Activity {
public void sel(){
new AsyncTask<void, void="">() {
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(10 * 1000);
//do something ... 里面持有對(duì)當(dāng)前activity的引用
return null;
}
}.execute();
}
}
原因分析和上面是一樣的见秽,Activity結(jié)束了,異步任務(wù)還未處理完讨盒。
解決辦法:使用軟引用解取,并在Activity的onDestroy里調(diào)用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 ... 里面持有對(duì)當(dāng)前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();
}
}
關(guān)于異步線程內(nèi)存泄漏的原理返顺,推薦看下這篇文章深入分析 ThreadLocal 內(nèi)存泄漏問(wèn)題
7. 集合的內(nèi)存泄露問(wèn)題
通常我們會(huì)添加一些對(duì)象的引用到集合中禀苦,當(dāng)我們不需要用到該集合對(duì)象時(shí)蔓肯,我們需要及時(shí)將該集合清空掉,如果不清空振乏,將導(dǎo)致這個(gè)集合會(huì)越來(lái)越大蔗包。如果集合是靜態(tài)的話,那情況將會(huì)更嚴(yán)重慧邮,因?yàn)槁暶鳛閟tatic的生命周期和整個(gè)app進(jìn)程的生命周期一致调限。
下面代碼
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);
}
}
檢測(cè)結(jié)果如下圖:
HashMap的value對(duì)應(yīng)的Activity對(duì)象未釋放,這里解決辦法我們可以使用上面多次提到過(guò)的使用弱應(yīng)用去處理HashMap的Value值误澳,當(dāng)然這是在最小修改的前提下進(jìn)行耻矮。如果需要對(duì)Activity進(jìn)行管理,這里建議不要使用HashMap忆谓,可以使用HashSet去做淘钟,同樣最好存放的是弱應(yīng)用對(duì)象,而且集合列表最好不要使用static修飾陪毡。修改后的代碼如下(使用HashMap或者HashSet存儲(chǔ)對(duì)象時(shí),最好覆蓋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));
}
}
}
當(dāng)然勾扭,我們?cè)谑褂眉蠒r(shí)毡琉,應(yīng)該注意不要使用staic去修飾。其次就是使用完集合之后需要將其致空妙色。如果是如下寫(xiě)法也會(huì)出現(xiàn)內(nèi)存泄漏:
public void sel(){
Vector vector = new Vector(10);
for (int i = 0; i < 100; i++) {
Object o = new Object();
vector.add(o);
o = null;
}
}
我們將對(duì)象置空其集合還會(huì)持有對(duì)該對(duì)象的引用桅滋,為此我們應(yīng)該在不使用Vector的時(shí)侯將vector 置null。這種情況比較常見(jiàn)的就是我們?cè)赗ecyclerview的適配器中的運(yùn)用身辨,我們?cè)诋?dāng)前活動(dòng)頁(yè)面銷毀的時(shí)候應(yīng)該將其對(duì)應(yīng)的所有集合都清空丐谋。
8.資源對(duì)象沒(méi)關(guān)閉造成的內(nèi)存泄漏
在開(kāi)發(fā)過(guò)程中我們經(jīng)常會(huì)使用到BraodcastReceiver,ContentObserver煌珊,InputStream号俐,Cursor,Stream定庵,Bitmap等資源吏饿。切記在資源不再使用的時(shí)候?qū)⑵溽尫牛P(guān)閉掉蔬浙。大多數(shù)頻發(fā)的OOM出現(xiàn)絕大部分是因?yàn)閳D片資源未回收猪落。在圖片資源使用完后可以通過(guò)recycler方法來(lái)進(jìn)行處理:
if(!mBitmap.isRecycled){
mBitmap.recycle();
mBitmap = null;
}
當(dāng)然,廣播的注銷畴博,內(nèi)容觀察者的注銷笨忌,輸入輸出流的關(guān)閉,cursor的關(guān)閉這些就不一一列舉了俱病。只要在使用的時(shí)候多留意下這些都不是問(wèn)題滴官疲。
總結(jié)
對(duì)于內(nèi)存泄漏問(wèn)題袱结,記住以下幾點(diǎn):
1、對(duì)于生命周期比Activity長(zhǎng)的對(duì)象如果需要應(yīng)該使用ApplicationContext袁余,在需要使用Context參數(shù)的時(shí)候先考慮Application.Context.
2擎勘、在引用組件Activity,F(xiàn)ragment時(shí)颖榜,優(yōu)先考慮使用弱引用棚饵。
3、在使用異步操作時(shí)注意Activity銷毀時(shí)掩完,需要清空任務(wù)列表噪漾,如果有使用集合,將集合清空并置空且蓬,釋放相應(yīng)的資源欣硼。
4、內(nèi)部類持有外部類的引用盡量修改成靜態(tài)內(nèi)部類中使用弱引用持有外部類的引用恶阴。
5诈胜、 留意活動(dòng)的生命周期,在使用單例冯事,靜態(tài)對(duì)象焦匈,全局性集合的時(shí)候應(yīng)該特別注意置空。
文章中部分代碼純手打昵仅,可能有個(gè)別單詞誤差缓熟,如果有誤差,還請(qǐng)各位看官理解摔笤,如果能留言指出就十分感謝了够滑。
等等,最后:盜用大牛的一句話吕世,技術(shù)無(wú)罪彰触,我是aserbao,微信公眾號(hào)aserbao寞冯,微博同名渴析。隨時(shí)歡迎撩(學(xué)習(xí)交流)。