Android内存管理机制和内存泄漏分析及优化

Android中的内存管理机制

分配机制

Android为每个进程分配内存的时候,采用了弹性的分配方式,也就是刚开始并不会一下分配很多内存给每个进程,而是给每一个进程分配一个“够用”的量。这个量是根据每一个设备实际的物理内存大小来决定的。随着应用的运行,可能会发现当前的内存可能不够使用了,这时候Android又会为每个进程分配一些额外的内存大小。但是这些额外的大小并不是随意的,也是有限度的,系统不可能为每一个App分配无限大小的内存。

Android系统的宗旨是最大限度的让更多的进程存活在内存中,因为这样的话,下一次用户再启动应用,不需要重新创建进程,只需要恢复已有的进程就可以了,减少了应用的启动时间,提高了用户体验。

回收机制

Android对内存的使用方式是“尽最大限度的使用”,这一点继承了Linux的优点。Android会在内存中保存尽可能多的数据,即使有些进程不再使用了,但是它的数据还被存储在内存中,所以Android现在不推荐显式的“退出”应用。因为这样,当用户下次再启动应用的时候,只需要恢复当前进程就可以了,不需要重新创建进程,这样就可以减少应用的启动时间。只有当Android系统发现内存不够使用,需要回收内存的时候,Android系统就会需要杀死其他进程,来回收足够的内存。但是Android也不是随便杀死一个进程,比如说一个正在与用户交互的进程,这种后果是可怕的。所以Android会有限清理那些已经不再使用的进程,以保证最小的副作用。

Android杀死进程有两个参考条件:
进程优先级:

Android为每一个进程分配了优先级的概念,优先级越低的进程,被杀死的概率就更大。Android中总共有5个进程优先级。具体含义这里不再给出。

  • 前台进程:正常不会被杀死
  • 可见进程:正常不会被杀死
  • 服务进程:正常不会被杀死
  • 后台进程:存放于一个LRU缓存列表中,先杀死处于列表尾部的进程
  • 空进程:正常情况下,为了平衡系统整体性能,Android不保存这些进程
回收收益:

当Android系统开始杀死LRU缓存中的进程时,系统会判断每个进程杀死后带来的回收收益。因为Android总是倾向于杀死一个能回收更多内存的进程,从而可以杀死更少的进程,来获取更多的内存。杀死的进程越少,对用户体验的影响就越小。

官方推荐的App内存使用方式
  • 当Service完成任务后,尽量停止它。因为有Service组件的进程,优先级最低也是服务进程,这会影响到系统的内存回收。IntentService可以很好地完成这个任务。
  • 在UI不可见的时候,释放掉一些只有UI使用的资源。系统会根据onTrimMemory()回调方法的TRIM_MEMORY_UI_HIDDEN等级的事件,来通知App UI已经隐藏了。这和onStop()方法还是有很大区别的,因为onStop()方法只是当一个Activity完全不可见的时候就会调用,比如说用户打开了我们程序中的另一个Activity。因此,我们可以在onStop()方法中去释放一些Activity相关的资源,比如说取消网络连接或者注销广播接收器等,但是UI相关的资源等onTrimMemory(TRIM_MEMORY_UI_HIDDEN)这个回调之后才去释放,这样可以保证如果用户只是从我们程序的一个Activity回到了另外一个Activity,界面相关的资源都不需要重新加载,从而提升响应速度。
@Override  
public void onTrimMemory(int level) {  
    super.onTrimMemory(level);  
    switch (level) {  
    case TRIM_MEMORY_UI_HIDDEN:  
        // 进行资源释放操作  
        break;  
    }  
}  
  • 在系统内存紧张的时候,尽可能多的释放掉一些非重要资源。系统会根据onTrimMemory()回调方法来通知内存紧张的状态,App应该根据不同的内存紧张等级,来合理的释放资源,以保证系统能够回收更多内存。当系统回收到足够多的内存时,就不用杀死进程了。
  • 检查自己最大可用的内存大小。这对一些缓存框架很有用,因为正常情况下,缓存框架的缓存池大小应当指定为最大内存的百分比,这样才能更好地适配更多的设备。通过getMemoryClass()和getLargeMemoryClass()来获取可用内存大小的信息。
  • 避免滥用Bitmap导致的内存浪费。
    根据当前设备的分辨率来压缩Bitmap是一个不错的选择,在使用完Bitmap后,记得要使用recycle()来释放掉Bitmap。使用软引用或者弱引用来引用一个Bitmap,使用LRU缓存来对Bitmap进行缓存。
  • 使用针对内存优化过的数据容器。针对移动设备内存有限的问题,Android提供了一套针对内存优化过的数据容器,来替代JDK原生提供的数据容器。但是缺点就是,时间复杂度被提高了。比如SparseArray、SparseBooleanArray、LongSparseArray、
  • 意识到内存的过度消耗。Enum类型占用的内存是常量的两倍多,所以避免使用enum,直接使用常量。
    每一个Java的类(包括匿名内部类)都需要500Byte的代码。每一个类的实例都有12-16 Byte的额外内存消耗。注意类似于HashMap这种,内部还需要生成Class的数据容器,这会消耗更多内存。
  • 抽象代码也会带来更多的内存消耗。如果你的“抽象”设计实际上并没有带来多大好处,那么就不要使用它。
  • 使用nano protobufs 来序列化数据。Google设计的一个语言和平台中立打的序列化协议,比XML更快、更小、更简单。
  • 避免使用依赖注入的框架。依赖注入的框架需要开启额外的服务,来扫描App中代码的Annotation,所以需要额外的系统资源。
  • 使用ZIP对齐的APK。对APK做Zip对齐,会压缩其内部的资源,运行时会占用更少的内存。
  • 合理使用多进程。

Android内存泄漏分析及优化

内存泄漏的根本原因
内存泄漏分析1.png

如上图所示,GC会选择一些它了解还存活的对象作为内存遍历的根节点(GC Roots),比方说thread stack中的变量,JNI中的全局变量,zygote中的对象(class loader加载)等,然后开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。如下图蓝色部分。

内存泄漏分析2.png

内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。下面分析一些可能导致内存泄漏的情景。

Android中常见的内存泄漏原因

1.使用static变量引起的内存泄漏

因为static变量的生命周期是在类加载时开始 类卸载时结束,也就是说static变量是在程序进程死亡时才释放,如果在static变量中引用了Activity那么这个Activity由于被引用,便会随static变量的生命周期一样,一直无法被释放,造成内存泄漏。

一般解决办法:
想要避免context相关的内存泄漏,需要注意以下几点:

  • 不要对activity的context长期引用(一个activity的引用的生存周期应该和activity的生命周期相同)
  • 如果可以的话,尽量使用关于application的context来替代和activity相关的context
  • 如果一个acitivity的非静态内部类的生命周期不受控制,那么避免使用它;正确的方法是使用一个静态的内部类,并且对它的外部类有一WeakReference,就像在ViewRootImpl中内部类W所做的那样,使用弱引用private final WeakReference<ViewRootImpl> mViewAncestor;。

下面的代码存在内存泄漏的问题,非静态内部类的静态实例导致内存泄漏。

/**
mDemo会获得并一直持有MemoryLeakActivity的引用。当MemoryLeakActivity销毁后重建,因为mDemo持有引用,无法被GC回收的,进程中会存在2个MemoryLeakActivity实例。所以,对于lauchMode不是singleInstance的Activity, 应该避免在activity里面实例化其非静态内部类的静态实例。
*/
public class MemoryLeakActivity extends AppCompatActivity{
    private TextView view;
    private static final String TAG = "MemoryLeakActivity";
    static Demo mDemo;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new TextView(MemoryLeakActivity.this);
        view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        view.setText("启动一个持有本对象的线程");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        mDemo = new Demo();
        mDemo.run();
    }
    class Demo{
        void run(){
            Log.i(TAG, "run: ");
        }
    }
}

解决方法:将Demo改成静态内部类

因为普通的内部类对象隐含地保存了一个引用,指向创建它的外围类对象。然而,当内部类是static的时,就不是这样了。嵌套类意味着: 1. 嵌套类的对象,并不需要其外围类的对象。 2. 不能从嵌套类的对象中访问非静态的外围类对象。

2.线程引起的内存泄漏

下面的代码存在内存泄漏的问题,启动线程的匿名内部类会持有MemoryLeakActivity.this的引用。如果线程还没有结束,Activity已经销毁那就会造成内存泄漏。

public class MemoryLeakActivity extends AppCompatActivity{
    private TextView view;
    private static final String TAG = "MemoryLeakActivity";
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new TextView(MemoryLeakActivity.this);
        view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        view.setText("启动一个持有本对象的线程");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(8000000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(runnable).start();
    }
}

解决办法:
1.合理安排线程执行的时间,控制线程在Activity结束前结束。
2.将内部类改为静态内部类,并使用弱引用WeakReference来保存Activity实例 因为弱引用 只要GC发现了 就会回收它 ,因此可尽快回收。

将匿名内部类改成静态类,避免了Activity context的内存泄漏问题

/**
 * 示例通过将线程类声明为私有的静态内部类避免了 Activity context 的内存泄漏问题,但
 * 在配置发生改变后,线程仍然会执行。原因在于,DVM 虚拟机持有所有运行线程的引用,无论
 * 这些线程是否被回收,都与 Activity 的生命周期无关。运行中的线程只会继续运行,直到
 * Android 系统将整个应用进程杀死
*/
public class MemoryLeakActivity extends AppCompatActivity{
    private TextView view;
    private static final String TAG = "MemoryLeakActivity";
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new TextView(MemoryLeakActivity.this);
        view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        view.setText("启动一个持有本对象的线程");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        new MyThread().start();
    }

    private static class MyThread extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(8000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } 
        }
    }
}

在Activity生命周期的onDestory中结束线程运行

/**
* 除了我们需要实现销毁逻辑以保证线程不会发生内存泄漏。在退出当前
* Activity 前使用 onDestroy() 方法结束你的运行中线程。
*/
public class MemoryLeakActivity extends AppCompatActivity{
    private TextView view;
    private static final String TAG = "MemoryLeakActivity";
    private static boolean mRunnale = false;
    private MyThread mThread; 
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new TextView(MemoryLeakActivity.this);
        view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        view.setText("启动一个持有本对象的线程");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        new MyThread().start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mThread.closeThread();
    }

    private static class MyThread extends Thread{
        @Override
        public void run() {
            mRunnale = true;
            while (true){
                //TODO
                Log.i(TAG, "run: do something");
            }
        }
        
        public void closeThread(){
            mRunnale = false;
        }
    }
}

3.Handler的使用造成的内存泄漏

由于在Handler的使用中,handler会发送message对象到 MessageQueue中 然后 Looper会轮询MessageQueue 然后取出Message执行,但是如果一个Message长时间没被取出执行,那么由于 Message中有 Handler的引用,而 Handler 一般来说也是内部类对象,Message引用 Handler ,Handler引用 Activity 这样 使得 Activity无法回收。或者说Handler在Activity退出时依然还有消息需要处理,那么这个Activity就不会被回收。

解决办法:
依旧使用 静态内部类+弱引用的方式 可解决
例如下面的代码

public class MemoryLeakActivity extends AppCompatActivity{
    private TextView view;
    private static final String TAG = "MemoryLeakActivity";
    private MyHandler mHandler;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        view = new TextView(MemoryLeakActivity.this);
        view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        view.setText("启动一个持有本对象的线程");
        view.setTextSize(40);
        view.setTextColor(Color.parseColor("#0000ff"));
        setContentView(view);
        mHandler = new MyHandler(this);
        mHandler.sendEmptyMessage(0);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //第三步,在Activity退出的时候移除回调
        mHandler.removeCallbacksAndMessages(null);
    }

    //第一步,将Handler改成静态内部类。
    static class MyHandler extends Handler{
        //第二步,将需要引用Activity的地方,改成弱引用。
        private WeakReference<MemoryLeakActivity> mActivityRef;
        public MyHandler(MemoryLeakActivity activity){
            mActivityRef = new WeakReference<MemoryLeakActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MemoryLeakActivity mla = mActivityRef == null ? null : mActivityRef.get();
            if(mla == null || mla.isFinishing()){
                return;
            }
            //TODO
            mla.view.setText("do something");

        }
    }
}

4.资源未被及时关闭造成的内存泄漏

比如一些Cursor 没有及时close 会保存有Activity的引用,导致内存泄漏

解决办法:
在onDestory方法中及时 close即可

5.BitMap占用过多内存

bitmap的解析需要占用内存,但是内存只提供8M的空间给BitMap,如果图片过多,并且没有及时 recycle bitmap 那么就会造成内存溢出。

解决办法:
及时recycle 压缩图片之后加载图片

其中还有一些关于 集合对象没移除,注册的对象没反注册,代码压力的问题也可能产生内存泄漏,但是使用上述的几种解决办法一般都是可以解决的。

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

推荐阅读更多精彩内容