1.前言
上一篇文章关于Android性能优化--启动优化探讨了启动优化相关的知识点,在本篇将介绍内存优化的相关优化。主要大纲参照如下
2.常见问题
常见的Android内存相关问题,通常可以分为以下三种,内存抖动、内存泄露、内存溢出。
- 内存抖动:在短时间内有大量的对象被创建或者被回收的现象,主要是循环中大量创建、回收对象。当系统内存不足,不断GC内存的时候,也有可能出现内存抖动情况。
- 内存泄露:当一个对象不在使用了,本应该被垃圾回收器回收。但是这个对象由于被其他正在使用的对象所持有,造成无法被回收的现象。
- 内存溢出:Android系统给每个应用分配的内存也有一个阀值,也就是Heap Size。当应用占用的内存加上我们申请的内存资源超过了系统分配的最大内存时就会抛出的Out Of Memory异常。
上述三者之间的是一个递进关系,内存抖动<内存泄露<内存溢出。对于一般应用主要是处理内存抖动和内存泄露两点,处理好这两点就会大大降低内存溢出的可能性。
3.内存管理
3.1 Java内存管理
JVM的内存回收对于大多数开发者来说接触的并不是很多。因为JVM本身是有一套内存回收的机制,对于开发者更多的是申请对象直接调用即可,其内部并不是很在意。下面主要通过内存存储和回收这两块介绍。
- 存储:JVM将可以存储内存的空间大概分为栈、本地方法栈、程序计数器、堆、方法区等模块。
- 栈:主要是针对方法使用的空间,当JVM在执行方法时,会在此区域中创建一个栈帧来存放方法的各种信息,比如返回值,局部变量表和各种对象引用等。
- 本地方法栈:专门提供给Native方法用的。
- 程序计数器:记录当前执行的位置。
- 堆:几乎所有对象、数组等都是在此分配内存的,在JVM内存中占的比例也是很大的,也是GC回收的主要阵地,平时我们说的新生代、老年代、永久代也是指这片区域。
- 方法区:存放类似类定义、常量、编译后的代码、静态变量等。
- 回收:针对上述各个模块的内存回收,通常所说的GC主要是对堆空间的回收,一般比较常用的方法为:标记-清除算法、复制算法、分代收集算法等其它方法和其变形。
3.2 Android内存管理
Android 系统主要是在Art和Dalvik虚拟机中的托管环境中跟踪每个内存分配,当发现有可回收的对象,进行内存回收。回收有两个目标:在程序中查找将来无法访问的数据对象,并回收那些对象使用的资源 。
- 进程间的内存管理:Android对于进程间的内存管理主要是通过内核交换守护程序和onTrimMemory()进程杀死来管理。
- 内核交换守护程序(kswapd):RAM中存在一个区域空间zRAM。当设备上的可用内存不足时,守护程序将变为活动状态。kswapd可以将缓存的私有脏页和匿名脏页移动到zRAM,并在其中进行压缩。
- onTrimMemory:系统用于
onTrimMemory()
通知应用程序内存即将用尽,并应减少其分配。如果这还不够,内核将开始杀死进程以释放内存。它使用低内存杀手(LMK)来执行此操作。PS:LMK 这就会涉及到应用保活等相关。
- 应用内存管理:Android应用内内存管理,主要是从Java层和Native 层优化。本文主要介绍如何从Java层进行内存管理优化,具体细节可以下面会一一介绍。
4.常见场景及解决方案
4.1 内存抖动
由于短时间内有大量对象进出Young Generiation区导致的,它伴随着频繁的GC。
- 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
- 注意自定义View的onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象。
如下面一部分代码就对应着内存抖动
Handler handler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
for (int i =0;i<100;i++){
String string[] = new String[10000];
}
handler.sendEmptyMessageDelayed(1,30);
}
};
通过Profile
查看其内存图
可以看到其内存图基本上是一个锯齿状,是因为这时候一直在创建对象和回收对象所致。
4.2 内存泄露
业内一般对内存泄露的原因总结为长生命周期对象引用短生命周期对象,导致短生命周期对象无法及时回收
所致。
- 单例引起的内存泄漏
public static FacebookAnalysis getInstance(Context context){
if (facebookAnalysis == null){
synchronized (FacebookAnalysis.class){
facebookAnalysis = new FacebookAnalysis(getAppEventsLoggerInstance(context));
}
}
return facebookAnalysis;
}
上面是一个常见的单例模式,如果参数引用Activity的Context,而单例模式的生命周期长于Activity。这里单例模式引用Activity的实例,当Activity被销毁,Activity无法被回收,造成内存泄露。
如果这里引用的Application的Context,将无任何影响。因为Application的生命周期与单例模式同样长。
- 静态集合添加对象,在使用完之后未及时释放。
for (int i = 0; i < 10; i++) {
Object obj = new Object();
list.add(obj);
obj = null;
}
此时list是一个静态的集合,obj单个对象,当list集合使用完毕,应当及时清除该集合,避免obj被静态对象引用。
- 匿名内部类&非静态内部类
Android 中常见的是对ListView
中各个元素设置点击事件,如果此时采用匿名内部类,会存在内存泄露的风险。常规做法是将该点击事件用接口和setTag()
的方式往外传递。
同样Handler在使用过程中也会出现内存泄露的风险,一般则是采用弱引用
的方式处理,或者在Activity 的onDestroy
方法中移除该Handler的所有消息handler.removeCallbacksAndMessages(null)
。 - 线程泄露
在主线程Activity中出现如下代码:
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
mIntent = (icicle == null) ? getIntent() : null;
new Thread(new Runnable() {
@Override
public void run() {
doSomeThing();
}
}).start();
}
当此时该Activity已经销毁,但是子线程中doSomeThing
方法未执行完成,此时会造成内存泄露。一般做法是当Activity销毁时取消该线程或者采用其他方式实现,总之原则是线程不持有Activity的上下文,如果持有,就应及时取消。
- 数据库游标,文件资源未及时关闭,广播未反向注册,服务未解绑等行为。
- Bitmap加载泄露,Bitmap的加载在Android一直是比较吃内存的,且容易出现内存泄露相关问题,一般都是采用统一的图片请求框架去处理图片加载缓存,这些框架都会从加载、压缩、缓存等策略对其做优化处理。同时Google 官方也是推荐使用统一库处理位图,具体可以在Glide官网查看。关于图片加载这块其实是很容易出现内存泄露的问题,在此暂时不作展开,后续会细说。
5.常用工具
5.1 Memory Profiler
Memory Profiler
是 Android Profiler
中的一个组件,可帮助您识别可能会导致应用卡顿、冻结甚至崩溃的内存泄漏和内存抖动。它显示一个应用内存使用量的实时图表,可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。从图中现象
可以看出应用此时存在内存抖动的现象,此时抓取红色部分,可以得到如下图:
-
A
区域为拖住怀疑动动的部分
-
B
区域为排序发现存在一组对象耗内存
-
D
选中
C
区域中任一对象,即可看见具体类为
MainActivity
,且可以看到行数,右击
Jump to Source
即可以跳入具体代码。
5.2 Memory Analyzer
Memory Analyzer MAT
是一个功能丰富的 JAVA 堆转储文件分析工具,可以帮助开发者发现内存漏洞和减少内存消耗。根据上面分析下面这段代码是存在内存泄露的问题:
public class CallBackManager {
public static ArrayList sCallBacks = new ArrayList<>();
public static void addCallBack(CallBack callBack) {
sCallBacks.add(callBack);
}
public static void removeCallBack(CallBack callBack) {
sCallBacks.remove(callBack);
}
}
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{
@Override
protected void onCreate( Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memoryleak);
ImageView imageView = findViewById(R.id.iv_memoryleak);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.splash);
imageView.setImageBitmap(bitmap);
CallBackManager.addCallBack(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
// CallBackManager.removeCallBack(this);
}
@Override
public void doWork() {
// do work
}
}
连续进入退出MemoryLeakActivity
,通过Android Studio
的Profile
可以查看到当时的内存入下图
可以看到此时内存处于一个波动状态,保存该文件,生成
memory-20191212T110510.hprof
文件,通过命令转换
D:\>hprof-conv D:\Android\log\memory-20191212T110510.hprof D:\Android\log\2.hprof
此时通过MAT打开转换后的文件如下:
选择
Histogram
,输入正则表达".
MemoryLeak. "可以搜索到具体包名,然后右键
List objects
->
With incoming references
然后选择
Path to GC Roots
->
With all references
(此处可以选择其他)。此时可以看到下面这张图
从图中即可看出内存泄露的位置,即该Activity被对象
sCallbacks
引用。在代码中添加方法
protected void onDestroy() {
super.onDestroy();
CallBackManager.removeCallBack(this);
}
再次抓取内存信息,并未出现上述结果。
5.3 LeakCanary
可以通过LeakCanary在开发阶段检测到引用的内存情况。LeakCanary 主要是通过监听Activity的onDestory,手动调用GC,然后通过ReferenceQueue+WeakReference,来判断Activity对象是否被回收,然后结合dump Heap的hpof文件,通过Haha开源库分析泄露的位置。具体使用可以参照leakcanary。
6. 总结
关于内存优化知识点很多,很细。但究其根本我认为是监控内存泄露和优化内存泄漏,各大厂商都有提过相关的方案
美团—Android线上OOM问题定位组件
微信 Android 终端内存优化实践
这些都具备参考价值。同时我们也可以采用一些Hook
黑科技相关方法进行部分内存性能消耗较大的业务进行监控,及时告知开发人员。例如:可以通过Epic监控项目中所有的setImageBitmap()
方法,此时就可以知道传入的Bitmap
是否有内存相关风险,一旦有风险,立马通知反馈。
以上为此次Android内存优化的总结,欢迎指正。
感谢:
https://developer.android.google.cn/topic/performance/memory-overview
https://time.geekbang.org/column/article/71610
https://coding.imooc.com/learn/list/308.html