安卓性能测试之应用内存泄漏总结
内存泄漏总结
一. 内存泄漏定义
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回收的,进程中会存在2个MainActivity实例(act1和重建后的MainActivity实例),这个 act1对象就是一个无用的但一直占用内存的对象,即无法回收的垃圾对象。所以,对于lauchMode不是singleInstance的 Activity, 应该避免在activity里面实例化其非静态内部类的静态实例。
解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用Application的Context。
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。
上面代码意味着:sBackground(GC Root)会持有TextView对象,而TextView持有Activity对象。所以导致Activity对象无法被系统回收。
避免方法:
·不要对activity的context长期引用(一个activity的引用的生存周期应该和activity的生命周期相同)
·如果可以的话,尽量使用关于application的context来替代和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最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。
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. 资源未关闭造成的内存泄漏
对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。
1)比如在Activity中register了一个BraodcastReceiver,但在Activity结束后没有unregister该BraodcastReceiver。
2)
资源性对象比如Cursor,Stream、File文件等往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它
们的缓冲不仅存在于
java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。
3)对于资源性对象在不使用的时候,应该调用它的close()函数将其关闭掉,然后再设置为null。在我们的程序退出时一定要确保我们的资源性对象已经关闭。
4)Bitmap对象不在使用时调用recycle()释放内存。2.3以后的bitmap应该是不需要手动recycle了,内存已经在java层了。
6. 线程造成的内存泄漏
示例:AsyncTask和Runnable
分析:
AsyncTask和Runnable都使用了匿名内部类,那么它们将持有其所在Activity的隐式引用。如果任务在Activity销毁之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏。
解决方法:
将AsyncTask和Runnable类独立出来或者使用静态内部类,这样便可以避免内存泄漏。
7. 使用ListView时造成的内存泄漏
初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象 缓存起来。当向上滚动ListView时,原先位于最上面的Item的View对象会被回收,然后被用来构造新出现在下面的Item。这个构造过程就是由 getView()方法完成的,getView()的第二个形参convertView就是被缓存起来的Item的View对象(初始化时缓存中没有 View对象则convertView是null)。
构造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
以构造ListView的BaseAdapter为例,在BaseAdapter中提共了方法:
public View getView(intposition, View convertView, ViewGroup parent)
来向ListView提供每一个item所需要的view对象。
如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费时间,也造成内存垃圾,给垃圾回收增加压力,如果垃圾回收来不及的话,虚拟机将不得不给该应用进程分配更多的内存,造成不必要的内存开支。
10.3、不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。可以适当的使用 hashtable , vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次 new 之后又丢弃。
四. 内存泄漏的检测
方法一:
实时抓取hprof文件,观察当前时刻内存占用情况。
工具:Eclipse的DDMS插件。
步骤:
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 虚拟内存占用大小 单位:kb(killobytes)
RSS 实际内存占用大小 单位:kb(killobytes)
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文件, 按照方法一的方式分析即可。