Android 内存优化知识点梳理总结

目录
  • RAM 和 ROM
  • 常见内存问题
  • 内存溢出
  • 内存泄漏
    • 常见内存泄漏场景
    • 静态变量或单例持有对象
    • 非静态内部类的实例生命周期比外部类更长导致的内存泄漏
    • Handler 导致的内存泄漏
    • postDelayed 导致的内存泄漏
    • View 的生命周期大于 Activity 时导致的内存泄漏
    • 集合中的对象未释放导致内存泄漏
    • WebView 导致的内存泄漏
  • 内存抖动
    • 解决方案
  • 其他优化点
    • App 内存过低时主动清理

前言:

Android 操作系统给每个进程都会分配指定额度的内存空间,App 使用内存来进行快速的文件访问交互。例如展示网络图片时,就是通过把网络图片下载到内存中展示,如果需要保存到本地,再从内存中保存到磁盘空间中。

RAM 和 ROM

手机一般有两种存储介质,一个是 RAM ,我们常说的内存,也称之为运行内存;另一个是 ROM ,即磁盘空间。 RAM 的访问速度一般会比 ROM 快,它是即插即用,断电会抹除所有数据,RAM 越大,可同时操作的数据就越多;ROM 是外部存储空间,相当于电脑的硬盘,主要是用来存储本地数据的。

App 运行时,会被加载到 RAM 中,又因为 App 所在进程会分配指定额度的空间,所以 App 的内存空间是有限的,内存的大小对 App 性能及正常运行都会有很大的影响。 当 App 所分配的内存空间不足时,会抛出 OOM 。所以对运行中的 App 的内存的优化就显得尤为重要。

常见内存问题

常见的内存问题包括:

  • 内存泄漏:因为 Java 对象无法被正常回收,如果长期运行程序,就会造成大量的无用对象占用内存空间,最终导致 OOM。
  • 内存抖动:频繁的创建对象,当对象数据到达一定程度会造成 GC ,如果短时间内频繁的 GC 就会造成 App 卡顿的现象,这个就叫内存抖动。
  • 内存溢出:当 App 申请内存空间时,没有足够的内存空间供其使用,就会导致内存溢出,即 Out Of Memory。

内存溢出

内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时 App 就运行不了,系统会提示内存溢出,抛出异常。

所以避免 OOM 的办法就是解决内存泄漏问题,或尽量在代码中节约使用内存两种思路。

内存泄漏

内存泄漏在 Android 中就是在当前App 的生命周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小。 需要注意的是,内存泄漏问题的出现,是和生命周期有关系的,从生命周期的角度考虑,就是生命周期短的对象被生命周期长的 GC Roots 对象持有引用,从而导致生命周期短的对象在该被回收的时候,无法被正确回收,该对象长期存活,但又毫无用处,白白地占用了内存空间。当这种对象过多时,就会造成 OOM 。

常见内存泄漏场景

无法回收无用对象的场景,可以统一理解为发生了内存泄漏,常见的 case 有:

  • 资源文件未关闭/回收
  • 注册对象未注销
  • 静态变量持有数据对象
  • 单例造成内存泄漏
  • 非静态内部类的实例持有外部类引用
  • Handler
  • 集合对象中的对象未释放
  • WebView 内存泄漏
  • View 的生命周期大于容器的生命周期

常见的诸如资源文件未关闭/为回收、注册对象未注销,导致观察者一致持有注册对象的引用,从而无法正常回收注册的对象。这里对其他几种场景进行详细的说明。

静态变量或单例持有对象

在 JVM 规范中,静态变量属于 GC Root 其中的一种,一般情况下它的生命周期都会比较长,所以如果一个对象的某个属性被静态变量持有了引用,就会导致该属性实例无法正常被回收。

以简单的示例代码说明:

class TestC {
    companion object {
        var leak: Any? = null
    }
}
class LeakCanaryActivity : ComponentActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent { LeakCanaryPage(actions()) }
    staticOOM()
	}
	private fun staticOOM() {
    Toast.makeText(this, "static own Context", Toast.LENGTH_SHORT).show()
    TestC.leak = this
	}
}

当我们打开这个LeakCanaryActivity后,返回上一个 Activity,此时查看 Profiler 排查内存泄漏的内容:

同样的道理,单例模式一般也是全局的生命周期且唯一的对象,如果被单例持有也会导致一样的问题。

object TestB {
    var leak: Any? = null
}
// 修改 LeakCanaryActivity 的 staticOOM 方法
	private fun staticOOM() {
    Toast.makeText(this, "static own Context", Toast.LENGTH_SHORT).show()
    TestB.leak = this
	}

非静态内部类的实例生命周期比外部类更长导致的内存泄漏

非静态内部类一般持有对外部类实例的引用,这个可以通过查看 class 文件发现,内部类的构造方法一般需要一个外部类类型的参数。所以如果一个内部类对象,生命周期更久的话就会造成内存泄漏。 这里一个比较明显的例子是多线程操作内部类对象时,外部类的生命周期已经结束时,因为内部类实例持有外部类的引用,导致外部类实例无法被正常回收:

class LeakCanaryActivity : ComponentActivity() {
	// ...
	// 执行这个方法
	private fun innerClassOOM() {
    Toast.makeText(this, "inner leak", Toast.LENGTH_SHORT).show()
    val inner = InnerLeak()
    Thread(inner).start()
	  finish()
	}
	// 内部类
	inner class InnerLeak: Runnable {
    override fun run() {
        Thread.sleep(15000)
    }
	}
}

当我们打开一个 Activity 后,立刻创建一个新的线程执行内部类,然后立刻关闭自身,此时因为 InnerLeak 仍在子线程中,子线程在 sleep ,导致,外部类生命周期已经结束(调用了 finish),内部类对象 inner 仍持有外部类LeakCanaryActivity的引用。 除了这种内部类的形式,也可以用匿名内部类的形式来写,都会导致内存泄漏。 另一方面,不光是多线程的场景,如果内部类对象被静态变量持有引用也是一样的效果,因为他们都持有了内部类的引用,导致内部类的生命周期比外部类的生命周期更长。

Handler 导致的内存泄漏

通过 Handler 发送消息时,消息对象 Message 本身会持有 Handler 对象:

// Handler#sendMessage(Message) 会执行到 enqueueMessage 方法
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
	  // 这里把 handler 自身保存到了 Message 的 target 属性中了
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

sendMessage 方法内部调用到enqueueMessage(MessageQueue, Message, long)时,会把 Handler 对象自身赋值到 Message 的 target 上,这样 message 就知道去找哪个 Handler 执行handleMessage(msg: Message)方法。也是因为这个持有,导致了如果消息没有立刻被执行,就会一直持有 Handler 对象,此时如果关闭 Activity ,就会导致内存泄漏。

原因是 Handler 以匿名内部类或内部类的形式声明并创建的,会持有外部 Activity 的引用。从而导致持有关系是:

Message -> Handler -> Activity

实现 Handler 内存泄漏的代码:

// in LeakCanaryActivity
private fun handlerOOM() {
    val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            if (msg.what == 12)
            Toast.makeText(this@LeakCanaryActivity, "handler executed", Toast.LENGTH_SHORT).show()
        }
    }
    Thread {
        handler.sendMessageDelayed(Message().apply { what = 12 }, 10000)
    }.start()
}

操作逻辑是,在 LeakCanaryActivity 中调用这个方法后,立刻 finish LeakCanaryActivity ,然后查看内存泄漏情况:

postDelayed 导致的内存泄漏

postDelayed 实际上是把 Runnable 封装成了一个 Message 对象,传入的 Runnable 参数被赋值给了 Message 的 callback :

public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}
private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

而最终执行逻辑的方法都是 sendMessageDelayed(Message, long),所以和 sendMessage 一样都会导致内存泄漏。与之不同的是,postDelayed的泄漏会多一个Message#callback因为 在调用postDelayed时,第一个是个匿名内部类对象,多了一个引用。

handler.postDelayed(object : Runnable {
    override fun run() {
        Log.d(TAG, "postdelay done")
    }
}, 10000)

View 的生命周期大于 Activity 时导致的内存泄漏

一个极其简单的内存泄漏场景是,当我在一个 Activity 内多次弹出 Toast 时,立刻关闭当前 Activity ,就会导致内存泄漏的情况出现:

// in LeakCanaryActivity
private fun toastOOM() {
    Toast.makeText(this, "toast leak", Toast.LENGTH_SHORT).show()
}

操作步骤:将上面的方法设置在某个点击事件中,快速连续点击几次,然后立刻关闭当前 Activity ,查看 Profiler:

集合中的对象未释放导致内存泄漏

最常见的场景是观察者模式,观察者模式中注册一些观察者对象,一般是保存到一个全局的集合中,如果观察者对象在释放时不及时注销,就会造成内存泄漏:

object LeakCollection {
	val list = ArrayList<Any>()
}

class LeakCanaryActivity : ComponentActivity() {
	// ...
	private fun collectionOOM() {
    LeakCollection.list.add(this)
	}
}

操作步骤:在 LeakCanaryActivity 内调用collectionOOM() ,然后立刻 finish 。

最常见的解决办法就是在 Activity 的 destroy 时,从 list 清除自身的引用。

WebView 导致的内存泄漏

网上都说 WebView 会导致内存泄漏。通过 Profiler 直接查看并没有明显的一个 Leaks 提示。那么如何排查这个内存泄露呢?

一个思路是参照对比实验:

  • 对照组 A :NoLeakActivity,一个空的 Activity,里面没有任何内容。
  • 对照组 B :LeakWebViewActivity, 一个包含 WebView 的 Activity 。

在同一个 Root Activity 中分别打开 A 和 B ,通过对比内存变化,来证明 WebView 是否真的造成了内存泄漏。

首先是打开了 NoLeakActivity, 并没有明显的内存变化。

然后返回到 LeakCannaryActivity ,内存还是没有变化。接着打开 LeakWebViewActivity ,发现内存明显上升,主要上升在 Native 、Others 和 Graphics 。 Graphics 可以理解,因为 loadUrl 失败了会显示一个失败页面,其中有个 icon 图片,所以主要分析的点是 Native 和 Others 。

然后返回到 LeakCanaryActivity, 内存基本没有变化。

为了证明,不是因为 NoLeakActivity 先打开,LeakWebViewActivity 后打开,所以内存中会有多余的 NoLeakActivity 相关的内存占用,我们再次打开 NoLeakActivity ,再返回,内存仍无明显变化。

所以,基本上可以证明,WebView 没有随着 Activity 的销毁而被回收。

但是如何解决这种情况呢?这个问题值得后续仔细研究一下。但目前网上的各种奇怪的解决方案(例如开启一个单独的进程)并不是合理的办法。 一个说法是,在 xml 里面是有 WebView 会出现内存泄漏,但是如果通过 addView 的形式去使用不会造成,以下是通过 addView 的形式添加 一个 WebView 对象的内存变化。

而这是通过 XML 的形式使用 WebView 的内存变化。

两种方法好像并没有什么区别,但有用的一点是,这里的内存变化,主要体现在 Native 上,证明 WebView 组件,会在 Native 层面生成一些内容。

这个部分的分析,后续可以再深入研究。从应用层面来看,WebView 并没有直接触发再 Java heap 上的内存泄漏。而是更底层的 Native heap 中。

另外需要注意的一点是,通过 LeakCanary 并不能精准的检测到内存泄漏,还是得用 Profiler。

内存抖动

短时间内频繁创建对象,导致虚拟机频繁触发GC操作,频繁的 GC 会导致画面卡顿。

解决方案

  • 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
  • 注意自定义 View 的 onDraw() 方法会被频繁调用,所以在这里面不应该频繁的创建对象。
  • 当需要大量使用 Bitmap 的时候,试着把它们缓存在数组中实现复用。
  • 对于能够复用的对象,同理可以使用对象池将它们缓存起来。

其他优化点

基本上减少内存优化的其他思路就是复用和压缩资源。

  • 图片资源过大,进行缩放处理。
  • 减少不必要的内存开销:一些基本数据类型的包装类,例如 Integer 占用 16 个字节,而 int 占用 4 个字节,所以尽量避免使用自动装箱的类。
  • 对象和资源进行复用。
  • 选择更合适的数据结构,避免数据结构分配过大导致的内存浪费。
  • 使用int 枚举或 String 枚举代替枚举类型 ,但枚举类型也会有比前者更好的特性,需要酌情使用。
  • 使用 LruCache 等缓存策略。
  • App 内存过低时主动清理。

App 内存过低时主动清理

实现 Application 中的 onTrimMemory/onLowMemory 方法去释放掉图片缓存、静态缓存来自保。

class BaseApplication: Application() {
    override fun onLowMemory() {
        super.onLowMemory()
    }
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
    }
}

到此这篇关于Android 内存优化知识点梳理总结的文章就介绍到这了,更多相关Android 内存优化 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android Bitmap详解及Bitmap的内存优化

    Android Bitmap详解及Bitmap的内存优化 一.Bitmap: Bitmap是Android系统中的图像处理的最重要类之一.用它可以获取图像文件信息,进行图像剪切.旋转.缩放等操作,并可以指定格式保存图像文件. 常用方法: public void recycle() // 回收位图占用的内存空间,把位图标记为Dead public final boolean isRecycled() //判断位图内存是否已释放 public final int getWidth() //获取位图的

  • Android内存优化操作方法梳理总结

    目录 内存泄露 非静态内部类创建静态实例 注册对象未注销或资源对象未关闭 类的静态变量引用耗费资源过多的实例 Handler引发的内存泄露 集合引发的内存泄露 检测工具 LeakCanary Android Studio Profiler 内存溢出 Bitmap优化 内存抖动 内存泄露 内存泄漏就是在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小,通俗点讲,就是无法回收无用对象.这里总结了实际开发中常见的一些内存泄露的场景示例和解决方案. 非静态内部类创建

  • 详解Android的内存优化--LruCache

    概念: LruCache 什么是LruCache? LruCache实现原理是什么? 这两个问题其实可以作为一个问题来回答,知道了什么是 LruCache,就只然而然的知道 LruCache 的实现原理:Lru的全称是Least Recently Used ,近期最少使用的!所以我们可以推断出 LruCache 的实现原理:把近期最少使用的数据从缓存中移除,保留使用最频繁的数据,那具体代码要怎么实现呢,我们进入到源码中看看. LruCache源码分析 public class LruCache<

  • Android 中对于图片的内存优化方法

    1. 对图片本身进行操作 尽量不要使用 setImageBitmap.setImageResource. BitmapFactory.decodeResource 来设置一张大图,因为这些方法在完成 decode 后,最终都是通过 Java 层的 createBitmap 来完成的,需要消耗更多内存.因此,改用先通过 BitmapFactory.decodeStream 方法,创建出一个 bitmap,再将其设为 ImageView 的 source,decodeStream 最大的秘密在于其直

  • Android内存优化杂谈

    Android内存优化是我们性能优化工作中比较重要的一环,这里其实主要包括两方面的工作: 1.优化RAM,即降低运行时内存.这里的目的是防止程序发生OOM异常,以及降低程序由于内存过大被LMK机制杀死的概率.另一方面,不合理的内存使用会使GC大大增多,从而导致程序变卡. 2.优化ROM,即降低程序占ROM的体积.这里主要是为了降低程序占用的空间,防止由于ROM空间不足导致程序无法安装. 本文的着重点为第一点,总结概述降低应用运行内存的技巧.在这里我们不再细述PSS.USS等概念与Android应

  • 详解Android内存优化策略

    目录 前言 一.内存优化策略 二.具体优化的点 1.避免内存泄漏 2.Bitmap等大对象的优化策略 (1) 优化Bitmap分辨率 (2) 优化单个像素点内存 (3) Bitmap的缓存策略 (4) drawable资源选择合适的drawable文件夹存放 (5) 其他大对象的优化 (6) 避免内存抖动 3.原生API回调释放内存 4.内存排查工具 (1)LeakCanary监测内存泄漏 (2)通过Proflier监控内存 (3)通过MAT工具排查内存泄漏 总结 前言 在开始之前需要先搞明白一

  • 总结Android App内存优化之图片优化

    前言 在Android设备内存动不动就上G的情况下,的确没有必要去太在意APP对Android系统内存的消耗,但在实际工作中我做的是教育类的小学APP,APP中的按钮.背景.动画变换基本上全是图片,在2K屏上(分辨率2048*1536)一张背景图片就会占用内存12M,来回切换几次内存占用就会增涨到上百兆,为了在不影响APP的视觉效果的前提下,有必要通过各种手段来降低APP对内存的消耗. 通过DDMS的APP内存占用查看工具分析发现,APP中占用内存最多的是图片,每个Activity中图片占用内存

  • 浅谈Android性能优化之内存优化

    1.Android内存管理机制 1.1 Java内存分配模型 先上一张JVM将内存划分区域的图 程序计数器:存储当前线程执行目标方法执行到第几行. 栈内存:Java栈中存放的是一个个栈帧,每个栈帧对应一个被调用的方法.栈帧包括局部标量表, 操作数栈. 本地方法栈:本地方法栈主要是为执行本地方法服务的.而Java栈是为执行Java方法服务的. 方法区:该区域被线程共享.主要存储每个类的信息(类名,方法信息,字段信息等).静态变量,常量,以及编译器编译后的代码等. 堆:Java中的堆是被线程共享的,

  • Android 内存优化知识点梳理总结

    目录 RAM 和 ROM 常见内存问题 内存溢出 内存泄漏 常见内存泄漏场景 静态变量或单例持有对象 非静态内部类的实例生命周期比外部类更长导致的内存泄漏 Handler 导致的内存泄漏 postDelayed 导致的内存泄漏 View 的生命周期大于 Activity 时导致的内存泄漏 集合中的对象未释放导致内存泄漏 WebView 导致的内存泄漏 内存抖动 解决方案 其他优化点 App 内存过低时主动清理 前言: Android 操作系统给每个进程都会分配指定额度的内存空间,App 使用内存

  • Android 线程优化知识点学习

    目录 前言 一.线程调度原理解析 线程调度的原理 线程调度模型 Android 的线程调度 线程调度小结 二.Android 异步方式汇总 Thread HandlerThread IntentService AsyncTask 线程池 RxJava 三.Android线程优化实战 线程使用准则 线程池优化实战 四.定位线程创建者 如何确定线程创建者 Epic实战 五.优雅实现线程收敛 线程收敛常规方案 基础库如何使用线程 基础库优雅使用线程 前言 在实际项目开发中会频繁的用到线程,线程使用起来

  • 常见Android编译优化问题梳理总结

    目录 编译常见问题 踩坑1 踩坑2 编译常见问题 在开发过程中,有碰到过一些由于编译优化导致的代码修改并不符合我们预期的情况.这也就是之前为什么我经常说编译产物其实是不太可以被信任的. 方法签名变更,底层仓库的方法变更但是上层模块并没有跟随一起重新编译导致的这个问题. 常量优化,将一些常量的调用点直接替换成常量的值. 删除空导包, 没有用的一些导包就会做一次剔除. 踩坑1 我们最近碰到一个 pipeline 相关而且很妖怪的问题.我们一个 pipeline 会检查apk产物中是否存在异常的方法调

  • android内存优化之图片优化

    对图片本身进行操作.尽量不要使用setImageBitmap.setImageResource.BitmapFactory.decodeResource来设置一张大图,因为这些方法在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存.因此,改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的source,decodeStream最大的秘密在于其直接调用JNI>>nativeDeco

  • 详解Android内存泄露及优化方案一

    目录 一.常见的内存泄露应用场景? 1.单例的不恰当使用 2.静态变量导致内存泄露 3.非静态内部类导致内存泄露 4.未取消注册或回调导致内存泄露 5.定时器Timer 和 TimerTask 导致内存泄露 6.集合中的对象未清理造成内存泄露 7.资源未关闭或释放导致内存泄露 8.动画造成内存泄露 9.WebView 造成内存泄露 总结 一.常见的内存泄露应用场景? 1.单例的不恰当使用 单例是我们开发中最常见和使用最频繁的设计模式之一,所以如果使用不当就会导致内存泄露.因为单例的静态特性使得它

  • 详解Android内存泄露及优化方案

    目录 一.常见的内存泄露应用场景? 1.单例的不恰当使用 2.静态变量导致内存泄露 3.非静态内部类导致内存泄露 4.未取消注册或回调导致内存泄露 5.定时器Timer 和 TimerTask 导致内存泄露 6.集合中的对象未清理造成内存泄露 7.资源未关闭或释放导致内存泄露 8.动画造成内存泄露 9.WebView 造成内存泄露 总结 一.常见的内存泄露应用场景? 1.单例的不恰当使用 单例是我们开发中最常见和使用最频繁的设计模式之一,所以如果使用不当就会导致内存泄露.因为单例的静态特性使得它

  • 浅谈Android应用的内存优化及Handler的内存泄漏问题

    一.Android内存基础 物理内存与进程内存 物理内存即移动设备上的RAM,当启动一个Android程序时,会启动一个Dalvik VM进程,系统会给它分配固定的内存空间(16M,32M不定),这块内存空间会映射到RAM上某个区域.然后这个Android程序就会运行在这块空间上.Java里会将这块空间分成Stack栈内存和Heap堆内存.stack里存放对象的引用,heap里存放实际对象数据. 在程序运行中会创建对象,如果未合理管理内存,比如不及时回收无效空间就会造成内存泄露,严重的话可能导致

随机推荐