内存泄漏和内存优化

  1. 内存泄漏产生的原因
    Android系统会为应用运行分配内存,当分配的内存不足时就会触发GC,GC采用的垃圾标记算法是根搜索算法。当代码中没有用的对象到GC Roots是可达的(也就是对象被引用),那么就会产生内存泄漏。内存泄漏一般是开发人员编码造成的泄漏、第三方框架造成的和Android系统造成的,后两种情况是不可控的,所以我们需要做的就是减少编码造成的内存泄漏。
  2. 内存泄漏的场景
  • 静态变量导致的内存泄漏
    静态变量sContext引用了Activity,导致Activity无法正常销毁。静态变量sView内部也持有当前Activity,Activity也无法释放。这就是静态变量导致的内存泄漏,这种一般是很明显发现的。
package tool.com.angeliur.tooldemo;

import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {

    private static Context sContext;
    private static View sView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sContext = this;
        sView = new View(this);
    }
}
  • 单例模式导致的内存泄漏
    单例模式的SingleManager 可以接受外部的注册并将外部的监听器存储起来。MainActivity实现OnDataArrivedListener接口并向SingleManager注册监听。由于缺少解注册的操作,MainActivity的对象被SingleManager所持有。单例模式的特点是生命周期和Application保持一致,因此Activity无法及时释放导致内存泄漏。
public class SingleManager {

    private List<OnDataArrivedListener> mOnDataArrivedListeners = new ArrayList();

    public SingleManager() {

    }

    public static SingleManager getInstance(){
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder{
        public static final SingleManager INSTANCE = new SingleManager();
    }

    public synchronized void registerListener(OnDataArrivedListener listener){
        if (!mOnDataArrivedListeners.contains(listener)){
            mOnDataArrivedListeners.add(listener);
        }
    }

    public synchronized void unregisterListener(OnDataArrivedListener listener){
        mOnDataArrivedListeners.remove(listener);
    }

    public interface OnDataArrivedListener{
        public void onDataArrived(Object data);
    }
}
public class MainActivity extends AppCompatActivity implements SingleManager.OnDataArrivedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SingleManager.getInstance().registerListener(this);
    }


    @Override
    public void onDataArrived(Object data) {

    }
}
  • 属性动画导致的内存泄漏
    属性动画中可以设置动画无限循环,如果在Activity中播放无限动画并且没有在onDestory中停止动画,动画就会一直播放下去,尽管已经无法在界面上看到动画效果,但是Activity的View会被动画持有,而View又持有Activity,导致Activity无法释放。所以需要在onDestory()中调用animator.cancle()来停止动画避免内存泄漏。
public class MainActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        ObjectAnimator animator = ObjectAnimator.ofFloat(button, "rotation", 0, 360);
        animator.setDuration(1000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.start();
    }

}
  • 非静态内部类的静态实例
    非静态内部类会持有外部类实例的引用,如果非静态内部类的实例是静态的,就会间接地长期维持着外部类的引用,导致外部类无法被回收。点击Button时,创建非静态内部类InnerClass的静态实例,该实例的生命周期和应用程序一样长,并且一直持有MainActivity的引用,导致MainActivity无法回收
public class MainActivity extends AppCompatActivity{

    private static Object innerClass;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                createInnerClass();
                finish();
            }
        });
    }

    void createInnerClass() {
        class InnerClass{

        }

        innerClass = new InnerClass();
    }

}
  • 多线程相关的匿名内部类/非静态内部类
    和非静态内部类一样,匿名内部类也会持有外部类实例的引用。多线程相关的类AsyncTask类、Thread类和实现Runnable接口的类,他们的匿名内部类/非静态内部类如果做耗时操作就可能内存泄漏。
    通过startAsyncTask()实例化一个AsyncTask,AsyncTask的异步任务在后台执行耗时任务期间,MainActivity被销毁了,被AsyncTask持有的MainActivity实例不会被垃圾回收期回收,直到异步任务结束。
public class MainActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startAsyncTask();
                finish();
            }
        });
    }

    void startAsyncTask() {
        new AsyncTask<Void,Void,Void>(){

            @Override
            protected Void doInBackground(Void... voids) {
                while (true);
            }
        }.execute();
    }
}

自定义的AsyncTask如果是非静态内部类也会发生内存泄漏,解决办法就是自定义一个静态的AsyncTask.

public class MainActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startAsyncTask();
                finish();
            }
        });
    }

    void startAsyncTask() {
        new MyAsyncTask().execute();
    }

    private static class MyAsyncTask extends AsyncTask<Void,Void,Void>{
        @Override
        protected Void doInBackground(Void... voids) {
            while (true);
        }
    }
}
  • Handler内存泄漏
    Handler的Message被存储在MessageQueue中,有些Message并不能马上被处理,它们在MessageQueue中存在的时间会很长,导致Handler无法被回收。如果Handler是非静态的,也会导致引用它的Activity或Service不能被回收。
    如下,Handler是非静态的匿名内部类的实例,它会隐形引用外部类MainActivity,当我们点击button时,MainActivity会结束,但是Handler中的消息还没有被处理,因此MainActivity无法被回收。
public class MainActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);
        
        final Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
            }
        };
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                handler.sendEmptyMessageDelayed(Message.obtain(),60000);
                finish();
            }
        });
    }
}

解决方案有两种:一种是使用一个静态的Handler,Handler持有的对象要使用弱引用。如下,MyHandler是一个静态内部类,它持有MainActivity对象使用了弱引用,避免了内存泄漏。第二种是在Activity的onDestory()方法中移除MessageQueue中的消息,在onDestory方法中将Callbacks和Messages全部清除掉。采用这种方案,Handler中的消息可能无法全部处理完,所以建议使用第一种方案。

public class MainActivity extends AppCompatActivity{

    private MyHandler myHandler = new MyHandler(this, activity1);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = findViewById(R.id.button);

        
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                myHandler.sendMessageDelayed(Message.obtain(),60000);
                finish();
            }
        });
    }
    
    private static class MyHandler extends Handler{
        private final WeakReference<MainActivity> mActivity;
        
        public MyHandler(MainActivity activity) {
            mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }

    @Override
    protected void onDestroy() {
        if (myHandler != null){
            myHandler.removeCallbacksAndMessages(null);
        }       
        super.onDestroy();
    }
}
  • 未正确使用Context
    对于不是必须使用Activity的Context的情况,可以考虑使用Application Context来代替,Dialog的Context必须使用Activity的Context。
    在单例模式中,AppSetting作为静态对象,生命周期会长于Activity。屏幕旋转时默认情况下系统会销毁Activity。因为当前Activity调用了setup方法,并传入了Activity Context,使得Activity被单例持有,导致垃圾回收期无法回收,产生内存泄漏。可以通过使用Application Context来解决。
public class AppSetting{
        private Context mAppContext;
        private static AppSetting mAppSetting = new AppSetting();
        private static AppSetting getInstance(){
            return mAppSetting;
        }
        
        public final void setup(Context context){
            mAppContext = context.getApplicationContext();
        }
    }
  • 静态View
    使用静态View可以避免每次启动Activity都去读取并渲染View,但是静态View会持有Activity的引用,导致Activity无法被回收。解决办法就是在onDestory中将静态View置为null
public class MainActivity extends AppCompatActivity{

    private static Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                finish();
            }
        });
    }

    @Override
    protected void onDestroy() {
        button = null;
        super.onDestroy();
    }
}
  • WebView
    WebView都会存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。解决办法是为WebView单开一个线程,使用AIDL与应用程序的主进程进行通信。WebView进程可以根据业务需求,在合适的时机进行销毁。
  • 资源对象未关闭
    Cursor、File等,往往都使用了缓冲,会造成内存泄漏。在资源对象不使用时,一定要确保他们已经关闭并将他们的引用置为null,通常在finally语句中进行关闭,防止出现异常时,资源未被释放的问题。
  • Bitmap对象
    临时创建的某个相对比较大的Bitmap对象,经过变换得到新的Bitmap对象后,尽快回收原始的Bitmap,这样能更快释放原始bitmap占用的空间。避免静态变量持有比较大的Bitmap对象或者其他大的数据对象,如果已经持有,要尽快置空该静态变量。
  • 监听器未关闭
    很多系统服务(比如TelephonyManager、SensorManager)需要register和unregister监听器,确保在合适的时候及时unregister那些监听器。自己手动添加的listener,记得在合适的时候及时移除这个listener。
  • 集合中对象没清理
    通常把一些对象的引用加入到集合中,当不需要该对象时,如果没有把该对象的引用从集合中清理掉,集合就会越来越大。如果这个集合是static的话,情况会更严重。
  1. 内存优化的方法
    要做好内存优化,首先要理解内存优化相关的原理,然后就是善于运用内存分析的工具。
  • Memory Monitor
    AS中的Memory Monitor包含了Logcat、Memory Monitor、CPU Monitor、GPU Monitor、Network Monitor。Memory Monitor可以监视应用程序的性能和内存使用情况,便于找到被分配的对象,定位内存泄漏,并跟踪连接设备中正在使用的内存数量。Memory Monitor的作用有:实时显示可用的和分配的Java内存图表、实时显示垃圾收集事件、启动垃圾收集事件、快速测试应用程序缓慢是否与过度垃圾收集有关、快速测试应用程序崩溃是否和内存耗尽有关。
    应用程序中出现大内存分配时,内存图中分配的内存会急剧上升。我们要判断这些是否是合理分配的内存,是bitmap还是其他大数据,并对这种数据优化,减少内存开销。
    内存抖动一般指在很短的时间内发生了多次内存分配和释放,严重的内存抖动会导致应用卡顿。内存抖动的原因主要是短时间内频繁创建对象(可能在循环中创建对象),内存为了应对这种情况,会频繁进行GC。非并行GC进行时,其他线程都会被挂起,等待GC操作完成后恢复工作。如果是频繁的GC会产生大量的暂停时间,导致界面绘制时间减少,使得多次绘制一帧的时间超过16ms,产生的现象就是界面卡顿。内存抖动在内存图中就会产生锯齿状的抖动图示。
  • Allocation Tracker
    Allocation Tracker用来跟踪内存分配,允许你在执行某些操作的同时监视在何处分配对象,了解这些分配使你能够调整与这些操作相关的方法调用,优化应用程序性能和内存使用。Allocation Tracker的作用:显示代码分配对象类型、大小、分配线程、堆栈跟踪的时间和位置。通过重复的分配/释放模式帮助识别内存变化。当与HPROF Viewer结合使用时,可以帮助你跟踪内存泄漏。如果在堆上看到一个bitmap对象,可以使用Allocation Tracker来找到其分配的位置。
  • Heap Dump
    Heap Dump的主要功能就是产看不同的数据类型在内存中的使用情况,可以帮助你找到大对象,也可以通过数据的变化发现内存泄漏。
  • 内存分析工具MAT
    如果想要深入分析并确定内存泄漏就需要分析疑似内存泄漏时所生成的堆存储文件。堆存储文件可以使用DDMS或者Memory Monitor来生成,输出的文件格式为hprof,而MAT就是分析堆存储文件的。MAT是eclipse的插件,使用AS开发需要单独下载。
  • LeakCanary
    使用MAT来分析内存问题,会有一些难度,并且效率也不是很高,对于一个内存泄漏问题可能要进行多次排查和对比。为了能迅速发现内存泄漏,Square公司基于MAT开源了LeakCanary。
    LeakCanary的使用:
dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.2'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.2'
}

public class LeakApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)){
            return;
        }
        LeakCanary.install(this);
    }
}

上述配置只能检测Activity的内存泄漏,如果还需要检测其他类的内存泄漏,需要使用RefWatcher来进行监控,改写LeakApplication。
非静态内部类LeakThread持有外部类MainActivity的引用,在LeakThread中做耗时操作,导致MainActivity无法被释放,所以存在内存泄漏。运行程序后,桌面会生成一个Leaks的应用图标。不断横竖屏,会闪出一个提示框" Dumping memory app will freeze",然后内存泄漏信息就会通过Notification展示出来。根据提示来对自己的代码进行分析。

public class LeakApplication extends Application {
    private RefWatcher refWatcher;
    @Override
    public void onCreate() {
        super.onCreate();
        refWatcher = setupRefWatcher();
        
    }

    private RefWatcher setupRefWatcher() {
        if (LeakCanary.isInAnalyzerProcess(this)){
            return RefWatcher.DISABLED;
        }
        return LeakCanary.install(this);
    }

    public static RefWatcher getRefWatcher(Context context) {
        LeakApplication application = (LeakApplication) context.getApplicationContext();
        return application.refWatcher;
    }
}
public class MainActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LeakThread leakThread = new LeakThread();
        leakThread.start();
    }

    class LeakThread extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(6 * 60 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = LeakApplication.getRefWatcher(this);
        refWatcher.watch(this);
    }
}

参考:《Android进阶解密》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容

  • 早上12点Check out后,本想着去洛杉矶市区溜达一天住一晚,然后再去Irvine。可是看找自己的那两个大箱子...
    恋上芥末绿阅读 257评论 0 1
  • 学习进度思维导图 判断一家公司是否存在破产危机有两个指标 一、负债占资产比率——那根棒子!(公司对外的负债多不多)...
    冷风过境_007阅读 478评论 0 1
  • 这几天上午上班下午带妞去输液时间充实而幸福,昨天老公在家一天,晚上回家做饭时我俩就有闲功夫打嘴仗了,而且在...
    栋姐阅读 83评论 0 1
  • 今天下午,我班根据假期作业情况分了六小组,第一组和第二组是作业完成较好的学生,这部分学生是想学习的;第四组...
    xingminyang阅读 285评论 0 3
  • 以前,经常问自己,什么是幸福,后来随着年龄不断增长,经历多了,接触的人也多了,才明白幸不幸福取决于我们的心态。 我...
    快乐的叶子yi阅读 267评论 0 0