安卓性能测试之应用内存泄漏总结

时间:2022-05-06 22:35:55

内存泄漏总结



. 内存泄漏定义

Java内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。



. 内存泄漏对应用的影响

android里面,出现内存泄漏会导致系统为应用分配的内存会不断减少,从而造成app在运行时会出现卡断(内存占用高时JVM虚拟机会频繁触发GC),影响用户体验。同时,可能会引起OOM(内存溢出),从而导致应用程序崩溃!



. 引发原因


1. 非静态内部类的静态实例容易造成内存泄漏


实例:



public class MainActivity extends Activity

{

static Demo sInstance = null;

 

@Override

public void onCreate(BundlesavedInstanceState)

{

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

if (sInstance == null)

{

sInstance= new Demo();

}

}

class Demo

{

voiddoSomething()

{

System.out.print("dosth.");

}

}

}


分析:

上面的代码中的sInstance实例类型为静态实例,在第一个MainActivity act1实例创建时,sInstance会获得并一直持有act1的引用。当MainAcitivity销毁后重建,因为sInstance持有act1 的引用,所以act1是无法被GC回收的,进程中会存在2MainActivity实例(act1和重建后的MainActivity实例),这个 act1对象就是一个无用的但一直占用内存的对象,即无法回收的垃圾对象。所以,对于lauchMode不是singleInstanceActivity, 应该避免在activity里面实例化其非静态内部类的静态实例。


解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用ApplicationContext



2. Activity使用静态成员


实例:


private static Drawable sBackground;

@Override

protected void onCreate(Bundle state) {

super.onCreate(state);

 

TextView label = new TextView(this);

label.setText("Leaks are bad");

 

if (sBackground == null) {

sBackground = getDrawable(R.drawable.large_bitmap);

}

label.setBackgroundDrawable(sBackground);

 

setContentView(label);

}


分析:


由于用静态成员sBackground 缓存了drawable对象,所以activity加载速度会加快,但是这样做是错误的。因为在Android 2.3系统上,它会导致activity销毁后无法被系统回收。

label .setBackgroundDrawable函数调用会将label赋值给sBackground的成员变量mCallback

上面代码意味着:sBackgroundGC Root)会持有TextView对象,而TextView持有Activity对象。所以导致Activity对象无法被系统回收。


避免方法:


·不要对activitycontext长期引用(一个activity的引用的生存周期应该和activity的生命周期相同)

·如果可以的话,尽量使用关于applicationcontext来替代和activity相关的context

·如果一个acitivity的非静态内部类的生命周期不受控制,那么避免使用它;正确的方法是使用一个静态的内部类,并且对它的外部类有一WeakReference,就像在ViewRootImpl中内部类W所做的那样。



3. 单例造成的内存泄漏


由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。


正确的实例:


// 使用了单例模式
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;
    }
}

这样不管传入什么Context最终将使用ApplicationContext,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。



4. Handler造成的内存泄漏


示例:创建匿名内部类的静态对象

public class MainActivity extends AppCompatActivity {

    private final Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);
       
setContentView(R.layout.activity_main);

        new Thread(new Runnable() {
            @Override
            public void run() {
                // ...
               
handler.sendEmptyMessage(0x123);
            }
        });
    }
}

分析:


MainActivity结束时,未处理的消息持有handler的引用,而handler又持有它所属的外部类也就是 MainActivity的引用。这条引用关系会一直保持直到消息得到处理,这样阻止了MainActivity被垃圾回收器回收,从而造成了内存泄漏。


解决方法


Handler类独立出来或者使用静态内部类,这样便可以避免内存泄漏。


 

5. 资源未关闭造成的内存泄漏


对于使用了BraodcastReceiverContentObserverFileCursorStreamBitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。

1)比如在Activityregister了一个BraodcastReceiver,但在Activity结束后没有unregisterBraodcastReceiver
2
) 资源性对象比如CursorStreamFile文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它 们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。
3
)对于资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉,然后再设置为null。在我们的程序退出时一定要确保我们的资源性对象已经关闭。
4
Bitmap对象不在使用时调用recycle()释放内存。2.3以后的bitmap应该是不需要手动recycle了,内存已经在java层了。



6. 线程造成的内存泄漏


示例:AsyncTaskRunnable


分析:

AsyncTaskRunnable都使用了匿名内部类,那么它们将持有其所在Activity的隐式引用。如果任务在Activity销毁之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏。


解决方法

AsyncTaskRunnable类独立出来或者使用静态内部类,这样便可以避免内存泄漏。



7. 使用ListView时造成的内存泄漏


初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象 缓存起来。当向上滚动ListView时,原先位于最上面的ItemView对象会被回收,然后被用来构造新出现在下面的Item。这个构造过程就是由 getView()方法完成的,getView()的第二个形参convertView就是被缓存起来的ItemView对象(初始化时缓存中没有 View对象则convertViewnull)。

构造Adapter时,没有使用缓存的convertView
解决方法:在构造Adapter时,使用缓存的convertView


8. 集合容器中的内存泄露


我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
解决方法:在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。



9.WebView造成的泄露


当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其长期占用的内存也不能被回收,从而造成内存泄露。
解决方法:为WebView另外开启一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。



10. 一些不良代码成内存压力


有些代码并不造成内存泄露,但是它们或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存,对内存的回收和分配造成很大影响的,容易迫使虚拟机不得不给该应用进程分配更多的内存,增加vm的负担,造成不必要的内存开支。

10.1 . Bitmap使用不当

第一、及时的销毁。

虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过Java堆的限制。因此,在用完Bitmap时,要 及时的recycle掉。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。


第二、设置一定的采样率。

有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存

第三、巧妙的运用软引用(SoftRefrence

有些时候,我们使用Bitmap后没有保留对它的引用,因此就无法调用Recycle函数。这时候巧妙的运用软引用,可以使Bitmap在内存快不足时得到有效的释放。



10.2,构造Adapter时,没有使用缓存的 convertView

  以构造ListViewBaseAdapter为例,在BaseAdapter中提共了方法:

  public View getView(intposition, View convertView, ViewGroup parent)

  来向ListView提供每一个item所需要的view对象。

如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费时间,也造成内存垃圾,给垃圾回收增加压力,如果垃圾回收来不及的话,虚拟机将不得不给该应用进程分配更多的内存,造成不必要的内存开支。



10.3、不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。可以适当的使用 hashtable vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次 new 之后又丢弃。



 

. 内存泄漏的检测


方法一:


实时抓取hprof文件,观察当前时刻内存占用情况。


工具:EclipseDDMS插件。


步骤:


1. 打开Eclipse,切换到DDMS模式,如下图。

安卓性能测试之应用内存泄漏总结




2. 手机设备连接电脑,Eclipse左侧会显示出设备上运行的进程包名,选中要检测的进程。


3. 点击左上方 “Dump HPROF file” 按钮,带红色箭头的,如下图。

安卓性能测试之应用内存泄漏总结




4. 右侧会显示出Overview 界面,首先是一个扇形图,如下图。

安卓性能测试之应用内存泄漏总结


 

 

 

 

 

 

 

 

 

 

 

鼠标放到每个扇形上,左下角就会显示对应的资源类型。


5. 找到除了Remainder之外最大的扇形,点击--> 选中 Path to GC roots --> 选择exclude weak/ soft references


6. 在进入的新界面查看资源,寻找和要检测的应用相关的资源即可。



方法二


执行monkey结束之后抓取hprof文件,然后分析结果。


工具:Eclipse Memory Analysys 插件;

win7虚拟机安装adb命令;

SDK中的hprof-conv工具可以使用。



准备工作:

1. Eclipse 安装Memory Analysys 插件,通过Eclipse Marketplace安装。

2. win7虚拟机安装adb命令,参考:http://blog.sina.com.cn/s/blog_60bdd37d0101ezbg.html

3. SDK中的hprof-conv工具,如果在tools目录下,拷贝到platform-tools目录。


操作步骤:


1. 首先手机连接电脑,实体机虚拟机都可以,只要安装了adb命令就可以跑monkey

执行monkey命令:


adb shell monkey -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY -c android.intent.category.DEFAULT -c android.intent.category.BROWSABLE -c android.intent.category.TAB -c android.intent.category.ALTERNATIVE -c android.intent.category.SELECTED_ALTERNATIVE -c android.intent.category.INFO -c android.intent.category.HOME -c android.intent.category.PREFERENCE -c android.intent.category.TEST -c android.intent.category.CAR_DOCK -c android.intent.category.DESK_DOCK -c android.intent.category.CAR_MODE -p com.android.settings --ignore-crashes --ignore-timeouts --ignore-security-exceptions --ignore-native-crashes --monitor-native-crashes -s 800 -v -v -v --throttle 1000 100000


红底的是包名,需要换成要检测的应用的包名,这是一句命令,如果要抓取普通的log,可以在后边加上要保存的位置。


执行monkey的时候,可以另起一个终端,通过 “adb shell ;ps命令查看应用当前占用内存的情况。


如果要通过ps命令查看内存使用情况,就必须要了解:


USER 进程所属用户

PID 进程ID

%CPU 进程占用CPU百分比

%MEM 进程占用内存百分比

VSIZE 虚拟内存占用大小 单位:kbkillobytes

RSS 实际内存占用大小 单位:kbkillobytes

TTY 终端类型

STAT 进程状态

START 进程启动时刻

TIME 进程运行时长

COMMAND 启动进程的命令


尤其是VSIZE RSS,查看内存泄漏以实际内存占用大小为衡量标准。


还可以通过命令行 ”adb shell dumpsys meminfo <进程名>查看详细的内存使用情况。


2. 抓取hprof文件。

在虚拟机上执行命令行: ”adb shell am dumpheap <进程名> <保存路径>

命令中的<保存路径>指的是手机目录。


3. 将手机上的hprof文件pull出到电脑上。


4. pull出来的hprof文件是不能被pc机识别的,需要转码。

通过命令:”hprof-conv <HPROF文件路径> <转换后的HPROF文件路径>”


5. 打开Eclipse,切换到Memory Analysys 模式,点击左上角的 “Open Dump Heap” 按钮,打开转码后的hprof文件, 按照方法一的方式分析即可。