一、内存泄漏
1、Handler导致的内存泄漏
// 错误示例
private Handler handler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
};
没有指定的话,这段代码通常会在主线程执行,主线程的handler除了处理当前activity的相关消息外,还会处理其他逻辑中的任务,而如果Activity被finish掉了,那么消息队列的消息依旧会由handler进行处理,若此时handler声明为内部类(非静态内部类),我们知道内部类天然持有外部类的实例引用,那么就会导致activity无法回收,进而导致activity泄露
2、Rxjava导致的内存泄漏
Observable.create(new ObservableOnSubscribe<XXX>() {
@Override
public void subscribe(final ObservableEmitter<XXX> emitter) throws Exception {
Observable.zip(.....)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<ImpressionBean>() {
@Override
public void accept(XXX impressionBean) throws Exception {
emitter.onNext(impressionBean);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
emitter.onNext(new XXX());
}
});
}
});
使用RxJava发布一个订阅后,当页面被finish而订阅逻辑还未完成时,如果没有及时取消订阅,就会导致Activity/Fragment无法被回收,从而引发内存泄漏
3、List导致的内存泄漏
object AppUtil {
private val activityStack = Stack<BaseActivity>()
fun addActivity(activity: BaseActivity) {
activityStack.add(activity)
}
fun removeActivity(activity: BaseActivity) {
activityStack.remove(activity)
}
}
我们通常会在一个应用中统计并记录Activty的堆栈,用来判断某个Activty是否已经启动,或者获取到当前正在运行的activity等,在这种情况下,如果添加进去的Activity没有被及时释放,那么在Activity执行finish以后却无法释放该对象,导致出现内存泄漏
4、WebView导致的内存泄漏
@Override
public void onDetachedFromWindow() {
if (isDestroyed()) return;
if (!mIsAttachedToWindow) {
Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
return;
}
mIsAttachedToWindow = false;
hideAutofillPopup();
nativeOnDetachedFromWindow(mNativeAwContents);
mContentViewCore.onDetachedFromWindow();
updateHardwareAcceleratedFeaturesToggle();
if (mComponentCallbacks != null) {
mContext.unregisterComponentCallbacks(mComponentCallbacks);
mComponentCallbacks = null;
}
mScrollAccessibilityHelper.removePostedCallbacks();
}
系统会在detach进行反注册component callback,注意到 onDetachedFromWindow() 方法的第一行,if (isDestroyed()) return;, 如果 isDestroyed() 返回 true 的话,那么后续的逻辑就不能正常走到,所以就不会执行unregister的操作,通过看代码,可以得到,调用主动调用 destroy()方法,会导致 isDestroyed() 返回 true
5、静态属性导致的内存泄漏
// 错误示例
public class ActivityMemory extends AppCompatActivity {
private static Configuration configuration;
@Override
protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configuration = getResources().getConfiguration();
}
}
静态属性持有了Activity的configuration,而configuration持有了Activity,会导致Actviity无法被系统回收
6、单例导致的内存泄漏
public class AppSettings {
private static AppSettings sInstance;
private Context mContext;
private AppSettings(Context context) {
this.mContext = context;
}
public static AppSettings getInstance(Context context) {
if (sInstance == null) {
sInstance = new AppSettings(context);
}
return sInstance;
}
}
AppSettings是个全局单例对象,它持有的Context如果不是Application,则此Context无法被系统释放,从而导致内存泄漏的问题,其本质上也可以认为是静态属性导致的内存泄漏
7、非静态内部类导致的内存泄漏
这里可以参考Handler的示例代码
8、未反注册导致的内存泄漏
public class MainActivity extends AppCompatActivity {
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 接收到广播需要做的逻辑
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.registerReceiver(mReceiver, new IntentFilter());
}
}
这里的广播接收器,如果在Activity的destory中没有反注册,那么这个广播接收器会一直一直持有Activity的引用,从而导致内存泄漏,类似的还有Eventbus、传感接收器,定位接收器等需要反注册的工具
9、Timer导致的内存泄漏
public class MainActivity extends AppCompatActivity {
private Timer mTimer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
mTimer.schedule(mTimerTask, 3000, 3000);
}
private void init() {
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
loopViewpager();
}
});
}
};
}
private void loopViewpager() {
}
}
原理同上,非静态内部类或者匿名内部类默认持有了外部类的对象,如果不及时释放,会导致被持有的对象无法被回收而导致内存泄漏
10、属性动画造成内存泄露
@Override
protected void onCreate() {
super.onDestroy();
mAnimator.start();
}
动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放
二、内存检测
1、内存泄漏检测
LeakCanary内存泄漏检测,核心思路是利用RefWatcher.watch() 创建一个 KeyedWeakReference来监控需要被检测的对象,当Activity执行finish方法后,LeakCanary会在后台线程中检测引用的对象是否被释放,如果没有,则调用GC再次尝试释放,如果还是没有被释放,则把内存dump到 .hprof中,通过HeapAnalyzer来分析这个文件,并建立导致内存泄漏的引用链,然后展示出来
2、内存占用检测
通过Android studio自带的Profile检测应用的内存占用情况,执行GC后,打开某一个页面,查看内存占用情况,这个差值就是新页面的内存占用,工具界面如图:
Google官方的详细说明文档:https://developer.android.google.cn/studio/profile/memory-profiler
三、内存优化
1、Bitmap优化
1)内存占用优化
Bitmap尽量采用“显示多大,采样多大”的方案,比如ImageView的大小是100*100,则图片的采样也应该是100*100,避免占用内存过大
常规的压缩方案是:
- 1、质量压缩:(改变图片的位深和透明度)
/**
* 质量压缩
* 设置bitmap options属性,降低图片的质量,像素不会减少
* 第一个参数为需要压缩的bitmap图片对象,第二个参数为压缩后图片保存的位置
* 设置options 属性0-100,来实现压缩(因为png是无损压缩,所以该属性对png是无效的)
*
* @param bmp
* @param file
*/
public static void qualityCompress(Bitmap bmp, File file) {
// 0-100 100为不压缩
int quality = 20;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
bmp.compress(Bitmap.CompressFormat.JPEG, quality, baos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
- 2、尺寸压缩(改变图片的大小)
/**
* 尺寸压缩(通过缩放图片像素来减少图片占用内存大小)
*
* @param bmp
* @param file
*/
public static void sizeCompress(Bitmap bmp, File file) {
// 尺寸压缩倍数,值越大,图片尺寸越小
int ratio = 8;
// 压缩Bitmap到对应尺寸
Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Config.ARGB_8888);
Canvas canvas = new Canvas(result);
Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
canvas.drawBitmap(bmp, null, rect, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
- 3、采样率压缩(去除部分图片信号)
private int computeSize() {
srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;
int longSide = Math.max(srcWidth, srcHeight);
int shortSide = Math.min(srcWidth, srcHeight);
float scale = ((float) shortSide / longSide);
if (scale <= 1 && scale > 0.5625) {
if (longSide < 1664) {
return 1;
} else if (longSide < 4990) {
return 2;
} else if (longSide > 4990 && longSide < 10240) {
return 4;
} else {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
}
} else if (scale <= 0.5625 && scale > 0.5) {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
} else {
return (int) Math.ceil(longSide / (1280.0 / scale));
}
}
2)及时释放内存
在Bitmap不要使用的时候,及时调用Bitmap.recycle()进行内存释放
3)借助第三方工具
项目中通常是使用Glide加载网络图片,需要注意的是,在加载drawable,mipmap,assets等资源时,也尽量使用Glide来加载,它内部会根据要展示的imageView的大小对图片资源进行压缩,这个地方很多开发人会容易忽略
2、线程池优化
/** 每次只执行一个任务的线程池 */
ExecutorService singleTaskExecutor = Executors.newSingleThreadExecutor();
/** 每次执行限定个数个任务的线程池 */
ExecutorService limitedTaskExecutor = Executors.newFixedThreadPool(3);
/** 所有任务都一次性开始的线程池 */
ExecutorService allTaskExecutor = Executors.newCachedThreadPool();
/** 创建一个可在指定时间里执行任务的线程池,亦可重复执行 */
ExecutorService scheduledTaskExecutor = Executors.newScheduledThreadPool(3);
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源
3、软引用(弱引用)
public class MainActivity extends AppCompatActivity {
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new MyHandler(this);
start();
}
private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}
private static class MyHandler extends Handler {
private WeakReference<MainActivity> activityWeakReference;
public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityWeakReference.get();
if (activity != null) {
if (msg.what == 1) {
// 做相应逻辑
}
}
}
}
}
对于内部类持有外部类对象的情况,可以采用弱引用的形式,避免内存泄漏
软引用和弱引用的区别:
软引用:只有在内存不足时,JVM虚拟机才会回收该对象
弱引用:JVM进行垃圾回收时总会被回收
4、改为静态内部类
示例代码参考上面的例子,由于静态内部类不持有所在类的引用,同样的静态内部类创建的对象也无法持有外部类的对象,从而避免了内存泄漏的问题
4、及时关闭资源
1)IO流的关闭
stream.close();
2)图片资源的回收
bitmap.recycle();
3)注册对象的释放
(EventBus、广播接收器、传感器监听器等)
override fun onDestroy() {
EventBus.getDefault().unregister(this)
super.onDestroy()
}
4)属性动画的释放
@Override
protected void onDestroy() {
super.onDestroy();
mAnimator.cancel();
}
5)计时器的释放
mTimer.cancel()
5、WebView内存优化
1)单进程处理
单开新的进程用来创建WebView,通过AIDL来与主进程进行通信,当WebView被关闭时,杀掉WebView所在进程,彻底释放内存,详细代码太长,可以参考这里:https://www.jianshu.com/p/b66c225c19e2,单进程的好处在于:1、可以做到彻底释放WebView资源,2、由于WebView内存占用受到显示内容的影响,启用独立进程可以申请更多的空间,避免程序申请内存过大导致被系统回收的问题
2)主动置空
根据上面排查的泄漏原因,通过动态创建的形式添加WebView,并且在WebView需要释放时,提前在父控件中移除后再调用WebView的destroy()方法释放对象
@Override
protected void onDestroy() {
if (mWebView != null) {
mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mWebView.clearHistory();
((ViewGroup) mWebView.getParent()).removeView(mWebView);
mWebView.destroy();
mWebView = null;
}
super.onDestroy();
}