当前位置:主页 > 移动开发 > Android代码 >

Android内存泄漏的轻松解决方法

时间:2021-11-13 15:29:41 | 栏目:Android代码 | 点击:

前言

内存管理的目的就是让我们在开发过程中有效避免我们的应用程序出现内存泄露的问题。内存泄露相信大家都不陌生,我们可以这样理解:「没有用的对象无法回收的现象就是内存泄露」。

如果程序发生了内存泄露,则会带来以下这些问题

下面我们从基础说起

基础知识

Java 的内存分配简述

Java四种不同的引用类型

与 Android 中的差异:在 2.3 以后版本中,即使内存够用,Android 系统会优先将 SoftReference 的对象提前回收掉, 其他和 Java 中是一样的。
因此谷歌官方建议用LruCache(least recentlly use 最少最近使用算法)。会将内存控制在一定的大小内, 超出最大值时会自动回收, 这个最大值开发者自己定。

什么是内存泄漏?

内存泄漏根本原因

长生命周期的对象持有短生命周期对象**强/软引用**,导致本应该被回收的短生命周期的对象却无法被正常回收。

例如在单例模式中,我们常常在获取单例对象时需要传一个 Context 。单例对象是一个长生命周期的对象(应用程序结束时才终结),而如果我们传递的是某一个 Activity 作为 context,那么这个 Activity 就会因为引用被持有而无法销毁,从而导致内存泄漏。

内存泄漏的危害

内存泄漏的典型案例

永远的单例(Singleton)

由于单例模式的静态特性,使得它的生命周期和我们的应用一样长,一不小心让单例无限制的持有 Activity 的强引用就会导致内存泄漏。

解决方案

把传入的 Context 改为同应用生命周期一样长的 Application 中的 Context。

通过重写 Application,提供 getContext 方法,那样就不需要在获取单例时传入 context。

public class BaseApplication extends Application{
 private static ApplicationContext sContext;
 @Override
 public void onCreate(){
 super.onCreate();
 sContext = getApplicationContext();
 }
 public static Context getApplicationContext(){
 return sContext;
 }
}

Handler引发的内存泄漏

由于 Handler 属于 TLS(Thread Local Storage)变量,导致它的生命周期和 Activity 不一致。因此通过 Handler 来更新 UI 一般很难保证跟 View 或者 Activity 的生命周期一致,故很容易导致无法正确释放。

例如:

public class HandlerBadActivity extends AppCompatActivity {
 private final Handler handler = new Handler(){//非静态内部类,持有外部类的强引用
 @Override
 public void handleMessage(Message msg) {
 super.handleMessage(msg);
 }
 };
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_handler_bad);
 // 延迟 5min 发送一个消息
 handler.postDelayed(new Runnable() {
 //内部会将该 Runable 封装为一个 Message 对象,同时将 Message.target 赋值为 handler
 @Override
 public void run() {
 //do something
 }
 }, 1000 * 60 * 5);
 this.finish();
 }
}

上面的代码中发送了了一个延时 5 分钟执行的 Message,当该 Activity 退出的时候,延时任务(Message)还在主线程的 MessageQueue 中等待,此时的 Message 持有 Handler 的强引用(创建时通过 Message.target 进行指定),并且由于 Handler 是 HandlerBadActivity 的非静态内部类,所以 Handler 会持有一个指向 HandlerBadActivity 的强引用,所以虽然此时 HandlerBadActivity 调用了 finish 也无法进行内存回收,造成内存泄漏。

解决方法

将 Handler 声明为静态内部类,但是要注意**如果用到 Context 等外部类的 非static 对象,还是应该使用 ApplicationContext 或者通过弱引用来持有这些外部对象**。

public class HandlerGoodActivity extends AppCompatActivity {
 private static final class MyHandler extends Handler{//声明为静态内部类(避免持有外部类的强引用)
 private final WeakReference<HandlerGoodActivity> mActivity;
 public MyHandler(HandlerGoodActivity activity){
 this.mActivity = new WeakReference<HandlerGoodActivity>(activity);//使用弱引用
 }
 @Override
 public void handleMessage(Message msg) {
 HandlerGoodActivity activity = mActivity.get();
 if (activity == null || activity.isFinishing() || activity.isDestroyed()) {//判断 activity 是否为空,以及是否正在被销毁、或者已经被销毁
 removeCallbacksAndMessages(null);
 return;
 }
 // do something
 }
 }
 private final MyHandler myHandler = new MyHandler(this);
}

慎用 static 成员变量

static 修饰的变量位于内存的方法区,其生命周期与 App 的生命周期一致。 这必然会导致一系列问题,如果你的 app 进程设计上是长驻内存的,那即使 app 切到后台,这部分内存也不会被释放。

解决方法

不要在类初始化时初始化静态成员,也就是可以考虑懒加载。架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。

当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下:

功能 Application Service Activity
Start an Activity NO1 NO1 YES
Show a Dialog NO NO YES
Layout Inflation YES YES YES
Start an Service YES YES YES
Bind an Service YES YES YES
Send a Broadcast YES YES YES
Register BroadcastReceiver YES YES YES
Load Resource Values YES YES YES

使用系统服务引发的内存泄漏

为了方便我们使用一些常见的系统服务,Activity 做了一些封装。比如说,可以通过 getPackageManager在 Activtiy 中获取 PackageManagerService,但是,里面实际上调用了 Activity 对应的 ContextImpl 中的 getPackageManager 方法

ContextWrapper#getPackageManager

@Override
public PackageManager getPackageManager() {
 return mBase.getPackageManager();
}

ContextImpl#getPackageManager

@Override
public PackageManager getPackageManager() {
 if (mPackageManager != null) {
 return mPackageManager;
 }
 IPackageManager pm = ActivityThread.getPackageManager();
 if (pm != null) {
 // Doesn't matter if we make more than one instance.
 return (mPackageManager = new ApplicationPackageManager(this, pm));//创建 ApplicationPackageManager
 }
 return null;
}

ApplicationPackageManager#ApplicationPackageManager

ApplicationPackageManager(ContextImpl context,
    IPackageManager pm) {
 mContext = context;//保存 ContextImpl 的强引用
 mPM = pm;
}

private UserManagerService(Context context, PackageManagerService pm,
 Object packagesLock, File dataDir) {
 mContext = context;//持有外部 Context 引用
 mPm = pm;
 //代码省略
}

PackageManagerService#PackageManagerService

public class PackageManagerService extends IPackageManager.Stub {
 static UserManagerService sUserManager;//持有 UMS 静态引用
 public PackageManagerService(Context context, Installer installer,
 boolean factoryTest, boolean onlyCore) {
  sUserManager = new UserManagerService(context, this, mPackages);//初始化 UMS
 }
}

遇到的内存泄漏问题是因为在 Activity 中调用了 getPackageManger 方法获取 PMS ,该方法调用的是 ContextImpl,此时如果ContextImpl 中 PackageManager 为 null,就会创建一个 PackageManger(ContextImpl 会将自己传递进去,而 ContextImpl 的 mOuterContext 为 Activity),创建 PackageManager 实际上会创建 PackageManagerService(简称 PMS),而 PMS 的构造方法中会创建一个 UserManger(UserManger 初始化之后会持有 ContextImpl 的强引用)。
只要 PMS 的 class 未被销毁,那么就会一直引用着 UserManger ,进而导致其关联到的资源无法正常释放。

解决办法

将getPackageManager()改为 getApplication()#getPackageManager() 。这样引用的就是 Application Context,而非 Activity 了。

远离非静态内部类和匿名类

因为使用非静态内部类和匿名类都会默认持有外部类的引用,如果生命周期不一致,就会导致内存泄漏。

public class NestedClassLeakActivity extends AppCompatActivity {

 class InnerClass {//非静态内部类

 }

 private static InnerClass sInner;//指向非静态内部类的静态引用

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_nested_class);
 if (sInner == null) {
  sInner = new InnerClass();//创建非静态内部类的实例
 }
 }
}

非静态内部类默认会持有外部类的引用,而外部类中又有一个该非静态内部类的静态实例,该静态实例的生命周期和应用的一样长,而静态实例又持有 Activity 的引用,因此导致 Activity 的内存资源不能正常回收。

解决方法

将该内部类设为静态内部类 也可以将该内部类抽取出来封装成一个单例

集合引发的内存泄漏

我们通常会把一些对象的引用加入到集合容器(比如ArrayList)中,当我们不再需要该对象时(通常会调用 remove 方法),并没有把它的引用从集合中清理掉(其中的一种情况就是 remove 方法没有将不再需要的引用赋值为 null),下面以 ArrayList 的 remove 方法为例

public E remove( int index) {
 // 数组越界检查
 RangeCheck(index);
 modCount++;
 // 取出要删除位置的元素,供返回使用
 E oldValue = (E) elementData[index];
 // 计算数组要复制的数量
 int numMoved = size - index - 1;
 // 数组复制,就是将index之后的元素往前移动一个位置
 if (numMoved > 0)
 System. arraycopy(elementData, index+1, elementData, index,
   numMoved);
 // 将数组最后一个元素置空(因为删除了一个元素,然后index后面的元素都向前移动了,所以最后一个就没用了),好让gc尽快回收
 elementData[--size ] = null; // Let gc do its work
 return oldValue;
}

WebView 引发的内存泄漏

WebView 解析网页时会申请Native堆内存用于保存页面元素,当页面较复杂时会有很大的内存占用。如果页面包含图片,内存占用会更严重。并且打开新页面时,为了能快速回退,之前页面占用的内存也不会释放。有时浏览十几个网页,都会占用几百兆的内存。这样加载网页较多时,会导致系统不堪重负,最终强制关闭应用,也就是出现应用闪退或重启。
由于占用的都是Native 堆内存,所以实际占用的内存大小不会显示在常用的 DDMS Heap 工具中( DMS Heap 工具看到的只是Java虚拟机分配的内存,即使Native堆内存已经占用了几百兆,这里显示的还只是几兆或十几兆)。只有使用 adb shell 中的一些命令比如 dumpsys meminfo 包名,或者在程序中使用 Debug.getNativeHeapSize()才能看到 Native 堆内存信息。

据说由于 WebView 的一个 BUG,即使它所在的 Activity(或者Service) 结束也就是 onDestroy() 之后,或者直接调用 WebView.destroy()之后,它所占用这些内存也不会被释放。

解决方法

把使用了 WebView 的 Activity (或者 Service) 放在单独的进程里。

使用 WebView 的页面(Activity),在生命周期结束页面退出(onDestory)的时候,主动调用WebView.onPause()==以及==WebView.destory()以便让系统释放 WebView 相关资源。

其他常见的引起内存泄漏原因

Android 3.0 以下,Bitmap 在不使用的时候没有使用 recycle() 释放内存。

非静态内部类的静态实例容易造成内存泄漏:即一个类中如果你不能够控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽量使用静态类和弱引用来处理(譬如ViewRoot的实现)。

警惕线程未终止造成的内存泄露;譬如在 Activity 中关联了一个生命周期超过 Activity 的 Thread,在退出 Activity 时切记结束线程。

一个典型的例子就是 HandlerThread 的 run 方法。该方法在这里是一个死循环,它不会自己结束,线程的生命周期超过了 Activity 生命周期,我们必须手动在 Activity 的销毁方法中中调用 thread.getLooper().quit() 才不会泄露。

对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。
创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。

避免代码设计模式的错误造成内存泄露;譬如循环引用,A 持有 B,B 持有 C,C 持有 A,这样的设计谁都得不到释放。

总结

您可能感兴趣的文章:

相关文章