Android 内存泄漏预防与治理

内存泄漏是指再也不被使用的对象的内存不能被GC回收,同时频繁的GC会形成卡顿。Android系统为每一个应用程序分配的内存是有限的,若是应用中内存泄漏较多,就很容易形成OOMjava

Java 内存分配策略

Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。数据库

• 静态存储区(方法区):主要存放静态数据、全局 static数据和常量。这块内存在程序编译时就已经分配好,而且在程序整个运行期间都存在。缓存

• 栈区 :当方法被执行时,方法体内的局部变量都在栈上建立,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。由于栈内存分配运算内置于处理器的指令集中,效率很高,可是分配的内存容量有限。app

• 堆区 : 又称动态内存分配,一般就是指在程序运行时直接 new 出来的内存。这部份内存在不使用时将会由 Java 垃圾回收器来负责回收。框架

内存泄漏预防

1.避免使用static静态变量

static修饰成员变量时,那么该变量就属于该类,而不是该类的实例。若是你这样作,那意味此成员变量的生命周期,会被拉长到与整个app进程生命周期一致。因此用static修饰的变量,它的生命周期是很长的,若是用它来引用一些资源耗费过多的对象,就容易出现内存泄露的状况dom

使用static静态变量,应注意:
第一,应该尽可能避免static成员变量引用资源耗费过多对象。
第二,在static变量引用对象的时候,在被引用对象再也不使用的时候,应及时释放引用,作置null操做 。ide

2.单例或者长生命周期类的持有会引发内存泄漏

单例模式只容许应用程序存在一个实例对象,而且这个实例对象的生命周期和应用程序的生命周期同样长,若是单例对象中拥有另外一个对象的引用的话,这个被引用的对象就不能被及时回收。(使用弱引用)工具

若是传入的context是Activity将会形成内存泄漏,若是是Application就不至于内存泄漏,由于Application的生命周期长。布局

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;
    }
}
复制代码

长生命周期形成的内存泄漏,如Application,由于它的生命周期和整个应用的生命周期同样长:post

public class MyApplication extends Application {


private Activity currenActivity;

public void setCurrenActivity(Activity currenActivity){
    this.currenActivity = currenActivity;
}


}
复制代码
public class TestActivity4 extends Activity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MyApplication application = (MyApplication) getApplication();
        application.setCurrenActivity(this);
    }
}
复制代码

3 .Handler引发的内存泄漏

public class TestActivity2 extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.i("TestActivity2","启动了");
            }
        },1000000);
    
    }
}
复制代码

若是启动Activity打开后,而后当即关闭了,这种状况下就会发生内存泄漏。咱们知道,Handler、Message、MessageQueue是相互关联在一块儿的,Handler经过发送消息Message与主线程进行交互,若是Handler发送的消息Message还没有被处理,该Message及发送它的Handler对象将被MessageQueue一直持有,这样就可能会致使Message没法被回收。本例中Runable为被内存泄漏的消息,又由于匿名内部类会持有外部类的引用,全部形成Activity的泄漏。不过本例中由于只会延迟一秒执行消息,因此这种内存泄漏的危害不是很大。对于Handler的使用,能够以以下方式:

public class TestActivity2 extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        new NewHandler(this).sendEmptyMessageDelayed(0,1000000);
    
    }

    private static class NewHandler extends Handler{
    
        private WeakReference<Activity> weakActivity;
    
        NewHandler(Activity activity){
            weakActivity = new WeakReference<>(activity);
        }
    
        @Override
        public void handleMessage(Message msg) {
            Activity activity = weakActivity.get();
            if(activity!=null){
                Log.i("TestActivity2",activity.getClass().getSimpleName()+"启动");
            }
        }
    }
}

复制代码

以静态类的方式定义Handler,这样就不会直接持有Activity的引用,而Activity由弱引用的方式持有,当一个对象仅仅只有弱引用,那它和没有引用是同样的,当GC启动时,它将会当即被回收。

因此:Handler类须要声明为static,不然会发生泄漏。 缘由是Message进入消息队列时,会持有对目标Handler的引用,若是Handler是内部类,内部类还会持有对外部类的引用, 为了不对外部类的泄漏,Handler应该声明为静态嵌套类,持有对外部类的弱引用。

4.资源未关闭形成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,不然这些资源将不会被回收,形成内存泄漏。

5.非静态内部类的静态实例容易形成内存泄漏,如静态的view或者单例模式

blog.csdn.net/linyukun642…

非静态内部类中建立了一个静态实例,致使该实例的生命周期和应用ClassLoader级别,又由于该静态实例又会隐式持有其外部类的引用,因此致使其外部类没法正常释放,出现了泄漏问题。

6.对象的注册与反注册没有成对出现形成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等

7.建立与关闭没有成对出现形成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等

8.尽量的复用资源;譬如系统自己有不少字符串、颜色、图片、动画、样式以及简单布局等资源可供咱们直接使用,咱们本身也要尽可能复用style等资源达到节约内存

9.对于有缓存等存在的应用尽可能实现onLowMemory()和onTrimMemory()方法

10.不要加载过大的Bitmap对象;譬如对于相似图片加载咱们要经过BitmapFactory.Options设置图片的一些采样比率和复用等

11.对批量加载等操做进行缓存设计,譬如列表图片显示,Adapter的convertView缓存等

内存泄漏治理

1.工具:Android Memory Profiler

当您首次打开 Memory Profiler 时,您将看到一条表示应用内存使用量的详细时间线,并可访问用于强制执行垃圾回收、捕捉堆转储和记录内存分配的各类工具。

粘贴图片.png | center | 587x270

图 1. Memory Profiler
如图1 所示,Memory Profiler 的默认视图包括如下各项:
1.用于强制执行垃圾回收 Event 的按钮。
2.用于捕获堆转储的按钮。
3.用于记录内存分配状况的按钮。 此按钮仅在链接至运行 Android 7.1 或更低版本的设备时才会显示。
4.用于放大/缩小时间线的按钮。
5.用于跳转至实时内存数据的按钮。
6.Event 时间线,其显示 Activity 状态、用户输入 Event 和屏幕旋转 Event。
7.内存使用量时间线,其包含如下内容:
◦ 一个显示每一个内存类别使用多少内存的堆叠图表,如左侧的 y 轴以及顶部的彩色键所示。
◦ 虚线表示分配的对象数,如右侧的 y 轴所示。
◦ 用于表示每一个垃圾回收 Event 的图标。
不过,若是您使用的是运行 Android 7.1 或更低版本的设备,则默认状况下,并非全部分析数据都可见。 若是您看到一条消息,其显示“Advanced profiling is unavailable for the selected process”,则须要启用高级分析以查看下列内容:
• Event 时间线
• 分配的对象数
• 垃圾回收 Event
在 Android 8.0 及更高版本上,始终为可调试应用启用高级分析。

2.如何计算内存

您在 Memory Profiler(图 2)顶部看到的数字取决于您的应用根据 Android 系统机制所提交的全部私有内存页面数。 此计数不包含与系统或其余应用共享的页面。

粘贴图片.png | center | 588x24

图 2. Memory Profiler 顶部的内存计数图例

内存计数中的类别以下所示:
• Java:从 Java 或 Kotlin 代码分配的对象内存。
• Native:从 C 或 C++ 代码分配的对象内存。 即便您的应用中不使用 C++,您也可能会看到此处使用的一些原生内存,由于 Android 框架使用原生内存表明您处理各类任务,如处理图像资源和其余图形时,即便您编写的代码采用 Java 或 Kotlin 语言。
• Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。 (请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。)
• Stack: 您的应用中的原生堆栈和 Java 堆栈使用的内存。 这一般与您的应用运行多少线程有关。
• Code:您的应用用于处理代码和资源(如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体)的内存。
• Other:您的应用使用的系统不肯定如何分类的内存。
• Allocated:您的应用分配的 Java/Kotlin 对象数。 它没有计入 C 或 C++ 中分配的对象。
当链接至运行 Android 7.1 及更低版本的设备时,此分配仅在 Memory Profiler 链接至您运行的应用时才开始计数。 所以,您开始分析以前分配的任何对象都不会被计入。 不过,Android 8.0 附带一个设备内置分析工具,该工具可记录全部分配,所以,在 Android 8.0 及更高版本上,此数字始终表示您的应用中待处理的 Java 对象总数。 与之前的 Android Monitor 工具中的内存计数相比,新的 Memory Profiler 以不一样的方式记录您的内存,所以,您的内存使用量如今看上去可能会更高些。 Memory Profiler 监控的类别更多,这会增长总的内存使用量,但若是您仅关心 Java 堆内存,则“Java”项的数字应与之前工具中的数值类似。 然而,Java 数字可能与您在 Android Monitor 中看到的数字并不是彻底相同,这是由于应用的 Java 堆是从 Zygote 启动的,而新数字则计入了为它分配的全部物理内存页面。 所以,它能够准确反映您的应用实际使用了多少物理内存。 注:目前,Memory Profiler 还会显示应用中的一些误报的原生内存使用量,而这些内存其实是分析工具使用的。 对于大约 100000 个对象,最多会使报告的内存使用量增长 10MB。 在这些工具的将来版本中,这些数字将从您的数据中过滤掉。

3.捕获堆转储

堆转储显示在您捕获堆转储时您的应用中哪些对象正在使用内存。 特别是在长时间的用户会话后,堆转储会显示您认为不该再位于内存中却仍在内存中的对象,从而帮助识别内存泄漏。 在捕获堆转储后,您能够查看如下信息:
• 您的应用已分配哪些类型的对象,以及每一个类型分配多少。
• 每一个对象正在使用多少内存。
• 在代码中的何处仍在引用每一个对象。
• 对象所分配到的调用堆栈。 (目前,若是您在记录分配时捕获堆转储,则只有在 Android 7.1 及更低版本中,堆转储才能使用调用堆栈。)

粘贴图片.png | center | 584x407

图 3. 查看堆转储

要捕获堆转储,在 Memory Profiler 工具栏中点击 Dump Java heap

粘贴图片.png | center | 47x39
在转储堆期间,Java 内存量可能会暂时增长。 这很正常,由于堆转储与您的应用发生在同一进程中,并须要一些内存来收集数据。 堆转储显示在内存时间线下,显示堆中的全部类类型,如图 4 所示。 注:若是您须要更精确地了解转储的建立时间,能够经过调用 dumpHprofData() 在应用代码的关键点建立堆转储。 要检查您的堆,请按如下步骤操做:
1 浏览列表以查找堆计数异常大且可能存在泄漏的对象。 为帮助查找已知类,点击 Class Name 列标题以按字母顺序排序。 而后点击一个类名称。 此时在右侧将出现 Instance View 窗格,显示该类的每一个实例。
2 在 Instance View 窗格中,点击一个实例。此时下方将出现 References,显示该对象的每一个引用。 或者,点击实例名称旁的箭头以查看其全部字段,而后点击一个字段名称查看其全部引用。 若是您要查看某个字段的实例详情,右键点击该字段并选择 Go to Instance。
3 在 References 标签中,若是您发现某个引用可能在泄漏内存,则右键点击它并选择 Go to Instance。 这将从堆转储中选择对应的实例,显示您本身的实例数据。 默认状况下,堆转储不会向您显示每一个已分配对象的堆叠追踪。 要获取堆叠追踪,在点击 Dump Java heap 以前,您必须先开始记录内存分配。 而后,您能够在 Instance View 中选择一个实例,并查看 Call Stack 标签以及 References 标签。不过,在您开始记录分配以前,可能已分配一些对象,所以,调用堆栈不能用于这些对象。 包含调用堆栈的实例在图标
粘贴图片.png | center
上用一个“堆栈”标志表示。 在您的堆转储中,请注意由下列任意状况引发的内存泄漏:
• 长时间引用 Activity、Context、View、Drawable 和其余对象,可能会保持对 Activity 或 Context 容器的引用。
• 能够保持 Activity 实例的非静态内部类,如 Runnable。
• 对象保持时间超出所需时间的缓存。

粘贴图片.png | center | 580x303

图 4. 捕获堆转储须要的持续时间标示在时间线中

在类列表中,您能够查看如下信息: • Heap Count:堆中的实例数。 • Shallow Size:此堆中全部实例的总大小(以字节为单位)。 • Retained Size:为此类的全部实例而保留的内存总大小(以字节为单位)。 在类列表顶部,您可使用左侧下拉列表在如下堆转储之间进行切换: • Default heap:系统未指定堆时。 • App heap:您的应用在其中分配内存的主堆。 • Image heap:系统启动映像,包含启动期间预加载的类。 此处的分配保证毫不会移动或消失。 • Zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的。 默认状况下,此堆中的对象列表按类名称排列。 您可使用其余下拉列表在如下排列方式之间进行切换: • Arrange by class:基于类名称对全部分配进行分组。 • Arrange by package:基于软件包名称对全部分配进行分组。 • Arrange by callstack:将全部分配分组到其对应的调用堆栈。 此选项仅在记录分配期间捕获堆转储时才有效。 即便如此,堆中的对象也极可能是在您开始记录以前分配的,所以这些分配会首先显示,且只按类名称列出。 默认状况下,此列表按 Retained Size 列排序。 您能够点击任意列标题以更改列表的排序方式。 在 Instance View 中,每一个实例都包含如下信息: • Depth:从任意 GC 根到所选实例的最短 hop 数。 • Shallow Size:此实例的大小。 • Retained Size:此实例支配的内存大小(根据 dominator 树)。

相关文章
相关标签/搜索