内存泄露和内存优化

内存泄露和内存优化

对于Android来说,每一个APP的内存是有限的。你过你的内存出现问题:泄露,长期占用过高,就会导致app易于被杀掉。频繁的gc导致app卡顿等现象。

常见情况

  • Activity的Context的使用

    • 界面的Context静态化
    • 单例式将界面的Context作为初始化入参数,并且在单例模式保存
    • 特殊的,在Android 6.0中,不能使用Activity的Context通过接口getSystemService()来获取各种Manager,如下所示:
    AActivityManager activityManager =(ActivityManager)MainActivity.this.getSystemService(Context.ACTIVITY_SERVICE);  
    

如上所示,在Android 6.0 中就会造成内存泄露

  • 非静态内部类持有外部类的引用

在Java中,非静态内部类(包括匿名内部类)都会持有外部类(一般是指Activity等页面)的引用,当两者的生命周期出现不一致的时候,很容易导致内存泄露。
  如下所示,非常常见的几种情况:
Hanlder

private Handler mHandler = new Handler() {
 @Override    
 public void handleMessage(Message msg)      
 {   
  super.handleMessage(msg);  
 }  
}; 

这里的Handler会引用Activity的引用,当handler调用postDelay的时候,若Activity已经finish掉了,因为这个 handler 会在一段时间内继续被 main Looper 持有,导致引用仍然存在,在这段时间内,如果内存吃紧至超出,是很危险的。

Thread

public class ThreadActivity extends Activity {  
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        new MyThread().start();  
    }  
  
    private class MyThread extends Thread {  
        @Override  
        public void run() {  
            super.run();  
            dosomthing();  
        }  
    }  
    private void dosomthing(){  
      
    }  
}  

假设MyThread的run函数是一个很费时的操作,当我们开启该线程后,将设备的横屏变为了竖屏,一般情况下当屏幕转换时会重新创建Activity,按照我们的想法,老的Activity应该会被销毁才对,然而事实上并非如此。由于我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。

Runnable

  public class MainActivity extends Activity {
    ...
    Runnable ref1 = new MyRunable();
    Runnable ref2 = new Runnable() {
        @Override
        public void run() {

        }
    };
       ...
    }

ref1和ref2的区别是,ref2使用了匿名内部类,也就是说当前的Activity会被ref2所应用,如果将这个引用传入到了一个异步线程,该线程的生命周期与Activity的生命周期不一致的时候,就会导致内存泄露。

  • Static变量造成内存泄露

1. 界面类的静态化: 静态Activity
  2. 界面中View的静态化: 静态View
  界面中View的静态化一定会导致页面内存泄露。界面中的View都是持有界面引用的,静态变量的生命周期与整个app的生命周期一致。
  3. 非静态内部类的静态化
  具体的 如下所示:

public class MainActivity extends AppCompatActivity {  

    private static Drawable sDrawable;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TextView lableView = new TextView(this);
    if(sDrawable == null) {
        sDrawable = getDrawable(R.drawable.icon);       
    }
    labelView.setBackgroundDrawable(sDrawable);
        setContentView(lableView);
    }
}  

View的setBackgroundDrawable()的源码如下所示:

public void setBackgroundDrawable(Drawable background) {
        ...

        if (background != null) {
            ...

            background.setCallback(this);
            ...
        } else {
            ...
        }

        ...
}

其中有一个background.setCallback(this);,所以这就导致这个静态变量指向的对象又持有了TextView这个对象的引用,TextView持有的确实整个Activity的引用。这样就导致了内存泄露。

我们再来看一个例子:

public class MainActivity extends AppCompatActivity {
    private static InnerClass sInnerClass;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak);
        sHello = new Hello();
    }
    public class InnerClass {}
}  

静态的非静态内部类对象sInnerClass持有了外部Acitivity的引用,当屏幕发生变化时,不会被释放。

  • <font size = 5>资源没有关闭</font>
      1. Cursor游标没有关闭
      数据库中才操作经常碰到cursor。
      2. InputStream、OutputStream等没有关闭
      文件读写、Socket读写等经常碰到
      3. 注册的广播等没有unRegister
      4. 一些CallBack的Listener没有被清除,举例:
void registerListener() {
    SensorManager sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
    Sensor snedor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
    sensorManager.registerListneer(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

getSystemService负责执行某些后台任务,或为硬件提供接口,如果context对象想要在服务内部的事件发生时被通知,需要注册监听器。然而这让服务持有了activity的引用,如果activity销毁时没有取消注册,那么你的activity就泄露了。

  • View添加到没有删除机制的容器中

  • 属性动画导致的内存泄露
      如果你设置你的动画为无限循环,而且没有在onDestroy中停止该动画,那么动画会一直播放下去,Activity的View会被动画吃持有,而View持有了Activiy。从而导致内存泄露。

  • <font size = 5>过期引用</font>

当一个数组扩容后又被缩减,比如size从0->200->100(一个栈先增长,后收缩),那么元素的index>=100的那些元素(被Pop掉的)都算是过期的元素,那些引用就是过期的引用(永远不会再被接触的应用)-来自Effective Java

public Object pop(){
      if(size==0) throw new EmptyStackException();
      Object result = elements[--size];
     elements[size] = null; //消除过期引用
     return result;
}

由于过期引用的存在,GC并不会去回收他们,我们需要手动的释放他们。

内存溢出和内存的查看方法

  • 使用第三方开源库

LeakCanary
在这里就不做具体的介绍了。网上的使用demo:
leakCanary Demo

  • adb shell命令

通过以下命令可以查看你APP的内存使用情况已经Activity和View等的个数情况,具体的

adb shell dumpsys meminfo packagename

其中,packagename就是你程序的报名,具体的示例,如下图所示:

dumpsysmeminfo.png

如上图所示:
  上面部分显示的是你的app所占用的内存总数(主要是看TOTAL,内存所实际占用的值)
  下面的部分可以看到你的一些对象的个数:如Views、Activities等。
当你进入一个acitivity的时候,activity的个数会增加,退出后会减少,如果只增加、不减少,就说明出现了内存泄露的问题。 (经过实际的测试,这个对有些手机,好不管用,就算我写个demo:只有一个Activity,什么也没有做,进来、退出、进来、activities个数会变大,不会立即变小,需要等一段时间才会变小)

  • DDMS

DDMS是Android开发环境中的Dalvik虚拟机(andoid4.4之前,4.4及其之后引入了ART虚拟机)调试监控服务。

1. update heap

ddms_update_heap.png

对一个activity进入退出反复多次看data object是否稳定在一个范围
  2. MAT(Memory Analyzer Tool)

ddms_hprof.png

dump hprof file : 点击后等待一会,会生成一个hprof文件。插件版本的MAT可以直接打开该文件,否则需要进行一步转换操作。 提供了这个工具 hprof-conv (位于 sdk/tools下), 转换命令如下所示:

./hprof-conv xxx-a.hprof xxx-b.hprof 

最后通过DDMS-File-open,打开的hprof文件即可进行分析内存泄露相关。

内存优化建议

  • 了解你机器的内存情况

通过以下代码可以查看每个进程可用的最大内存,即heapgrowthlimit值

ActivityManager actManager = getApplicationContext.getSystemService(Context.ACTIVITY_SERVICE);s int memClass = actManager.getMemeoryClass(); //以M为单位

通过以下代码可以获取 应用程序的最大可用内存

long maxMemory = Runtime.getRuntime().maxMemeory(); //以字节为单位

两者的区别:
  单位不一致 前者以M为单位,后者以字节为单位。
  具体的以lenovo的一款手机(S850T, Android版本为4.4.2)为例: 经过测试两者得到的值一致均是128M。

使用场景
  当你进行图片加载的时候,都会使用到LRUCache,初始化的时候设置缓存的大小。一般来说都设置为当前最大内存的1/8,如果你就是一个图片应用你直接1/4也可以。

long cacheSize = Runtime.getRuntime().maxMemeory();
mLruCache = new LruCache<String, Bitmap>(cacheSize)
        {
            @Override
            protected int sizeOf(String key, Bitmap value)
            {
                return value.getRowBytes() * value.getHeight();
            };
        };
  • 当界面不可见、内存紧张的时候释放内存

android4.0(包含4.0)之后引入了onTrimMemory(int level)(4.0之前为onLowMemory) ,系统会根据不同的内存状态来毁掉,参数 level 代表了你app的不同状态,Application、Activity、Fragment、Service、ContentProvider均可以响应。具体如下:

TRIM_MEMORY_UI_HIDDEN: 应用程序被隐藏了,如按了Home或者Back导致UI不可见,这个时候,我们应该释放一些内存。

以下三个是我们的应用程序真正运行时的回调:
  TRIM_MEMORY_RUNNING_MODERATE: 程序正常运行,并不会被杀掉,但是手机的内存有点低了,系统可能开始根据LRU规则来杀死进程了。
  TRIM_MEMORY_RUNNING_LOW: 程序正常运行,并不会被杀掉,但是手机内存非常的低了,应该释放一些资源了,否则影响性能。
  TRIM_MEMORY_RUNNING_CRITICAL: 程序正在运行,但是系统已经根据LRU杀死了大部分缓存的进程了,此时我们需要释放内存,否则系统可能会干掉你。

以下三个是当应用程序是缓存时候的回调:
  TRIM_MEMORY_BACKGROUND: 内存不足,并且该进程是后台进程。
  TRIM_MEMORY_MODERATE: 内存不足,并且该进程在后台进程列表的中部。
  TRIM_MEMORY_COMPLETE:内存不足,并且该进程在后台进程列表的最后一个,马上就要被清理了,这个时候应该把一切尽可能释放的都释放掉。

通常在我们开始进行架构设计的时候,就要考虑到哪些东西是要常驻的,哪些东西是缓存后要被清理, 一般情况下,以下资源都要被清理:
缓存:包括文件缓存、图片的缓存、比如第三方图片缓存库。
一些动态生成的View: 比如一般应用的图片轮播View,在你的应用隐藏后,根本不需要轮播。

案例分析:
  1. LRUCache缓存的清理方式:trimToSize()接口可以重新设置缓存的大小。evictAll()接口可以清楚所有的LRUCache缓存内容。
  2. 暴力清理界面中的View

  • 图片资源的压缩
      1. res中资源到压缩: 使用有损压缩工具,比如:tinyPng,压缩后的图片肉眼根本看不出来,压缩率可以达到50%以上。
      2. BitmapFactory的压缩。
      通过BitmapFactory的Options设置,降低采样率,压缩图片到适合的大小,同时注意使用若引用和缓存机制。
      Bitmap.Config设置图片的格式为RGB565,这个设置肉眼是看不出色彩的丢失,而且比RGB8888占存小的多。
      使用BitmapFactory.Options.inBitmap字段。如果这个选项被设置,那么使用该Options 的decode方法将会尝试复用一个已经存在的bitmap来加载新的bitmap。这意味着bitmap的内存将被复用,避免分配和释放内存来提升性能。然后,使用inBitmap有一些限制。特别是在Android4.4(API level19)之前,只有尺寸相同的bitmap才能使用该特性。具体的见使用示例
      3. 将图片资源放在合适的drawable目录下。
  • 使用Android优化过的类和集合
      1. SparseArrry<T>来替代HashMap<int, T>
      2. LongSparseArray<T>, key为long,替代HashMap<long, T>
      3. SimpleArrayMap<K, T>和ArrayMap<K,T>替代HashMap<K, T>, ArrayMap是通过时间来换取效率,在数千之内建议使用ArrayMap。
  • 避免创建不必要的对象

在短时间内创建了大量的对象,然后有释放,这样就引起了内存抖动。频繁的引起GC操作,会导致内存的卡顿。
  1. 字符串的拼接:StringBuffer(非线程安全)和StringBuilder(线程安全)的使用
  2. 自定义View中不要在onDraw中定义画笔等对象
  3. 在循环函数内避免创建重复的对象,将多个函数都经常用到的不可变对象拿出来统一进行初始化,在一开始写的时候就要特别的注意,否则后边修改起来很是麻烦(主要是再找到他很麻烦)
  4. 在循环的内部不要使用try catch操作,将其拿到外面来。
  5. 不要在循环中进行文件的操作:比如判断文件是否存在,这相对是一个很耗时的操作

案例说明
  SimpleDateFromat是用来时间转换的,一般的,开发者都会定义个专门用于时间转化的static的函数:

public static String paserTimeToYM(long time)
    {
        SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault());
        return format.format(new Date(time));
    } 

假如你在for循环中调用此函数。就不停的重复创建SimpleDateFromat对象。你应该将对象创建拿出来,放在类中,或者是重新定义一个时间转换函数,入惨为已经创建好的SimpleDateFormat对象。
  还需要注意的是:假如你的循环量很大,不建议在for循环中进行时间转换,而是在你用到的时候才进行转换,比如显示出来。

  • 不要扩大变量的作用域
classs A
{
    private B mB;
    public A(B b) {
        this.mB = b;
        //就在构造函数中进行了对mB进行了一些操作
    }
    //后续再也没有用到过mB
}

class B
{
    public B() {
    }
    public static void main(String[] args)
    {
    }
}

如上所示的简单代码:类A的构造函数中,传入了类B的对象,并且类A中定义了成员变量mB,但是mB就在构造函数中用了一下,后续再也没有用,在类A中mB的生命周期和A一致。本来mB的作用域就在构造函数,结果扩大为整个类。

  • 不要让生命周期比Activity长的对象持有Activity的引用

这样的错误很多,比如:将Activity的Context传给单例模式,毫不知情的将Activity的Context传给非静态内部类或者是匿名内部类。

  • 尽量的使用Application的Context

Application的生命周期是整个app,他会一直在。
  1. 在界面类中直接使用getApplicationContext。
  2. 在其他地方使用MyApplication(extends Application)的getInstance操作。如下所示:

public class MyApplication extends Application
{
    private static Context sContext;
 
    @Override
    public void onCreate()
    {
        Log.d(tag, "onCreate");
        sContext = this;
    }

    public static Context getAppContext()
    {
        return sContext;
    }
}

总之一句话:能使用Application的Context,就不要使用Activity的。

  • 移除回调
      1. handler的removeCallbacksAndMessages(null)
      2. setXXXCallback(null)、 setXXXListener(null),需要注意的是,要进行callback调用的地方就需要进行判断了
  • 常量的使用

关于enum和static。Android强烈建议不要使用enum,他会使得内存消耗变大为原来的2倍以上。

  • 使用代码混淆剔除不需要的代码

  • 请使用静态内部类+WeakReference的方式

非静态内部类和匿名内部类会持有页面的应用,请使用静态内部类,并将页面的引用通过WeakReference的方式传递过去。

  • 合理的使用多进程

android对单个进程都有一个内存允许的最大内存限制。加入你在你的app中又启动一个进程,这样你的内存限制就变为了原来的2倍。
  启动多进程的方法很简单,只需要在AndroidManifest.xml声明的四大组件的标签中增加"android:process"属性即可。
  进程分为两种:私有进程和全局进程。私有进程在名称签名添加冒号即可。
  但是多进程有一些需要注意的地方:
  1. Application的onCreate会被调用多次。一般程序会将程序的一些初始化的操作放在这里,这点需要注意。
  2. 多进程之间的通讯必须使用AIDL接口,需要注意的一点是:AIDL之间传递大量数据是有一个限制的。 传递内容过大会出现:TransactionToolLargeException。官方文档说明:最大的限制为1M。
  3. 多进程导致 静态成员、单例模式和SharedPreference 都变的不可靠。
  4. 多进程之间传递数据的效率:有些手机在传递大量数据的时候,效率很差。
  5. 多进程传递对象需要实现序列化操作。
  6. AIDL支持的数据类型:基本数据类型;String和CharSequence;List仅仅支持ArrayList,里面的每一个对象都必须支持序列化,Map只支持HashMap,里面的key和value都必须支持序列化(必须被AIDL支持)。
  7. AIDL服务端可以使用CopyOnWriteArrayList和ConcurrentHashMap来进行自动线程同步,客户端拿到的依然是ArrayList和HashMap。
  8.AIDL服务端和客户端之间做监听器,服务端需要使用RemoteCallbackList,否则客户端的监听器无法收到通知(因为服务端实质还是一份新的序列化后的监听器实例,并不是客户端那份)。
  9.客户端调用远程服务方法时,因为远程方法运行在服务端的binder线程池中,同时客户端线程会被挂起,所以如果该方法过于耗时,而客户端又是UI线程,会导致ANR,所以当确认该远程方法是耗时操作时,应避免客户端在UI线程中调用该方法。同理,当服务器调用客户端的listener方法时,该方法也运行在客户端的binder线程池中,所以如果该方法也是耗时操作,请确认运行在服务端的非UI线程中。另外,因为客户端的回调listener运行在binder线程池中,所以更新UI需要用到handler。

我们将在进程常驻中进行简单的示例分析,实现多进程的相互唤醒操作。

  • 请不要使用注解框架

程序注解框架极大的方便了程序开发者,不需要开发者大量的写findViewById(), setOnclickListener()等方法,但是程序注解框架是将类中的所有相关方法都缓存在内容中不会释放,这些内存就会越来越大,从而得不到释放。而且一般程序注解方法都是用到了Java的反射机制。这个是不建议使用的(虽然有时候反射不得不使用)。

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

推荐阅读更多精彩内容