前言:

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 内存优化 内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

Android 内存优化知识点梳理总结的更多相关文章

  1. html5 canvas合成海报所遇问题及解决方案总结

    这篇文章主要介绍了html5 canvas合成海报所遇问题及解决方案总结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Html5 video标签视频的最佳实践

    这篇文章主要介绍了Html5 video标签视频的最佳实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  3. HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题

    HTML5在微信内置浏览器下,在右上角菜单的调整字体导致页面显示错乱的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

  4. ios – containerURLForSecurityApplicationGroupIdentifier:在iPhone和Watch模拟器上给出不同的结果

    我使用默认的XCode模板创建了一个WatchKit应用程序.我向iOSTarget,WatchkitAppTarget和WatchkitAppExtensionTarget添加了应用程序组权利.(这是应用程序组名称:group.com.lombax.fiveminutes)然后,我尝试使用iOSApp和WatchKitExtension访问共享文件夹URL:延期:iOS应用:但是,测试NSURL

  5. Ionic – Splash Screen适用于iOS,但不适用于Android

    我有一个离子应用程序,其中使用CLI命令离子资源生成的启动画面和图标iOS版本与正在渲染的启动画面完美配合,但在Android版本中,只有在加载应用程序时才会显示白屏.我检查了config.xml文件,所有路径看起来都是正确的,生成的图像出现在相应的文件夹中.(我使用了splash.psd模板来生成它们.我错过了什么?这是config.xml文件供参考,我觉得我在这里做错了–解决方法在config.xml中添加以下键:它对我有用!

  6. ios – 无法启动iPhone模拟器

    /Library/Developer/CoreSimulator/Devices/530A44CB-5978-4926-9E91-E9DBD5BFB105/data/Containers/Bundle/Application/07612A5C-659D-4C04-ACD3-D211D2830E17/ProductName.app/ProductName然后,如果您在Xcode构建设置中选择标准体系结构并再次构建和运行,则会产生以下结果:dyld:lazysymbolbindingFailed:Symbol

  7. Xamarin iOS图像在Grid内部重叠

    heyo,所以在Xamarin我有一个使用并在其中包含一对,所有这些都包含在内.这在Xamarin.Android中看起来完全没问题,但是在Xamarin.iOS中,图像与标签重叠.我不确定它的区别是什么–为什么它在Xamarin.Android中看起来不错但在iOS中它的全部都不稳定?

  8. 在iOS上向后播放HTML5视频

    我试图在iPad上反向播放HTML5视频.HTML5元素包括一个名为playbackRate的属性,它允许以更快或更慢的速率或相反的方式播放视频.根据Apple’sdocumentation,iOS不支持此属性.通过每秒多次设置currentTime属性,可以反复播放,而无需使用playbackRate.这种方法适用于桌面Safari,但似乎在iOS设备上的搜索限制为每秒1次更新–在我的情况下太慢了.有没有办法在iOS设备上向后播放HTML5视频?解决方法iOS6Safari现在支持playbackRat

  9. Swift思量与初探:我需要学习Swift吗?

    最近,除了N多的基于Swift的服务端开发框架,笔者不由深思,到底该这么评价Swift呢?前两点在Swift的语法和语言特性中已经表现得淋漓尽致:像是尾随闭包,枚举关联值,可选值和强制的类型安全等都是Swift显而易见的优点。综上所述,Swift拥有着被广泛使用以及当做第一学习语言的潜质。Swift在语法层次上会更加高级,并且Swift并没有使用GC机制,因此可以与C更好地相兼容。Swift中的注释与C语言的注释非常相似。

  10. 使用 Swift 语言编写 Android 应用入门

    Swift标准库可以编译安卓armv7的内核,这使得可以在安卓移动设备上执行Swift语句代码。做梦,虽然Swift编译器可以胜任在安卓设备上编译Swift代码并运行。这需要的不仅仅是用Swift标准库编写一个APP,更多的是你需要一些框架来搭建你的应用用户界面,以上这些Swift标准库不能提供。简单来说,构建在安卓设备上使用的Swiftstdlib需要libiconv和libicu。通过命令行执行以下命令:gitclonegit@github.com:SwiftAndroid/libiconv-libi

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部