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

前一阵我想看一下内存优化的好处与方法,我就在网上找了一下,发现好多这方面的博客,而且总结的也比较好。我就找了一篇为自己保存一下,顺便大家一起分享一下。

转载地址:https://blog.csdn.net/u014633728/article/details/77949760

Android中的内存管理机制

分配机制

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

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

回收机制

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

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

进程优先级:

Android为每一个进程分配了优先级的概念,优先级越低的进程,被杀死的概率就更大。Android中总共有5个进程优先级。具体含义这里不再给出。 
- 前台进程:正常不会被杀死 
- 可见进程:正常不会被杀死 
- 服务进程:正常不会被杀死 
- 后台进程:存放于一个LRU缓存列表中,先杀死处于列表尾部的进程 
- 空进程:正常情况下,为了平衡系统整体性能,Android不保存这些进程


回收收益:

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

官方推荐的App内存使用方式

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

Android内存泄漏分析及优化

内存泄漏的根本原因

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

内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到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 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 Thread(runnable).start();
        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 压缩图片之后加载图片

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

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值