Android性能优化-内存泄漏的几个案例

JVM内存管理

Java采用GC进行内存管理。深入的JVM内存管理知识,推荐《深入理解Java虚拟机》。

关于内存泄漏我们要知道,JVM内存分配的几种策略。

1. 静态的

静态的存储区,内存在程序编译的时候就已经分配好了,这块内存在程序整个运行期间都一直存在,它主要存放静态数据、全局的static数据和一些常量。

2.栈式的

在执行方法时,方法一些内部变量的存储都可以放在栈上面创建,方法执行结束的时候这些存储单元就会自动被注释掉。栈 内存包括分配的运算速度很快,因为内在在处理器里面。当然容量有限,并且栈式一块连续的内存区域,大小是由操作系统决定的,他先进后 出,进出完成不会产生碎片,运行效率高且稳定

3.堆式的

也叫动态内存 。我们通常使用new 来申请分配一个内存。这里也是我们讨论内存泄漏优化的关键存储区。GC会根据内存的使用情况,对堆内存里的垃圾内存进行回收。

堆内存是一块不连续的内存区域,如果频繁地new/remove会造成大量的内存碎片,GC频繁的回收,导致内存抖动,这也会消耗我们应用的性能

我们知道可以调用 System.gc();进行内存回收,但是GC不一定会执行。

面对GC的机制,我们是否无能为力?其实我们可以通过声明一些引用标记来让GC更好对内存进行回收。

类型 回收时机 生命周期
StrongReference (强引用) 任何时候GC是不能回收他的,哪怕内存不足时,系统会直接抛出异常OutOfMemoryError,也不会去回收 进程终止
SoftReference (软引用) 当内存足够时不会回收这种引用类型的对象,只有当内存不够用时才会回收 内存不足,进行GC的时候
WeakReference (弱引用) GC一运行就会把给回收了 GC后终止
PhantomReference (虚引用) 如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收 任何时候都有可能

开发时,为了防止内存溢出,处理一些比较占用内存并且生命周期长的对象时,可以尽量使用软引用和弱引用。

Tip:成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体),因为他们属于类,类对象最终还是要被new出来的。
局部变量的基本数据类型和引用存在栈中,应用的对象实体存储在堆中。因为它们属于方法当中的变量,生命周期会随着方法一起结束

内存泄漏的定义

当一个对象已经不需要使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用,从而导致了对象不能被GC回收。

这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。

内存泄漏与内存溢出的区别

  • 内存泄漏(Memory Leak)

进程中某些对象已经没有使用的价值了,但是他们却还可以直接或间接地被引用到GC Root导致无法回收。当内存泄漏过多的时候,再加上应用本身占用的内存,日积月累最终就会导致内存溢出OOM

  • 内存溢出(OOM)

当应用的heap资源超过了Dalvik虚拟机分配的内存就会内存溢出

内存泄漏带来的影响

  • 应用卡顿

泄漏的内存影响了GC的内存分配,过多的内存泄漏会影响应用的执行效率

  • 应用异常(OOM)

过多的内存泄漏,最终会导致 Dalvik可分配的内存越来越少,更加容易出现OOM

Android开发常见的内存泄漏

(1)单例造成的内存泄漏

错误示例

当调用getInstance时,如果传入的context是Activity的context。

只要这个单例没有被释放,那么这个Activity也不会被释放一直到进程退出才会释放。

public class TestUtil {

    private static TestUtil instance;

    private Context context;

    private TestUtil(Context context){
        this.context=context;
    }

    public static TestUtil getInstance(Context mContext) {
        if (instance == null) {
            instance = new TestUtil(mContext);
        }
        return instance;
    }
}

解决方案

能使用Application的Context就不要使用Activity的Content,Application的生命周期伴随着整个进程的周期

(2)Handler造成的内存泄漏

错误示例

mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。

private TextView mTextView;
    private Handler mHandler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            mTextView.setText("");
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView= (TextView) findViewById(R.id.tv_home);
        initData();
    }

    private void initData() {
        Message msg=Message.obtain();
        mHandler.sendMessage(msg);
    }

解决方案

创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息

private TextView mTextView;
    private Handler mHandler=new MyHandler(this);

    private static class MyHandler extends Handler{
        private WeakReference<Context> reference;

        public MyHandler(Context context) {
            reference=new WeakReference<>(context);
        }

        @Override
        public void handleMessage(Message msg) {
            TestActivity testActivity= (TestActivity) reference.get();
            if (testActivity!=null){
                testActivity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView= (TextView) findViewById(R.id.tv_home);
        initData();
    }

    private void initData() {
        Message msg=Message.obtain();
        mHandler.sendMessage(msg);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
(3)线程造成的内存泄漏

错误示例

异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏

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

            @Override
            protected Void doInBackground(Void... params) {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }.execute();


        new Thread(new Runnable(){

            @Override
            public void run() {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

解决方案

使用 静态内部类,避免了Activity的内存资源泄漏,当然在Activity销毁时候也应该取消相应的任务AsyncTask.cancel(),避免任务在后台执行浪费资源

static class MyAsyncTask extends AsyncTask<Void,Void,Void>{
        private WeakReference<Context> reference;

        public MyAsyncTask(Context context) {
            reference=new WeakReference<>(context);
        }

        @Override
        protected Void doInBackground(Void... params) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            TestActivity testActivity= (TestActivity) reference.get();
            if (testActivity!=null){
                testActivity.mTextView.setText("");
            }
        }
    }

    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

        /**
         * 执行
         */
        new MyAsyncTask(this).execute();
        new Thread(new MyRunnable()).start();
(4)资源未关闭造成的内存泄漏

错误示例

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

解决方案

在Activity销毁时及时关闭或者注销

(5)使用了静态的Context(View持有Activity的引用)
private static Drawable mDrawable;

    private ImageView iv;

    private void setWidget() {
        iv=new ImageView(this);
        mDrawable= ContextCompat.getDrawable(this,R.drawable.ad);
        iv.setImageDrawable(mDrawable);
    }

分析一下,有一个静态的Drawable对象,当我给ImageView设置这个Drawable时,ImageView像上面那个例子一样,保存了这个mDrawable的引用,然而ImageView传入了this,也就是ImageView同样持有一个MainActivity的mContext。因为被static修饰的mDrawable是常驻内存的,MainActivity是它的间接引用,所以当MainActivity被销毁时,也不能被GC掉,所以也造成了内存泄漏。

解决方案

应该及时将静态的应用 置为null,而且一般不建议将View及Activity设置为静态,在View中不要持有静态资源,静态资源不要持有Activity的Context,用Application的Context代替

(6)注册了系统的服务,但onDestory未注销

错误示例

private SensorManager sensorManager;

    private void setListener() {
        sensorManager= (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        Sensor sensor=sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
        sensorManager.registerListener(sensorEventListener,sensor,SensorManager.SENSOR_DELAY_NORMAL);


    }

    //sensorEventListener监听传感器发出的信号
    private SensorEventListener sensorEventListener=new SensorEventListener() {
        //传感器的数值发生变化时调用此方法
        @Override
        public void onSensorChanged(SensorEvent sensorEvent) {
            //values数组中第一个下标的值就是当前的光照强度
            float value=sensorEvent.values[0];
            mTextView.setText("Current light level is "+value+" lx");
        }

        //传感器的精度发生变化时调用此方法
        @Override
        public void onAccuracyChanged(Sensor sensor, int i) {

        }
    };

解决方案

//不需要用的时候记得移除监听
@Override
    protected void onDestroy() {
        super.onDestroy();
        if(sensorManager!=null){
            sensorManager.unregisterListener(sensorEventListener);
        }
    }
(7)不需要用的监听未移除会发生内存泄露

错误示例

mTextView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //监听布局完成,获取宽高
            }
        });

解决方案

mTextView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //监听布局完成,获取宽高

                //计算完成,要移除监听
                mTextView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
(8)内部类

非静态内部类持有外部类的一个引用。因此,如果我们在一个外部类中定义一个静态变量,这个静态变量是引用内部类对象。将会导致内存泄漏!因为这相当于间接导致静态引用外部类。

static InnerClass innerClass;


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        innerClass=new InnerClass();
    }

    class InnerClass{

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

推荐阅读更多精彩内容