3.内存优化
1
2
3
4
//内存优化:内存优化主要体现在两个地方,第一个是内存抖动,第二个是内存泄漏
1. 内存抖动: 主要是由于快速的内存创建和释放造成的,容易产生GC(Profiler);
2. 内存泄漏:主要是由于长生命周期的引用了短生命周期的对象造成的(leakcary);
3. 检测方式:AS的Profiler(Memory Profiler/Memory Analyzer)主动检测,leakcary被动的自动检测;

一、背景介绍

1
2
1.内存是大问题但缺乏关注
2.压死骆驼的最后一根稻草

内存问题

1
2
3
1.内存抖动:锯齿状、GC导致卡顿
2.内存泄露:可用内存减少、频繁GC
3.内存溢出:OOM、程序异常

工具选择

1
2
3
1.Memory Profiler
2.Memory Analyzer
3.LeakCanary

Memory Profiler(点击Studio底部Android Profiler界面中MEMORY区域的任意位置,即可开启Memory Profiler)

1
2
3
4
5
6
优点:
1.实时图表展示应用内存使用量
2.识别内存泄露、抖动等
3.提供捕获堆转储、强制GC以及跟踪内存分配的能力
4.方便直观
5.线下平时使用

img

1
2
3
4
5
6
7
8
9
1对应的按键用于强制内存回收。
2对应的按键用于抓取进程内存的dump信息。
3对应的按键用于记录内存的分配信息(连接Android 7.1及以下才会有此按键)。
初次点击时,对应统计的开始时间点;再次点击时,对应统计的结束时间点。
进程在两个时间点之间的内存分配信息,将被Memory Profiler记录和分析。
4对应的区域用于缩放时间轴。
5对应的按键用于显示实时的内存数据。
6对应的区域用于记录事件发生的时间点及大致持续的时间(例如activity状态改变、用户操作界面等事件)。
7对应的区域用于显示内存使用情况对应的时间轴(与标注6结合,就可以看出各事件带来的内存变化情况)。

img

1
2
3
4
5
6
7
8
9
如上图所示,其中: 
Java表示Java代码或Kotlin代码分配的内存;
Native表示C或C++代码分配的内存(即使App没有native层,调用framework代码时,也有可能触发分配native内存);
Graphics表示图像相关缓存队列占用的内存;
Stack表示native和java占用的栈内存;
Code表示代码、资源文件、库文件等占用的内存;
Others表示无法明确分类的内存;
Allocated表示Java或Kotlin分配对象的数量(Android8.0以下时,仅统计Memory Profiler启动后,进程再分配的对象数量;
8.0以上时,由于系统内置了统计工具,Memory Profiler可以得到整个app启动后分配对象的数量)。

点击左上角的标记2按钮,则出现下图可以查看各个对象的对应信息,如bitmap的大小和预览图image-20230918154331037

Memory Analyzer

1
2
3
强大的Java Heap分析工具,查找内存泄露及内存占用
生成整体报告、分析问题等
线下深入使用

LeakCanary

1
2
3
1.自动内存泄露检测
文档:https://github.com/square/leakcanary
2.线下集成

二、Android内存管理机制

Java内存区域划分

1
2
3
4
5
6
7
8
1.方法区(Method Area):用于存储类的信息、常量、静态变量等,方法区在程序启动时就被创建,所有线程共享。
其中,常量池用于存放字符串常量、类和接口常量等.
2.堆(Heap):用于存储对象实例。堆在程序运行时动态分配和回收,由垃圾回收器负责管理。
堆可以分为新生代和老年代两部分新生代又分为Eden空间、Survivor 0空间和Survivor 1空间.
虚拟机栈(VM Stack):每个线程在运行时都会创建一个栈,用于存储方法的局部变量、操作数栈、动态连接等。
栈顺随着方法3的进入和退出而动态创建和销毁。
4.本地方法栈(Native Method Stack): 与虚拟机栈类似,但用于执行本地方法(即使用其他语言编写的方法)
5.程序计数器(Program Counter) : 用于记录当前线程执行的字节码指令地址。

java内存回收算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1. 标记-清除算法(Mark and Sweep):
首先标记所有活动对象,然后清除所有未标记的对象。这种算法存在的问题是标记和清除效率不高并会产生大量的碎片,
导致内存空间的浪费。

2. 复制算法(Copying):
2.1将内存划分为大小相等的两块
2.2一块内存用完之后复制存活对象到另一块
2.3清理另一块内存
2.4实现简单,运行高效(相对标记算法而言)
2.5浪费一半空间,代价大
将内存分为两个区域,每次只使用其中一个区域,当该区域满了之后,将所有活动对象复制到另一个区域中,
然后清除原来的区域。这种算法的优点是简单高效,但是会浪费一半的内存空间。

3. 标记-整理算法(Mark and Compact):
1.标记过程与“标记-清除”算法一样
2.存活对象往一端进行移动
3.清理其余内存
4.避免标记-清理导致的内存碎片
5.避免复制算法的空间浪费
首先标记所有活动对象,然后将它们移动到内存的一端,然后清除另一端的所有未标记对象。
这种算法可以避免碎片问题,但是需要移动对象,可能会影响程序的性能。

4. 分代收集算法(Generational):
1.结合多种收集算法优势
2.新生代对象存活率低,复制
3.老年代对象存活率高,标记-整理
将内存分为几个代,每个代的对象生命周期不同。新创建的对象放在第一代,经过多次回收后仍然存活的对象会被移到下一代,
最终存活的对象会被移到老年代。这种算法可以根据对象的生命周期来优化回收效率。

5. 引用计数算法(Reference Counting):
对每个对象维护一个引用计数器,当有新的引用指向该对象时,计数器加一,当引用失效时,计数器减一。
当计数器为零时,说明该对象已经不再被引用,可以回收。这种算法的问题是无法处理循环引用的情况

Android内存管理机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1.内存弹性分配,分配值与最大值受具体设备影响
2.OOM场景:内存真正不足、可用内存不足
3.Dalvik与Art区别
1.Dalvik仅固定一种回收算法
2.Art回收算法可运行期选择(5.0之后都是这种算法)
3.Art具备内存整理能力,减少内存空洞
4.Low Memory Killer
1.进程分类(根据进程下面优先级,先考虑优先级低的进程进行回收)
1.前台进程
2.可见进程
3.服务进程
4.后台进程
5.空进程
2.回收收益

三、内存抖动实战

内存抖动介绍

1
2
3
定义:内存频繁分配和回收导致内存不稳定
表现:频繁GC、内存曲线呈锯齿状
危害:导致卡顿、OOM

内存抖动导致OOM

1
2
1.频繁创建对象,导致内存不足及碎片(不连续)
2.不连续的内存片无法被分配,导致OOM

内存抖动解决实战:使用Memory Profiler初步排查

创建一个模拟内存抖动的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.optimize.performance.memory;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

import com.optimize.performance.R;

/**
* 模拟内存抖动的界面
*/
public class MemoryShakeActivity extends AppCompatActivity implements View.OnClickListener {

@SuppressLint("HandlerLeak")
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 创造内存抖动
for (int index = 0; index <= 100; index++){
String arg[] = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0,30);
}
};

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory);
findViewById(R.id.bt_memory).setOnClickListener(this);

}

@Override
public void onClick(View v) {
mHandler.sendEmptyMessage(0);
}

@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}

点击执行后发送内存抖动时Memory Profiler的现象:image-20230918171625886

查找具体引起抖动的方式:点击record开始记录,再点击stop停止记录(如下图);

image-20230918172026002

左侧可以查看到不同对象的size大小,选中后右侧上面随意选择一个对象底部则会显示对象的调用信息,点击右键出现Jump to Source跳转到方法的调用处;

内存抖动解决技巧:找循环或者频繁调用的地方

四、内存泄漏实战

内存泄露介绍

1
2
3
定义:内存中存在已经没有用的对象
表现:内存抖动、可用内存逐渐变少
危害:内存不足、GC频繁、OOM

Memory Analyzer

1
2
文档:https://www.eclipse.org/mat/downloads.ph
转换: hprof-conv 原文件路径 转后文件路径

模拟内存泄漏代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.optimize.performance.memory;
import java.util.ArrayList;
public class CallBackManager {
public static ArrayList<CallBack> sCallBacks = new ArrayList<>();
public static void addCallBack(CallBack callBack) {
sCallBacks.add(callBack);
}
public static void removeCallBack(CallBack callBack) {
sCallBacks.remove(callBack);
}
}


package com.optimize.performance.memory;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;
import com.optimize.performance.R;
/**
* 模拟内存泄露的Activity
*/
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memoryleak);
ImageView imageView = findViewById(R.id.iv_memoryleak);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
imageView.setImageBitmap(bitmap);
CallBackManager.addCallBack(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
//这行解决内存泄漏的代码
CallBackManager.removeCallBack(this);
}
@Override
public void dpOperate() {
// do sth
}
}

通过多次进入再退出上面的MemoryLeakActivity类则会出现下图这种内存泄漏情况:image-20230919091415496

通过record左边的down按钮并保存文件,再通过命令转换文件格式:

image-20230919091622418

在使用Eclipse Memory Analyzer软件打开转换后的文件

image-20230919091803750

点击 Histogram 按钮

image-20230919091925652

在上图位置可以搜索怀疑存在内存泄漏的类,并查看显示的该类剩余的对象数量;

选中类的对象右键点击List Objects->with incoming references(哪些强引用引向了我)->选中类对象->Path to GC->with all references(所有对象都计算在内)分析后如下图,有个小圆点的则是目标内存泄漏的类

image-20230919092826271

总结

1
2
1.使用Memory Profiler初步观察
2.通过Memory Analyzer结合代码确认

五、MAT的使用

1
2
3
MAT工具全称为Memory Analyzer Tool,一款详细分析Java堆内存的工具,该工具非常强大

代码太多,可百度查看使用教程:如https://www.jianshu.com/p/034b72b91d3a/

六、优雅的检测不合理的图片

Bitmap内存模型

1
2
3
4
5
6
1.API10之前Bitmap自身在Dalvik Heap中,像素在Native
2.API10之后像素也被放在Dalvik Heap中
3.API26之后像素在Native
4.获取Bitmap占用内存
- getByteCount
- 宽*高*一像素占用内存

常规方式

1
2
3
当景:图片对内存优化至关重要、图片宽高大于控件宽高
实现:继承ImageView,覆写实现计算大小
缺点:侵入性强,不通用

ARTHook介绍

1
2
3
4
5
6
7
8
挂钩,将额外的代码钩住原有方法,修改执行逻辑
- 运行时插桩
- 性能分析
优点:
- 无侵入性
- 通用性强
缺点:
- 兼容问题大,开源方案不能带的线上环境

Epic简介

1
2
3
4
5
6
7
Epic是一个虚拟机层面、以Java Method为粒度的运行时Hook框架
- 支持Android4.0-9.0
- https://github.com/tiann/epic
Epic使用
- compile 'me.weishu:epic:0.3.6
- 继承XC_MethodHook,实现相应逻辑
- 注入Hook : DexposedBridge.findAndHookMethod

实战自定义Epic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//compile 'me.weishu:epic:0.3.6

package com.optimize.performance.memory;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;

import com.optimize.performance.utils.LogUtils;
import com.taobao.android.dexposed.XC_MethodHook;

public class ImageHook extends XC_MethodHook {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 实现我们的逻辑
ImageView imageView = (ImageView) param.thisObject;
checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
}

private static void checkBitmap(Object thiz, Drawable drawable) {
if (drawable instanceof BitmapDrawable && thiz instanceof View) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
final View view = (View) thiz;
int width = view.getWidth();
int height = view.getHeight();
if (width > 0 && height > 0) {
// 图标宽高都大于view带下的2倍以上,则警告
if (bitmap.getWidth() >= (width << 1)
&& bitmap.getHeight() >= (height << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
}
} else {
final Throwable stackTrace = new RuntimeException();
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int w = view.getWidth();
int h = view.getHeight();
if (w > 0 && h > 0) {
if (bitmap.getWidth() >= (w << 1)
&& bitmap.getHeight() >= (h << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
}
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
}
});
}
}
}
}


private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
String warnInfo = new StringBuilder("Bitmap size too large: ")
.append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
.append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
.append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
.toString();

LogUtils.i(warnInfo);
}
}
1
2
3
4
5
6
7
8
//调用执行ImageHook
DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
}
});

再在MemoryLeakActivity的activity_memoryleak的xml中将imageview宽高修改为50

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/iv_memoryleak"
android:layout_width="50dp"
android:layout_height="50dp" />

</android.support.constraint.ConstraintLayout>

启动下面MemoryLeakActivity类,然后会执行检测,运行后弹出警告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.optimize.performance.memory;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.widget.ImageView;
import com.optimize.performance.R;
/**
* 模拟内存泄露的Activity
*/
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memoryleak);
ImageView imageView = findViewById(R.id.iv_memoryleak);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
imageView.setImageBitmap(bitmap);
CallBackManager.addCallBack(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
CallBackManager.removeCallBack(this);
}
@Override
public void dpOperate() {
// do sth
}
}

七、线上内存监控方案

常规实现一

1
设定场景线上Dump:Debug.dumpHprofData()

常规实现一流程

1
超过最大内存80%(只在特定情况下执行,如占用可用内存超过80%)->内存Dump->回传文件(wifi或无流量空闲时上传)->MAT手动分析

实现一问题:

1
2
3
1.Dump文件太大,和对象数正相关,可裁剪
2.上传失败率高、分析困难
3.配合一定策略,有一定效果

常规实现二

1
2
3
1.LeakCanary带到线上
2.预设泄露怀疑点
3.发现泄露回传

实现二问题:

1
2
1.不适合所有情况,必须预设怀疑点
2.分析比较耗时、也容易OOM

LeakCanary原理

1
2
3
4
1.监控生命周期,onDestroy添加RefWatcher检测
2.二次确认断定发生内存泄露
3.分析泄露,找引用链
4.监控组件+分析组件

LeakCanary定制

1
2
3
1.预设怀疑点->自动找怀疑点
2.分析泄露链路慢->分析Retain size大的对象
3.分析OOM->对象裁剪,不全部加载到内存

线上监控完整方案

1
2
3
1.待机内存、重点模块内存、OOM率
2.整体及重点模块GC次数、GC时间
3.增强的LeakCanary自动化内存泄露分析

八、内存优化技巧总结

优化大方向

1
2
3
1.内存泄露
2.内存抖动
3.Bitmap

优化细节

1
2
3
4
5
6
1.LargeHeap属性(如:系统默认256m,申请了LargeHeap属性则默认内存*2
2.onTrimMemory(低内存回调,低内存时主动做一些处理,关闭一些功能等)
3.使用优化过的集合:SparseArray
4.谨慎使用SharedPreference(调用时会将缓存的东西全部加载到内存中去,或者大数据量的东西不要用SharedPreference)
5.谨慎使用外部库(怕存在隐患)
6.业务架构设计合理

九、常见问题

1
2
3
4
5
6
7
8
9
10
11
1.你们内存优化项目的过程是怎么做的
- 分析现状、确认问题
- 针对性优化
- 效率提升
2.你做了内存优化最大的感受是什么
- 磨刀不误砍柴工
- 技术优化必须结合业务代码
- 系统化完善解决方案
3.如何检测所有不合理的地方
- ARTHook
- 重点强调区别