公司的項目基本該有的功能都做完了次哈,之前都是在趕工,改需求吆录,媽的需求改得還真TM頻繁窑滞,從開始做的時候就改到要發(fā)布,這種情況已經(jīng)持續(xù)一年多了,一直到現(xiàn)在哀卫,害得每次都趕工巨坊,著實是蛋疼,真他媽的不想干了聊训。因為之前都是趕功能抱究,根本就沒時間對app進行內(nèi)存優(yōu)化恢氯,現(xiàn)在沒什么事做了带斑,就來優(yōu)化下內(nèi)存。
對于android手機而言勋拟,內(nèi)存是很寶貴的勋磕,不像PC一樣。手機內(nèi)存本來就不多敢靡,如果開發(fā)上還不注意節(jié)約內(nèi)存的開銷挂滓,很容易導致可用內(nèi)存變得越來越少,到你的app的使用內(nèi)存超過向系統(tǒng)申請內(nèi)存時啸胧,系統(tǒng)就不得不把你的app進程給kill掉赶站。所以我們?yōu)榱瞬唤o系統(tǒng)增加太多的壓力和不讓我們app給系統(tǒng)干掉,還是優(yōu)化下內(nèi)存開銷吧纺念。
- 什么是內(nèi)存泄露
在java中贝椿,如果一個對象沒有可用價值了,但又被其他引用所指向陷谱,那么這個對象對于gc來說就不是一個垃圾烙博,
所以不會對其進行回收,但是我們認為這應(yīng)該是個垃圾烟逊,應(yīng)該被gc回收的渣窜。這個對象得不到gc的回收,
就會一直存活在堆內(nèi)存中宪躯,占用內(nèi)存乔宿,就跟我們說的霸著茅坑不拉屎的道理是一樣的。這樣就導致了內(nèi)存的泄露访雪。
所以我們在開發(fā)中就應(yīng)該盡量避免內(nèi)存泄露详瑞,讓app使用更加流暢。
- 內(nèi)存泄露檢測工具
1冬阳、MAT蛤虐,下載地址: MAT ,這個工具功能很強大,但是學習成本比較高肝陪,我用了幾遍就不想用了驳庭,實在是麻煩,每次都要導出.hprof文件,然后通過命令行把.hprof轉(zhuǎn)換成MAT可以識別的文件饲常,里面的功能也要學習斷時間才行蹲堂。
2、LeakCanary贝淤, 下載地址:LeakCanary ,這個工具實在是太刁了柒竞,方便,使用簡單播聪,在通知欄通知內(nèi)存泄露朽基,我非常喜歡。以下講解下他的使用离陶,當然稼虎,你也可以看官方文檔。
1.1招刨、首先我們在build.gradle中的dependencies中添加以下代碼
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' // or 1.4-beta1
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
debugCompile 是在調(diào)試的時候使用的霎俩,releaseCompile 是在release.apk包中不使用的,這樣配置也就不用我們修改任何代碼了沉眶,我們正式打的包是不會出現(xiàn)leakcanary那些提示的打却。
1.2、在Application中的onCreat方法添加代碼:
LeakCanary.install(this);
經(jīng)過以上配置就可以監(jiān)聽activity是否存在內(nèi)存泄露了谎倔,什么代碼都不用加了柳击。如果我們需要監(jiān)聽某個對象是否存在內(nèi)存泄露,我們可以這樣做:
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(testInstance);
- 內(nèi)存泄露案例1
public class LeakActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
}
},1000 * 60);
}
}
以上代碼很簡單传藏,handler在post了一個1分鐘后執(zhí)行的runnable腻暮,而這個runnable是掛載在主線程的√赫欤看似很平常哭靖,沒什么不對,我們運行程序后侈离,可以看到以下截圖:
從上圖我們可以看到试幽,LeakActivity的對象發(fā)生了內(nèi)存泄露,是由mMessageQueue這個對象導致的卦碾。當我們啟動LeakActivity的時候铺坞,handler會把Runnable對象封裝成一個Message,然后 post 這個Message進MessageQueue,等待60秒后執(zhí)行。我們結(jié)束LeakActivity時洲胖,并沒有取消掉MessageQueue里的Message济榨,所以Message里的Runnable會一直等到1分鐘結(jié)束后執(zhí)行里面的run方法绿映,在這過程中MessageQueue會一直持有Message的對象引用腐晾,然而Runnable是封裝在這個Message的丐一,所以他們之間的引用關(guān)系就像這樣:MessageQueue->Message->Runnable->Handler->Activity藻糖。所以這就導致當前activity與MessageQueue一直有關(guān)聯(lián),導致LeakActivity的對象不能被gc回收库车,從而導致內(nèi)存泄露。(關(guān)于Handler柠衍、Message、MessageQueue拧略、Looper之間的工作原理可以去看下源碼)
所以要避免上面的內(nèi)存泄露芦岂,我們可以這樣做瘪弓,在activity的onDestroy方法中干掉handler的所有callback和message:
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
}
這樣就ok了垫蛆。
在activity中開啟的線程也是一樣腺怯,如果activity結(jié)束了而線程還在跑,一樣會導致activity內(nèi)存泄露呛占,因為"非靜態(tài)內(nèi)部類對象都會持有一個外部類對象的引用"晾虑,你創(chuàng)建的線程就是activity中的一個內(nèi)部類,持有activity對象的引用帜篇,當activity結(jié)束了,但線程還在跑笙隙,就會導致activity內(nèi)存泄露。
上面的例子是很容易看出是否有內(nèi)存泄露签钩,那么接下來的例子就沒那么容易看出來了坏快,而且開發(fā)中使用的頻率是很高的。
- 內(nèi)存泄露案例2
LeakActivity1
public class LeakActivity1 extends AppCompatActivity {
private TestManager testManager = TestManager.getInstance();
private MyListener listener=new MyListener() {
@Override
public void doSomeThing() {}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
testManager.registerListener(listener);
}
}
TestManager
public class TestManager {
private TestManager(){}
private static final TestManager INSTANCE = new TestManager();
private MyListener listener;
public static TestManager getInstance() {
return INSTANCE;
}
public void registerListener(MyListener listener) {
this.listener = listener;
}
public void unregisterListener() {
listener = null;
}
}
interface MyListener {
void doSomeThing();
}
運行LeakActivity1后出現(xiàn)了內(nèi)存泄露昧旨,如下圖:
由上圖我們可以看出,LeakActivity1還是內(nèi)存泄露了鸣戴。我按照上面標注的順序說下粘拾,第1個中的leaks是內(nèi)存泄露的意思,代表是LeakActivity1的對象instance內(nèi)存泄露了缰雇,2中references是引用的意思入偷,導致內(nèi)存泄露是由LeakActivity1中的一個實現(xiàn)了MyListener的匿名類導致的疏之,這里的引用就是指這個實現(xiàn)類的引用暇咆,3中的reference是指TestManager類中的listener這個引用,4中指出了最終導致內(nèi)存泄露的根本源頭為TestManger類中的INSTANCE爸业。
上面的代碼中,我們定義了個TestManager,并且使用單例模式拯爽,然后在activity實現(xiàn)MyListener接口钧忽,再通過testManager.registerListener(listener);注冊個回調(diào)。我們開發(fā)中很經(jīng)常這樣干耸黑。但是我們這里的是單例,如下圖备禀,當程序執(zhí)行LeakActivity1時奈揍,讀到private TestManager testManager=TestManager.getInstance(); 這句代碼時,首先會在棧內(nèi)存中開辟內(nèi)存存儲testManager變量另患,然后讀到TestManager.getInstance();時候蛾绎,會加載TestManager類鸦列,因為TestManager中有static對象鹏倘,static跟類的生命周期是一樣的,類一加載纤泵,static就加載了,類一被銷毀玻褪,static才會跟著銷毀(static是存在方法區(qū))公荧,這時候jvm會在方法區(qū)中存儲變量INSTANCE,然后在堆內(nèi)存開辟空間存放INSTANCE對象窟社,然后把地址值付給INSTANCE變量晤揣,使INSTANCE變量就指向這個對象(類似c語言的指針),activity類也是這樣的一種執(zhí)行關(guān)系昧识。
因為這是一個單例跪楞,當app進程被干掉的時候侣灶,堆內(nèi)存中的INSTANCE對象才會被釋放,所以INSTANCE對象的生命周期是很長的褥影,LeakActivity1中凡怎,listener持有當前activity的對象,然后testManager.registerListener(listener);執(zhí)行完统倒,TestManager中的listener就持有activity中l(wèi)istener的對象,而TestManager中的INSTANCE是static的耸成,生命周期長,activity銷毀的時候INSTANCE依然還在井氢,INSTANCE還在,那么TestManager類中的全局變量也還是存在的骗卜,所以TestManager中的listener變量還在左胞,還一直持有LeakActivity1中的listener對象引用,所以最終是INSTANCE導致LeakActivity1內(nèi)存泄露遍烦。
所以躺枕,要解決這個問題,可以這樣做拐云,在activity的onDestroy方法中注銷注冊的listener:
@Override
protected void onDestroy() {
testManager.unregisterListener();
super.onDestroy();
}
這樣做后TestManager中的listener不再持有LeakActivity1中的listener對象引用叉瘩,所以LeakActivity1被銷毀后listener對象也可被回收了。
最終薇缅,問題又解決了,當然你也可以直接把INSTANCE置null汤徽。
- 總結(jié)
1灸撰、小心使用static
2、線程生命周期要跟activity同步
3浮毯、小心使用第三方j(luò)ar包(我開發(fā)中就遇到過jar包中持有activity對象導致的內(nèi)存泄露)
4亲轨、網(wǎng)絡(luò)請求也是線程操作的,也應(yīng)該與activity生命周期同步惦蚊,在onDestroy的時候cancle掉請求
5讯嫂、盡量使用application代替activity和context: Context context = activity.getApplication();這樣就使得context不是指向Activity了兆沙,指向全局的application,這樣就沒內(nèi)存泄露可說了千扔。
等等......
不單單是activity會出現(xiàn)內(nèi)存泄漏的库正,其他的類對象也可能會泄漏,對象回收不了龙誊,那么類中的其他變量值也會在喷楣,如果不處理,量一多铣焊,還是挺可怕的。今天就寫到這了叽讳,公司現(xiàn)在就剩我一個人了熊昌,該回去吃飯了。