Android 内存优化,看过来 ~

380 阅读6分钟

内存泄露

内存泄漏就是在当前应用周期内不再使用的对象被 GC Roots 引用,导致不能回收,使实际可使用内存变小,通俗点讲,就是无法回收无用对象,这里总结了实际开发中常见的一些内存泄露的场景示例和解决方案。

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

该实例的生命周期和应用一样长,非静态内部类会自动持有外部类的引用,这就导致该静态实例一直持有外部类 Activity 的引用。

class MemoryActivity : AppCompatActivity() {

    companion object {
        var test: Test? = null
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_memory)
        test = Test()
    }

    inner class Test {

    }

}

解决方案:将非静态内部类改为静态内部类

class MemoryActivity : AppCompatActivity() {

    companion object {
        var test: Test? = null
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_memory)
        test = Test()
    }
     //kotlin 的静态内部类
     class Test {

    }

}

注册对象未注销或资源对象未关闭

注册了像 BraodcastReceiver,EventBus 这种,没有在页面销毁时注销的话,会引发泄露问题,所以应该在 Activity 销毁时及时注销。

类的静态变量引用耗费资源过多的实例

类的静态变量生命周期等于应用程序的生命周期,若其引用耗资过多的实例,如 Context,当引用实例需结束生命周期时,会因静态变量的持有而无法被回收,从而出现内存泄露,这种情况比较常见的有单例持有 Context。

class SingleTon private constructor(val context: Context) {

    companion object {
        private var instance: SingleTon? = null

        fun getInstance(context: Context) =
            if (instance == null) SingleTon(context) else instance!!
    }

}

当我们在 Activity 中使用时,当 Activity 销毁,就会出现内存泄露

class MemoryActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_memory)
        SingleTon.getInstance(this)
    }

}

这种情况可以使用 applicationContext,因为 Application 的生命周期就等于整个应用的生命周期

class SingleTon private constructor(context: Context) {

    private var context: Context

    init {
        this.context = context.applicationContext
    }

    companion object {
        private var instance: SingleTon? = null

        fun getInstance(context: Context) =
            if (instance == null) SingleTon(context) else instance!!
    }

}

Handler 引发的内存泄露

class MemoryActivity : AppCompatActivity() {

    private val tag = javaClass.simpleName
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            Log.i(tag, "handleMessage:$msg")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_memory)
        thread(start = true) {
            handler.sendEmptyMessageDelayed(1, 10000)
        }
    }

}

当 Activity 被 finish 时,延迟发送的消息仍会存活在 UI 线程的消息队列中,直到 10s 后才被处理,这个消息持有 Handler 的引用,由于非静态内部类或匿名类会隐式持有外部类的引用,Handler 隐式持有外部类也就是 Activity 的引用,这个引用会一直存在直到这个消息被处理,所以垃圾回收机制就没法回收而导致内存泄露。

解决方案:静态内部类 + 弱引用,静态内部类不会持有外部类的引用,如需 Handler 内调用外部类 Activity 的方法,可以让 Handler 持有外部类 Activity 的弱引用,这样 Activity 就不会有泄露风险了。

class MemoryActivity : AppCompatActivity() {

    companion object {
        private const val tag = "uncle"
    }

    private lateinit var handler: Handler

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_memory)
        handler = MyHandler(this)
        thread(start = true) {
            handler.sendEmptyMessageDelayed(1, 10000)
        }
    }

    class MyHandler(activity: Activity) : Handler(Looper.getMainLooper()) {
        private val reference = WeakReference(activity)
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            if (reference.get() != null) {
                Log.i(tag, "handleMessage:$msg")
            }
        }
    }

}

集合引发的内存泄露

先看个例子,我们定义一个栈,装着所有的 Activity

class GlobalData {
    companion object {
        val activityStack = Stack<Activity>()
    }
}

然后每启动一个 Activity,就把此 Activity 加进去,这个时候,如果你没有在 Activity 销毁时清掉集合中对应的引用,就会出现泄露问题。当然,实际开发中我们不会写这么傻逼的代码,这只是简单提个醒,需要注意一下集合中的一些引用,如果会导致泄露的,记得及时在销毁时清除。

class MemoryActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_memory)
        GlobalData.activityStack.push(this)
    }

}

检测工具

排查内存泄露,需要一些工具的支持,这里主要介绍常用的两个,LeakCanary 和 Android Studio Profiler。

LeakCanary

一行代码引入

    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'

当你测试包安装时,手机上就会有个伴生APP,用来记录内存泄露信息的。

1.png

就拿上面集合引发的泄露例子来说,LeakCanary 就会弹出通知并且 Leaks APP 中显示内存泄露信息,我们以此来定位内存泄露问题。

2.png

3.png

4.png

Android Studio Profiler

同样,我们拿上面集合的泄漏例子来看,首先,我们点击 MEMORY

5.png

然后,普通的内存问题选择 Capture heap dump 就行了

6.png

点击 Record,就会抓取一段时间的内存分配信息

7.png

Leaks 就是记录内存泄漏的,然后我们点击进去,就可以看到具体类位置了

8.png

再点击进去具体的类,就可以看到泄漏的原因啦

9.png

内存溢出

Android 系统中每个应用程序可以向系统申请一定的内存,当申请的内存不够用的时候,就会产生内存溢出,俗称 OOM,全称 Out Of Memory,就是内存用完了。在实际开发中,出现这种现象通常是因为内存泄露太多或大图加载问题,内存泄露上面已经讲了,那么,下面就主要讲讲图片的优化吧!

Bitmap 优化

(1)及时回收 Bitmap 内存,这时可能有人就要问了,Android 有自己的垃圾回收机制,为什么还要我们去回收呢?因为生成 Bitmap 最终是通过 JNI 方法实现的,也就是说,Bitmap 的加载包含两部分的内存区域,一是 Java 部分,一是 C 部分。Java 部分会自动回收,但是 C 部分不会,所以需要调用 recycle 来释放 C 部分的内存。那如果不调用就一定会出现泄露吗?那也不是的,Android 每个应用都在独立的进程,进程被干掉的话,内存也就都被释放了。

        if (bitmap != null && !bitmap.isRecycled) {
            bitmap.recycle()
            bitmap = null
        }

(2)捕获异常,Bitmap 在使用的时候,最好捕获一下 OutOfMemoryError 以免 Crash 掉,你还可以设置一个默认的图片。

        var bitmap: Bitmap? = null
        try {
            bitmap = BitmapFactory.decodeFile(filePath)
            imageView.setImageBitmap(bitmap)
        } catch (e: OutOfMemoryError) {
            //捕获异常
        }

        if (bitmap == null) {
            imageView.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.picture))
        }

(3)压缩,对于分辨率比较高的图片,我们应该加载一个缩小版,这里采用的是采样率压缩法。

        val options = BitmapFactory.Options()
        //设置为 true 可以让解析方法禁止为 bitmap 分配内存,返回 Null,同时能获取到长宽值,从而根据情况进行压缩
        options.inJustDecodeBounds = true
        BitmapFactory.decodeResource(resources, R.drawable.large_picture, options)
        val imgHeight = options.outHeight
        val imgWidth = options.outWidth
        //通过改变 inSampleSize 的值来压缩图片
        var inSampleSize = 1
        //imgWidth 为图片的宽,viewWidth 为实际控件的宽
        if (imgHeight > viewHeight || imgWidth > viewWidth) {
            val heightRatio = round(imgHeight / viewHeight.toFloat()).toInt()
            val widthRatio = round(imgWidth / viewWidth.toFloat()).toInt()
            //取较大值(确保压缩后两个维度都不超过目标尺寸,避免拉伸)
            //或取较小值(确保压缩后至少有一个维度满足目标尺寸)
            //这里选择较小值
            inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio
        }
        options.inSampleSize = inSampleSize
        //计算完后 inJustDecodeBounds 重置为 false
        options.inJustDecodeBounds = false
        val bitmap = BitmapFactory.decodeResource(resources, R.drawable.large_picture, options)
        imageView.setImageBitmap(bitmap)

如果程序中的图片是本地资源或是自己服务器上的,那这个大小我们可以自行调整,只要注意图片不要太大,及时回收 Bitmap,基本上能避免 OOM 的发生。如果图片来源是外界,这个时候就要特别注意了,可以采用压缩图片或捕获异常,避免 OOM 的产生而导致程序崩溃。

内存抖动

内存抖动是指短时间内频繁地创建和回收大量对象,导致内存使用剧烈波动(表现为锯齿状曲线),从而频繁触发 GC,最终可能会导致卡顿或 OOM,因为大量临时对象频繁创建会导致内存碎片,当需要分配内存时,虽然总体上还有剩余内存,但由于这些内存不连续,无法整块分配,系统会视为内存不够,故导致 OOM。

示例一:在循环中创建对象

// 每次循环都创建新对象
for (i in 0 until 1000) {
    val point = Point(i, i) 
    drawPoint(point)
}

示例二:在 onDraw() 等高频回调方法中创建对象,onDraw 可能每秒被调用数十次(如滑动、动画),在此处 new 对象极易引发抖动。

override fun onDraw(canvas: Canvas) {
    val paint = Paint() // 每帧新建
    canvas.drawText("Hello", 100f, 100f, paint)
}

示例三:String 是不可变对象,a + b 实际会创建新 StringBuilder 和新 String。

var result = ""
for (i in 0 until 10_000) {
    result += "item$i" // 每次都创建新 String 和 StringBuilder
}