android技术一些收藏

Android内存全面分析

2020-08-05  本文已影响0人  momxmo

文章转载至:https://www.jianshu.com/p/63f9846ae1f1

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

Java 内存

引用

引用类型(reference type)指向一个对象,不是原始值,指向对象的变量是引用变量,在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变量,方法区包含的都是在整个程序中永远唯一的元素。

图解:

image

一个程序运行时,内存的整个过程:

image

注意:

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

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

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

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

Java是如何管理内存

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

内存溢出和内存泄漏

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

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

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

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

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

注意
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泄漏两种情况:

​图片,每一款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(匿名类)

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

解决:

 private static class NimbleHandler extends Handler {...}
 private static class NimbleTimerTask extends TimerTask {...}
 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类型,是个枚举类型,对应如下:

image

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

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

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

常用解决方案:

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原生代码。分析较为复杂的页面,多层嵌套。案例的页面结构基本如下:有两种方式,这两种方式基本饱含类大部分新闻/电商(原生)的主页结构。

image

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

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

1.只清理Bitmap-使用HashMap

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

解决方案:

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

image

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

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

首先要知道一点映射关系:在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();
        }
    }
}

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

2.使用弱引用缓存呢?

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

3.强引用 + LRU 算法

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

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

说说Glide的方式
案例分析
页面可见才加载数据不可见回收,缓存的管理交给第三方。

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

方案:

其他

当然如果你不放心,你还可以把所有的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不行?

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

其他内存问题

内存分析工具

参考

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

上一篇下一篇

猜你喜欢

热点阅读