Android 性能优化:Activity 泄漏来源、static 使用、AsyncTask/Thread/Handler、监听器未移除,以及动画、IO、Cursor、Bitmap 和集合等资源释放

本文转载自:https://blog.csdn.net/m0_37796683/article/details/102590141


1. 阅读前问题卡:Android 性能优化

建议先读:先读第 3 节“概述”建立性能目标,再读第 4 节“内存优化”理解生命周期与资源回收,随后按需进入布局、绘制和图片章节。


1.1 阅读前先看这几个问题

  1. Android 性能优化中的“更快、更稳、更省”分别对应哪些用户可感知的结果,后文各章节如何展开这些目标?
  2. 内存泄漏与 OOM 有什么区别?
  3. 为什么“持有引用者的生命周期长于被持有者”会让 Activity 无法回收?
  4. 非静态内部类, 通过什么引用或生命周期关系造成泄漏?
  5. AsyncTask ,通过什么引用或生命周期关系造成泄漏?
  6. Thread ,通过什么引用或生命周期关系造成泄漏?
  7. Handler ,通过什么引用或生命周期关系造成泄漏?
  8. 静态变量,通过什么引用或生命周期关系造成泄漏?
  9. 监听器,通过什么引用或生命周期关系造成泄漏?
  10. 动画资源未关闭,通过什么引用或生命周期关系造成泄漏?
  11. IO 流,File 文件类,或者 Sqlite、Cursor 等资源为关闭,通过什么引用或生命周期关系造成泄漏?
  12. 使用集合类,通过什么引用或生命周期关系造成泄漏?


1.3 自检清单

  • 我能用一句话说明 Android 性能优化的三类目标,以及它们对应的用户感受。
  • 我能区分内存泄漏和 OOM,并能沿着引用关系说出至少两种泄漏来源。
  • 我能解释 Activity 销毁时取消任务、移除监听、清空 Handler 消息和关闭资源的共同边界。
  • 我能把布局层级、测量绘制、onDraw()、帧时间和过度绘制连成一条渲染主线。
  • 我能说清四种图片压缩方式、像素格式和图片缓存库的关键差异。
  • 我能复述线程池的执行顺序,并说明列表复用、分页和缓存分别面向什么问题。


2. 前言


2.1 生命周期与资源回收

一场活动宣布结束,只相当于通知散场。只要还有人保留场地钥匙,或者设备和物料没有撤走,场地就不能真正交还。对应到 Activity,调用 finish() 或进入 onDestroy() 只是发出了生命周期结束的信号:非静态内部类、异步任务、Handler 和监听器像仍持有钥匙的人,可能沿着引用链继续持有 Activity;动画、流、游标、Bitmap 和集合则像未清走的设备与物料,可能继续占用资源。

因此,本节判断的不是“Activity 是否已经用完”,而是“谁仍在持有它、这段引用会持续多久,以及应在何时解除”。使用静态内部类或应用级 Context,是为了减少不必要的 Activity 引用;取消任务、清空 Handler 消息、移除监听器和关闭资源,则是在生命周期结束时主动收尾。至于不同对象应在何时解除引用或释放资源,仍要结合正文中的具体机制判断。


2.2 布局、测量与绘制

剧场每次开场前,都要确定布景尺寸和摆放位置,再完成灯光与舞台呈现;如果准备超时,观众就会感到停顿。对应到 Android 界面的每一帧,测量决定各个 View 占多大空间,布局确定它们放在哪里,绘制再把内容显示到屏幕上。布局层级过深或重复测量,会增加开场前的准备工作;同一区域被多层内容反复覆盖,则像在同一块舞台上叠放布景,最终画面没有增加,系统却完成了多次绘制。

FrameLayoutLinearLayoutRelativeLayoutConstraintLayout 会影响层级与测量成本,includemergeViewStub 分别涉及复用、减少层级或延迟加载。选择这些手段时,要解决的是同一个问题:让测量、布局和 onDraw() 在每帧有限的时间内完成,并避免不必要的重复绘制。


2.3 图片、像素与 APK 资源

处理电子照片时,需要分清四件事:导出的文件有多大、画面由多少个色点组成、每个色点保留多少颜色信息,以及是否为不同相框保存多份尺寸不同的副本。降低导出质量可能让文件变小,却不会自动减少画面里的色点;缩小照片长宽才会减少色点数量;颜色记录得越细,一张展开后的照片占用的空间越多;保存的副本越多,总存储量也越大。

对应到 Android,质量压缩和 PNG、JPEG、WEBP 等格式主要影响文件大小与格式能力;采样率和尺寸压缩改变实际处理的像素数量,矩阵负责调整显示尺度;ARGB/RGB 像素格式决定单个像素的内存成本;GlidePicasso 等图片库还要决定按什么尺寸加载和缓存。APK 体积是另一个层面的问题,可以把它看成整理整件行李:图片资源压缩、无效资源清理和 Gradle 配置共同决定最终安装包有多大。


2.4 并发任务与列表数据

服务大厅同时来了很多人时,常设窗口先接待;窗口忙满后,后来的人进入等候区;等候区也满了,才会加开临时窗口;所有窗口和等候区都达到上限后,就必须按预先约定的方式处理新来的人。对应到 ThreadPoolExecutor,常设窗口数量是核心线程数,最多能开放的窗口数量是最大线程数,等候区是任务队列,临时窗口空闲后保留多久由保活时间决定,系统满载后的处理方式就是拒绝策略。

列表优化处理的是另一类问题。仓库反复发货时,不会每件货都新造一个箱子,也不会一次搬来全部库存;常用货物还会放在近处,避免每次都去远处取。对应到列表,item 复用减少重复创建,分页把数据分批加载,内存与磁盘缓存减少重复的网络请求。前一个场景说明并发任务如何排队和限流,后一个场景说明列表数据如何复用和分批取得,两者不能混作同一套机制。


3. 概述

在 Android 中,性能优化是细分领域中最难,且也是知识面涉及最深和最广的方向之一。

  • 更快:App 流畅不卡顿,快速响应;
  • 更稳:App 稳定运行,程序不崩溃(Crash)和无响应(ANR);
  • 更省:节省资源,包括电量,内存,网络资源等。


4. 内存优化

谈及内存优化,我们先来普及一下内存泄漏和内存溢出的知识:(熟悉的可以跳过)

  • 内存泄漏 (memory leak):指程序在申请内存后,无法释放已经申请的内存空间。就是说你在使用资源的时候,系统为你开辟一段空间,你使用完后忘记释放资源,这时资源一直被占用(未被回收)。虚拟机宁愿抛出 OOM,也不愿意去回收被占用的内存。
  • 内存溢出 (out of memory,也叫 OOM):系统在申请内存空间时,没有足够的内存空间供其使用。简单来说就是系统不能再分配你所需要的内存空间,比如你申请需要 100M 的内存空间,但是系统仅剩 90M 了,这时候就会内存溢出。举个例子,一个盘子最多只能装 4 个果子,你却装了 5 个果子,结果掉地上了不能吃,这就是溢出。(溢出会导致应用 Crash 崩溃)

内存泄漏的本质原因:本该被回收的对象没有被回收,继续停留着内存空间中,导致内存被占用。其实是持有引用者的生命周期 > 被持有引用者的生命周期。过多内存泄漏会把内存空间占用完,最终会导致内存溢出。

从机制上的角度来说,由于存在 java 垃圾回收机制 (GC),理论上不存在内存泄漏,那么造成内存泄漏的绝大部分是外部人为因素,无意识持有引用。

常见的内存溢出和解决方案:


4.1 非静态内部类引用 Activity 的泄漏

在 java 中,非静态(匿名) 内部类默认持有外部类的引用,而静态内部类不会持有外部类的引用。这种原因造成内存泄漏常见的三种情况:静态实例、多线程、Handler 等。


4.1.1 非静态内部类创建静态实例

非静态内部类,创建静态实例,会导致内存泄漏,通常项目中,通常在 Activity 启动的时候创建单例数据,避免重复创建相同的数据,会在 Activity 内部创建一个非静态内部类的静态实例。反例:

public class NotStaticActivity extends AppCompatActivity {
    //非静态内部类实例引用,设置为静态
    private static InnerClass sInnerClass;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //保证非静态内部类实例只有一个
        if (sInnerClass == null) {
            sInnerClass = new InnerClass();
        }
    }

    //非静态内部类
    private class InnerClass {
        //……
    }
}

这样就在 Activity 里面创建了一个非静态内部类的静态实例 InnerClass,每次启动 Activity 都会使用该单例数据;

关键在于这条隐藏的引用链:

静态变量 sInnerClass
        ↓
InnerClass 实例
        ↓
外部 NotStaticActivity 实例

InnerClass 是非静态内部类。Java 会在它内部隐含一个类似这样的字段:

private NotStaticActivity this$0;

所以执行:

sInnerClass = new InnerClass();

实际上相当于:

sInnerClass 持有 InnerClass
InnerClass 持有创建它的 NotStaticActivity

sInnerClassstatic,它属于类本身,通常会一直存在到应用进程结束。即使 Activity 执行了 finish()onDestroy(),静态变量仍然持有 InnerClass,于是 InnerClass 又持有旧的 Activity

static sInnerClass -> InnerClass -> 旧 Activity

因此旧 Activity 仍然“可达”,垃圾回收器不能回收它。这就是内存泄漏。

注意,onDestroy() 只是生命周期回调,不等于对象马上被 GC。

这样做虽然避免了资源的重复创建,但是会造成内存泄漏

因为非静态内部类 InnerClass ,默认持有外部类 Activity 的引用,而该内部类 InnerClass 又是一个静态的成员变量,则该实例的生命周期和应用的生命周期一样长,那么 InnerClass 则会一直持有 Activity 的引用,导致 Activity 在销毁的时候无法被 GC 回收。

造成内存泄漏的原因:非静态外部类 (InnerClass) 的生命周期 = 应用的生命周期,而且持有外部类 Activity 的引用,在 Activity 在销毁的时候无法被 GC 回收,从而导致内存泄漏。

  • **解决方法 1:**将非静态内部类设置为静态内部类(静态内部类默认不持有外部类的引用)
  //静态内部类
    private static class InnerClass2 {
        //……
    }
  • **解决方法 2:**将外部类抽取出来封装成一个单例(Java 设计模式—单例模式)。
public class InnerClass3 extends AppCompatActivity {
    private static final String TAG = InnerClass3.class.getSimpleName();
    private static InnerClass3 sInnerClass3;

    //单例
    public InnerClass3 getInstance() {
        if (sInnerClass3 == null) {
            sInnerClass3 = new InnerClass3();
        }
        return sInnerClass3;
    }
}

多线程造成内存泄漏:我们知道线程类属性非静态(匿名)内部类,多线程的使用主要是 AsyncTask、实现 Runnable 接口,继承 Thread 类,在使用线程类时需要注意内存泄漏。


4.1.2 AsyncTask 的使用

造成内存泄漏的原因:在 AsyncTask 里面处理耗时操作时,AsyncTask 还没操作完就将 AsyncTaskActivity 退出,但是此时 AsyncTask 已然持有 AsyncTaskActivity 的引用,导致 AsyncTaskActivity 无法被回收处理。

解决办法:在使用 AsyncTask 的时候,在 AsyncTaskActivity 销毁时也取消 AsyncTask 相关的任务 AsyncTask.cancel(),避免任务在后台浪费资源,避免内存泄漏。举个正例:

public class AsyncTaskActivity extends AppCompatActivity {
    private AsyncTask<Void, Void, Void> mAsyncTask;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //AsyncTask执行任务
        mAsyncTask = new AsyncTask<Void, Void, Void>() {
            //开始之前的准备工作
            @Override
            protected void onPreExecute() {
                super.onPreExecute();
            }

            //相当于子线程,耗时操作
            @Override
            protected Void doInBackground(Void... voids) {
                return null;
            }

            //主线程显示进度
            @Override
            protected void onProgressUpdate(Void... values) {
                super.onProgressUpdate(values);
            }

            //相当于主线程,获取数据更新UI
            @Override
            protected void onPostExecute(Void aVoid) {
                super.onPostExecute(aVoid);
            }
        };

        //执行异步线程任务
        mAsyncTask.execute();
    }

    @Override
    protected void onDestroy() {
       //强制退出AsyncTask
        mAsyncTask.cancel(true);
        super.onDestroy();
    }
}


4.1.3 Thread 类使用

我们来看看 Thread 类的错误用法,反例:

public class ThreadActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //方式一:新建内部类
        new MyThread().start();

        //方式二:匿名Thread内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //自定义Thread
    private class MyThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

上面两种写法中,MyThread 和匿名 Runnable 都定义在 ThreadActivity 内部,因此会隐式持有外部 Activity 的引用。

线程启动后,正在运行的线程会持有 MyThread,或间接持有匿名 Runnable

如果任务持续时间超过 ThreadActivity 的生命周期,或者无法及时取消,就可能沿引用链继续保留 ThreadActivity,使其无法被 GC 回收,造成内存泄漏。

线程结束且不存在其他引用时,这条引用链会断开,不一定形成永久泄漏。

补充:这里真正的非静态内部类是 MyThread 和匿名 RunnableThread 本身不是内部类。

  • **解决方法 1:**将线程类 Thread 修改为静态内部类,线程类 Thread 则不再持有外部类的引用,如下例子:
   //自定义Thread,设置为静态内部类
    private static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
  • **解决方法 2:**当外部类结束时,强制结束线程,使得线程类的生命周期与外部类的生命周期一致。注意:Thread.stop() 停止线程方法已经过时,stop() 停止线程会产生不可预期的错误,不建议使用,可以参考 Thread 线程停止的正确方式。
  @Override
  protected void onDestroy() {
        //Thread.stop方法以及过时而且这样停止线程会产生不可预期的错误,
        //mMyThread.stop();
        super.onDestroy();
    }


4.1.4 Handler 造成的内存泄漏

Handler 使用不当也会造成内存泄漏,我们来看看反例:

public class HandlerActivity extends AppCompatActivity {
    private Handler mHandler;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //创建Handler
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                //接收信息
                switch (msg.what) {
                    case 1:
                        Log.e(TAG, "Handler==" + msg.obj);
                        break;
                    default:
                        break;
                }
            }
        };

        //创建线程模拟发送数据
        new Thread(new Runnable() {
            @Override
            public void run() {
                //封装信息数据
                Message message = Message.obtain();
                message.what = 1;
                message.obj = "Handler使用";
                //Handler发送信息
                mHandler.sendMessage(message);
            }
        }).start();
    }
}

原因分析:

  • 通过内部类创建的 mHandler 对象,会隐式持有 Activity 的引用(HandlerActivity);
  • 当 mHandler.sendMessage(msg) 时,最后会将 mHandler 装入到 Message 中,并把这条 Message 推送到 MessageQueue 中;
  • MessageQueue 是在一个 Looper 线程中不断轮询处理消息的;
  • 如果 Activity 销毁时,handler 消息队列中,还有没处理或者正在处理消息,而 Message 持有 Handler 的引用(Android Handler 消息机制使用和原理),Handler 又持有 Activity 的引用,所以导致 Activity 无法被回收。

我们用图解说明一下:

img

  • **解决方法 1:**当外部类结束生命周期时,清空 Handler 内消息队列;
   @Override
    protected void onDestroy() {
        //当外部类结束生命周期时,清空Handler内消息队列;
        mHandler.removeCallbacksAndMessages(null);
        super.onDestroy();
    }
  • **解决方法 2:**将匿名内部类改为静态内部类,并对上下文或者 Activity 使用弱引用。
    //将匿名内部类改为静态内部类,并对上下文或者Activity使用弱引用。
    private static class MyHandler extends Handler{
        //定义弱引用实例
        WeakReference<Activity> mWeakReference;
        public MyHandler(HandlerActivity activity) {
            //构造方法中将Activity使用弱引用
            mWeakReference = new WeakReference<Activity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (mWeakReference.get() != null){
                //UI更新
            }
        }
    }

静态 static 可以解决内存泄漏问题,使用弱引用也可以解决内存泄漏,但是需要等到 Handler 中的任务执行完才释放 Activity,没有直接 static 释放得快。(Android 的四种引用)


4.2 static 关键字修饰的成员变量


4.2.1 静态变量的单例模式

我们知道被 static 修饰的成员变量的生命周期等于 app 应用程序的生命周期,如果静态成员变量持有 Activity 的引用,则会静态成员变量的生命周期大于 Activity 的引用的生命周期,当 Activity 的引用需要销毁回收时,导致静态成员变量持有 Activity 的引用而无法回收,从而导致内存泄漏。这里有一个非常典型的例子:单例模式(Java 设计模式—单例模式)。

public class SingleInstanceClass extends AppCompatActivity {
    private static final String TAG = SingleInstanceClass.class.getSimpleName();

    private Context mContext;
    private static SingleInstanceClass sInstanceClass;

    //构造方法传入Activity的引用
    public SingleInstanceClass(Context context) {
        mContext = context;
    }

    //单例方式获取实例
    public static SingleInstanceClass getInstance(Context context) {
        if (sInstanceClass == null) {
            sInstanceClass = new SingleInstanceClass(context);
        }
        return sInstanceClass;
    }
}

在开发中单例经常需要持有 Context 的实例,如上面代码,如果一个 Activity 调用 getInstance(Context context) 方法,则在 Activity 销毁时 SingleInstanceClass 仍然持有 Activity 的引用,导致 Activity 无法回收,出现内存泄漏。

  • **解决办法 1:**保证 Context 的生命周期与应用的生命周期一致
     public SingleInstanceClass(Context context) {
        mContext = context.getApplicationContext();
    }
  • **解决办法 2:**使用弱引用代替强引用持有实例(弱引用的使用)
    public SingleInstanceClass(Context context) {
        //弱引用
        WeakReference<Context> weakReference = new WeakReference<>(context);
        mContext = weakReference.get();
    }


4.2.2 错误使用静态变量

使用静态方法是十分方便的,但是创建的对象建议不要全局化,全局话变量必须加上 static,全局化的变量或者对象会造成内存泄漏。


4.3 未移除监听

对于监听类相关资源,需要在 Activity 销毁时,进行注销和回收,否则导致资源不回收造成内存泄漏。


4.3.1 广播监听
public class LisenterActivity extends AppCompatActivity {
    private static final String TAG = LisenterActivity.class.getSimpleName();
    private MyReceiver mMyReceiver;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //1.自定义广播
        mMyReceiver = new MyReceiver();
        //创建过滤器,增加手势参数Action
        IntentFilter filter = new IntentFilter();
        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
        //动态注册广播
        registerReceiver(mMyReceiver, filter);
    }

    //自定义广播接收类
    private class MyReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //对接收到的广播进行处理,intent里面包含数据
        }
    }

    @Override
    protected void onDestroy() {
        //正例
        //对于监听类相关资源,需要在Activity销毁时进行注销和回收,否则导致资源不回收造成内存泄漏。
        //解除广播
        unregisterReceiver(mMyReceiver);
        super.onDestroy();
    }
}

Activity 销毁的时候 unregisterReceiver() 解除广播,回收资源。


4.3.2 addOnWindowFocusChangeListener 监听
public class ListenerActivity extends AppCompatActivity implements ViewTreeObserver.OnWindowFocusChangeListener{
    private static final String TAG = ListenerActivity.class.getSimpleName();
    private TextView mTextView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mTextView = new TextView(this);
        //监听执行完回收对象,不用考虑内存泄漏
        //textView.setOnClickListener(null);

        //add监听,放到集合里面,需要考虑内存泄漏
        mTextView.getViewTreeObserver().addOnWindowFocusChangeListener(this);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        //监听View的加载,加载处理计算他的宽高
    }

   @Override
    protected void onDestroy() {
        //正例
        //对于监听类相关资源,需要在Activity销毁时进行注销和回收,否则导致资源不回收造成内存泄漏。

        //解除控件监听
        mTextView.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
        super.onDestroy();
    }
}

Activity 销毁的时候解除监听,回收资源。


4.4 相关资源未关闭

对于资源的使用,在 Activity 销毁,或者不需要的场景时,需要手动关闭/注销这些资源,否则这些资源不会被回收。


4.4.1 动画相关资源

在动画结束或不需要动画的时候,或在 Activity 销毁的时候结束并回收动画。

 private void startAnimation() {
        mAnimator = ObjectAnimator.ofFloat(mTextView, "rotationY", 0, 360);
        mAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
            }
            @Override
            public void onAnimationEnd(Animator animation) {
                //动画结束时,清除控件动画并退出动画
                mTextView.clearAnimation();
                mAnimator.cancel();
            }
            @Override
            public void onAnimationCancel(Animator animation) {
            }
            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });
    }

    @Override
    protected void onDestroy() {
        //清除控件动画并退出动画
        mTextView.clearAnimation();
        mAnimator.cancel();
        super.onDestroy();
    }

动画中有无限循环的动画,动画播放时,Activity 会被 View 所持有,从而导致 Activity 无法被释放,需要 cancel() 退出动画。


4.4.2 IO 流相关类

在使用 IO 流,File 文件类或者 Sqlite、Cursor 等资源时要及时关闭,这些资源在读写操作时一般都进行了缓冲,如果不及时关闭,这些缓冲对象就会一直被占用得不到释放,以致内存泄漏,所以在在它们不需要使用时,及时关闭,缓冲释放资源,避免内存泄漏。

  //IO流使用完毕后及时关闭
    InputStream stream = null;
        try {
        stream = new FileInputStream("name");
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        try {
            //关闭流
            stream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

注意:关闭语句必须要 finally 中关闭,否则可能因为异常未关闭导致资源,导致 Activity 泄漏。


4.4.3 游标 Cursor
//查询数据库,返回Cursor
Cursor cursor = db.query(SQLiteHelper.TABLE_NAME, COLUMNS, key + " = ?", new String[]{values}, null, null, null);
cursor.close();

对于数据库游标 Cursor,使用完毕后关闭。


4.4.4 Bitmap 类
 //Bitmap 使用完毕后回收
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
bitmap.recycle();
bitmap = null;

对于图片资源 Bitmap,Android 分配给图片的资源只有 8M,如果 1 个 Bitmap 对象占用资源较多时,当不再使用时应该 recycle() 回收对象像素所占用的内存,最后赋值为 Null。


4.4.5 集合类

项目中我们常常把一些对象加入到集合中,如果不需要这些对象时,如果不把他们从集合中清理掉,那么 GC 就无法回收该对象。如果集合声明为静态的话,泄漏就更严重了,所以在不需要使用的时候将对象从集合中移除。

ArrayList<String> arrayList = new ArrayList();
for (int i = 0; i < 10; i++) {
    String s = new String();
    arrayList.add(s);
    s = null;
}

//虽然释放了对象本身:s=null;但是集合list仍然引用这该对象,导致GC无法回收该对象
arrayList.clear();
arrayList = null;


4.5 内存优化总结

  • 非静态内部类引用 Activity
    • 非静态内部类的静态实例:改为静态内部类,或把外部类抽取为单例。
    • AsyncTask:在 Activity 销毁时调用 AsyncTask.cancel() 取消任务。
    • Thread:改为静态内部类,或在外部类结束时终止线程;原文同时指出 Thread.stop() 已过时且不建议使用。
    • Handler:在外部类结束时清空消息队列,或改为静态内部类并对上下文或 Activity 使用弱引用。
  • static 关键字修饰的成员变量:
    • 静态变量的单例模式:让 Context 的生命周期与应用一致,或使用弱引用。
    • 错误使用静态变量:原文建议对全局化对象使用 static 修饰。
  • 未移除监听:在 Activity 销毁时解除广播接收者或 addOnWindowFocusChangeListener 监听。
  • 相关资源未关闭:在不再需要时结束动画,关闭 IO 流和 Cursor,回收 Bitmap,并清理集合。


5. 回答问题卡的问题


问题 1:Android 性能优化中的“更快、更稳、更省”分别对应哪些用户可感知的结果,后文各章节如何展开这些目标?

答:

  • 更快指的是 APP 的使用流程,不卡顿,能快速响应;
  • 更稳指的是 APP 能稳定运行,程序不出现崩溃 Crash 或者无响应 ANR
  • 更省指定是能节省资源,包括电量,内存和网络资源;

问题 2:内存泄漏与 OOM 有什么区别?

答:

内存泄漏指的是:

  • 首先举个例子,我们去酒店订房间,在入住结束后,需要把房卡交给前台,告知前台后续不续房子了,但是如果我们直接走,不把房卡还回去,那么前台就默认我们一直续房子,不会去帮我们结束房子的使用;
  • 当我们使用资源时,系统会去分配一块内存空间,但是在使用完资源后,我们没有对资源进行手动释放,那么这个资源会一直占用这块内存空间,虚拟机不会去帮我们释放这块空间;
  • 当有很多处资源,在使用完后没有手动释放,就会导致有多块内存空间一直被使用,后续再使用其他资源,需要申请内存空间时,发现系统已经没有这么多内存空间了,就会导致内存溢出 OOM;

内存溢出指的是:

  • 首先举个例子,我们要在一个盘子中放 5 个苹果,但是这个盘子只能放 4 个苹果,那多的一个苹果就会掉到地上;
  • 当我们要使用资源时,需要系统为我们申请一段内存,但是系统中已经没有这么大的内存了, 系统无法满足这次内存申请

内存泄漏和内存溢出的区别:

  • 内存泄漏指的是,使用完的内存没有被释放;
  • 内存溢出指的是,系统再为资源申请一块内存,发现已经不够了,那么多余的内存就会溢出;

问题 3:为什么“持有引用者的生命周期长于被持有者”会让 Activity 无法回收?

答:

先来看这段代码:

public class NotStaticActivity extends AppCompatActivity {
    //非静态内部类实例引用,设置为静态
    private static InnerClass sInnerClass;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //保证非静态内部类实例只有一个
        if (sInnerClass == null) {
            sInnerClass = new InnerClass();
        }
    }

    //非静态内部类
    private class InnerClass {
        //……
    }
}

在 NotStaticActivity 中有一个非静态内部类 InnerClass,因为非静态内部类,是依托于外部类存在的,所以在非静态内部类中,编译器会生成一个外部类的的引用:

class NotStaticActivity {
    class InnerClass {
        // 编译器自动生成,源码中看不到
        private final NotStaticActivity this$0;

        InnerClass(NotStaticActivity outer) {
            this.this$0 = outer;
        }
    }

    void create() {
        InnerClass inner = new InnerClass(this);
    }
}

当调用非静态内部类的构造方法时,本质是 new 外部类引用.内部类() 来创建非静态内部类的示例;

这时候,如果将静态变量,指向非静态内部类的实例,那么这个静态变量,将持有外部类的引用:

//非静态内部类实例引用,设置为静态
private static InnerClass sInnerClass;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    //保证非静态内部类实例只有一个
    if (sInnerClass == null) {
        sInnerClass = new InnerClass();
    }
}

同时,静态变量随着整个 APP 进程存在而存在;

那么就会导致,当外部类准备被销毁时,GC 发现外部类依旧处于引用链上,也就是指向非静态内部类实例的静态变量,还持有着外部类的实例,进而导致这个外部类对应的内存空间无法被回收,出现内存泄漏的问题;

所以可以考虑将非静态内部类,修改为静态内部类,这样内部类将不再依托于外部类,内部类不需要持有外部类的引用,外部类就可以正常 GC 回收;

问题 4:非静态内部类, 通过什么引用或生命周期关系造成泄漏?

答:

  • 因为非静态内部类依托于外部类,在创建时会持有外部类的引用;
  • 这时候如果将静态变量,指向非静态内部类的实例,会导致在整个进程中,外部类的引用会一直被非静态内部类对应的静态变量持有,导致外部类无法被 GC 回收,造成内存泄漏;

问题 5:AsyncTask ,通过什么引用或生命周期关系造成泄漏?

答:

  • 文示例中的 AsyncTask 是在 Activity 内创建的匿名非静态内部类。正是在这种写法下,仍在执行的任务持有 Activity,任务生命周期超过 Activity 才会造成泄漏。
  • 如果在 AsyncTask 中处理耗时任务的过程中,Activity 被退出了,但是 AsyncTask 依旧持有 Activity 的引用,导致 Activity 无法被销毁;
  • 所以在销毁 Activity 前,需要取消 AsyncTask 执行的耗时任务;

问题 6:Thread ,通过什么引用或生命周期关系造成泄漏?

答:

要通过 Thread 起一个线程,通常有两种方式,分别是:

  • 写一个 MyThread 类,继承 Thread;
  • 直接 new Thread() ,在其中创建非静态匿名内部类,实现 Runnable 接口,重写其 run() 方法

通过上面两种方法创建的 Thread 对象调用 start(),启动线程;

  • 当 MyThread 是一个非静态内部类,或者 new Thread() 创建匿名非静态内部类 Runnable 时,都会持有外部类的引用;
  • 线程启动时,又会持有 MyThread 或者 间接持有匿名 Runnable
  • 当外部类需要销毁时,如果线程的耗时任务还未执行完,会导致因为外部类被线程对象持有,导致无法被 GC;
  • 所以,在外部类被销毁前,可以对线程调用 stop() ,停止并释放线程,但是这种方法可能会出现不可预期的错误,不建议使用;
  • 解决 Thread 出现内存泄漏问题的最好方法,是将继承 Thread 的非静态内部类修改为静态内部类,可以避免持有外部类引用,避免内存泄露;

问题 7:Handler ,通过什么引用或生命周期关系造成泄漏?

答:

  • 首先,如果通过匿名非静态内部类创建 Handler ,Handler 会持有外部类 Activity 的引用;
  • 当使用 handler 的 handleMessage 处理 message 时,message 内部会有一个字段,专门指向与之关联的 handler;
  • 当 message 被 handler 推送至 MessageQueue 时,会通过 loop 去死循环读取队列中的消息;
  • handler 处理 loop 读取的 message,处理完后,会调用 message 内部的 recycleUnchecked(),清除 message 对象中对 handler 的引用;
  • 当 Activity 被销毁时,如果队列中仍然有未处理的 message,并且 message 关联 handler,handler 关联 Activity;
  • 因为 message 未被 looper 读取,并交由 handler 处理,无法在读取完成后对 message 调用 recycleUnchecked(),会导致 Activity 无法被 gc 回收,进而造成内存泄漏;
  • 所以,如果要解决因为 handler 造成的内存泄漏问题,本质可以从 message 的处理、使用非静态内部类创建 handler 实例这两个方向解决;
  • 首先可以在销毁 activity 前,清空队列中的 message;
  • 其次,可以使用静态内部类,继承 Handler 类,通过上下文或者 Activity 使用弱引用;

问题 8:静态变量,通过什么引用或生命周期关系造成泄漏?

答:

  • 被 static 修饰的静态变量,生命周期会和 APP 应用程序生命周期相同,如果静态变量持有 activity 的引用,会导致 activity 被销毁时,因为还处于引用链上,无法被可达性分析识别为需要回收的垃圾,最终出现内存泄漏;
  • 比如使用单例模式,如果构造方法需要传入 context,在 Activity 内部调用构造方法,传入 activity 的 context,就会让静态单例对象持有 activity 引用,在 activity 页面被退出时,因为还被单例对象持有引用,导致无法被销毁,造成内存泄漏;
  • 解决方式是,activity 调用单例模式构造方法时,传入的上下文参数不要传 activity 的,而是传应用程序的,这样单例模式就不会持有 activity 的引用;

问题 9:监听器,通过什么引用或生命周期关系造成泄漏?

答:

  • 使用非静态内部类,继承 BroadcastReceiver 实现自定义广播类,然后在 onCreate 注册广播;
  • 在 activity 被销毁时,需要先进行反注册广播,再销毁 activity,避免广播后续持有 activity 引用,造成内存泄漏;
  • 原文还包括 addOnWindowFocusChangeListener监听器被注册,或加入观察者集合后,如果没有在 Activity 销毁时移除,监听关系仍会延长相关对象的生命周期;
  • 因此需要覆盖广播注销和 ViewTreeObserver 监听移除两类场景。

问题 10:动画资源未关闭,通过什么引用或生命周期关系造成泄漏?

答:

  • 在动画结束回调中,清除控件动画 clearAnimation() 并退出动画 cancel(),在 activity 被销毁时,先 cancel 退出动画;
  • 持续运行的动画,持有目标 View,View 又可能持有 Activity Context;无限循环动画时,这条链会在 Activity 销毁后继续存在。
  • 因此在动画结束、不再需要动画时,或 Activity 销毁时,取消动画并清理控件动画。

问题 11: IO 流,File 文件类,或者 Sqlite、Cursor 等资源为关闭,通过什么引用或生命周期关系造成泄漏?

答:

  • 使用 IO 流,File 文件类,或者 Sqlite、Cursor 等资源,需要在用完后及时关闭;
  • 因为这些资源会在进行读写操作时,缓存一些对象,持有相关的引用,如果不及时关闭,那么这些缓存的对象无法被回收,造成内存泄漏;

问题 12:使用集合类,通过什么引用或生命周期关系造成泄漏?

答:

  • 集合会持有放进去的元素。
  • 即使把元素对应的局部变量 s 设为 null,只要集合里还有这个元素,集合对它的引用仍在,GC 就不能回收它。
  • 如果集合类对象是被静态变量引用的,那么造成的内存泄漏会更严重,因为外部引用持续作为集合类元素,没有及时处理,那么造成内存泄漏的时间就更长;
ArrayList<String> arrayList = new ArrayList();
for (int i = 0; i < 10; i++) {
    String s = new String();
    arrayList.add(s);
    s = null;
}

//虽然释放了对象本身:s=null;但是集合list仍然引用这该对象,导致GC无法回收该对象
arrayList.clear();
arrayList = null;

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐