一、前期基础知识储备
(1)什么是内存?
JAVA是在JVM所虚拟出的内存环境中运行的,JVM的内存可分为三个区:堆(heap)、栈(stack)和方法区(method)。
栈(stack):是简单的数据结构,但在计算机中使用广泛。栈最显著的特征是:LIFO(Last In, First Out, 后进先出)。后来者居上。(跟线程中队列的顺序恰好相反)栈中只存放基本类型和对象的引用(不是对象)。
堆(heap):堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。JVM只有一个堆区(heap),且被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。
方法区(method):又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
(2)什么是内存泄漏?
提及内存泄露,就不得不提到内存溢出,这两个比较容易混淆的概念,我们来分析一下。
内存泄露:ML (Memory Leak),程序在向系统申请分配内存空间后(new),在使用完毕后未释放。结果导致一直占据该内存单元,我们和程序都无法再使用该内存单元,直到程序结束,这是内存泄露。
内存泄漏对程序的影响是:容易使得应用程序发生内存溢出,即 OOM 。
内存溢出:OOM(Out of Memory),程序向系统申请的内存空间超出了系统能给的。比如内存只能分配一个int类型,我却要塞给他一个long类型,系统就出现OOM。小明向他同桌小红要橡皮,周一要一块,用一半没还;周二要一块,用一半没还;周三要一块,用一半没还;周四周五也是一样,然后到下周一他就失去这个同桌了,并且被叫了家长。
内存溢出对程序的影响是:导致ANR错误甚至应用Crush。OOM错误最终会导致ANR错误,即应用程序无响应,经常无响应的应用程序用着是会有点“上火”,比如“XXX掌游宝”(捞钱倒是有一手,换头像10元起步),这个ANR错误真的是每次打开有会报错,有时候真的是十分的恼火。
二、Android内存泄漏原因分析
(1)内存泄漏原因分析
需掌握的概念:①栈中只存放基本类型变量和对象的引用,栈(stack)可以自行清除不用的内存空间;②栈中引用的对象的本体存放在堆中,但在JAVA中堆内存不会随着方法的结束而清空;③所以如果我们不停的创建新对象,堆(heap)的内存空间就会被消耗尽;
④聪明的Java引入了垃圾回收(garbage collection)去处理堆内存的回收,perfect;
⑤但是实际情况是,仍然有很多情况导致内存泄漏:如果对象一直被引用无法被回收,造成内存的浪费,无法再被使用。所以对象无法被GC回收就是造成内存泄露的原因!
(2)Java垃圾回收机制分析
垃圾回收(garbage collection,简称GC)可以自动清空堆中不再使用的对象。在JAVA中对象是通过引用使用的。如果再没有引用指向该对象,那么该对象就无从处理或调用该对象,这样的对象称为不可到达(unreachable)。垃圾回收用于释放不可到达的对象所占据的内存。
回收机制分析:我们将栈定义为root,遍历栈中所有的对象的引用,再遍历一遍堆中的对象。因为栈中的对象的引用执行完毕就删除,所以我们就可以通过栈中的对象的引用,查找到堆中没有被指向的对象,这些对象即为不可到达对象,对其进行垃圾回收。
但是:如果持有对象的强引用,垃圾回收器GC是无法在内存中回收这个对象。
————————————————————我是重点分隔线——————————————————
(3)内存泄漏的真实原因
内存泄露的直接原因是:本该回收的对象,因为某些原因(对象的强引用被另外一个正在使用的对象所持有,且没有及时释放),进而造成内存单元一直被占用,浪费空间,甚至可能造成内存溢出!
内存泄漏的本质原因是:持有引用者的生命周期>被引用者的生命周期!
强引用:实际编码中最常见的一种引用类型。常见形式如:A a = new A();等。对于这类引用GC任何时候不会对其进行内存回收,在内存不足的情况下宁愿抛出Out of Memory(OOM内存溢出)。类似这样的都是强引用:
private final MessageReceived mMessageReceived=new MessageReceived(this);
————————————————————我是重点分隔线——————————————————
其实在Android中会造成内存泄露的情景无外乎两种:
全局进程(process-global)的static变量。这个无视应用的状态,持有Activity的强引用的怪物。
活在Activity生命周期之外的线程。没有清空对Activity的强引用。
小结:虽然现在手机内存在不停的提升,内存泄露兴许不会像dalvik时代由于虚拟机内存过小造成各种花样OOM。但是过量的内存泄露依然会造成内存溢出,影响用户体验,在如今定制系统层出不穷、机型花样越来越多的情况下解决好内存泄露的问题会让适配和稳定性进一步提高!
三、Android中什么导致内存泄漏
有开发者总结了如下一张表,这里供读者参考:
①资源对象没关闭造成的内存泄漏——描述: 资源性对象比如(Cursor游标,Stream流,BroadCastReceiver等)往往都用了一些缓冲,我们在不使用的时候或者在使用完之后,应该及时关闭它们比如close()方法,以便它们的缓冲及时回收内存。
Cursor cursor = sqLiteDatabase.rawQuery(query, null);
try {
if (cursor.moveToFirst()) {
do {
try {
long time = cursor.getLong(cursor.getColumnIndex(KEY_HISTORY_ID));
String math = cursor.getString(cursor.getColumnIndex(KEY_HISTORY_INPUT));
String result = cursor.getString(cursor.getColumnIndex(KEY_HISTORY_RESULT));
ResultEntry resultEntry = new ResultEntry(math, result, color, time, type);
itemHistories.add(resultEntry);
Log.d(TAG, "txtResult: DatabaseHelper查询了" + time + " " + math + " " + result);
} catch (Exception e) {
e.printStackTrace();
}
} while (cursor.moveToNext());
}
cursor.close(); //关闭cursor游标对象
} catch (Exception e) {
Log.d(TAG, "Error query: " + e.getMessage());
}
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US);
File files = getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File outFile = new File(files, sdf.format(new Date()).concat(".").concat("jpg"));
FileOutputStream outStream = null;
try {
outStream = new FileOutputStream(outFile);
outStream.write(data);
outStream.flush();
outStream.close(); //关闭输入流
Log.e(TAG, "image saved to file " + outFile.getAbsolutePath());
Glide.with(getContext()).load(outFile.getAbsolutePath()).into(mCameraSnapshot);
} catch (IOException e) {
e.printStackTrace();
}
//动态注册的广播需要反注册
@Override
protected void onDestroy() {
super.onDestroy();
if (receiver != null) {
unregisterReceiver(receiver);
}
}
②构造Adapter时,没有使用缓存的convertView——常见于使用ListView,如果,不使用convertView,那么每次列表加载都会加载一次对象,用户反复加载,就会造成大量的对象创建,得不到释放,极容易发生内存泄漏。
public View getView(int position, ViewconvertView, ViewGroup parent) {
View view = new Xxx(...);
... ...
return view;
}
修改为代码:
public View getView(int position, ViewconvertView, ViewGroup parent) {
View view = null;
if (convertView != null) {
view = convertView;
populate(view, getItem(position));
...
} else {
view = new Xxx(...);
...
}
return view;
};
③对于不再需要使用的对象,显示的将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null ——有时我们会手工的操作Bitmap对象,如果一个Bitmap对象比较占内存,当它不在被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存,比如在Activity或者Fragment中的onDestory中添加以下代码:
if(bitmap != null && !bitmap.isRecycled()){
bitmap.recycle();
bitmap = null; //回收Bitmap
}
或者在使用的时候,就及时回收不再使用的Bitmap对象:
if (myBitmap != null) {
myBitmap.recycle();
myBitmap = null;
}
Bitmap original = BitmapFactory.decodeFile(myFile);
myBitmap = Bitmap.createScaledBitmap(original, displayWidth, displayHeight, true);
original.recycle();
original = null;
④对于生命周期比Activity长的对象如果需要应该使用ApplicationContext ——这是一个很隐晦的内存泄漏的情况。有一种简单的方法来避免context相关的内存泄漏。最显著地一个是避免context逃出他自己的范围之外。使用Application context。这个context的生存周期和你的应用的生存周期一样长,而不是取决于activity的生存周期。如果你想保持一个长期生存的对象,并且这个对象需要一个context,记得使用application对象。你可以通过调用 Context.getApplicationContext() or Activity.getApplication()来获得。
⑤集合中对象没清理造成的内存泄漏——我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。
⑥使用Service之后,没有关闭服务——如果Service停止失败也会导致内存泄漏。
Intent floatWinIntent = new Intent(BasicCalculatorActivity.this, FlowWindowService.class);
stopService(floatWinIntent);
因为系统会倾向于把这个Service所依赖的进程进行保留,如果这个进程很耗内存,就会造成内存泄漏。建议使用IntentService,可以自动停止。
内存泄漏对我们来说并不是可见的,因为它是在堆中活动,而要想检测程序中是否有内存泄漏的产生,通常我们可以借助LeakCanary、MAT等工具来检测应用程序是否存在内存泄漏,当检测到程序中有内存泄漏的产生时,它将告诉我们该内存泄漏是由谁产生的和该内存泄漏导致谁泄漏了而不能回收,供我们复查。
⑦使用WebView之后没有进行回收 —— 推荐在代码里动态添加WebView
//Fragment中添加
private WebView mWebView;
private FrameLayout frameWebView;
frameWebView = view.findViewById(R.id.webView);
mWebView=new WebView(getActivity());
frameWebView.addView(mWebView);
//回收
@Override
public void onDestroy() {
super.onDestroy();
if( mWebView!=null) {
mWebView.removeAllViews();
mWebView.destroy();
}
}
⑧ 使用Handler时 造成的内存泄露;
解决方式:重写onDestory()方法,将消息队列清空,防止内存泄露。
@Override
protected void onDestroy() {
super.onDestroy();
if (handler != null){
handler.removeCallbacks(myRunnable);
}
}
推荐阅读《Android开发:详解Handler的内存泄露》,非常详细,有原因也有解决方式。
三、内存泄露工具篇
1)使用adb命令,检测context引用不当引发的内存泄露;
某种全局性的操作或者某个内部类持有了Activity的引用,并且一直不去释放它,导致Activity也一直无法释放,这使得GC也无法回收这个部分的内存,最终导致内存泄露。
举个例子,这里使用Handler引起内存泄露:
public class SecondActivity extends AppCompatActivity {
private Handler handler = new Handler();
private MyRunnable myRunnable = new MyRunnable();
class MyRunnable implements Runnable{
@Override
public void run() {
handler.postDelayed(this,10000);
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_two);
handler.postDelayed(myRunnable,10000);
}
}
在Terminal窗口输入命令 adb shell dumpsys meminfo <packageName> 查看内存使用情况:
首次进入应用,只有一个主Activity,这时点击按钮跳转到SecondActivity,输入命令,窗口显示如下:
此时出现了2个Activity,MainActivity和SecondActivity,但当退出SecondActivity时,输入命令行发现:
依旧显示为了两个Activity,重复操作,多次打开关闭SecondActivity,会发现,每次打开都会新增一个。通过adb命令可以清楚的知道context引起的内存泄露。
2)使用Android Profile定位出问题的代码;
AndroidStudio3.0新增Android Profile,点击 View > Tool Windows > Android Profiler,连接设备,运行需要调试的程序。
依然使用1)中的例子,经由Android Profile定位;进入退出SecondActivity三次,然后点击Android Profile中的按钮:
先点击回收按钮,然后点击第二个按钮,检测当前Java堆,选择按包名排序,得到结果如下:
点击SecondActivity,右侧弹出界面:
会发现有三个SecondActivity实例,这个肯定是有问题的,SecondActivity被什么东西持有了,退出SecondActivity时实例没有得到释放。继续点击SecondActivity,得到结果如下:
可以很清晰的知道是myRunnable实例持有了SecondActivity的实例,造成了内存泄露。
最后处理这个Handler引起的内存泄露:重写Activity的onDestroy()方法清空handler的队列。
@Override
protected void onDestroy() {
super.onDestroy();
if (handler != null){
handler.removeCallbacks(myRunnable);
}
}
可以再次用adb命令验证下,重复切换Activity,最后会有几个Activity实例。也可以点击Android Profile右上角Live按钮,复位,进行再次检测:
最后 祝各位开发者/读者少bug少泄漏少溢出少crash!!!
2020/03/27补充:
笔者最新博客《实际开发项目中使用 LeakCanary-快速检测应用的内存泄漏问题》使用开源库做检测,实际项目为例。