Android的内存泄露相关知识

目录

基础知识介绍

什么是引用?

LeakCanary的原理

1.debugAPK中清单文件的节点是怎么来的

2.LeakCanary是怎么运行的

1.静态变量引用上下文导致的内存泄露

2.非静态内部类造成的内存泄露

3.Android动画造成的内存泄露

4.Handle造成的内存泄露

5.单例类造成的内存泄露

6.对应资源未关闭


在介绍内存泄露的时候我们首先需要了解一些基本的知识体系

基础知识介绍

什么是引用?

当我们创建一个对象的时候他在内存中的表现形式是这样的

        

 new A(); 是创建对象, 那么a是什么呢???
     a就是引用,他承接了new A(), 且引用了A(), a保存的是new A()创建的内存的首地址,所以, 引用大小≠ 所引用的对象创建内存的大小,也就是a实际保存的是对象的地址的大小,一般通常为4-8byte
是否可以直接被回收???
      显然是不可能的,因为此时a正引用着new A(), 就导致new A()无法被回收,如果想要回收,那么就需要置空也就是 a = null,此时a不在引用着new A(),也就是引用链断掉了,new A()会被GC回收掉

LeakCanary的原理

1.debugAPK中清单文件的节点是怎么来的

        当我们导入leakCanary依赖后,在程序运行到模拟器的时候,如果leakCanary发现了内存泄露,他会通知我们,但是我们在App模块中并没有添加过通知的权限,那么他的权限是哪里来的???
        这是我们App模块中声明的清单文件,喏并没有对应的通知权限,也没有发现其他的什么配置,不急我们来解析一下debugAPK来看一下

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.quick.androidmemoryleak">

    <application
        android:allowBackup="true"
        android:name=".MyApp"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidMemoryLeak"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 找到对应的debugAPK文件,然后拖拽到AndroidStudio当中,然后找到清单文件

当我们点开清单文件后,我们发现一些有趣的事情,因为太多我这里就不截图了 :

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    android:compileSdkVersion="32"
    android:compileSdkVersionCodename="12"
    package="com.quick.androidmemoryleak"
    platformBuildVersionCode="32"
    platformBuildVersionName="12">

    <uses-sdk
        android:minSdkVersion="24"
        android:targetSdkVersion="32" />

    <uses-permission
        android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <uses-permission
        android:name="android.permission.POST_NOTIFICATIONS" />

    <application
        android:theme="@ref/0x7f100202"
        android:label="@ref/0x7f0f001c"
        android:icon="@ref/0x7f0d0000"
        android:name="com.quick.androidmemoryleak.MyApp"
        android:debuggable="true"
        android:allowBackup="true"
        android:supportsRtl="true"
        android:extractNativeLibs="false"
        android:fullBackupContent="@ref/0x7f120000"
        android:roundIcon="@ref/0x7f0d0001"
        android:appComponentFactory="androidx.core.app.CoreComponentFactory"
        android:dataExtractionRules="@ref/0x7f120001">

        <activity
            android:name="com.quick.androidmemoryleak.LeakActivity"
            android:exported="true" />

        <activity
            android:name="com.quick.androidmemoryleak.MainActivity"
            android:exported="true">

            <intent-filter>

                <action
                    android:name="android.intent.action.MAIN" />

                <category
                    android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <provider
            android:name="leakcanary.internal.LeakCanaryFileProvider"
            android:exported="false"
            android:authorities="com.squareup.leakcanary.fileprovider.com.quick.androidmemoryleak"
            android:grantUriPermissions="true">

            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@ref/0x7f120002" />
        </provider>

        <activity
            android:theme="@ref/0x7f10044e"
            android:label="@ref/0x7f0f003d"
            android:icon="@ref/0x7f0d0002"
            android:name="leakcanary.internal.activity.LeakActivity"
            android:exported="true"
            android:taskAffinity="com.squareup.leakcanary.com.quick.androidmemoryleak">

            <intent-filter
                android:label="@ref/0x7f0f0051">

                <action
                    android:name="android.intent.action.VIEW" />

                <category
                    android:name="android.intent.category.DEFAULT" />

                <category
                    android:name="android.intent.category.BROWSABLE" />

                <data
                    android:scheme="file" />

                <data
                    android:scheme="content" />

                <data
                    android:mimeType="*/*" />

                <data
                    android:host="*" />

                <data
                    android:pathPattern=".*\.hprof" />

                <data
                    android:pathPattern=".*\..*\.hprof" />

                <data
                    android:pathPattern=".*\..*\..*\.hprof" />

                <data
                    android:pathPattern=".*\..*\..*\..*\.hprof" />

                <data
                    android:pathPattern=".*\..*\..*\..*\..*\.hprof" />

                <data
                    android:pathPattern=".*\..*\..*\..*\..*\..*\.hprof" />

                <data
                    android:pathPattern=".*\..*\..*\..*\..*\..*\..*\.hprof" />
            </intent-filter>
        </activity>

        <activity-alias
            android:theme="@ref/0x7f10044e"
            android:label="@ref/0x7f0f003d"
            android:icon="@ref/0x7f0d0002"
            android:name="leakcanary.internal.activity.LeakLauncherActivity"
            android:enabled="@ref/0x7f040003"
            android:exported="true"
            android:taskAffinity="com.squareup.leakcanary.com.quick.androidmemoryleak"
            android:targetActivity="leakcanary.internal.activity.LeakActivity"
            android:banner="@ref/0x7f0700a4">

            <intent-filter>

                <action
                    android:name="android.intent.action.MAIN" />

                <category
                    android:name="android.intent.category.LAUNCHER" />

                <category
                    android:name="android.intent.category.LEANBACK_LAUNCHER" />
            </intent-filter>
        </activity-alias>

        <activity
            android:theme="@ref/0x7f10044f"
            android:label="@ref/0x7f0f006e"
            android:icon="@ref/0x7f0d0002"
            android:name="leakcanary.internal.RequestPermissionActivity"
            android:taskAffinity="com.squareup.leakcanary.com.quick.androidmemoryleak"
            android:excludeFromRecents="true" />

        <receiver
            android:name="leakcanary.internal.NotificationReceiver" />

        <provider
            android:name="leakcanary.internal.MainProcessAppWatcherInstaller"
            android:enabled="@ref/0x7f040006"
            android:exported="false"
            android:authorities="com.quick.androidmemoryleak.leakcanary-installer" />

        <provider
            android:name="leakcanary.internal.PlumberInstaller"
            android:enabled="@ref/0x7f040005"
            android:exported="false"
            android:authorities="com.quick.androidmemoryleak.plumber-installer" />

        <provider
            android:name="androidx.startup.InitializationProvider"
            android:exported="false"
            android:authorities="com.quick.androidmemoryleak.androidx-startup">

            <meta-data
                android:name="androidx.emoji2.text.EmojiCompatInitializer"
                android:value="androidx.startup" />

            <meta-data
                android:name="androidx.lifecycle.ProcessLifecycleInitializer"
                android:value="androidx.startup" />
        </provider>
    </application>
</manifest>

        奇怪了,这些权限和provider节点都是谁添加的呢??? 最终我们在从远程仓库下载下来的arr包中找到了对应的权限以及provider的声明

 总结:为什么打包出来的debugApk文件会有读写权限的申请,以及多余的Provider因为我们导入依赖后,从远程仓库下载下来的arr包有对应的清单文件,我们当前的app模块也依赖了leakCanary,在打包apk的时候,Gradle会去把依赖的第三方文件的库里的配置文件的节点进行合并打包,所以,打包出来的apk文件才有一些我们并没有添加的权限

2.LeakCanary是怎么运行的

    通过查看debug包我们发现LeakCanary声明了很多个内容提供者,首先我们要明确内容提供者的功能,我们之前的内容提供者都是为了给别的进程提供数据,访问系统的通讯录和相册之类的,跨进程来访问,但是LeakCanary的内容提供者不是为了这样做的,他是为了利用Activity的启动流程
    当Application启动完成后会去启动App中注册的内容提供者,然后才会去启动Activity,在MainProcessAppWatcherInstaller这个LeakCanary的内容提供者类中我们发现了,他会去获取应用的Application对象,整体的流程就是利用Activity的启动流程,被动的去调用AppWatcher.manualInstall(application)这行代码

最终会调用到 ActivityWatcher的install方法中,通过registerActivityLifecycleCallbacks方法来注册通过LeakCanary内部实现的Application的内部接口来完成对Activity生命周期的监控

override fun install() {
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
  }

 最后通过ActivityWatcher类中的该类型实现监控

 private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        reachabilityWatcher.expectWeaklyReachable(
          activity, "${activity::class.java.name} received Activity#onDestroy() callback"
        )
      }
    }

那么问题又来了,是怎么样实现监控呢???
    这个时候由引出来了一个 引用队列的知识,利用引用队列来检测对象是否被GC回收掉了,如果引用队列中有值则是被回收了,反之则是没有被回收

 /**
         *     sr 引用了 string, 一起放到引用队列后,引用队列会存在一个 WeakReference,引用队列引用了sr,
         *     引用队列与普通容器HasMap,LinkList不同的是当我们添加到队列中,我们就能在容器中找到对象,而引用队列是相反的也就是引用队列里面引用了sr,
         *  但是里面并没有存在sr,只是一个很弱的引用引用了sr,当GC发生的时候sr引用的对象被回收掉不存在的时候,也就是sr为null,此时sr才会被添加到引用队
         *  列中
         */
        ReferenceQueue<String> queue = new ReferenceQueue<>();
        String string = new String("hello");
        String string2 = new String("hello");
        WeakReference<String> sr = new WeakReference<String>(string, queue);
        WeakReference<String> sr2 = new WeakReference<String>(string2,queue);
        string = null;
        string2 = null;
        System.out.println(sr.get());// hello
        WeakReference<String> k;
        while ((k = (WeakReference<String>) queue.poll()) != null) {
            System.out.println("回收了:" + k);
        }
        System.gc();// 通知GC进行回收
        System.out.println(sr.get());// null
        while ((k = (WeakReference<String>) queue.poll()) != null) {
            System.out.println("回收了:" + k);
        }

 思考一个问题,引用队列这么设计的好处是什么???
        引用队列的应用场景就是GC回收的时候,如果引用队列引用的对象释放内存之后才被添加到队列中,那么可以减少很多内存资源

1.静态变量引用上下文导致的内存泄露

使用静态变量引用上下文可能会导致内存泄漏的原因是静态变量在整个应用程序的生命周期内保持不变,而且在这种情况下,Activity 的实例将一直被引用,导致它无法被垃圾回收,从而造成内存泄漏。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        MyTestSingle.instances = this
    }
}
class MyTestSingle() {
    companion object {
        var instances: Activity ? = null
    }
}

 

改正:
使用弱引用即可

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        MyTestSingle.instances = this
    }
}
class MyTestSingle() {
    companion object {
         var instance: WeakReference<MainActivity>? = null
    }
}

2.非静态内部类造成的内存泄露

 这段代码中发生内存泄露的原因是因为在MainActivity中持有了对InnerClass的引用,而InnerClass是一个非静态内部类,会默认持有对外部类OuterClass的引用。即使在onDestroy方法中将innerClass置空,由于非静态内部类会持有外部类的引用,因此即使将innerClass置空,仍然会造成内存泄露。为避免内存泄露,需要避免在Activity中持有对非静态内部类的引用,或者在不需要使用时及时释放对其的引用。

class OuterClass { 
    
    inner class InnerClass { 
        fun show() { 
            
        } 
    } 
} 
 
class MainActivity : AppCompatActivity() { 
    // 在MainActivity 中持有InnerClass的引用
    private var innerClass: OuterClass.InnerClass? = OuterClass().InnerClass() 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
        innerClass?.show() 
    } 
    override fun onDestroy() { 
        super.onDestroy() 
        innerClass = null// 非静态内部类即使置空也会造成内存泄露 
    } 
} 

改正:

class OuterClass { 
     
    object  InnerClass { 
        fun show() { 
           
        } 
    } 
} 

class MainActivity : AppCompatActivity() { 
    private var innerClass: OuterClass.InnerClass? = OuterClass.InnerClass 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
        innerClass?.show() 
    } 
    override fun onDestroy() { 
        super.onDestroy() 
        //innerClass = null
    } 

 这段代码中会造成Activity的泄露是因为InnerClass是一个静态内部类,而静态内部类不会持有外部类的引用。因此,在这段代码中并不会发生内存泄露,因为InnerClass是静态内部类,不会持有对OuterClass的引用。所以即使在onDestroy方法中不将innerClass置空,也不会造成内存泄露。

 注意:
  即使使用弱引用来引用非静态内部类仍然会造成内存泄露

在这段代码中,虽然使用了WeakReference来持有对InnerClass的引用,但仍然会发生内存泄露的原因是因为在InnerClass是一个非静态内部类,默认会持有对OuterClass的引用。即使使用了WeakReference来持有InnerClass的引用,但由于InnerClass会持有对OuterClass的引用,导致OuterClass实例无法被及时释放,从而导致内存泄露。为避免内存泄露,需要避免在Activity中持有对非静态内部类的引用,或者在不需要使用时及时释放对其的引用。

class OuterClass { 
    inner class InnerClass { 
        fun show() { 
           
        } 
    } 
} 

class MainActivity : AppCompatActivity() { 
    private var innerClass: WeakReference<OuterClass.InnerClass> = WeakReference(OuterClass().InnerClass()) 
    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.activity_main) 
        innerClass?.get()?.show() 
    } 
   
} 

3.Android动画造成的内存泄露


上述代码我们将自定义View直接设置为View.GONE之后重新点击跳转Activity之后,然后再次设置为GONE,不断重复,我们发现仍然会造成内存泄露,即使重写setVisibility方法来关掉对应的属性动画还会造成内存泄露
改正:

 public void setVisibility(int visibility) {
        Log.d(TAG, "setVisibility");
        super.setVisibility(View.INVISIBLE);
        /**
         *   即使进行了清除动画的操作,仍然会继续执行,因为,当前类还保存在内存中怎么办????
         *   系统GC会帮我们关闭掉,在跳转页面的时候
         */
        mShapeView.clearAnimation();
        mShadowView.clearAnimation();
}

4.Handle造成的内存泄露

class MainActivity : AppCompatActivity() {
    private val mHandle = Handler()
    companion object {
        const val TAG = "MainActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        /**
         *  利用Handle不断的发送任务执行
         */
        mHandle.postDelayed(object : Runnable{
            override fun run() {
                Log.d(TAG, "=========")
                mHandle.postDelayed(this, 1000L)
            }
        },1000L)
    }
}

       我们利用Handle创建了一个内存泄露的场景, 不断的去退出Activity然后重新进入Activity,发现我们的Activity并没有被回收,造成了Activity级别的内存泄露原因是它持有了对 Handler 实例的引用。由于 Handler 是一个非静态内部类,它会隐式地持有对外部类 MainActivity 的引用,导致 MainActivity 无法被回收,从而造成内存泄漏
改正:
使用静态类继承Handle,并且如果想要使用Activity的上下文的话,可以通过弱引用的方式接收Activity
 

5.单例类造成的内存泄露

在调用 getInstance() 方法的时候传入的context 参数是 Activity 等上下文,就会导致内存泄露,以 MainActivity 为例,当我们启动一个 Activity ,并调用 getInstance()方法去获取SingleClass 的单例,传入 Activity作为 context ,这样 SingleClass 类的单例 mSingleTest就持有了 Activity 的引用,当我们退出 Activity 时,该 Activity 就没有用了,但是因为 mSingleTest对象作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个 Activity 的引用,导致这个Activity 对象无法被回收释放,这就造成了内存泄露。

class SingleClass private constructor(context: Context) {
    private val mContext: Context = context

    companion object {
        @SuppressLint("StaticFieldLeak")
        @Volatile
        private var mSingleTest: SingleClass? = null
        fun getInstance(context: Context): SingleClass? {
            if (mSingleTest == null) {
                synchronized(SingleClass::class.java) {
                    if (mSingleTest == null) {
                        mSingleTest = SingleClass(context)
                    }
                }
            }
            return mSingleTest
        }
    }

    fun show() {
        Log.d(MainActivity.TAG, "用于测试是否造成了内存泄露")
    }
}
class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        SingleClass.getInstance(context = this)?.show()
    }
}

改正:
如果需要再单例类中使用上下文可以使用,如下的方式

class SingleClass private constructor(context: Context) {
    private val mContext: Context = context.applicationContext

    companion object {
        @SuppressLint("StaticFieldLeak")
        @Volatile
        private var mSingleTest: SingleClass? = null
        fun getInstance(context: Context): SingleClass? {
            if (mSingleTest == null) {
                synchronized(SingleClass::class.java) {
                    if (mSingleTest == null) {
                        mSingleTest = SingleClass(context)
                    }
                }
            }
            return mSingleTest
        }
    }

    fun show() {
        Log.d(MainActivity.TAG, "用于测试是否造成了内存泄露")
    }
}

6.对应资源未关闭

FileOutputStream fos = new FileOutputStream(fileName);
fos.write(sb.toString().getBytes());
         

改正:

FileOutputStream fos = new FileOutputStream(fileName);
fos.write(sb.toString().getBytes());
fos.flush();
fos.close();

  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值