Android内存的全面分析-让你吃透

我希望通过这篇文章能够把Android内存相关的基础和大部分内存相关问题如:溢出、泄漏、图片等等产生的都讲解清楚,会从java内存逐步讲解到android内存并结合具体场景分析、总结常见内存问题原因,并给出解决办法。文章有点长,文字也较多,可能还有点啰嗦,若有不正确,请指出,我会进行优化改进。

Java 内存

引用

引用类型(reference type)指向一个对象,不是原始值,指向对象的变量是引用变量,在java里面除去基本数据类型的其它类型都是引用数据类型,用类的一个类型声明的变量被指定为引用类型,这是因为它正在引用一个非原始类型,引用实际上是存储对象的地址

值传递和引用传递:

  • ​ “在Java里面参数传递都是按值传递”这句话的意思是:按值传递是传递的值的拷贝,按引用传递其实传递的是引用的地址值,所以统称按值传递。
  • ​ 在Java里面只有基本类型和按照下面这种定义方式的String是按值传递,其它的都是按引用传递。就是直接使用双引号定义的字符串方式:String str = "Java快车";

“=”的含义

在JAVA里,“=”不能被看成是一个赋值语句,它不是在把一个对象赋给另外一个对象,它的执行过程实质上是将右边对象的地址传给了左边的引用,使得左边的引用指向了右边的对象在初始化时,“=”语句左边的是引用,右边new出来的是对象。

this指针

this 关键字是类内部当中对自己的一个引用,可以返回对象的自己这个类的引用,同时还可以在一个构造函数当中调用另一个构造函数,Java中关键字this指针只能用于方法内,当一个对象被创建后,JVM就会给这个对象分配一个引用自身的指针,就是this。this只能在类中的非静态方法(实例方法)中使用,静态方法(类方法)和静态代码块中不能出现this。this只和特定对象关联,不个类关联,所以同一个类的不同对象有不同的this。

内存模型

每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。JVM的内存主要可分为3个区:堆(heap)、栈(stack)和方法区(method)。(其他暂不考虑)

堆区(Heap)

​只存对象本身,不存基本类型(局部变量)和引用对象, JVM只有一个堆区,并被所有线程共享

栈区(Stack)

​栈中只保存基础数据类型的对象和对象引用。每个线程一个栈区,每个栈区中的数据都是私有的,其他栈不能访问。栈分三个部分:基本类型变量区,执行环境上下文,操作指令区。为即时调用的方法开辟空间栈(Stack)该区域具有先进后出的特性。当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

方法区(method)

又叫静态区,跟堆一样,被所有线程共享, 方法区包含所有的class和static变量,方法区包含的都是在整个程序中永远唯一的元素。

图解:

Java内存运行区域说明.png

一个程序运行时,内存的整个过程:
内存加载过程.jpg

注意:

1、类里的基本类型的成员变量存放在哪里?
​实例变量和对象驻留在堆上,局部变量驻留在栈上 。在类中声明的变量是成员变量,也叫全局变量,放在堆中的,同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量,当声明的是基本类型的变量其变量名及其只时放在堆类存中的。引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。但这和书上所说:堆区(Heap)-- 只存对象本身,不存基本类型和引用对象 有些区别。

2、方法是通过什么访问类中的变量的?

  • 成员变量:包括实例变量和类变量,用static修饰的是类变量,不用static修饰的是实例变量,所有类的成员变量可以通过this来引用。
  • 类变量:静态域,静态字段,或叫静态变量,它属于该类所有实例共有的属性。而且所有的实例都可以修改这个类变量的值(这个类变量没有被final修饰的情况),而且访问类变量的时候不用实例,直接用类名.的方式就可以。
  • 成员方法:包括实例方法和类方法,用static的方法就是类方法,不用static修饰的就是实例方法。实例方法必须在创建实例之后才可以调用。
  • 类方法:和类变量一样,可以不用实例,直接用类就可以调用类方法。

3、在类方法中可用this来调用本类的类方法:错误

4、final 修饰的变量存放在哪里?
堆内的!!并且在方法区内存中只有一份!!与所有线程共享访问! final 声明一个变量只是表明这个变量的值不可改变,修饰类的时候,只是表明这个类不能被继承

Java是如何管理内存

​ Java的内存管理就是对象的分配和释放问题。在Java中,程序员需要通过关键字new为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间,对象的释放是由GC决定和执行的。GC它也加重了JVM的工作,这也是Java程序运行速度较慢的原因之一。因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

内存溢出和内存泄漏

内存溢出
内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。

内存泄漏
Java内存泄漏是指对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。在堆上分配的内存没有被释放,从而失去对其控制,这样会造成程序能使用的内存越来越少,导致系统运行速度减慢,严重情况会使程序当掉。

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连,被引用着;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

那如何避免内存泄漏和溢出

要避免内存泄漏,就需要使对象符合GC回收的条件:对象不再被引用。那如何显示的使对象符合垃圾回收条件?

  • 空引用 :当对象没有对他可到达引用时,他就符合垃圾回收的条件。Object obj=null;

  • 重新为引用变量赋值:可以通过设置引用变量引用另一个对象来解除该引用变量与一个对象间的引用关系。

  • 方法内创建的对象:所创建的局部变量仅在该方法的作用期间内存在。一旦该方法返回,在这个方法内创建的对象就符合垃圾收集条件。但有一种明显的例外情况,就是方法返回对象。

  • 隔离引用:这种情况中,被回收的对象仍具有引用,这种情况称作隔离岛。若存在这两个实例,他们互相引用,并且这两个对象的所有其他引用都删除,其他任何线程无法访问这两个对象中的任意一个。也可以符合垃圾回收条件。

  • 尽量不要重写 finalize(),所有类从 Object 类继承这个方法。

  • 尽量少用静态变量 ,因为静态变量是全局的,GC 不会回收的。

  • 如果非要使用某个可能会造成泄漏的对象,考虑:软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)

注意
final修饰的变量会不会内存泄漏? ​final 声明一个变量只是表明这个变量的值不可改变,修饰类的时候,只是表明这个类不能被继承而已,使用不当还是会泄漏。

Android内存

Android 内存处理一直是android开发者必须要面临的问题,如果持有对象的强引用,垃圾回收器是无法在内存中回收这个对象。良好的内存优化和处理能让app流畅的运行。但一个app内存的占用不是越少越好,频繁的内存gc也会增加负担,造成卡顿。找到适合具体场景的内存处理方案,才是最适合的。

内存溢出泄漏问题

一般内存泄漏(traditional memory leak)的原因是:由忘记释放分配的内存导致的。(Cursor忘记关闭等)。逻辑内存泄漏(logical memory leak)的原因是:当应用不再需要这个对象,当仍未释放该对象的所有引用,在Android开发中,最容易引发的内存泄漏问题的是Context。比如Activity的Context,就包含大量的内存引用,例如View Hierarchies和其他资源。一旦泄漏了Context,也意味泄漏它指向的所有对象。Android机器内存有限,太多的内存泄漏容易导致OOM。Activity.onDestroy()被视为Activity生命的结束,程序上来看,它应该被销毁了,或者Android系统需要回收这些内存(注:当内存不够时,Android会回收看不见的Activity)。Acticity泄漏两种情况:

  • 全局进程(process-global)的static变量,这个无视应用的状态,持有Activity的强引用的怪物。
  • 活在Activity生命周期之外的线程,没有清空对Activity的强引用。

​图片,每一款app都离不开图片,然而图片才是内存占用的大户。Bitmap的不当使用,导致内存溢出。如在类似电商和新闻类的app中有大量的图片要进行处理,图片的处理就要用到Bitmap,Android的内存是有限的,如果不对图片进行良好的优化,就会导致内存溢出,程序卡顿,程序崩溃。

问题种类:

Static Activities/Static Views

在类中定义了静态Activity变量,把当前运行的Activity实例赋值于这个静态变量。在类中定义了静态view变量。
解决:使用软引用/在onDestroy时把View=null;

Sensor Manager(传感器管理)

通过Context.getSystemService(int name)可以获取系统服务、传感器等。这些服务工作在各自的进程中,帮助应用处理后台任务,处理硬件交互。如果需要使用这些服务,可以注册监听器,这会导致服务持有了Context的引用,如果在Activity销毁的时候没有注销这些监听器,会导致内存泄漏。
解决:在Activity结束时注销监听器,sensorManager.unregisterListener(this, sensor);

Inner Classes(内部类)

Activity中有个内部类,这样做可以提高可读性和封装性,但内部类的优势之一就是可以访问外部类,不幸的是,如果用static修饰内部类变量,就会导致内存泄漏,就是内部类持有外部类实例的强引用。
解决:不用static,要么不写成内部类。

Anonymous Classes(匿名类)

匿名类也维护了外部类的引用。所以内存泄漏很容易发生。常用示例:

  • 使用AsycTsk
    当你在Activity中定义了匿名的AsyncTsk。当异步任务在后台执行耗时任务期间,Activity不幸被销毁了(注:用户退出,系统回收),这个被AsyncTask持有的Activity实例就不会被垃圾回收器回收,直到异步任务结束。

  • Handler
    定义匿名的Runnable,用匿名类Handler执行。Runnable内部类会持有外部类的隐式引用,被传递到Handler的消息队列MessageQueue中,在Message消息没有被处理之前,Activity实例不会被销毁了,于是导致内存泄漏。

  • Threads/TimerTask
    通过Thread和TimerTask来展现内存泄漏,只要是匿名类的实例,不管是不是在工作线程,都会持有Activity的引用,导致内存泄漏。

解决:

  • 静态内部类不持有外部类的引用:
    private static class NimbleTask extends AsyncTask<Void, Void, Void> {...}
    private static class NimbleHandler extends Handler {...}
    private static class NimbleTimerTask extends TimerTask {...}

  • 如果你坚持使用匿名类,只要在生命周期结束时中断线程就可以。

  • 静态内部类非要用引用外部类,可以和软引用结合使用:
    private static class MyHandler extends Handler {
    WeakReference<MainActivity> mActivity;
    MyHandler(MainActivity mActivity){
    this.mActivity = new WeakReference<MainActivity>(mActivity);
    }
    @Override
    public void handleMessage(Message msg) {
    //TODO
    }
    }

注意
不论哪一种,都不要忘记在生命结束时调用响应的关闭方法或者移除、清理等,例如:在Activity onStop或者onDestroy的时候,取消掉该Handler对象的Message和Runnable,removeCallbacks(Runnable r)和removeMessages(int what)等。

Image(Bitmap)

什么是bitmap?Bit即比特,是目前计算机系统里边数据的最小单位,8个bit即为一个Byte。一个bit的值,或者是0,或者是1;也就是说一个bit能存储的最多信息是2。Bitmap可以理解为通过一个bit数组来存储特定数据的一种数据结构;由于bit是数据的最小单位,所以这种数据结构往往是非常节省存储空间。

Bitmap是Android系统中的图像处理的最重要类之一。用它可以获取图像文件信息,进行图像剪切、旋转、缩放等操作,并可以指定格式保存图像文件。

Bitmap占用的内存,图片(BitMap)占用的内存=图片长度 * 图片宽度*单位像素占用的字节数。前两个分别代表长度与宽度(像素单位),单位像素占用字节数其大小由BitmapFactory.Options的inPreferredConfig变量决定。
inPreferredConfig为Bitmap.Config类型,是个枚举类型,对应如下:

B27D06AA-9111-42AC-8F3E-745D954FD974.png

我们一般常用RGB_565。具体场景具体选择,他们这些格式有什么区别-具体参考

注意:一张200k的图片到内存中并非200k!一般远大于200k,具体可以自己写demo测试。

为什么图片会引起内存问题?
使用图片不当为什么会造成oom或者卡顿?因为安卓系统为每个程序分配的内存大小是有限的,当图片(Bitmap)加载过多、过大,超出了给定的内存就会出现内存溢出,或者内存泄漏引起(比如:1M大小Bitmap的泄漏了,我连续创建1000个)。

常用解决方案:

  • 缓存图像到内存,或者采用软引用缓存到内存,而不是在每次使用的时候都从新加载到内存。和软引用结合是因为内存不足时GC会自动回收软引用的对象。

  • 调整图像大小(缩略图),可以根据控件大小调整相应的图片大小。注意:我们从网上下载图片到控件中,一般缓存到内存的是调整过的图片大小而不是原图,比如:glide的DiskCacheStrategy.RESULT(缓存转换后的资源)。

  • 采用低内存占用量的编码方式,比如Bitmap.Config.ARGB_565比Bitmap.Config.ARGB_8888更省内存。

  • 及时回收图像,如果引用了大量Bitmap对象,而应用又不需要同时显示所有图片,可以将暂时用不到的Bitmap对象及时回收掉recycle()。问题:为什么有GC了还要手动释放?

  • 自定义堆内存分配大小,优化Dalvik虚拟机的堆内存分配。(这个有点难,看看就行了,一般用不到),使用:

private final staticfloatTARGET_HEAP_UTILIZATION = 0.75f; 
//在程序onCreate时就可以调用
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);

private final static int CWJ_HEAP_SIZE = 6*1024* 1024 ;  
 //设置最小heap内存为6MB大小  
VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);

上述的方案有些并不一定很好!

案例分析:

说明,这里分析的案例都是基于Android原生代码。分析较为复杂的页面,多层嵌套。案例的页面结构基本如下:有两种方式,这两种方式基本饱含类大部分新闻/电商(原生)的主页结构。


页面结构.jpg

首先对于上图的结构,有几点基础要讲解:

  • Viewpage:适配器有两种FragmentPagerAdapter和FragmentStatePagerAdapter,他们都会预先加载,但他们的缓存方式又不同,最低缓存两页。FragmentPagerAdapter会把每次显示的fragment都缓存,FragmentStatePagerAdapter会把看不见的fragment回收,所以用FragmentStatePagerAdapter,fragment会执行相应的生命周期。具体参考

  • Recycleview:recycleview会复用View,比如你有一百个item,每个item里都有imageview,但是recycleview并不会创建一百个imageview,有可能当前ImageView加载图片1,当滑动是有可能会加载图片2。具体多少个是根据页面来的,请自行测试。

下面将给出图片内存处理的思路:

1.只清理Bitmap-使用HashMap

上述页面较为复杂。而且每一个看的见的页面(fragment)都包含大量的图片,因为有很多商品,而且页面基本都是列表,列表也包含列表,并且页面非常多,它具体是采用Fragment+Viewpage+FragmentPagerAdapter+Fragment+Recycleview的结构。有时候我们只想清理图片,因为图片占用内存最高,如何处理?

解决方案:

试想下,一个view控件如果加载到内存能用多大空间?比如我创建1000个imageview,其实非常少:

BE88E2B1-1AA3-4FD9-924E-F57724BEA66D.png

其实内存几乎被图片(bitmap)占领了,只要页面不可见时把页面上控件里的Bitmap清理掉就ok了。这样只有控件占用内存。那么该怎么做呢?

方案:我们需要缓存所有的imageview(第三方控件不会缓存ImageView)和bitmap,然后根据判断imageview不可见时,去掉引用!把imageview上bitmap的引用去掉(ImageView.setImageBitmap(null)),这样只有缓存对象持有Bitmap的引用,在循环调用recycle()进行清理。
参考上面内存优化的常用方法:调整片大小、降低图片编码、做缓存。但这里的难点是:

  • 我们根据什么策略缓存?
  • 什么时候释放内存?因为只要看不见就立马释放那缓存就没有意义了,每次都会重新加载,这很愚蠢!
  • 如何判断控件ImageView不可见?
  • 如何根据ImageView不可见时释放对应的bitmap,他们的映射关系怎么建立?

首先要知道一点映射关系:在recycleview中一个ImageView能对应多个Bitmap,因为View会复用,但当前显示的ImageView只能对应一个Bitmap。一个Bitmap也可以对应多个ImageView。一个Bitmap只能对应一个Url,但一个url能对应多个bitmap。要处理ImageView和Bitmap的映射关系,就需要缓存他们两个。

建立映射关系:那我们可以根据url和控件大小进行进行bitmap映射的:

public String getKeyForBitmap(String url) {
    final int targetBitmapWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
    final int targetBitmapHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
    return curUrl.length() + "_" + url.hashCode() + "_" + targetBitmapWidth + "_" + targetBitmapHeight;
}

判断释放条件:释放内存是根据达到运行时内存的80%:

public void checkMemory() {
    Runtime runtime = Runtime.getRuntime();
    //判断运行时内存是否达到80%,超过就释放
    if (runtime.totalMemory() * 1f / runtime.maxMemory() > 0.8) {
        trimMemory();
    }
}

判断view是否可见:ImageView的不可见-本身不可见,父控件不可见,context没有了(activity销毁了)。isShown()方法就能判断View是不是可见:

public boolean isAbleToRecycle() {
    return getContext() == null || !isShown() || getWindowVisibility() == View.GONE;
}

具体缓存代码:

public class MemoryCacheController {

    private static MemoryCacheController instance;
    private String keyForBitmap;

    public static MemoryCacheController getInstance() {
        if (null == instance)
            instance = new MemoryCacheController();
        return instance;
    }

    /**
     * 把建立过的set都存储,免得每次都去新建
     */
    private LinkedList<Set> setLinkedList = new LinkedList<>();
    /**
     * 根据具体key缓存bitmap,key是根据大小和url计算得来
     */
    private HashMap<String, Bitmap> bitmapMap = new HashMap<>(100);
    /**
     * 根据bitmap映射imageview,set集合用来存放所有映射过的控件
     */
    private HashMap<Bitmap, Set<ImgView>> bitmap2viewSetMap = new HashMap<>(100);
    /**
     * 根据ImageView映射bitmap
     */
    private HashMap<ImgView, Bitmap> imgViewBitmapHashMap = new HashMap<>(100);

    /**
     * 把ImageView和Bitmap加入缓存。
     *
     * @param imgView
     * @param keyForBitmap 缓存的key是根据Url和控件大小合成的特殊字符串
     * @param bitmap
     */
    public synchronized void put(ImgView imgView, String keyForBitmap, Bitmap bitmap) {

        Bitmap lastBitmap = imgViewBitmapHashMap.get(imgView);

        // 映射关系是否已存在
        if (lastBitmap == bitmap) {
            return;

        } else if (lastBitmap != null) {// bitmap更换,映射关系调整
            // 得到bitmap对应的所有imageview
            Set lastBitmapViewSet = bitmap2viewSetMap.get(lastBitmap);
            if (null != lastBitmapViewSet) {
                //移除bitmap映射的当前的imageview
                lastBitmapViewSet.remove(imgView);
            }

            //建立新的映射关系
            bitmapMap.put(keyForBitmap, bitmap);
            imgViewBitmapHashMap.put(imgView, bitmap);
            Set viewSet = bitmap2viewSetMap.get(bitmap);
            if (null == viewSet) {
                viewSet = obtainSet();
                bitmap2viewSetMap.put(bitmap, viewSet);
            }
            viewSet.add(imgView);
        }
    }

    public synchronized Bitmap get(String keyForBitmap) {
        return bitmapMap.get(keyForBitmap);
    }

    /**
     * 释放内存
     */
    public synchronized void trimMemory() {

        LinkedList<Bitmap> recyclerBitmapList = new LinkedList<>();
        for (Bitmap bitmap : bitmap2viewSetMap.keySet()) {
            Set<ImgView> imgViewSet = bitmap2viewSetMap.get(bitmap);
            boolean needRecycle = true;
            for (ImgView imgView : imgViewSet) {
                if (imgView.getCurBitmap() == bitmap) {
                    // 判断View是否可见
                    if (!imgView.isAbleToRecycle()) {
                        needRecycle = false;
                        break;
                    }
                }
            }
            if (needRecycle) {
                recyclerBitmapList.add(bitmap);//把bitmap添加,说明他能释放了
            }
        }

        LinkedList<String> keyList = new LinkedList<>();
        for (Map.Entry<String, Bitmap> entry : bitmapMap.entrySet()) {
            if (recyclerBitmapList.contains(entry.getValue())) {
                keyList.add(entry.getKey());
            }
        }

        for (String url : keyList) {
            bitmapMap.remove(url);
        }

        // 先释放ImageView的引用,在释放bitmap
        for (Bitmap bitmap : recyclerBitmapList) {
            Set set = bitmap2viewSetMap.get(bitmap);
            for (ImgView imgView : bitmap2viewSetMap.get(bitmap)) {
                imgView.setImageBitmap(null);
                imgViewBitmapHashMap.remove(imgView);
            }
            set.clear();
            setLinkedList.add(set);
            bitmap2viewSetMap.remove(bitmap);
            if (null != bitmap && !bitmap.isRecycled()) {
                bitmap.recycle();
            }
        }

    }

    private Set obtainSet() {
        Set set = setLinkedList.poll();
        if (null == set) set = new HashSet(1);
        return set;
    }

    public void checkMemory() {
        Runtime runtime = Runtime.getRuntime();
        //判断运行时内存是否达到80%,超过就释放,在Activity的onLowMemory里调用checkMemory
        if (runtime.totalMemory() * 1f / runtime.maxMemory() > 0.8) {
            trimMemory();
        }
    }
}

上面这种只是一种方案和思路,也只是适合当前的场景下,但问题也很多:

  • ImageView和Bitmap的映射是比较麻烦的。
  • 判断内存什么时候释放,80%其实也不一定很好(安卓机型重多,很难判断)。
  • 应用占有的内存量会不断攀升,直到内存不足时,出现断崖式的内存回收
  • GC 的时间可能会比较长,造成界面会有明显的卡顿。
  • GC 回收的内存,没有区分,可能回收了最近在使用的 Bitmap,造成二次加载
  • 页面一加载就会缓存,很多页面没用了也清理不掉,造成垃圾缓存。

2.使用弱引用缓存呢?

​ 弱引用也会出现断崖式回收,回收时间长,没有区分,最严重的,新的 Android 系统开始每次 GC 都会回收弱引用,这就使内存缓存没有用处。

3.强引用 + LRU 算法

给定一个固定图片缓存大小,将所有的使用的 Bitmap 用强引用的方式管理起来,并利用 LRU 算法,将旧的 Bitmap 释放,新的 bitmap 增加。LruCache的核心思想很好理解,就是要维护一个缓存对象列表--LinkedHashMap,其中对象列表的排列方式是按照访问顺序实现的,即一直没访问的对象,将放在队尾,即将被淘汰。而最近访问的对象将放在队头,最后被淘汰。当栈满的时候就从栈底回收掉最旧的那个引用,这样,图片缓存不会无限制的增长,内存量也能处在一个较理想的范围,申请和释放。

但这个思路也会有问题:
虽然图片缓存的内存不会无限制增长,但会周期性的释放和申请。特别是对于一个长列表页面,图片会不断的申请,不断的释放。因为最终的内存释放还是GC去处理,快速滑动时,会造成大量的图片申请内存,大量的图片释放,系统的 GC 会很频繁,就产生了所谓的 内存抖动 。内存的抖动同样也会造成界面卡顿,在快速滑动时,会非常明显。但要比弱引用的方案好多了。

说说Glide的方式
  • Glide 构建了一个 BitmapPool , Bitmap 申请和回收都是透过 BitmapPool 来处理的。新加载图片时,会先从 BitmapPool 里面找有没有相应大小的 Bitmap ,有则直接使用,没有才会申请新的 Bitmap ;回收时,则会提交给 BitmapPool , 供下次使用。 这种方式极大的减少了 Bitmap 的申请和回收操作,使得 GC 频度降低了很多
  • Glide使用了默认使用了LruCache技术来处理内存缓存。
  • Glide 的内存缓存有个 active 的设计 从内存缓存中取数据时,不像一般的实现用 get,而是用 remove ,再将这个缓存数据放到一个 value 为软引用的 activeResources map 中,并计数引用数,在图片加载完成后进行判断,如果引用计数为空则回收掉。
  • 内存缓存更小图片 Glide 以 url、viewwidth、viewheight、屏幕的分辨率等做为联合 key,将处理后的图片缓存在内存缓存中,而不是原始图片以节省大小。
案例分析
页面可见才加载数据不可见回收,缓存的管理交给第三方。

我们针对上述 (复杂的页面结构) 处理:始终保持缓存两页(因为页面太多),看不见就去掉引用,等待回收。当然我没有他们的源码,但是可以分析怎么做出效果。要处理的问题:

  • 页面可见才加载数据(网络请求)。
  • 页面不可见,清理引用,等待Gc回收。
  • Bitmap的缓存交给Glide,Glide会自动判断清理,但是我们要让不用的Bitmap有且只有Glide持有它的引用,ImageView不能持有。这样Glide在释放Bitmap的时才能成功,不然Bitmap发现ImageView持有引用是无法释放的。
方案:
  • 对于Viewpage+pageadapter,它始终会预先加载下一页,所以会走Fragment的onCreate系列生命周期。一般我们初识化相关的工作都会在onCrate里处理,所以没等滑到那一页就加载了数据。所以这时我们要使用懒加载——懒加载就是在页面可见才加载数据。核心是Fragment的setUserVisibleHint()方法。
  • 在也面不可见时要清理引用,就要让Fragment走onDestroy,如果我们使用Viewpage+FragmentPageAdapter,由于FragmentPageAdapter的特性会缓存所有加载过的页面,不会销毁Fragment,不会走onDestroy系列生命周期!所以这里我们使用Viewpage+FragmentStatePageAdapter,特别适合多页面的情况,FragmentStatePageAdapter会在页面不可见时回收Fragment,然后调用onDestroy生命周期。
  • 当Glide发现内存不够用,需要清理一部分缓存时,这时由于我们在clear() 里清理了相关View的引用, 而且之前RecycleView会复用View,比如ImageView上一个加载Bitmap1,在滑动时复用有可能加载Bitmap2,这时Bitmap就只有Glide引用了。所以只有Glide持有无用Bitmap的引用,这时就可以放心处理,你也不用担心OOM了。

其他

当然如果你不放心,你还可以把所有的ImageView都缓存(HashMap),然后在onDestroy里调用清理。一可以根据View是否可见来判断是否要清理引用,例如:

public class ImageViewCash {

    private static ImageViewCash instance;

    public static ImageViewCash getInstance() {
        if (null == instance)
            instance = new ImageViewCash();
        return instance;
    }

    /**
     * 根据Context做为缓存的key
     */
    private HashMap<Context, Set<ImageView>> ImageViewCash = new HashMap<>(100);

    public HashMap<Context, Set<ImageView>> getImageViewCash() {
        return ImageViewCash;
    }

    public void setImageViewCash(HashMap<Context, Set<ImageView>> imageViewCash) {
        ImageViewCash = imageViewCash;
    }

    /**
     * 清理引用
     *
     * @param context
     */
    public synchronized void trimReference(Context context) {
        Set<ImageView> sets = ImageViewCash.get(context);
        for (ImageView imgView : sets) {
            // 判断ImageView是否可见
            if (imgView.isShown() || imgView.getContext() == null) {
                imgView.setImageBitmap(null);
            }
        }
    }
}

为什么HashMap不行?

  • HashMap是无序的,也就是说,迭代HashMap所得到的元素顺序并不是它们最初放置到HashMap的顺序。HashMap的这一缺点往往会造成诸多不便,因为在有些场景中,我们确需要用到一个可以保持插入顺序的Map。
  • LinkedHashMap。虽然LinkedHashMap增加了时间和空间上的开销,但是它通过维护一个额外的双向链表保证了迭代顺序。特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。因此,根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap 和 保持访问顺序的LinkedHashMap,其中LinkedHashMap的默认实现是按插入顺序排序的。
    正是由于LruCache采用了LinkedHashMap,才能是内存相对稳定。

Finalizer

FinalReference由JVM来实例化,VM会对那些实现了Object中finalize()方法的类实例化一个对应的FinalReference。注意:实现的finalize方法体必须非空。

Finalizer是FinalReference的子类,该类被final修饰,不可再被继承,JVM实际操作的是Finalizer。当一个类满足实例化FinalReference的条件时,JVM会调用Finalizer.register()进行注册。(PS:后续讲的Finalizer其实也是在说FinalReference。)

JVM在类加载的时候会遍历当前类的所有方法,包括父类的方法,只要有一个参数为空且返回void的非空finalize方法就认为这个类在创建对象的时候需要进行注册。

GC回收问题
  • 对象因为Finalizer的引用而变成了一个临时的强引用,即使没有其他的强引用,还是无法立即被回收;

  • 对象至少经历两次GC才能被回收,因为只有在FinalizerThread执行完了f对象的finalize方法的情况下才有可能被下次GC回收,而有可能期间已经经历过多次GC了,但是一直还没执行对象的finalize方法;

  • CPU资源比较稀缺的情况下FinalizerThread线程有可能因为优先级比较低而延迟执行对象的finalize方法;

  • 因为对象的finalize方法迟迟没有执行,有可能会导致大部分f对象进入到old分代,此时容易引发old分代的GC,甚至Full GC,GC暂停时间明显变长,甚至导致OOM;

  • 对象的finalize方法被调用后,这个对象其实还并没有被回收,虽然可能在不久的将来会被回收。

​在我们写代码的时候,也要加强Finalizer对象的理解和警觉,了解哪些系统类是有Finalizer对象,并了解Finalizer对内存,性能和稳定性所带来的影响。特别是我们自己写类的时候,要尽量避免重写finalize方法,即使重写了也要注意该方法的实现,不要有耗时操作,也尽量不要抛出异常等。[具体参考]

其他内存问题

  • webview内存泄漏
  • 个别手机输入法内存泄漏(华为手机)
  • …(Google)

内存分析工具

参考

http://blog.qiji.tech/archives/10029
http://childe.net.cn/2017/04/01/JDK%E6%BA%90%E7%A0%81-FinalReference/
http://wiki.jikexueyuan.com/project/java-special-topic/platorm-memory.html
https://www.jianshu.com/p/63aead89f3b9

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

推荐阅读更多精彩内容