前言
在android開發(fā)中喉恋,我們都或多或少的會遇到一些內(nèi)存泄漏的問題,雖然大都知道哪些情況會導(dǎo)致內(nèi)存泄露母廷,但是還是不可避免的會遇到類似的問題轻黑,因此,知道如何去查找內(nèi)存泄露就顯得非常重要了琴昆。本篇和大家分享下如何進(jìn)行內(nèi)存泄漏的定位分析氓鄙,以及對內(nèi)存占用的優(yōu)化分析。相信大家看了之后會有所收獲业舍。
為了有一個良好的分析體驗(yàn)抖拦,我特意新建了一個用于分析內(nèi)存方面的項目升酣,該項目是一個簡易的新聞客戶端,結(jié)構(gòu)上大致是這樣的态罪,mvp開發(fā)模式噩茄,網(wǎng)絡(luò)數(shù)據(jù)方面采用Retrofit + rxjava,列表使用LRecyclerView复颈,新聞頁面由ViewPager將十幾個不同類型的新聞列表Fragment頁面組合在一起绩聘。種情況由于頁面的切換,以及數(shù)據(jù)列表的刷新加載等耗啦,在開發(fā)中還是比較典型的凿菩,在內(nèi)存控制上也是有較高要求的,因此是比較適合用來做內(nèi)存分析的帜讲。
內(nèi)存快照分析方法
這里我們直接使用Android Studio的內(nèi)存分析工具進(jìn)行分析衅谷。打開Android Monitor,可看到Logcat舒帮,切換到Monitors会喝,可看到內(nèi)存陡叠,CPU相關(guān)信息玩郊。
- 找到當(dāng)前分析的應(yīng)用,這里為com.test.memory枉阵。
- 點(diǎn)擊幾次Initiate GC译红,用于通知垃圾收集進(jìn)行垃圾回收,避免無效的內(nèi)存分析兴溜。
-
點(diǎn)擊Dump Java Heap侦厚,過一會就會打開內(nèi)存快照。
接下來分析內(nèi)存快照
- 點(diǎn)擊選擇PackageTreeView拙徽,這樣就可以按包名層級進(jìn)行類的查找刨沦。
- 左上部分就是應(yīng)用相關(guān)的類的內(nèi)存信息了。通常我們只需按包名com.test.memory找到自己應(yīng)用下的類進(jìn)行分析膘怕,這里找到NewsListFragment進(jìn)行分析想诅,它代表一種類型的新聞列表頁面挣柬。
- 可以看到TotalCount這一列是12泰佳,也就是說當(dāng)前有12個NewsListFragment對象,也就是12個NewsListFragment新聞列表頁面了货岭,因?yàn)橹坝袑⑺蓄愋偷捻撁娑即蜷_過了忘古。
- Shallow Size這一欄徘禁,可以看到是2880,代表的意思就是NewsListFragment的所有對象占用了多少內(nèi)存髓堪,這里是12個的總大小送朱,因此一個NewsListFragment的大小是240娘荡。注意,這里僅僅是指NewsListFragment本身占用的內(nèi)存驶沼,而作為它的引用屬性對象所占的內(nèi)存是不算其中的它改,比如它持有的視圖View的大小是不算其中的,而只算一個int類型引用的大小商乎,4字節(jié)央拖。所以Shallow Size通常并不大,因?yàn)樗皇钱?dāng)前對象本身的大小鹉戚,不算它引用對象的大小在其中鲜戒。
- Retained Size這一欄,是1757826抹凳,也就是1.75M大小了遏餐,也就是說12個NewsListFragment所持有的總大小是1.75M,這里的持有大小赢底,它不但包括NewsListFragment本身的大小失都,還包括它持有對象的大小,并且是它持有對象可被回收的大小幸冻。因此Retained Size是指粹庞,如果NewsListFragment這個對象被回收時,它最終能被回收的內(nèi)存洽损,也就是它本身的內(nèi)存庞溜,和一部分只有被它引用的對象的內(nèi)存,而還被其他對象持有的內(nèi)存是不算在其中的碑定,例如context對象流码,它不僅被NewsListFragment引用,所以它的內(nèi)存大小是不算入在Retained Size中的延刘。
- 右上部分代表的是所選擇類的所有對象和其屬性所占內(nèi)存情況漫试。例如這里是NewsListFragment類的12個對象的具體內(nèi)存情況,和它其中各個屬性引用對象的內(nèi)存情況碘赖。這里可以分析其中的哪些屬性或引用對象占用的內(nèi)存較高驾荣。
-
下面的部分指的是當(dāng)前的NewsListFragment對象被哪些對象引用了,可以查看它的引用樹崖疤,可用于查找最終導(dǎo)致內(nèi)存泄露無法被釋放的最終根源秘车。
內(nèi)存泄露分析
明白了如何查看內(nèi)存快照信息,知道它們代表的含義之后劫哼,接下來舉一個例子來分析下內(nèi)存泄露問題叮趴。場景是這樣的,在每個新聞列表頁面?zhèn)€NewsListFragment的onCreateView方法時权烧,我會添加一個LeakAnimView在其上眯亦,并開始執(zhí)行縮放動畫伤溉,當(dāng)onDestoryView時移除LeakAnimView,并停止它的動畫妻率。代碼如下:
public class NewsListFragment extends BaseListFragment<NewsPresenter>
implements NewsContract.View {
private LeakAnimView animView;
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if(ControInfos.isTestLeak){
//如果測試內(nèi)存泄露問題乱顾,則執(zhí)行
animView = new LeakAnimView(view.getContext());
RelativeLayout parent = (RelativeLayout) view;
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(100, 100);
params.addRule(RelativeLayout.CENTER_IN_PARENT);
parent.addView(animView, params);
animView.start();
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
if(ControInfos.isTestLeak){
//如果測試內(nèi)存泄露問題,則執(zhí)行
if(animView != null && animView.getParent() != null){
RelativeLayout parent = (RelativeLayout) animView.getParent();
animView.cancel();
parent.removeView(animView);
animView = null;
}
}
}
...
}
下面是LeakAnimView的實(shí)現(xiàn):
/**
* 存在內(nèi)存泄露的動畫View宫静,由于動畫Cancel之后走净,還是會回調(diào)onAnimationEnd,所以需要額外判斷是否取消狀態(tài)孤里,否則動畫會一直執(zhí)行下去伏伯,導(dǎo)致內(nèi)存泄露問題
*/
public class LeakAnimView extends View{
private static final String TAG = "AnimView";
private AnimatorSet animatorSet, animatorSet2;
private ObjectAnimator scaleX, scaleY;
private ObjectAnimator scaleX2, scaleY2;
private boolean isAnimating;
public LeakAnimView(Context context) {
super(context);
init();
}
public LeakAnimView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
setBackgroundColor(Color.RED);
animatorSet = new AnimatorSet();
scaleX = ObjectAnimator.ofFloat(this, "scaleX", 0.5f, 1f);
scaleY = ObjectAnimator.ofFloat(this, "scaleY", 0.5f, 1f);
animatorSet.play(scaleX).with(scaleY);
animatorSet.setDuration(500);
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.d(TAG, "animatorSet onAnimationStart");
}
@Override
public void onAnimationEnd(Animator animation) {
Log.d(TAG, "animatorSet onAnimationEnd");
//取消動畫時,該方法依然會被回調(diào)捌袜,所以下個動畫會執(zhí)行说搅,存在內(nèi)存泄露問題,所以要做狀態(tài)的判斷
if(ControInfos.exitstLeak){
//這里存在內(nèi)存泄露問題
animatorSet2.start();
}else{
//這里解決了內(nèi)存泄露問題
if(isAnimating){
animatorSet2.start();
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
Log.d(TAG, "animatorSet onAnimationCancel");
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animatorSet2 = new AnimatorSet();
scaleX2 = ObjectAnimator.ofFloat(this, "scaleX", 1f, 0.5f);
scaleY2 = ObjectAnimator.ofFloat(this, "scaleY", 1f, 0.5f);
animatorSet2.play(scaleX2).with(scaleY2);
animatorSet2.setDuration(500);
animatorSet2.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.d(TAG, "animatorSet2 onAnimationStart");
}
@Override
public void onAnimationEnd(Animator animation) {
Log.d(TAG, "animatorSet2 onAnimationEnd");
//取消動畫時虏等,該方法依然會被回調(diào)弄唧,所以下個動畫會執(zhí)行,存在內(nèi)存泄露問題霍衫,所以要做狀態(tài)的判斷
if(ControInfos.exitstLeak){
//這里存在內(nèi)存泄露問題
animatorSet.start();
}else{
//這里解決了內(nèi)存泄露問題
if(isAnimating){
animatorSet.start();
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
Log.d(TAG, "animatorSet2 onAnimationCancel");
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
public void start(){
if(isAnimating){
return;
}
isAnimating = true;
animatorSet.start();
}
public void cancel(){
if(isAnimating){
isAnimating = false;
if(animatorSet.isRunning() || animatorSet.isStarted()){
animatorSet.cancel();
}
if(animatorSet2.isRunning() || animatorSet2.isStarted()){
animatorSet2.cancel();
}
}
}
}
上面只給出測試導(dǎo)致內(nèi)存泄露的部分候引,其他代碼實(shí)現(xiàn)可以看項目源碼。其實(shí)導(dǎo)致內(nèi)存泄露的原因也比較簡單慕淡,但是如果對動畫不是很熟悉的話背伴,容易踩這個坑沸毁,做一個循環(huán)動畫峰髓,動畫1執(zhí)行完后執(zhí)行動畫2,動畫2執(zhí)行完后執(zhí)行動畫1息尺,如此循環(huán)携兵。重點(diǎn)是取消的時候,除了會回調(diào)onAnimationCancel之外搂誉,仍然會回調(diào)onAnimationEnd徐紧,而如果不在其中做標(biāo)記判斷的話,那么又會去執(zhí)行下一個動畫炭懊,那么取消方法并不能停止動畫并级,動畫會一直持有LeakAnimView,然后導(dǎo)致NewsListFragment即便是所屬的Activity頁面關(guān)閉了也不能被釋放侮腹,這時就存在內(nèi)存泄露問題了嘲碧。
如圖所示,NewsListFragment對象是12個父阻,而LeakAnimView卻有62個之多愈涩,如果左右滑動更多的話望抽,會一直增加,而從底部引用樹中也可以看出是動畫導(dǎo)致的內(nèi)存泄露履婉。那么關(guān)閉頁面之后煤篙,看看這些NewsListFragment和LeakAnimView能不能被回收
發(fā)現(xiàn)這些對象并沒有隨著所屬Activity頁面的關(guān)閉而被回收。那么在修改了內(nèi)存泄露問題之后毁腿,看看效果是怎么樣的
可以看到辑奈,LeakAnimView變成了12個,無論怎么樣左右滑動頁面已烤,它都只保存在12以內(nèi)身害,這說明內(nèi)存泄露不存在了,同時當(dāng)將頁面關(guān)閉時草戈,可以看到LeakAnimView和NewsListFragment對象數(shù)量都為0塌鸯,都被回收了。
內(nèi)存占用分析
上面我們通過分析將內(nèi)存泄露的問題解決了唐片,但是我們深知當(dāng)前的狀態(tài)并不是完美的丙猬。雖然不存在內(nèi)存泄露,但是內(nèi)存占用的問題還是可以進(jìn)行優(yōu)化的费韭。特別是在每個列表頁面數(shù)據(jù)量大茧球,頁面的布局復(fù)雜,帶有重量級的控件在其中時星持,如果這些不能隨著PageAdapter的滑動進(jìn)行一定的釋放的話抢埋,內(nèi)存占用也是會非常高,導(dǎo)致內(nèi)存溢出的問題督暂。這里我們還是以LeakAnimView來做個例子吧揪垄,我們知道,當(dāng)我們?yōu)g覽過所有的NewsListFragment頁面后逻翁,NewsListFragment的對象數(shù)量維持在12饥努,相應(yīng)的LeakAnimView也是在12個。
但是不覺得有點(diǎn)奇怪嗎八回?我在NewsListFragment的onDestroyView中是做了移除操作的酷愧,并且將animView設(shè)為null了,照理說應(yīng)該沒有被其他對象引用了缠诅,應(yīng)該是可以被回收的溶浴,這樣的話,除了有兩三個LeakAnimView對象還存在之外管引,其他應(yīng)該都是被回收的啦士败,但是為啥沒有呢?我們看下其中一個LeakAnimView對象的引用樹汉匙,發(fā)現(xiàn)了問題拱烁。
當(dāng)前的LeakAnimView對象被android.widget.RelativeLayout.DependencyGraph.Node@318059736 (0x12f534d8)給引用了生蚁,當(dāng)然它還有被其他給引用,不過經(jīng)分析戏自,有效的引用是屬于RelativeLayout.DependencyGraph.Node的邦投,那這個是干嘛用的,跟進(jìn)代碼發(fā)現(xiàn)擅笔,原來RelativeLayout中有個Node來管理它的子View志衣,每個子View作為一個節(jié)點(diǎn)Node,DependencyGraph則是用來管理節(jié)點(diǎn)Node猛们,Node還持有的當(dāng)前的LeakAnimView對象的話念脯,說明Node沒有被釋放,執(zhí)行release方法,也就是DependencyGraph沒有執(zhí)行clear方法弯淘。
public class RelativeLayout{
...
private static class DependencyGraph {
...
void clear() {
final ArrayList<Node> nodes = mNodes;
final int count = nodes.size();
for (int i = 0; i < count; i++) {
nodes.get(i).release();
}
nodes.clear();
mKeyNodes.clear();
mRoots.clear();
}
static class Node {
...
void release() {
view = null;
dependents.clear();
dependencies.clear();
sPool.release(this);
}
}
}
}
再找哪里調(diào)用了DependencyGraph的clear方法绿店,發(fā)現(xiàn)是在RelativeLayout的sortChildren方法中,而sortChildren是在onMeasure方法中被調(diào)用的
public class RelativeLayout{
...
private void sortChildren() {
final int count = getChildCount();
if (mSortedVerticalChildren == null || mSortedVerticalChildren.length != count) {
mSortedVerticalChildren = new View[count];
}
if (mSortedHorizontalChildren == null || mSortedHorizontalChildren.length != count) {
mSortedHorizontalChildren = new View[count];
}
final DependencyGraph graph = mGraph;
graph.clear();
for (int i = 0; i < count; i++) {
graph.add(getChildAt(i));
}
graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mDirtyHierarchy) {
mDirtyHierarchy = false;
sortChildren();
}
...
}
}
也就是說onMeasure沒有在NewsListFragment執(zhí)行onDestroyView時執(zhí)行庐橙。那這個怎么解決假勿,我現(xiàn)在也沒有比較好的解決方案,想了一個做驗(yàn)證性的方法态鳖,通過反射主動調(diào)用RelativeLayout的sortChildren方法
public class NewsListFragment extends BaseListFragment<NewsPresenter>
implements NewsContract.View {
...
@Override
public void onDestroyView() {
super.onDestroyView();
if(ControInfos.isTestLeak){
//如果測試內(nèi)存泄露問題转培,則執(zhí)行
if(animView != null && animView.getParent() != null){
Log.e("NewsListFragment", "remove pre, animView parent : " + animView.getParent());
RelativeLayout parent = (RelativeLayout) animView.getParent();
animView.cancel();
parent.removeView(animView);
//這里通過反射主動調(diào)用RelativeLayout的sortChildren方法,達(dá)到清除animView被RelativeLayout.DependencyGraph.Node持有引用的問題
ReflectUtil.invokeMethod(parent.getClass().getName(), "sortChildren", parent, null, new Object[]{});
Log.e("NewsListFragment", "remove post, animView parent : " + animView.getParent());
Log.e("NewsListFragment", "remove post, parent size : " + parent.getChildCount());
animView = null;
}
}
}
}
現(xiàn)在測試一下看看效果浆竭。
很欣喜的看到浸须,這個只有2個LeakAnimView對象了(當(dāng)前的NewsListFragment和旁邊的NewsListFragment所持有的LeakAnimView對象)。說明確實(shí)是由于被RelativeLayout.DependencyGraph.Node持有的引用導(dǎo)致LeakAnimView對象不能被回收了邦泄。當(dāng)然通過反射去實(shí)現(xiàn)不一定是合適的辦法删窒,大家可以想想其他更合適的方法去實(shí)現(xiàn)。
顯然虎韵,這樣省去了10個LeakAnimView對象所占用的內(nèi)存易稠,那么再延伸到NewsListFragment持有的View的話,是不是可以想辦法去實(shí)現(xiàn)回收其他10個NewsListFragment中的View的內(nèi)存呢包蓝,那么想想,內(nèi)存占用是不是會減少很多企量?具體怎么去做需要大家自己去做嘗試和驗(yàn)證测萎。
總結(jié)
好啦,到總結(jié)的時候了届巩。無論是內(nèi)存泄露的檢測分析硅瞧,還是內(nèi)存占用的優(yōu)化分析,都可以通過查看Android Studio導(dǎo)出的內(nèi)存快照進(jìn)行分析恕汇。內(nèi)存泄露問題著重看類對象的數(shù)量Total Size腕唧,看是否符合預(yù)期或辖,而內(nèi)存占用則更注重去找內(nèi)存占用較大的對象Shallow Size,分析它的數(shù)量枣接,以及哪里占用了較大內(nèi)存颂暇,分析是否合理,然后進(jìn)行針對性的優(yōu)化但惶,更深的體會就得自己親自嘗試了耳鸯。