理解Android中的引用类型

Android中的对象有着4种引用类型,垃圾回收器对于不同的引用类型有着不同的处理方式,了解这些处理方式有助于我们避免写出会导致内存泄露的代码。

出处: Allen's Zone
作者: Allen Feng

引用

首先我们要理解:什么是引用(reference)?

在Java中,一切都被视为对象,引用则是用来操纵对象的途径。

对象和引用之间的关系可以用遥控器(引用)来操纵电视机(对象)这个场景来理解。只要手持这个遥控器,就能保持与电视机的连接。当我们想要改变频道或者音量时,实际操控的是遥控器(引用),再由遥控器(引用)来调控电视机(对象),达到操控的目的。

来看一段代码:

Car myCar = new Car(); 
myCar.run();

上面这句话的意思是,创建一个Car的对象,并将这个新建的对象的引用存储在myCar中,此时myCar就是用来操作这个对象的引用。当我们获得myCar,就可以使用这个引用去操作对象中的方法或者字段了。

注意,当我们尝试在一个未指向任何对象的引用上去操作对象时,就会遇到经典的空指针异常(NullPointerException)。可以理解成我们手持遥控器,房间里却没有电视机可与之对象(没有可以用来操控的对象)。

Car myCar;
myCar.run();

GC与内存泄露

Java的一个重要优点就是通过垃圾收集器(Garbage Collection,GC)自动管理内存的回收,开发者不需要通过调用函数来释放内存。在Java中,内存的分配是由程序分配的,而内存的回收是由GC来完成。
GC为了能够正确释放对象,会监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用

Android中采用了标注与清理(Mark and Sweep)回收算法:

从"GC Roots"集合开始,将内存整个遍历一次,保留所有可以被GC Roots直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收。

Android内存的回收管理策略可以用下面的过程来展示:

图自Google I/O: Memory Management for Android Apps

上面三张图片描述了GC的遍历过程。
每个圆形节点代表一个对象(内存资源),箭头表示对象引用的路径(可达路径),黄色表示遍历后的当前对象与GC Roots存在可达路径。当圆形节点与GC Roots存在可达路径的时候,表示当前对象正在被使用,GC不会将其回收。反之,若圆形节点与GC Roots不存在可达路径,意味着这个对象不再被程序引用,GC可以将之回收。

在Android中,每一个应用程序对应有一个单独的Dalvik虚拟机实例,而每一个Dalvik虚拟机的大小是固定的(如32M,可以通过ActivityManager.getMemoryClass()获得)。这意味着我们可以使用的内存不是无节制的。所以即使有着GC帮助我们回收无用内存,还是需要在开发过程中注意对内存的引用。否则,就会导致内存泄露。

结合上文所述,内存泄露指的是:

我们不再需要的对象资源仍然与GC Roots存在可达路径,导致该资源无法被GC回收。

Android中的对象有着4种引用类型,垃圾回收器对于不同的引用类型有着不同的处理方式,了解这些处理方式有助于我们避免写出会导致内存泄露的代码。

Strong reference(强引用)

强引用我们最常用的一种引用类型。当我们使用new关键字去新建一个对象的时候,创建的就是强引用。

比如:

MyObject object = new MyObject();

这段代码的意思是:一个新的MyObject对象被创建了,并且一个指向它的强引用被存储在object中。

当一个对象具有强引用,那么垃圾回收器是绝对不会的回收和销毁它的。对象的强引用可以在程序中到处传递。很多情况下,会同时有多个引用指向同一个对象。

强引用的存在限制了对象在内存中的存活时间。假如对象A中包含了一个对象B的强引用,那么一般情况下,对象B的存活时间就不会短于对象A。如果对象A没有显式的把对象B的引用设为null的话,就只有当对象A被垃圾回收之后,对象B才不再有引用指向它,才可能获得被垃圾回收的机会。

下面,我们举一个例子:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new MyAsyncTask(this).execute();
    }

    private class MyAsyncTask extends AsyncTask { 

        @Override
        protected Object doInBackground(Object[] params) {
            
            // 模拟耗时任务
            try {
                Thread.sleep(60000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return doSomeStuff();
        }
        private Object doSomeStuff() {
            return new Object();
        }
        @Override
        protected void onPostExecute(Object object) {
            super.onPostExecute(object);
            // 更新UI
        }
    }
}

这段代码里,MyAsyncTask会跟随Activity的onCreate去创建并开始执行一个长时间的耗时任务,并在耗时任务完成后去更新MainActivity中的UI。这是一个很常见的使用场景,却会导致内存泄露问题:

在Java中,非静态内部类会在其整个生命周期中持有对它外部类的强引用

MainActivity被销毁时,MyAsyncTask中的耗时任务可能仍没有执行完成,所以MyAsyncTask会一直存活。此时,由于MyAsyncTask持有着其外部类,即MainActivity的引用,将导致MainActivity不能被垃圾回收。如果MainActivity中还持有着Bitmap等大对象,反复进出这个页面几次可能就会出现OOM Crash了。

那么我们如何避免这样的问题出现呢?请看下文。

WeakReference (弱引用)

弱引用通过类WeakReference来表示。弱引用并不能阻止垃圾回收。如果使用一个强引用的话,只要该引用存在,那么被引用的对象是不能被回收的。弱引用则没有这个问题。在垃圾回收器运行的时候,如果对一个对象的所有引用都是弱引用的话,该对象会被回收。

我们调整一下上面例子中的代码,使用弱引用去避免内存泄露:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new MyAsyncTask(this).execute();
    }

    private static class MyAsyncTask extends AsyncTask {
        private WeakReference<MainActivity> mainActivity;    
        
        public MyAsyncTask(MainActivity mainActivity) {   
            this.mainActivity = new WeakReference<>(mainActivity);            
        }
        @Override
        protected Object doInBackground(Object[] params) {

            // 模拟耗时任务
            try {
                Thread.sleep(30000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return doSomeStuff();
        }
        private Object doSomeStuff() {
            //do something to get result
            return new Object();
        }
        @Override
        protected void onPostExecute(Object object) {
            super.onPostExecute(object);
            if (mainActivity.get() != null){
                // 更新UI
            }
        }
    }
}

大家可以注意到,主要的不同点在于,我们把MyAsyncTask改为了静态内部类,并且其对外部类MainActivity的引用换成了:

private WeakReference<MainActivity> mainActivity;

修改之后,当MainActivity destroy的时候,由于MyAsyncTask是通过弱引用的方式持有MainActivity,所以并不会阻止MainActivity被垃圾回收器回收,也就不会有内存泄露产生了。

SoftReference(软引用)

我们可以把软引用理解成一种稍强的弱引用。使用类SoftReference来表示。

很多人可能会把弱引用和软引用搞混,注意他们的区别在于:如果一个对象只具有软引用,若内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,才会回收这些对象的内存。

而只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

所以从引用的强度来讲: 强引用 > 软引用 > 弱引用。

表面上看来,软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。

但是,在实践中,使用软引用作为缓存时效率是比较低的,系统并不知道哪些软引用指向的对象应该被回收,哪些应该被保留。过早被回收的对象会导致不必要的工作,比如Bitmap要重新从SdCard或者网络上加载到内存。

所以使用软引用去缓存对象,虽然确实可以避免OOM问题,却不适用于某些场景。在Android开发中,一种更好的选择是使用LruCache,LRU是Least Recently Used的缩写,即“最近最少使用”,它的内部会维护一个固定大小的内存,当内存不足的时候,会根据策略把最近最少使用的数据移除,让出内存给最新的数据。具体实现有兴趣的同学可以自行研究。

PhantomReference(虚引用)

一个只被虚引用持有的对象可能会在任何时候被GC回收。虚引用对对象的生存周期完全没有影响,也无法通过虚引用来获取对象实例,仅仅能在对象被回收时,得到一个系统通知(只能通过是否被加入到ReferenceQueue来判断是否被GC,这也是唯一判断对象是否被GC的途径)。

我们都知道,java的Object类里面有个finalize方法,它的工作原理是这样的:一旦垃圾回收器准备好释放对象占用的内存空间,将首先调用其finalize方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。但是,问题在于,虚拟机不能保证finalize何时被调用,因为GC的运行时间是不固定的。

使用虚引用就可以解决这个问题,虚引用主要用来跟踪对象被垃圾回收的活动,主要用来实现比较精细的内存使用控制,这对于Android设备来说是很有意义的。比如,我们可以在确定一个Bitmap被回收后,再去申请另外一个Bitmap的内存,通过这种方式可以使得程序所消耗的内存维持在一个相对较低且稳定的水平。

虚引用的使用demo可以参考这篇文章:How to use PhantomReference


Refers:

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

推荐阅读更多精彩内容