Android 性能优化:Activity 泄漏来源、static 使用、AsyncTask/Thread/Handler、监听器未移除,以及动画、IO、Cursor、Bitmap 和集合等资源释放
Android 性能优化:Activity 泄漏来源、static 使用、AsyncTask/Thread/Handler、监听器未移除,以及动画、IO、Cursor、Bitmap 和集合等资源释放
本文转载自:https://blog.csdn.net/m0_37796683/article/details/102590141
1. 阅读前问题卡:Android 性能优化
建议先读:先读第 3 节“概述”建立性能目标,再读第 4 节“内存优化”理解生命周期与资源回收,随后按需进入布局、绘制和图片章节。
1.1 阅读前先看这几个问题
- Android 性能优化中的“更快、更稳、更省”分别对应哪些用户可感知的结果,后文各章节如何展开这些目标?
- 内存泄漏与 OOM 有什么区别?
- 为什么“持有引用者的生命周期长于被持有者”会让 Activity 无法回收?
- 非静态内部类, 通过什么引用或生命周期关系造成泄漏?
- AsyncTask ,通过什么引用或生命周期关系造成泄漏?
- Thread ,通过什么引用或生命周期关系造成泄漏?
- Handler ,通过什么引用或生命周期关系造成泄漏?
- 静态变量,通过什么引用或生命周期关系造成泄漏?
- 监听器,通过什么引用或生命周期关系造成泄漏?
- 动画资源未关闭,通过什么引用或生命周期关系造成泄漏?
- IO 流,File 文件类,或者 Sqlite、Cursor 等资源为关闭,通过什么引用或生命周期关系造成泄漏?
- 使用集合类,通过什么引用或生命周期关系造成泄漏?
1.3 自检清单
- 我能用一句话说明 Android 性能优化的三类目标,以及它们对应的用户感受。
- 我能区分内存泄漏和 OOM,并能沿着引用关系说出至少两种泄漏来源。
- 我能解释 Activity 销毁时取消任务、移除监听、清空 Handler 消息和关闭资源的共同边界。
- 我能把布局层级、测量绘制、
onDraw()、帧时间和过度绘制连成一条渲染主线。 - 我能说清四种图片压缩方式、像素格式和图片缓存库的关键差异。
- 我能复述线程池的执行顺序,并说明列表复用、分页和缓存分别面向什么问题。
2. 前言
2.1 生命周期与资源回收
一场活动宣布结束,只相当于通知散场。只要还有人保留场地钥匙,或者设备和物料没有撤走,场地就不能真正交还。对应到 Activity,调用 finish() 或进入 onDestroy() 只是发出了生命周期结束的信号:非静态内部类、异步任务、Handler 和监听器像仍持有钥匙的人,可能沿着引用链继续持有 Activity;动画、流、游标、Bitmap 和集合则像未清走的设备与物料,可能继续占用资源。
因此,本节判断的不是“Activity 是否已经用完”,而是“谁仍在持有它、这段引用会持续多久,以及应在何时解除”。使用静态内部类或应用级 Context,是为了减少不必要的 Activity 引用;取消任务、清空 Handler 消息、移除监听器和关闭资源,则是在生命周期结束时主动收尾。至于不同对象应在何时解除引用或释放资源,仍要结合正文中的具体机制判断。
2.2 布局、测量与绘制
剧场每次开场前,都要确定布景尺寸和摆放位置,再完成灯光与舞台呈现;如果准备超时,观众就会感到停顿。对应到 Android 界面的每一帧,测量决定各个 View 占多大空间,布局确定它们放在哪里,绘制再把内容显示到屏幕上。布局层级过深或重复测量,会增加开场前的准备工作;同一区域被多层内容反复覆盖,则像在同一块舞台上叠放布景,最终画面没有增加,系统却完成了多次绘制。
FrameLayout、LinearLayout、RelativeLayout 和 ConstraintLayout 会影响层级与测量成本,include、merge 和 ViewStub 分别涉及复用、减少层级或延迟加载。选择这些手段时,要解决的是同一个问题:让测量、布局和 onDraw() 在每帧有限的时间内完成,并避免不必要的重复绘制。
2.3 图片、像素与 APK 资源
处理电子照片时,需要分清四件事:导出的文件有多大、画面由多少个色点组成、每个色点保留多少颜色信息,以及是否为不同相框保存多份尺寸不同的副本。降低导出质量可能让文件变小,却不会自动减少画面里的色点;缩小照片长宽才会减少色点数量;颜色记录得越细,一张展开后的照片占用的空间越多;保存的副本越多,总存储量也越大。
对应到 Android,质量压缩和 PNG、JPEG、WEBP 等格式主要影响文件大小与格式能力;采样率和尺寸压缩改变实际处理的像素数量,矩阵负责调整显示尺度;ARGB/RGB 像素格式决定单个像素的内存成本;Glide、Picasso 等图片库还要决定按什么尺寸加载和缓存。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
而 sInnerClass 是 static,它属于类本身,通常会一直存在到应用进程结束。即使 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和匿名Runnable,Thread本身不是内部类。
- **解决方法 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的引用(AndroidHandler消息机制使用和原理),Handler又持有Activity的引用,所以导致Activity无法被回收。
我们用图解说明一下:

- **解决方法 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;
更多推荐



所有评论(0)