Andorid开发——内存篇

前言:2018年三月,不得不再次跳槽.这次跳槽由于种种原因比以往都要急迫点。幸运的是正好赶上金三月,面试邀请还是很多的。基本每天都有几家面试。但是面试十家左右,感觉到目前的处境,以及程序员就业的竞争环境。——艰难
学历,技术,年龄,等各种尴尬问题凸现。

在移动设备开发中,内存管理是所有程序员面临的一道难关。内存泄漏,内存溢出等问题又是Android面试中必问的问题。当然 关于此类问题,各个大佬都做出的相应回答范文,但是正因为太在意这些回答范文,而对andorid内存管理机制原理似懂非懂,知其然而不知其所以然。

这里是一些内存概念,我本身不是科班出身,所以就手打了一遍,加深印象。如果你是计算机出身,可以跳过这段介绍。同时也希望有一定计算基础的同学帮忙指正。因为看的比较杂,没有系统的梳理。也没有一个老师可以请教,一些概念都是自己总结,大家互相交流。

1、内存相关名词介绍

地址寄存器:
用来保存当前CPU所访问的内存单元的地址,由于在内存和CPU之间存在操作速度上的差异。所以必须使用地址寄存器来保持地址信息,一直到内存读写操作完成为止。

RAM:
是一个读写存储器,是程序运行时临时存放数据的,是动态存放,每次开关机,内存都会清楚。相当于系统运行时的数据动态缓存区。

物理地址:
加载到内存地址寄存器中的内存“硬件内存”,是内存单元指向的地址。

逻辑地址:
由CPU控制生成的地址。是一个程序级别的概念。逻辑地址分配非常灵活,例如:在一个数组中,我可以通过逻辑地址的分配保证数组元素地址的连续性。当然逻辑地址最终要通过一定的方式映射到RAM中的物理地址上,而这个物理地址才是元素存储的真正地址,但是不一定是连续的。

虚拟内存:
是操作系统级别的概念,只计算机呈现出要比它实际内存量大的多的内存量。这里又要引申出一个概念 交换空间。

交换空间:
在系统中运行的每个进程都要使用到内存,但是不是每个进程都需要时时刻刻使用系统分配的内存空间。当系统运行所需内存超过系统分配的内存空间时。内存会释放某些进程所占用但未使用的部分内存。 例如 火车运行在铁轨上,在火车行驶过后的铁轨铺设在火车未行使的道路上,理论上或者只需要使用很少的铁轨,就能行驶很长距离。

进程所拥有的内存空间指的是虚拟内存,虚拟地址/逻辑地址与进程息息相关。所以离开进程谈虚拟内存没有任何意义。

2、Android中的进程

Android是一个构建在linux系统上的开源移动开发平台,在Android中,多数情况下每个程序都是在各自独立的linux进程中运行。Android中分为 native进程,java进程。

Native进程:是采用C/c++实现的,不包过Dalvik实例的Linux进程。
java进程: 实例化Dalvik虚拟机的linux进程

也就是说我平常开发程序是运行在java进程中。每个java进程会实例化一个dalvik 虚拟机。进程中的内存是JVM 分配与管理的)

JVM : 是一个虚构出来的计算机,是通过实际的计算机上 仿真模拟计算机功能来实现的,它有自己完善的 (虚拟)硬件架构,(处理器,堆栈,寄存器)使用java虚拟机程序就是为了支持与操作系统无关,在任何系统中都可以运行的程序。——java语言的跨平台特性。

DVM: 是google公司自己设计用于Andorid平台的java虚拟机,也就是说 本质上。Dalivk也是一个java虚拟机,是Andoroid中java程序的运行基础。(由java编译流程得出(DVM+dx编译器=JVM))

Dalvik可以允许多个instance 运行,也就是说每一个Android 的App是独立跑在一个VM中.
一个应用,一个进程,一个Dalvik!

这里写图片描述

2-1、Android中虚拟内存怎么分配与管理?

1、分配机制:
为每一个进程分配一个合理的内存大小,保证每一个进程都能够正愁运行。

    详细:Android为每一个进程分配内存的时候,采用了弹性的分配方式,也就开始并不会一下子分配很多内存给每一个进程,而是给每一个进程分配一个“够用”的量,这一个量是根据每一个设备实际的物理内存大小来决定的,这个时候Android又会为每一个进程分配一些额外的内存大小。但是这些额外的内存并不是随意分配的。也是有限额分配的。Andorid系统的宗旨是最大限度的让更多的进程存活在内存中,因为这样的话,下一次用户再启动应用。不需要重新创建进程,只要恢复进程就可以了,

2、管理进程内存机制:
为了使系统能够正确决定在内存不足时,应该终止那个进程,Android根据每个进程中运行的组件以及组件的状态把进程放入一个重要性分级中,进程的类型按照重要性排序分为 前台进程、可见进程、服务进程,后台进程,空进程;

前台进程:进程中有组件 例如Activity正与用户进行交互  onpesume,或 service 中的一个回调正在运行等

可见进程:可以按照Activity生命周期的onstart理解,例如屏幕上的前台进程是一个对话框,但是并没有覆盖全屏幕,可见进程中Activity于用户来说是可见,单不能交互。

服务进程:进程中拥有的Service ,已经通过startService开启了服务,虽然用户无法看到进程中的具体组件,但是它们所做的事情确实用户真正关心的(例如下载,播放歌曲)。

后台进程:当前用户看不到。进程组件onstop已经被调用。组件正常的生命周期已经走完。系统可以在任何时刻终止该进程,以确保其他优先级进程的内存需求。系统会把此类进程放在LRU列表中,以确保内存不足时 用户最后一个看到进程,最后一个被销毁。(LRU :最近最少使用算法)

空进程:进程没有任何活动组件。保留此类进程是为了确保用户下次打开此类app,不用重新创建进程,这样可以提升启动速度。

3、进程中内存管理 ——这也是我们Android开发真正关心的内容

Java采用的GC(垃圾回收机制)进行内存管理Android 虚拟机的垃圾回收采用的是“根搜索算法”,GC 会从根节点(GC Roots)开始对heap进行遍历,到最后,部分没有直接或者间接引用到GCRoots节点的对象就是需要回收的垃圾,会被GC回收掉,而内存泄露出现的原因就是存在了无效的引用,导致本来需要被GC的对象没有被回收。

垃圾回收算法相关
(1).可回收对象的判定
①.引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它的时候,计数器的值就加1;当引用失效的时候,计数器的值就减1;任何时刻计数器为0的对象是不可能再被引用的。
这种方法实现简单,判断效率也很高;但是该算法有一个致命的缺点就是难以解决对象相互引用的问题:试想有两个对象,相互持有对方的引用,而没有别的对象引用到这两者,那么这两个对象就是无用的对象,理应被回收,但是由于他们互相持有对方的引用,因此他们的引用计数器不为0,因此他们不能被回收。
②.可达性分析算法
为了解决上面循环引用的问题,Java采用了一种全新的算法——可达性分析算法。这个算法的核心思想是,通过一系列称为“GC Roots”的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径成为“引用链”,当一个对象到GC Roots没有一个对象相连时,则证明此对象是不可用的(不可达)。

这里写图片描述

无论是引用计数法还是可达性分析算法,判断对象的存活与否都与“引用”有关。在JDK1.2之前,“引用”的解释为:如果reference类型的数据中储存的数值代表的是另外一块内存的起始地址,就称这个数据代表着一个引用。在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用。

强引用:就是指在程序代码之中普遍存在的,类似于“Object obj = new Object();”这样的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象。

软引用:用来描述一些还有用但并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收的范围,进行第二次回收——如果这次回收还没有腾出足够的内存,才会内存溢出抛出异常。在JDK1.2之后,提供了SoftReference来实现软引用。

弱引用:也是用来描述非必须对象的,但是他的强度比软引用更弱一些。被弱引用引用的对象,只能生存到下一次GC之前,当GC发生时,无论无论当前内存是否足够,都会回收掉被弱引用关联的对象。JDK1.2之后,提供了WeakRefernce类来实现弱引用。

虚引用:是最弱的一种引用,一个对象有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置一个虚引用关联的唯一目的就是能够在这个对象被收集器回收的的时候收到一个系统的通知。

3-1、内存分配与内存存储区域介绍:

栈 : 在执行方法时,方法一些内部变量的存储都可以放在栈上面创建,方法执行结束时候这些存储单元就会自动被注释掉。栈内存包括分配的运算速度很快,
因为在处理器里面,容量有限,并且栈 是一块连续的内存区域,大小是由操作系统决定,它先进后出,进出完全不会残生碎片,运行效率高且稳定。

堆: 又称动态内存,我们通常使用 new 来申请分配一个内存。GC会根据内存的使用情况,对堆内的垃圾进行回收,堆 内存是一块不连续的内存区域。 如果频繁new|remove
会造成大量的内存碎片,GC频繁的回收导致内存抖动。会消耗我们的应用性能。

4、内存泄露、内存溢出、优化。

内存泄露:进程中的某些对象已经没有使用的价值了,但是他们却还可以直接或间接地被引用GCroot,当内存泄露过多的时候,再加上应用本身占用的内存,时间长了就会导致内存溢出 oom.

4-1单例导致内存泄露

1.单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄露。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。

public class AppSettings {
    private static AppSettings sInstance;
    private Context mContext;

    private AppSettings(Context context) {
        this.mContext = context;
        // 改进  为了避免内存泄露,我可以获取全局上下文
        this.mContext = context.getApplicationContext();
    }
    public static AppSettings getInstance(Context context) {
        //这里我调用 getInstance 传入参数(getActivity)
        if (sInstance == null) {
            sInstance = new AppSettings(context);
            // 静态单利对象就持有了传入activity对象。当前activity退出,内存并不会回收
            //(因为sIntance作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个Activity的引用)
        }
        return sInstance;
    }
}

2、静态变量导致内存泄露
静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。
比如下面这样的情况,在Activity中为了避免重复的创建info,将sInfo作为静态变量:

public class MainActivity2 extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        start();
    }
    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
        //即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,
        // 那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,
        // 然后等待Looper的轮询处理(MessageQueue和Looper都是与线程相关联的,MessageQueue是Looper引用的成员变量,
        // 而Looper是保存在ThreadLocal中的)。那么当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,
        // 那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。
}
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
                // 做相应逻辑
            }
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
        // 优化 在activity退出的时候,就把mHandler的回调和发送的消息给移除掉。
    }
}

3、开启线程 或者IO流并没有关闭Timer读秒等引起的内存泄露。

4、属性动画造成内存泄露
动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。

@Override
protected void onDestroy() {
    super.onDestroy();
    mAnimator.cancel();
}

4-2、内存溢出
当应用的heap 资源超过Dalvik虚拟机分配的内存时 就会内存溢出。
发生场景多集中于图片加载,或者是列表拉去数据,没有做分页的情况下,一次拉去过去数据,申请内存大于dalvik 分配剩余内存时。

关于处理大图片
Android对于图片编码格式的不同,加载到手机占用的内存大小也不一样。

Android中RGB编码格式
1、 RGB888
2、 RGB565
3、ARGB555
4、ARGB8888

Andorid中占用的内存计算 图片高度 * 图片宽度 * 单位像素占用内存
这里的单位像素占用内存就是根据编码格式确定的。

Andorid BitmapConfig类中提供的 ARGB8888 ARGB4444 RGB565等常量,可以设置加载进入内存的图片编码格式,当然这样计算也是不准备的,还要考虑图片存放的目录和手机屏幕密度的影响。

图片实际大小与加载到imageview中所占用的大小影响

一个imageview控件大小是 100*100;而加载的图片是200的话,如果直接加载那么部分内存是浪费掉的。
这里我采用 bitmapFactory.options来加载所需的尺寸的图片。这里引用一个采样率 inSampleSize
当 inSampleSize的值为2时,加载的是原始大小的图片1/2,inSampleSize的取值只能是2的倍数。如果外界传给系统的inSampleSize不是2的倍数,例如3,那么系统会向下去整并且选择一个最接近2的指数来代替。

BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream、decodeByteArray,分别支持从文件系统,资源,输入流,以及字节码中加载出一个bitmap对象。其中decodeFile,decodeResource又间接调用了decodeStream方法。

// 压缩图片边界,返回一个bitmap对象
    public static Bitmap decodeSampleBitmapForRrsource(Resources res, int resId, int reqwidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true; // 设置inJustDecodeBounds 为true时,BitmapFactory会获取bitmap的原始宽高但是不会去真实加载图片
        BitmapFactory.decodeResource(res, resId, options);
        options.inSampleSize = calculateInSampleSize(options, reqwidth, reqHeight);
        // 计算出图片采样率之后,真正的去加载图片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    // 计算需要压缩图片的采样率
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqwidth, int reqHeight) {
        final int width = options.outWidth;
        final int height = options.outHeight;
        int insampleSize = 1;
        if (height > reqHeight || width > reqwidth) {
            final int halfwidth = width / 2;
            final int halfheight = height / 2;
            while ((halfwidth / insampleSize >= width) && (halfheight / insampleSize >= height)) {
                insampleSize *= 2;
            }
        }
        return 1;
    }

在开发中 对于图片的处理远远没有这么简单。这里引申出andorid中的缓存策略,前面讲解进程回收 ;LRU :最近最少使用算法。
它的核心思想是当缓存满时,会优先淘汰那些最近最少使用的缓存对象,采用LRU算法的缓存由两种缓存类,内存缓存:LruCache 本地缓存:DiskLruCache

LruCache 是android 3.1版本提供的一个缓存类。可以通过Support—v4 兼容。LruCache 内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象。

DisLruCache 用于实现存储设备缓存。 它不属于android SDK的一部分。可以同过百度网址下载到源码

当然以上我的都没有使用过,关于图片处理单方框架太多太多了。从最早期的 xutils-bitmaputils iamgeloader 到现在的Picasso Glide.

它们基本都是使用三级缓存策略:

什么是三级缓存

网络缓存, 不优先加载, 速度慢,浪费流量
本地缓存, 次优先加载, 速度快
内存缓存, 优先加载, 速度最快

三级缓存原理

首次加载 Android App 时,肯定要通过网络交互来获取图片,之后我们可以将图片保存至本地SD卡和内存中
之后运行 App 时,优先访问内存中的图片缓存,若内存中没有,则加载本地SD卡中的图片
总之,只在初次访问新内容时,才通过网络获取图片资源

站在巨人肩膀上看世界,参考或者拷贝以下博客。
Android内存管理分析总结
内存泄露优化

猜你喜欢

转载自blog.csdn.net/qq_30974087/article/details/79729398