内存优化是Android开发中的一个重要课题,它关系到应用的性能和稳定性,以及用户的体验和留存。为了有效地进行内存优化,我们需要搭建一个内存优化的体系,从不同的角度和层次来分析和解决内存问题。本文将从以下几个方面入手,介绍如何搭建一个内存优化的体系:
一、设备分级优化策略
设备分级优化策略是一种根据设备性能的好坏来使用不同的内存分配和回收策略的方法,它可以针对不同类型的用户提供不同质量的服务,从而提高用户满意度和留存率。
①、设备性能评估:设备性能评估是指对设备的硬件参数(如CPU、RAM、ROM等)和软件参数(如Android版本、ROM版本等)进行综合评估,给设备打上不同等级的标签(如高端机、中端机、低端机等)。设备性能评估可以通过以下几种方法进行:
- 静态评估:静态评估是指在应用启动时或者安装时获取设备的硬件参数和软件参数,并根据预先定义好的规则给设备打上标签。静态评估的优点是简单快速,缺点是不能反映设备在运行时的实际性能。例如:
// 一个简单的静态评估方法,它根据设备可用内存大小给设备打上高端机、中端机或低端机的标签
public String getDeviceLevel() {
// 获取ActivityManager对象
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
// 获取设备可用内存大小(单位为MB)
int memorySize = activityManager.getMemoryClass();
// 根据内存大小判断设备等级
if (memorySize >= 256) {
return "高端机";
} else if (memorySize >= 128) {
return "中端机";
} else {
return "低端机";
}
}
- 动态评估:动态评估是指在应用运行时根据设备的实时性能数据(如CPU使用率、内存使用率、电量消耗等)给设备打上标签。动态评估的优点是能够反映设备在运行时的实际性能,缺点是需要消耗更多的资源,并且可能影响用户体验。例如:
// 一个简单的动态评估方法,它根据设备的CPU使用率给设备打上高性能、中性能或低性能的标签
public String getDevicePerformance() {
// 获取CPU使用率(单位为百分比)
int cpuUsage = getCpuUsage();
// 根据CPU使用率判断设备性能
if (cpuUsage < 50) {
return "高性能";
} else if (cpuUsage < 80) {
return "中性能";
} else {
return "低性能";
}
}
二、内存分配和回收策略:
内存分配和回收策略是指根据设备的不同等级来使用不同的内存分配和回收策略,从而提高内存利用率和减少内存抖动。
①、图片加载策略:图片加载策略是指根据设备的不同等级来使用不同的图片加载策略,从而减少图片占用的内存空间和触发的GC次数。图片加载策略可以从以下几个方面进行:
-
1、图片格式:图片格式是指图片在内存中的编码方式,不同的图片格式会占用不同的内存空间。一般来说,ARGB_8888格式的图片会占用最多的内存空间,每个像素占用4个字节;RGB_565格式的图片会占用较少的内存空间,每个像素占用2个字节;但是RGB_565格式的图片会损失一些颜色信息,导致图片质量下降。因此,我们可以根据设备的不同等级来选择不同的图片格式,对于高端机用户可以使用ARGB_8888格式的图片,对于低端机用户可以使用RGB_565格式的图片。例如:
```java // 一个简单的图片加载策略,它根据设备等级来选择不同的图片格式 public Bitmap loadBitmap(String url) { // 获取设备等级 String deviceLevel = getDeviceLevel(); // 创建一个BitmapFactory.Options对象,用来设置图片加载选项 BitmapFactory.Options options = new BitmapFactory.Options(); // 根据设备等级设置图片格式 if (deviceLevel.equals("高端机")) { options.inPreferredConfig = Bitmap.Config.ARGB_8888; } else if (deviceLevel.equals("低端机")) { options.inPreferredConfig = Bitmap.Config.RGB_565; } // 使用BitmapFactory.decodeStream方法从网络流中加载图片,并返回Bitmap对象 return BitmapFactory.decodeStream(getInputStreamFromUrl(url), null, options); } ```
2、图片尺寸:图片尺寸是指图片在内存中的宽度和高度,不同的图片尺寸会占用不同的内存空间。一般来说,越大的图片会占用越多的内存空间;但是如果图片尺寸超过了显示的View或屏幕的大小,就会造成内存的浪费,因为多余的像素并不会显示出来。因此,我们可以根据设备的不同等级和显示的View或屏幕的大小来选择合适的图片尺寸,对于高端机用户可以使用原始尺寸的图片,对于低端机用户可以使用缩小后的图片。例如:
// 一个简单的图片加载策略,它根据设备等级和显示的View大小来选择合适的图片尺寸
public Bitmap loadBitmap(String url, ImageView imageView) {
// 获取设备等级
String deviceLevel = getDeviceLevel();
// 创建一个BitmapFactory.Options对象,用来设置图片加载选项
BitmapFactory.Options options = new BitmapFactory.Options();
// 根据设备等级设置图片格式
if (deviceLevel.equals("高端机")) {
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
} else if (deviceLevel.equals("低端机")) {
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
// 如果设备等级是低端机,就设置图片加载选项为只获取图片边界信息,而不加载图片本身
if (deviceLevel.equals("低端机")) {
options.inJustDecodeBounds = true;
}
// 使用BitmapFactory.decodeStream方法从网络流中加载图片,并返回Bitmap对象
Bitmap bitmap = BitmapFactory.decodeStream(getInputStreamFromUrl(url), null, options);
// 如果设备等级是低端机,就根据显示的View大小计算合适的缩放比例,并重新加载图片
if (deviceLevel.equals("低端机")) {
// 获取显示的View的宽度和高度
int viewWidth = imageView.getWidth();
int viewHeight = imageView.getHeight();
// 获取图片的原始宽度和高度
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;
// 计算缩放比例,取宽度和高度中较大的值
int scale = Math.max(imageWidth / viewWidth, imageHeight / viewHeight);
// 设置图片加载选项为使用缩放比例加载图片
options.inJustDecodeBounds = false;
options.inSampleSize = scale;
// 重新使用BitmapFactory.decodeStream方法从网络流中加载图片,并返回Bitmap对象
bitmap = BitmapFactory.decodeStream(getInputStreamFromUrl(url), null, options);
}
// 返回Bitmap对象
return bitmap;
}
②、动画和重功能策略:动画和重功能策略是指根据设备的不同等级来使用不同的动画和重功能策略,从而减少动画和重功能对内存和CPU的消耗。
-
1、动画开关:动画开关是指根据设备的不同等级来决定是否开启或关闭一些复杂或消耗资源的动画效果,从而提高应用的流畅性和稳定性。对于高端机用户可以开启所有的动画效果,对于低端机用户可以关闭一些非必要或影响性能的动画效果。例如:
// 一个简单的动画开关策略,它根据设备性能来决定是否开启或关闭一个转场动画效果 public void startTransitionAnimation() { // 获取设备性能 String devicePerformance = getDevicePerformance(); // 根据设备性能设置动画开关 if (devicePerformance.equals("高性能")) { // 如果设备性能是高性能,就开启转场动画效果 overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); } else if (devicePerformance.equals("低性能")) { // 如果设备性能是低性能,就关闭转场动画效果 overridePendingTransition(0, 0); } }
-
2、重功能开关:重功能开关是指根据设备的不同等级来决定是否开启或关闭一些复杂或消耗资源的功能,从而提高应用的可用性和兼容性。对于高端机用户可以开启所有的功能,对于低端机用户可以关闭一些非必要或影响性能的功能。例如:
// 一个简单的重功能开关策略,它根据设备等级来决定是否开启或关闭一个视频播放功能 public void startVideoPlayer() { // 获取设备等级 String deviceLevel = getDeviceLevel(); // 根据设备等级设置重功能开关 if (deviceLevel.equals("高端机")) { // 如果设备等级是高端机,就开启视频播放功能 videoView.setVideoPath(videoUrl); videoView.start(); } else if (deviceLevel.equals("低端机")) { // 如果设备等级是低端机,就关闭视频播放功能,并显示一个提示信息 videoView.setVisibility(View.GONE); textView.setText("您的设备不支持视频播放功能,请升级您的设备"); } }
三、建立统一的缓存管理组件
缓存管理组件是一种用来管理应用中各种缓存数据(如图片缓存、网络缓存、数据库缓存等)的组件,它可以根据系统不同的状态去释放相应的缓存与内存,从而提高应用的响应速度和稳定性。
①、缓存分类:缓存分类是指根据缓存数据的不同特点和重要性,将缓存数据分为不同的类别(如强引用缓存、软引用缓存、弱引用缓存、虚引用缓存等)。不同类别的缓存数据会有不同的生命周期和回收策略。例如:
1、强引用缓存:强引用缓存是指使用强引用(如普通变量)来保存缓存数据的方式,它可以保证缓存数据不会被GC回收,除非手动释放或者应用退出。强引用缓存适合保存一些非常重要或者频繁使用的缓存数据,但是也要注意控制其大小和数量,避免OOM。例如:
// 一个简单的强引用缓存示例,它使用一个HashMap来保存图片对象
public class ImageCache {
// 创建一个HashMap对象,用来保存图片对象
private HashMap<String, Bitmap> map = new HashMap<>();
// 将图片对象添加到HashMap中
public void put(String key, Bitmap bitmap) {
map.put(key, bitmap);
}
// 从HashMap中获取图片对象
public Bitmap get(String key) {
return map.get(key);
}
// 从HashMap中移除图片对象
public void remove(String key) {
map.remove(key);
}
// 清空HashMap中所有图片对象
public void clear() {
map.clear();
}
}
2、软引用缓存:软引用缓存是指使用软引用(如SoftReference)来保存缓存数据的方式,它可以保证缓存数据在内存充足时不会被GC回收,但是在内存不足时会被GC回收。软引用缓存适合保存一些重要性较低或者容易重新获取的缓存数据,但是也要注意及时释放不再需要的缓存数据,避免内存泄漏。例如:
// 一个简单的软引用缓存示例,它使用一个HashMap来保存图片对象的软引用
public class ImageCache {
// 创建一个HashMap对象,用来保存图片对象的软引用
private HashMap<String, SoftReference<Bitmap>> map = new HashMap<>();
// 将图片对象的软引用添加到HashMap中
public void put(String key, Bitmap bitmap) {
map.put(key, new SoftReference<>(bitmap));
}
// 从HashMap中获取图片对象的软引用,并返回图片对象
public Bitmap get(String key) {
SoftReference<Bitmap> reference = map.get(key);
if (reference != null) {
return reference.get();
} else {
return null;
}
}
// 从HashMap中移除图片对象的软引用
public void remove(String key) {
map.remove(key);
}
// 清空HashMap中所有图片对象的软引用
public void clear() {
map.clear();
}
}
3、弱引用缓存:弱引用缓存是指使用弱引用(如WeakReference)来保存缓存数据的方式,它可以保证缓存数据在被引用时不会被GC回收,但是在没有被引用时会被GC回收。弱引用缓存适合保存一些重要性最低或者最容易重新获取的缓存数据,但是也要注意及时释放不再需要的缓存数据,避免内存泄漏。例如:
// 一个简单的弱引用缓存示例,它使用一个HashMap来保存图片对象的弱引用
public class ImageCache {
// 创建一个HashMap对象,用来保存图片对象的弱引用
private HashMap<String, WeakReference<Bitmap>> map = new HashMap<>();
// 将图片对象的弱引用添加到HashMap中
public void put(String key, Bitmap bitmap) {
map.put(key, new WeakReference<>(bitmap));
}
// 从HashMap中获取图片对象的弱引用,并返回图片对象
public Bitmap get(String key) {
WeakReference<Bitmap> reference = map.get(key);
if (reference != null) {
return reference.get();
} else {
return null;
}
}
// 从HashMap中移除图片对象的弱引用
public void remove(String key) {
map.remove(key);
}
// 清空HashMap中所有图片对象的弱引用
public void clear() {
map.clear();
}
}
4、虚引用缓存:虚引用缓存是指使用虚引用(如PhantomReference)来保存缓存数据的方式,它可以保证缓存数据在被回收前可以执行一些清理操作,但是不能通过虚引用获取缓存数据。虚引用缓存适合保存一些需要在回收前执行清理操作的缓存数据,但是也要注意及时释放不再需要的缓存数据,避免内存泄漏。例如:
// 一个简单的虚引用缓存示例,它使用一个HashMap和一个ReferenceQueue来保存图片对象的虚引用,并在回收前执行清理操作
public class ImageCache {
// 创建一个HashMap对象,用来保存图片对象的虚引用
private HashMap<String, PhantomReference<Bitmap>> map = new HashMap<>();
// 创建一个ReferenceQueue对象,用来接收被回收的虚引用
private ReferenceQueue<Bitmap> queue = new ReferenceQueue<>();
// 将图片对象的虚引用添加到HashMap和ReferenceQueue中
public void put(String key, Bitmap bitmap) {
PhantomReference<Bitmap> reference = new PhantomReference<>(bitmap, queue);
map.put(key, reference);
}
// 从HashMap中获取图片对象的虚引用,并返回图片对象(注意:这个方法可能返回null,因为虚引用不能保证获取到缓存数据)
public Bitmap get(String key) {
PhantomReference<Bitmap> reference = map.get(key);
if (reference != null) {
return reference.get();
} else {
return null;
}
}
// 从HashMap中移除图片对象的虚引用
public void remove(String key) {
map.remove(key);
}
// 清空HashMap中所有图片对象的虚引用
public void clear() {
map.clear();
}
// 在回收前执行清理操作,如关闭文件流、释放资源等
public void clean() {
PhantomReference<Bitmap> reference = (PhantomReference<Bitmap>) queue.poll();
while (reference != null) {
// 执行清理操作
reference.clean();
// 移除虚引用
remove(reference.getKey());
// 获取下一个虚引用
reference = (PhantomReference<Bitmap>) queue.poll();
}
}
}
②、OnTrimMemory/LowMemory回调:
OnTrimMemory/LowMemory回调是指Android系统在内存不足时会通知应用进行内存释放的机制,它可以让应用根据系统不同的状态去释放相应的缓存与内存,从而提高应用的稳定性和用户体验。
1、OnTrimMemory回调:
OnTrimMemory回调是指Android系统在内存不足时会通知应用的Component(如Activity、Service等)进行内存释放的方法,它可以让应用根据不同的内存等级(如TRIM_MEMORY_RUNNING_LOW、TRIM_MEMORY_UI_HIDDEN等)去释放相应的缓存与内存。例如:
public class MyActivity extends Activity {
// 创建一个ImageCache对象,用来管理图片缓存
private ImageCache imageCache = new ImageCache();
// 重写onTrimMemory方法,接收系统的内存通知
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
// 根据不同的内存等级释放不同的缓存数据
switch (level) {
case TRIM_MEMORY_RUNNING_LOW:
// 当应用运行时内存低时,释放一些低优先级的缓存数据,如软引用缓存、弱引用缓存等
imageCache.releaseLowPriorityCache();
break;
case TRIM_MEMORY_UI_HIDDEN:
// 当应用UI隐藏时,释放一些和UI相关的缓存数据,如强引用缓存、动画缓存等
imageCache.releaseUiRelatedCache();
break;
case TRIM_MEMORY_COMPLETE:
// 当应用即将被杀死时,释放所有的缓存数据,避免内存泄漏
imageCache.releaseAllCache();
break;
}
}
}
2、LowMemory回调:
LowMemory回调是指Android系统在内存极度不足时会通知应用的Application进行内存释放的方法,它可以让应用在紧急情况下释放尽可能多的缓存与内存。例如:
```java
// 以下代码是一个简单的LowMemory回调示例,它在收到系统的内存通知时释放所有的缓存数据
public class MyApplication extends Application {
// 创建一个ImageCache对象,用来管理图片缓存
private ImageCache imageCache = new ImageCache();
// 重写onLowMemory方法,接收系统的内存通知
@Override
public void onLowMemory() {
super.onLowMemory();
// 释放所有的缓存数据,避免内存泄漏
imageCache.releaseAllCache();
}
}
```
四、低端机避免使用多进程
多进程是一种让应用在多个独立的进程中运行的方式,它可以提高应用的稳定性和安全性,但是也会增加应用的内存消耗和通信成本。一般来说,一个空进程也会占用10MB左右的内存,如果应用使用了多个进程,那么它们之间还需要通过Binder机制进行跨进程通信,这也会消耗一定的内存和CPU资源。因此,对于低端机用户,我们应该尽可能减少使用多进程,只在必要的情况下使用多进程。例如:
①、服务进程:
服务进程是指运行在后台的进程,android:process:是否需要在单独的进程中运行,当设置为android:process=”:remote”时,代表Service在单独的进程中运行。它可以执行一些长时间或者重要的任务,如下载、音乐播放等。服务进程可以提高应用的稳定性和用户体验,但是也会占用一定的内存空间。对于低端机用户,我们可以根据服务的重要性和用户需求来决定是否使用服务进程,或者使用前台服务来提高服务的优先级,避免被系统杀死。例如:
public class DownloadService extends Service {
// 定义一个NotificationManager对象,用来管理通知
private NotificationManager notificationManager;
// 定义一个Notification对象,用来显示通知
private Notification notification;
// 定义一个NotificationChannel对象,用来设置通知渠道(Android 8.0以上需要)
private NotificationChannel notificationChannel;
// 定义一个下载任务对象,用来执行下载操作
private DownloadTask downloadTask;
@Override
public void onCreate() {
super.onCreate();
// 初始化NotificationManager对象
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// 初始化Notification对象
notification = new NotificationCompat.Builder(this, "download")
.setContentTitle("下载服务")
.setContentText("正在下载...")
.setSmallIcon(R.mipmap.ic_launcher)
.setProgress(100, 0, false)
.build();
// 如果Android版本大于等于8.0,就初始化NotificationChannel对象,并将其添加到NotificationManager中
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationChannel = new NotificationChannel("download", "下载服务", NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(notificationChannel);
}
// 启动前台服务,提高服务的优先级,避免被系统杀死
startForeground(1, notification);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 获取下载的文件地址
String fileUrl = intent.getStringExtra("fileUrl");
// 初始化下载任务对象,并执行下载操作
downloadTask = new DownloadTask(new DownloadListener() {
@Override
public void onProgress(int progress) {
// 更新通知的进度条
notification.setProgress(100, progress, false);
notificationManager.notify(1, notification);
}
@Override
public void onSuccess() {
// 下载成功后,更新通知的文本,并发送一个广播
notification.setContentText("下载完成");
notificationManager.notify(1, notification);
sendBroadcast(new Intent("DOWNLOAD_SUCCESS"));
// 停止前台服务和自身服务
stopForeground(true);
stopSelf();
}
@Override
public void onFailed() {
// 下载失败后,更新通知的文本,并发送一个广播
notification.setContentText("下载失败");
notificationManager.notify(1, notification);
sendBroadcast(new Intent("DOWNLOAD_FAILED"));
// 停止前台服务和自身服务
stopForeground(true);
stopSelf();
}
});
downloadTask.execute(fileUrl);
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
// 如果下载任务还在进行中,就取消下载任务
if (downloadTask != null) {
downloadTask.cancel(true);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
②、插件进程:
插件进程是指运行在独立的进程中的插件,它可以提供一些额外的功能,如地图、支付等。插件进程可以提高应用的扩展性和安全性,但是也会增加应用的内存消耗和通信成本。对于低端机用户,我们可以根据插件的重要性和用户需求来决定是否使用插件进程,或者使用按需加载的方式来减少不必要的内存占用。例如:
// 使用RePlugin框架来加载和启动一个地图插件
public class MapActivity extends Activity {
// 定义一个RePluginCallback对象,用来接收插件加载和启动的回调
private RePluginCallback callback = new RePluginCallback() {
@Override
public void onInstallPluginFailed(String s, InstallResult installResult) {
// 当插件安装失败时,显示一个提示信息
Toast.makeText(MapActivity.this, "插件安装失败:" + s, Toast.LENGTH_SHORT).show();
}
@Override
public void onPrepareAllocPitActivityFailed(Intent intent) {
// 当为插件分配坑位失败时,显示一个提示信息
Toast.makeText(MapActivity.this, "为插件分配坑位失败:" + intent, Toast.LENGTH_SHORT).show();
}
@Override
public void onStartActivityCompleted(String s, String s1, boolean b) {
// 当启动插件完成时,显示一个提示信息
Toast.makeText(MapActivity.this, "启动插件完成:" + s + ", " + s1 + ", " + b, Toast.LENGTH_SHORT).show();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_map);
// 按需加载地图插件,如果已经安装过就直接启动,如果没有安装过就先下载再安装再启动,并传入回调对象
RePlugin.startActivity(MapActivity.this,
RePlugin.createIntent("com.example.map", "com.example.map.MapActivity"),
"com.example.map",
callback);
}
}
五、线下大图片检测
大图片检测是一种用来检测应用中是否存在超过View或屏幕大小的图片的方法,它可以帮助我们发现和避免一些不必要的内存浪费和GC触发。
①、使用ARTHook或Epic等框架来监控ImageView设置的Bitmap是否超过View或屏幕的大小:
ARTHook或Epic等框架是一些可以在运行时修改或替换方法的框架,它们可以让我们在不修改源码的情况下,对ImageView的setImageBitmap方法进行拦截和处理,从而实现对大图片的检测和警告。例如:
// 使用Epic框架来拦截ImageView的setImageBitmap方法,并判断Bitmap是否超过View或屏幕的大小
public class BigImageDetector {
// 定义一个屏幕的宽度和高度
private static int screenWidth;
private static int screenHeight;
// 在Application中初始化屏幕的宽度和高度
public static void init(Context context) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
screenWidth = displayMetrics.widthPixels;
screenHeight = displayMetrics.heightPixels;
}
// 使用Epic框架来拦截ImageView的setImageBitmap方法
public static void hook() {
Epic.hook(ImageView.class, "setImageBitmap", new HookCallback() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
// 获取ImageView对象
ImageView imageView = (ImageView) param.thisObject;
// 获取Bitmap对象
Bitmap bitmap = (Bitmap) param.args[0];
if (bitmap != null) {
// 获取Bitmap的宽度和高度
int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
// 获取ImageView的宽度和高度
int viewWidth = imageView.getWidth();
int viewHeight = imageView.getHeight();
// 判断Bitmap是否超过View或屏幕的大小,如果是,就打印或上报一个警告信息
if (bitmapWidth > viewWidth || bitmapHeight > viewHeight ||
bitmapWidth > screenWidth || bitmapHeight > screenHeight) {
Log.w("BigImageDetector", "Big image detected: bitmap size = " + bitmapWidth + "x" + bitmapHeight +
", view size = " + viewWidth + "x" + viewHeight +
", screen size = " + screenWidth + "x" + screenHeight);
}
}
}
});
}
}
②、使用Memory Profiler或MAT等工具来分析hprof文件中是否存在大图片:
Memory Profiler或MAT等工具是一些可以对应用的内存使用情况进行分析和优化的工具,它们可以让我们从hprof文件中查看应用中存在的所有对象,以及它们的类型、大小、数量等。通过这些工具,我们可以发现应用中是否存在大图片,并找出它们的来源和引用路径。例如:
从示例图中可以看出:
- Heap Dump:这个功能可以让我们捕获应用在某个时间点的堆转储文件,并查看其中的内容。从堆转储文件中,我们可以看到应用中存在的所有类和实例,以及它们的类型、大小、数量等。通过对比不同时间点的堆转储文件,我们可以发现哪些类和实例是短期存在的,导致内存波动。
- Allocation Tracking:这个功能可以让我们记录应用在一段时间内创建的所有对象,并查看其中的内容。从对象分配跟踪中,我们可以看到应用中创建的所有对象,以及它们的类型、大小、数量、调用栈等。通过分析对象分配跟踪,我们可以找出哪些代码位置是创建了大量对象,导致GC频繁。
使用这两个功能,我们可以定位到导致内存抖动的代码位置和原因。经过分析,我们发现:
- 在堆转储文件中,我们发现有一个Bitmap对象占用了20MB的内存空间,它的宽度和高度分别是4000和3000,远远超过了屏幕的大小。这说明这个Bitmap对象是一个大图片,导致内存浪费。
- 在对象分配跟踪中,我们发现这个Bitmap对象是在MainActivity的onCreate方法中创建的,它是从一个网络地址加载的。这说明这个大图片是从网络加载的,可能没有经过合适的压缩和缩放。
六、线下重复图片检测
重复图片检测是一种用来检测应用中是否存在相同或者相似的图片的方法,它可以帮助我们发现和避免一些不必要的内存占用和GC触发。重复图片检测可以从以下几个方面进行:
①、使用haha库或MAT等工具来分析hprof文件中是否存在重复的Bitmap对象:
haha库或MAT等工具是一些可以对应用的内存使用情况进行分析和优化的工具,它们可以让我们从hprof文件中查看应用中存在的所有对象,以及它们的类型、大小、数量等。通过这些工具,我们可以发现应用中是否存在重复的Bitmap对象,并找出它们的来源和引用路径。例如:
经过分析,我们发现:
- 在堆转储文件中,我们发现有两个Bitmap对象占用了相同的内存空间,它们的宽度和高度分别是1000和800,它们的内容也完全相同。这说明这两个Bitmap对象是重复的图片,导致内存占用。
- 在对象分配跟踪中,我们发现这两个Bitmap对象分别是在MainActivity的onCreate方法和SecondActivity的onCreate方法中创建的,它们都是从一个本地文件加载的。这说明这两个重复图片是从本地加载的,可能没有经过合适的缓存和复用。
七、建立全局的线上Bitmap监控
线上Bitmap监控是一种用来监控应用在线上环境中对Bitmap的分配和回收情况的方法,它可以帮助我们发现和避免一些潜在的内存问题,如Bitmap滥用或泄漏。
①、对Bitmap的分配和回收进行追踪:
对Bitmap的分配和回收进行追踪是指在应用中对Bitmap类进行字节码插桩,从而在每次创建或销毁一个Bitmap对象时记录相关的信息,如Bitmap对象的引用、大小、类型、数据源、创建时间、销毁时间等。这样,我们就可以得到应用中所有创建出来的Bitmap对象的完整信息,以及它们的生命周期。例如:
// 一个简单的字节码插桩示例,它在Bitmap类的构造方法和recycle方法中插入了一些代码,用来记录Bitmap对象的信息
public class Bitmap {
// 定义一个WeakHashMap对象,用来保存所有创建出来的Bitmap对象及其信息
private static WeakHashMap<Bitmap, BitmapInfo> map = new WeakHashMap<>();
// 定义一个DateFormat对象,用来格式化时间
private static DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 在构造方法中插入代码,用来记录Bitmap对象的创建信息
public Bitmap(int width, int height, Config config) {
// 调用原始的构造方法
super(width, height, config);
// 创建一个BitmapInfo对象,用来保存Bitmap对象的信息
BitmapInfo info = new BitmapInfo();
// 设置Bitmap对象的引用
info.bitmap = this;
// 设置Bitmap对象的大小(单位为字节)
info.size = width * height * (config == Config.ARGB_8888 4 : 2);
// 设置Bitmap对象的类型(如ARGB_8888或RGB_565)
info.type = config.name();
// 设置Bitmap对象的数据源(如网络、本地、资源等)
info.source = getSource();
// 设置Bitmap对象的创建时间
info.createTime = dateFormat.format(new Date());
// 将Bitmap对象及其信息添加到WeakHashMap中
map.put(this, info);
}
// 在recycle方法中插入代码,用来记录Bitmap对象的销毁信息
public void recycle() {
// 调用原始的recycle方法
super.recycle();
// 从WeakHashMap中获取Bitmap对象及其信息
BitmapInfo info = map.get(this);
if (info != null) {
// 设置Bitmap对象的销毁时间
info.destroyTime = dateFormat.format(new Date());
// 将更新后的信息重新放入WeakHashMap中
map.put(this, info);
}
}
}
②、将所有创建出来的Bitmap放入一个WeakHashMap中,并记录创建Bitmap的数据、堆栈等信息:
将所有创建出来的Bitmap放入一个WeakHashMap中,并记录创建Bitmap的数据、堆栈等信息是指在应用中使用一个WeakHashMap对象来保存所有创建出来的Bitmap对象及其信息,如Bitmap对象的引用、大小、类型、数据源、创建时间、销毁时间等。这样,我们就可以通过遍历WeakHashMap中的键值对来获取所有创建出来的Bitmap对象及其信息,以及它们的生命周期。例如:
// 以下代码是一个简单的WeakHashMap示例,它用来保存所有创建出来的Bitmap对象及其信息
public class BitmapMonitor {
// 定义一个WeakHashMap对象,用来保存所有创建出来的Bitmap对象及其信息
private static WeakHashMap<Bitmap, BitmapInfo> map = new WeakHashMap<>();
// 定义一个方法,用来向WeakHashMap中添加一个Bitmap对象及其信息
public static void add(Bitmap bitmap, BitmapInfo info) {
map.put(bitmap, info);
}
// 定义一个方法,用来从WeakHashMap中获取一个Bitmap对象及其信息
public static BitmapInfo get(Bitmap bitmap) {
return map.get(bitmap);
}
// 定义一个方法,用来从WeakHashMap中移除一个Bitmap对象及其信息
public static void remove(Bitmap bitmap) {
map.remove(bitmap);
}
// 定义一个方法,用来遍历WeakHashMap中的所有键值对,并打印或上报Bitmap对象及其信息
public static void traverse() {
for (Map.Entry<Bitmap, BitmapInfo> entry : map.entrySet()) {
Bitmap bitmap = entry.getKey();
BitmapInfo info = entry.getValue();
// 打印或上报Bitmap对象及其信息
Log.d("BitmapMonitor", "bitmap: " + bitmap +
", size: " + info.size +
", type: " + info.type +
", source: " + info.source +
", createTime: " + info.createTime +
", destroyTime: " + info.destroyTime);
}
}
}
③、每隔一定时间查看WeakHashMap中有哪些Bitmap仍然存活来判断是否出现Bitmap滥用或泄漏:每隔一定时间查看WeakHashMap中有哪些Bitmap仍然存活来判断是否出现Bitmap滥用或泄漏是指在应用中使用一个定时器或者一个后台线程,每隔一定时间(如1分钟)就遍历一次WeakHashMap中的键值对,查看有哪些Bitmap对象仍然存活,并判断它们是否存在滥用或泄漏的情况。如果发现有滥用或泄漏的情况,就打印或上报相应的警告信息。例如:
// 以下代码是一个简单的定时器示例,它每隔1分钟就遍历一次WeakHashMap中的键值对,并判断是否存在Bitmap滥用或泄漏
public class BitmapMonitor {
// 定义一个Timer对象,用来执行定时任务
private static Timer timer = new Timer();
// 定义一个TimerTask对象,用来实现定时任务的逻辑
private static TimerTask task = new TimerTask() {
@Override
public void run() {
// 遍历WeakHashMap中的所有键值对,并判断是否存在Bitmap滥用或泄漏
for (Map.Entry<Bitmap, BitmapInfo> entry : map.entrySet()) {
Bitmap bitmap = entry.getKey();
BitmapInfo info = entry.getValue();
// 如果Bitmap对象仍然存活,并且已经超过1分钟没有被销毁,就认为是滥用或泄漏,并打印或上报警告信息
if (bitmap != null && !bitmap.isRecycled() &&
System.currentTimeMillis() - info.createTime > 60 * 1000) {
Log.w("BitmapMonitor", "Bitmap abuse or leak detected: bitmap: " + bitmap +
", size: " + info.size +
", type: " + info.type +
", source: " + info.source +
", createTime: " + info.createTime +
", destroyTime: " + info.destroyTime);
}
}
}
};
// 在Application中初始化Timer和TimerTask,并设置定时任务的执行间隔
public class MyApplication extends Application {
// 创建一个Timer对象,用来执行定时任务
private Timer timer = new Timer();
// 创建一个TimerTask对象,用来实现定时任务的逻辑
private TimerTask task = new TimerTask() {
@Override
public void run() {
// 遍历WeakHashMap中的所有键值对,并判断是否存在Bitmap滥用或泄漏
for (Map.Entry<Bitmap, BitmapInfo> entry : map.entrySet()) {
Bitmap bitmap = entry.getKey();
BitmapInfo info = entry.getValue();
// 如果Bitmap对象仍然存活,并且已经超过1分钟没有被销毁,就认为是滥用或泄漏,并打印或上报警告信息
if (bitmap != null && !bitmap.isRecycled() &&
System.currentTimeMillis() - info.createTime > 60 * 1000) {
Log.w("BitmapMonitor", "Bitmap abuse or leak detected: bitmap: " + bitmap +
", size: " + info.size +
", type: " + info.type +
", source: " + info.source +
", createTime: " + info.createTime +
", destroyTime: " + info.destroyTime);
}
}
}
};
@Override
public void onCreate() {
super.onCreate();
// 设置定时任务的执行间隔(如1分钟)
timer.schedule(task, 0, 60 * 1000);
}
}
八、建立线上应用内存监控体系
线上应用内存监控体系是一种用来监控应用在线上环境中的内存使用情况和问题的方法,它可以帮助我们及时发现和解决一些影响用户体验和稳定性的内存问题,如OOM、ANR等。线上应用内存监控体系可以从以下几个方面进行:
①、引入一系列的内存监控指标:
引入一系列的内存监控指标是指在应用中使用一些工具或框架,如Firebase Performance Monitoring、AppDynamics等,来收集和展示应用在线上环境中的一些关键的内存监控指标,如发生频率、发生时各项内存使用状况、发生时App的当前场景、内存异常率、触顶率等。这样,我们就可以通过查看这些指标来了解应用的整体和细节的内存表现,以及发现一些异常或风险的情况。例如:
从示例图中可以看出:
- Dashboard:这个功能可以让我们查看应用在线上环境中的整体性能概览,包括应用启动时间、网络延迟、网络成功率、网络负载、崩溃率等。
- Memory Metrics:这个功能可以让我们查看应用在线上环境中的平均内存使用情况,以及按照不同维度(如百分比区间、设备型号等)进行分析和对比。
- Memory Issues:这个功能可以让我们查看应用在线上环境中出现的OOM(OutOfMemoryError)问题,以及按照不同维度(如百分比区间、设备型号等)进行分析和对比。
- Memory Sessions:这个功能可以让我们查看应用在线上环境中的每个用户会话的内存使用情况,以及查看每个会话的详细信息和时间线,从而发现一些异常或风险的情况。
②、利用LeakCanary、Probe等组件来实现自动化内存泄漏分析:LeakCanary、Probe等组件是一些可以在应用运行时自动检测和分析内存泄漏的组件,它们可以让我们在不需要手动操作的情况下,及时发现和定位应用中存在的内存泄漏问题,并提供详细的泄漏信息和引用路径。例如:
从示例图中可以看出:
- Leak List:这个功能可以让我们查看应用运行时发现的所有内存泄漏问题,以及它们的简要信息,如泄漏对象的类型、数量、大小等。
- Leak Details:这个功能可以让我们查看某个内存泄漏问题的详细信息,如泄漏对象的引用链、GC根、排除引用等。
- Leak Trace:这个功能可以让我们查看某个内存泄漏问题的引用路径,以及每个引用节点的类型、名称、值等。
- Memory Leak Detector:这个功能可以让我们在应用运行时开启或关闭内存泄漏检测,以及设置检测的频率和范围。
- Memory Leak Report:这个功能可以让我们查看应用运行时生成的所有内存泄漏报告,以及它们的简要信息,如泄漏对象的类型、数量、大小等。
- Memory Leak Graph:这个功能可以让我们查看某个内存泄漏报告中包含的所有对象之间的引用关系,以及每个对象的类型、名称、值等。
使用这些组件,我们可以快速地发现和定位应用中存在的内存泄漏问题,并进行相应的优化和修复。经过分析,我们发现:
- 在LeakCanary中,我们发现有一个MainActivity对象被一个匿名内部类持有,导致无法被GC回收。这说明这个MainActivity对象是一个典型的非静态内部类导致的内存泄漏,我们需要将其改为静态内部类或者使用弱引用来避免泄漏。
- 在Probe中,我们发现有一个Bitmap对象被一个静态变量持有,导致无法被GC回收。这说明这个Bitmap对象是一个典型的静态变量导致的内存泄漏,我们需要在不使用时将其置为null或者使用软引用来避免泄漏。