这两天在用LeakCanary查项目的内存泄漏问题,在此记录一下。
内存泄漏的五种常见原因
- 单例造成的内存泄漏
Android的单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏。因为单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。常见案例如下:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
1、传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长 ;
2、传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。
修改方式如下:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
- 非静态内部类创建静态实例造成的内存泄漏
非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。
常见案例如下:
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mManager == null){
mManager = new TestResource();
}
//...
}
class TestResource {
//...
}
}
应将内部类设为静态内部类,这样静态内部类就不会持有外部类的引用,修改如下:
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mManager == null){
mManager = new TestResource();
}
//...
}
static class TestResource {
//...
}
}
- Handler造成的内存泄漏
Handler的使用造成的内存泄漏问题应该说最为常见,由于Handler的非静态匿名内部类会持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。
常见案例如下:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//...
}
};
private void loadData(){
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
解决这类问题的方式是,创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象。这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destroy时或者Stop时还应该移除消息队列中的消息
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private static class MyHandler extends Handler {
private WeakReference<Context> reference;
public MyHandler(Context context) {
reference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if(activity != null){
//...
}
}
}
private void loadData() {
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
- 线程造成的内存泄漏
对于线程造成的内存泄漏,也是平时比较常见的,不过其原因也是匿名内部类,内部类会默认持有对外部类的引用,当线程为完成时,可能就会造成外部类的资源无法回收。
常见案例如下:
public class MainActivity extends AppCompatActivity {
//...
new Thread(new Runnable() {
@Override
public void run() {
//...
}
}).start();
}
解决方案也是将内部类设置为静态内部类
public class MainActivity extends AppCompatActivity {
//...
new Thread(new MyRunnable()).start();
static class MyRunnable implements Runnable{
@Override
public void run() {
//...
}
}
}
- 资源未关闭造成的内存泄漏
对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
参考原文
项目中查出的内存泄漏
第一处
public static void hideIME(View edit, boolean clearFocus) {
if (edit == null) {
return;
}
if (clearFocus) {
edit.clearFocus();
}
InputMethodManager imm = (InputMethodManager) edit.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(edit.getWindowToken(), 0);
}
代码分析发现,登录界面点击空白区域隐藏软键盘时,会调用
InputMethodManager
,查看其源码发现它是一个单例模式(代码如下),所以调用的时候如果是用edit.getContext()
就会造成edit
及其所在view的资源无法释放,此处应改成Application.getContext()
public final class InputMethodManager {
//...
static InputMethodManager sInstance;
public static InputMethodManager getInstance() {
synchronized (InputMethodManager.class) {
if (sInstance == null) {
IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);
IInputMethodManager service = IInputMethodManager.Stub.asInterface(b);
sInstance = new InputMethodManager(service, Looper.getMainLooper());
}
return sInstance;
}
}
}
第二处
此处是Volley造成的内存泄漏,在查看了Github上的提交记录后,终于找到了原因。先看一下
NetwokDispatcher
的一段代码如下:
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while (true) {
Request<?> request;
try {
// Take a request from the queue.
request = mQueue.take();
//...
}catch{
//...
}
//...
}
}
从代码中可以看到,线程会不断从
mQueue
中获取request,获取不到时就会阻塞等待,这是没有问题的,但是我们发现request
的声明是在while
循环体中的,如果当线程阻塞住时,最后一个request
的内存就有可能因为再次分配给Request<?> request
而没有释放,这样就会造成request及其内部的listener所引用的资源无法释放。解决方案如下:
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Request<?> request;
while (true) {
request = null;
try {
// Take a request from the queue.
request = mQueue.take();
//...
}catch{
//...
}
//...
}
}
此外还要将Request及其子类中的listener和errorListener都置为null
public abstract class Request<T> implements Comparable<Request<T>> {
//...
private Response.ErrorListener mErrorListener;
void finish(final String tag) {
if (mRequestQueue != null) {
mRequestQueue.finish(this);
onFinish();
}
}
/**
* clear listeners when finished
*/
protected void onFinish() {
mErrorListener = null;
}
//...
}
子类如ImageRequest
public class ImageRequest extends Request<Bitmap> {
//...
private Response.Listener<Bitmap> mListener;
@Override
protected void onFinish() {
super.onFinish();
mListener = null;
}
//...
}