Android 性能优化 - 内存 [进阶]
本文主要总结 Android
开发过程中对内存使用上的优化,通过及时有效的管理内存空间可以避免内存泄漏和 OOM
的发生。
前言
四种引用类型:
- 强引用:默认引用类型,强引用的对象即使抛出
OOM
也不会被回收。 - 软引用
SoftReference
:当内存不足时会被回收。 - 弱引用
WeakReference
:在GC
时,一旦发现了只具有弱引用的对象,都会进行回收。 - 虚引用
PhantomReference
:任何时候都可以被GC
回收。
内存问题主要体现在:
- 内存泄漏:指的是当一个对象不再被使用时,由于其他对象仍然持有该对象的强引用,导致该对象无法被释放,造成内存空间的浪费,大量占据内存空间可能引发
OOM
; - 内存溢出:即
OOM
,当向系统申请内存时,没有足够的内存空间时会引发OOM
,OOM
发生的情况很多,而且最后Crash
的地方并不一定是问题的根源,可能是其他的操作占用了大量内存。 - 内存抖动:指的是频繁的有大量的对象创建和销毁,引发高频的系统
GC
,当GC
线程启动时其他线程都会暂停,会造成页面的卡顿等。
解决内存问题的原则:
- 避免创建大对象,如不使用
inSampleSize
的Bitmap
等 - 避免大量创建重复对象,如在循环中创建对象
- 避免生命周期不可控的对象引用,如在子线程中引用上下文
- 避免少的开辟新的内存空间,建议尽量复用可复用的内存空间,如后文介绍的
ByteArrayPool
以及Bitmap
的inBitmap
属性
内存分析和监控
- 获取分配的内存和可用内存大小
1 | long totalMemory = Runtime.getRuntime().totalMemory(); |
- Android Profiler
使用 AndroidStudio
,通过 View -> ToolWindows -> Android Profiler,可以查看内存、网络、CPU 变化情况,还可以 Dump
内存记录,用于内存分析。
- Memory View
使用 AndroidStudio
,通过 View -> ToolWindows -> Memory View,可以结合断点调试 dump
指定断点处的内存使用情况,进行内存分析。
- 内存监控
当应用内存不足时,会调用 Application
的 onTrimMemory()
方法,我们可以在这里做一些清理内存的操作,避免内存过大造成 OOM
1 | public class MyApplication extends BaseApplication { |
LeakCanary
LeakCanary-GitHub : A memory leak detection library for Android and Java.
LeakCanary
是 squre
开源的一个用于在 Android
和 Java
平台下检测内存泄漏的工具,提供了两种依赖方式,在 release
版本下不会进行内存泄漏的检测,避免性能问题。
1 | compile "com.squareup.leakcanary:leakcanary-android-no-op:1.5.1" |
在 Application
中初始化
1 | public class MyApplication extends BaseApplication { |
初始化之后就可以自动检测 Activity
的内存泄露问题,如果需要检测 Fragment
等其他对象的内存问题,需要在希望对象被回收的时候注册检测监听
1 | public class MyFragment extends BaseFragment { |
内部类导致内存泄漏
在内存问题上我们简单的把内部类分为静态内部类和非静态内部类:
静态内部类:与外部类独立,内部类的创建不需要依赖外部类实例,而是依赖于
Class
本身,他只能访问外部类的静态变量和方法,可以看作一个完全独立的类,与外部类完全隔离,不会存在内存泄漏的问题。非静态内部类:也就是其他类型的内部类,主要包括局部内部类、匿名内部类等,他们的创建依赖于外部类的实例,非静态内部类可以访问外部的非静态成员,甚至是私有成员,这是因为内部类中隐式的持有了外部类的引用,在编译后,会形成
OuterClass$InnerClass
类,而这个类中就会持有外部类的引用,因此当外部类被销毁后,由于内部类仍旧强引用持有了外部类,因此外部类不能被及时回收,造成内存泄漏问题。
不是说所有的内部类都会造成内存泄漏,外部类销毁时通常也会销毁内部类,内存泄漏往往发生在由于一些原因导致内部类无法被销毁的情况,如生命周期不统一。
生命周期不统一导致内存泄漏
生命周期不统一指的是 ObjB
引用了 ObjA
,但是 ObjB
比 ObjA
生命更长,当 ObjA
自己销毁时,由于 ObjB
还在活跃,导致 ObjA
无法被回收。
因为内部类持有外部类的引用,如果内部类存在于一个新的线程里面,那么内部类的生命周期就依赖于新的线程的生命周期,两者不一致,当外部类被销毁时,就会造成内存问题,比较常见的体现在 Handler
、AsyncTask
和 Thread
等涉及线程的操作。
- Handler
1 | Handler mHandler = new Handler(){ |
在 Activity
中使用 Handler
发送的 Message
排队在 Looper
线程的 MessageQueue
中,同时 Message
中持有 Handler
的引用,而由于Handler
匿名内部类隐式的持有了外部类的引用,就相当于 Activity
被强关联在了这条消息上面。即 Activity -> Handler -> Message -> MessageQueue -> Looper
当 Activity
被销毁后还是会按照指定的时间发送该消息,造成内存泄漏。因此在 Activity
非静态的声明一个 Handler
会警告:匿名的 Handler
可能会引发内存泄漏
1 | This Handler class should be static or leaks might occur (anonymous android.os.Handler) |
这个问题尤其体现在使用 Handler
发送了一个延时消息,当 Activity
被销毁后,这个消息才被发送出来,开始执行。
除了匿名声明 Handler
之外,当使用 Handler
发送一个 Runnable
时也会存在一样的问题,这里 Handler
不是内部类了,但是发送的 Runnale
里面也会隐式持有外部类的应用,即 Activity -> Runnale -> Message -> MessageQueue -> Looper
1 | Handler mHandler = new Handler(); |
- Thread 和 AsyncTask
通常我们在子线程执行耗时任务,执行完成后再回到主线程操作,如下面的例子中,Thread
和 AsyncTask
结束的时机是没办法控制的,而他们都会持有外部 Activity
的引用,导致无法回收。
1 | new Thread(new Runnable() { |
针对生命周期不一致问题的优化方案
单纯的内部类不会造成内存泄漏,不用过分保护,对于生命周期不可控的内部类,采用静态声明,结合虚引用来将内部类和外部类隔离
1 | static class NoLeakHandler extends Handler { |
使用线程进行操作时,也需要使用虚引用与 Context
交互
1 | class NoLeakAsyncTask extends AsyncTask<Void, Void, Void> { |
当 Activity
销毁时,将 Handler
的消息队列清空
1 | mHandler.removeCallbacksAndMessages(null); |
静态引用导致内存泄漏
静态引用的变量不依赖于某个类实例,所以他不会随着某个实例的销毁而随之销毁,因此一旦声明静态引用了某个对象实例,即使抛出异常也不会被回收。
使用静态变量要慎重,避免为了简单的共享数据静态的声明占用大量内存的数据对象,尤其是集合类对象。
类比上面的说法,也可以看作生命周期不统一造成的问题,因为静态引用的变量具有和 Application
相同的生命周期,而 Activity
的生命周期通常较短。
静态引用 Context 导致内存泄漏
这边单独拿出来说是因为在 Android
中静态引用 Context
也是内存泄漏重灾区,如下列举常见的几种可能静态引用 Context
的场景如:
- 某个对象没有静态引用
Context
,但是这个对象在其他位置被静态引用了,导致Context
间接的静态引用。 - 单例,单例其实也是静态的引用,不能在单例中引用
Context
。 Toast
,为了管理Toast
,比如避免大量Toast
排队通常会写一个ToastUtils
,里面就会静态持有Toast
对象,而Toast
中是引用了Context
的。View
,由于View
中引用了Context
,静态的View
就很危险。Animator
,属性动画通常绑定到一个View
上面,静态引用Animator
相当于静态引用了View
。Animation
,补间动画中并没有显式的引用View
或者Context
,但是他有一个mListenerHandler
,这个监听在View.draw()
方法中如果当前View
的Animation
不为空,会给他一个mAttachInfo.mHandler
,而这里面引用了ViewRootImpl.mContext
。- 特别注意静态引用的集合数据类型,如
List
和Map
,里面通常会存储大量的对象,如果这些对象中有某些对象引用了Context
,同样会造成内存泄漏。
当不可避免的需要静态引用 Context
时,使用虚引用代替
1 | WeakReference<Context> mContextWeakRef; |
尽可能使用 Application
的 Context
,而不是 Activity
的。
1 | Context appContext = context.getApplicationContext(); |
资源回收不及时
指的是一些对象我们使用完后要及时关闭、回收或者解除注册,来保证内存可以被及时回收,如:
- Cursor
- IO 流
- Bitmap
- Animation
- BroadcastReceiver
尽可能及时的回收资源和内存空间
1 | // Cursor |
Bitmap 占用大量内存
图片是应用运行过程中内存占用的大户,大多数的 OOM
,都是因为 Bitmap
处理不当导致的,因此把 Bitmap
单独拿出来说一下。
- 图片质量要求不是那么高的时候,使用
RBG_565
Config | 描述 |
---|---|
ALPHA_8 | 8位Alpha位图 |
RGB_565 | 16位RGB位图 |
ARGB_4444 | 16位ARGB位图 |
ARGB_8888 | 32位ARGB位图 |
1 | Bitmap bitmap = null; |
- 针对显示的大小计算
inSampleSize
对图片进行采样显示
1 | public Bitmap decodeFile(String filePath) { |
- 使用
inBitmap
优化Bitmap
内存使用
参考1: Android Developer Manage Memory
参考2: 关于 inBitmap 的知乎回答
参数 inBitmap
主要是用来复用已经存在 Bitmap
内存空间,他要求你将一个已经存在的 Bitmap
放入 options
,这样新创建的 Bitmap
将会重新放在这块内存空间上,减少了内存空间的重复开辟和回收。
这个参数结合 LruCache
将会有更好的使用效果,在 LruCache
中被移除的 Bitmap
不用立刻进行回收,而是存储起来为下一个将要创建 Bitmap
提供内存空间,从而避免开辟过多的内存,造成浪费和 OOM
。
重复创建对象
在代码中常常有一些循环操作和发生频率比较高的操作,在这类操作中应该尽量避免创建对象,虽然不会导致内存泄漏但是频繁创建和销毁对象会占用大量内存和引起显著的内存抖动,例如:
- 高频操作,接入
Weex
时用到自定义请求支持OkHttpClientAdapter
,每次发送请求都会走这个Adapter
,同时创建OkHttpClient
然后发送请求,在实际应用中请求发送频率很高,因此创建了大量的OkHttpClient
,后来将创建对象的代码提取到构造方法中,这个问题得到了改善。 - 循环,开发过程中,循环次数往往不可控,应该避免在循环中创建新的对象。
- 字符串拼接操作,每次字符串拼接都会产生一个新的字符串,因此如果有频繁的拼接操作,请使用
StringBuilder
。 - 方法名的定义导致使用的不当,在
getXXX()
方法中不应该进行创建对象的操作,如果有要加入仅创建一次的判断,因为当别人和自己在使用该方法可能会直接调用,因为对使用者来说,这只是一个获取操作,就会频繁的使用它,并不知道内部返回了一个全新的对象。
1 | private ViewModel mViewModel; |
大量使用 byte 内存问题
通常我们不会大量使用 byte[]
,一般用他来处理 IO
流,但是当我们有需求频繁进行 IO
时,比如文件读取、网络请求等需求,每次创建新的 byte[]
会占据大量的内存空间,原则上我们应该尽量减少内存空间的开辟,针对这种场景我们可以使用 ByteArrayPool
来管理和复用已经存在的内存空间,避免内存占用过多和内存抖动的发生。
下面是参考 Glide
源码中的一个设计,ByteArrayPool
是一个单例,里面维护一个 Queue
,每次使用 byte[]
时,从 Queue
中取出并删除,使用完了再放回去,以便其他人可以继续使用已经开辟的内存空间。
1 | public final class ByteArrayPool { |
目前维护的几个项目,求 ✨✨✨✨
- SocialSdk 登录分享功能原生接入
- LightAdapter 轻量级适配器
- ImageEditor 图片处理,裁剪旋转,贴纸涂鸦,滤镜等
- WeexCube Weex 容器方案
- Kotlin 学习系列总结,共计 22 篇
- 本文链接: http://cdevlab.top/article/666476b4/
- 版权声明: 版权所有,转载请注明出处!