Android performance optimization using powerful LeakCanary to detect memory leaks and solutions

This article mainly introduces the use of LeakCanary to detect memory leaks and solutions for Android performance optimization. Interested students can find out.

foreword

When talking about how to avoid memory leaks in performance optimization during the interview, few people gave a comprehensive answer. So I decided to take the time to study and summarize this knowledge, and share how we detect memory leaks. Our company uses the open source framework LeakCanary to detect memory leaks.

What is a memory leak?

Some objects have only a limited lifetime. When their tasks are complete, they are garbage collected. If the object is still referenced by a series of references when the object's lifetime should have ended, this will cause a memory leak. As the leaks accumulate, the app will run out of memory.

What are the effects of memory leaks?

It is one of the main causes of application OOM. Due to the limited memory allocated by the Android system for each application, when there are many memory leaks in an application, it will inevitably cause the memory required by the application to exceed the memory limit allocated by the system, which will cause memory overflow and cause the application to crash.

What is LeakCanary?

leakCanary is an open source framework of Square, which is a memory leak detection library for Android and Java. If a memory leak is detected in an activity, LeakCanary will automatically display a notification, so it can be understood as a fool-like memory leak detection tool. It can greatly reduce the oom problems encountered in development and greatly improve the quality of APP.

LeakCanary captures common memory leaks and solutions

1.) Memory leaks caused by incorrect use of singletons

In normal development, the singleton design pattern is a design pattern that we often use. In development, a singleton often needs to hold a Context object. If the life cycle of the held Context object is shorter than that of the singleton, or the Context cannot be released and recycled, it may cause memory leaks. The wrong writing is as follows:

public class LoginManager {
    
    
  private static LoginManager mInstance;
  private Context mContext;

  private LoginManager(Context context) {
    
    
    this.mContext = context;
  }


  public static LoginManager getInstance(Context context) {
    
    
    if (mInstance == null) {
    
    
      synchronized (LoginManager.class) {
    
    
        if (mInstance == null) {
    
    
          mInstance = new LoginManager(context);
        }
      }
    }
    return mInstance;
  }

  public void dealData() {
    
    
  }

}

We call it in an Activity, and then closing the Activity will cause a memory leak.

LoginManager.getInstance(this).dealData();
LeakCanary detection results are as follows:

insert image description here
The solution is to ensure that the life cycle of Context and AppLication are the same. The modified code is as follows:

public class LoginManager {
    
    
  private static LoginManager mInstance;
  private Context mContext;

  private LoginManager(Context context) {
    
    
    this.mContext = context.getApplicationContext();
  }


  public static LoginManager getInstance(Context context) {
    
    
    if (mInstance == null) {
    
    
      synchronized (LoginManager.class) {
    
    
        if (mInstance == null) {
    
    
          mInstance = new LoginManager(context);
        }
      }
    }
    return mInstance;
  }

  public void dealData() {
    
    
  }

}
2.) Memory leak caused by Handler

In the early years, Handler was used frequently. It is a bridge for communication between worker threads and UI threads, but now a large number of open source frameworks have encapsulated it. Here we simulate a common usage method to simulate memory leaks.

public class MainActivity1 extends AppCompatActivity {
    
    
  private Handler mHandler = new Handler();
  private TextView mTextView;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mTextView = (TextView) findViewById(R.id.text);
    //模拟内存泄露
    mHandler.postDelayed(new Runnable() {
    
    
      @Override
      public void run() {
    
    
        mTextView.setText("lcj");
      }
    }, 3 * 60 * 1000);
    finish();
  }
 
  @Override
  protected void onDestroy() {
    
    
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
    mHandler=null;
    LApplication.getRefWatcher().watch(this);
  }
}

The above code creates an mHandler object through an internal class. At this time, mHandler will implicitly hold an external class object reference, which is MainActivity. When the postDelayed method is executed, this method will load your Handler into a Message and push this Message to the MessageQueue. The MessageQueue is continuously polling and processing messages in a Looper thread. Then when the Activity exits, there are still unprocessed or processing messages in the message queue. The Message in the message queue holds a reference to the mHandler instance, and mHandler holds a reference to the Activity, so the memory resources of the Activity cannot be recovered in time, causing a memory leak.

LeakCanary detection results are as follows:

insert image description here

To avoid memory leaks caused by Handler, we need to remove all messages and all Runnables in the message queue when the Activity is closed and exited. The above code only needs to call mHandler.removeCallbacksAndMessages(null); in the onDestroy() function.

public class MainActivity1 extends AppCompatActivity {
    
    
  private Handler mHandler = new Handler();
  private TextView mTextView;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mTextView = (TextView) findViewById(R.id.text);
    //模拟内存泄露
    mHandler.postDelayed(new Runnable() {
    
    
      @Override
      public void run() {
    
    
        mTextView.setText("lcj");
      }
    }, 3 * 60 * 1000);
    finish();
  }
 
  @Override
  protected void onDestroy() {
    
    
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
    mHandler=null;
    LApplication.getRefWatcher().watch(this);
  }
}
3.) Memory leaks caused by threads

In the earliest period, most of the time-consuming operations were processed by Thread+Handler, which was gradually replaced by AsyncTask, until now RxJava is used to handle asynchrony. Here we take AsyncTask as an example. Most people may handle a time-consuming operation in this way and then notify the UI of the update result:

public class MainActivity extends AppCompatActivity {
    
    
  private AsyncTask<Void, Void, Integer> asyncTask;
  private TextView mTextView;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mTextView = (TextView) findViewById(R.id.text);
    testAsyncTask();
    finish();
  }
 
  private void testAsyncTask() {
    
    
    asyncTask = new AsyncTask<Void, Void, Integer>() {
    
    
      @Override
      protected Integer doInBackground(Void... params) {
    
    
        int i = 0;
        //模拟耗时操作
        while (!isCancelled()) {
    
    
          i++;
          if (i > 1000000000) {
    
    
            break;
          }
          Log.e("LeakCanary", "asyncTask---->" + i);
        }
        return i;
      }
 
      @Override
      protected void onPostExecute(Integer integer) {
    
    
        super.onPostExecute(integer);
        mTextView.setText(String.valueOf(integer));
      }
    };
    asyncTask.execute();
 
  }
 
  @Override
  protected void onDestroy() {
    
    
    super.onDestroy();
    LApplication.getRefWatcher().watch(this);
  }
 
}

For the above example, when processing a time-consuming operation, the exit operation may be performed before the main activity is processed, but at this time the AsyncTask still holds a reference to the main activity, which will cause the main activity to fail to release and recycle, causing a memory leak.

LeakCanary detection results:

insert image description hereHow to solve this memory leak? When using AsyncTask, the AsyncTask.cancel() method of the corresponding task should also be canceled when the Activity is destroyed, so as to avoid wasting resources when the task is executed in the background, thereby avoiding memory leaks.

public class MainActivity3 extends AppCompatActivity {
    
    
  private AsyncTask<Void, Void, Integer> asyncTask;
  private TextView mTextView;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mTextView = (TextView) findViewById(R.id.text);
    testAsyncTask();
    finish();
  }
 
  private void testAsyncTask() {
    
    
    asyncTask = new AsyncTask<Void, Void, Integer>() {
    
    
      @Override
      protected Integer doInBackground(Void... params) {
    
    
        int i = 0;
        //模拟耗时操作
        while (!isCancelled()) {
    
    
          i++;
          if (i > 1000000000) {
    
    
            break;
          }
          Log.e("LeakCanary", "asyncTask---->" + i);
        }
        return i;
      }
 
      @Override
      protected void onPostExecute(Integer integer) {
    
    
        super.onPostExecute(integer);
        mTextView.setText(String.valueOf(integer));
      }
    };
    asyncTask.execute();
 
  }
 
  private void destroyAsyncTask() {
    
    
    if (asyncTask != null && !asyncTask.isCancelled()) {
    
    
      asyncTask.cancel(true);
    }
    asyncTask = null;
  }
 
  @Override
  protected void onDestroy() {
    
    
    super.onDestroy();
    destroyAsyncTask();
    LApplication.getRefWatcher().watch(this);
  }
 
}
4.) Memory leaks caused by creating static instances of non-static inner classes

Sometimes we need an Activity that can rotate with the screen, such as a video playback Activity. At this time, in order to prevent the reinitialization of some parameters caused by multiple calls to the onCreate method, we generally choose to create an internal class and a static instance to save these parameters, such as the following implementation:

public class MainActivity extends AppCompatActivity {
    
    
  private static Config mConfig;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //模拟内存泄露
    if (mConfig == null) {
    
    
      mConfig = new Config();
      mConfig.setSize(18);
      mConfig.setTitle("老九门");
    }
    finish();
  }
 
 
  @Override
  protected void onDestroy() {
    
    
    super.onDestroy();
    LApplication.getRefWatcher().watch(this);
  }
 
  class Config {
    
    
    private int size;
    private String title;
 
    public int getSize() {
    
    
      return size;
    }
 
    public void setSize(int size) {
    
    
      this.size = size;
    }
 
    public String getTitle() {
    
    
      return title;
    }
 
    public void setTitle(String title) {
    
    
      this.title = title;
    }
  }
}

The above code does not seem to have any problems. In fact, the internal class will hold an external class reference. Here, the external class is MainActivity. However, the internal class instance is a static static variable whose life cycle is the same as the Application life cycle. Therefore, when MainActivity is closed, the internal class static instance still holds a reference to MainActivity, resulting in MainActivity being unable to be recycled and released, causing memory leaks. LeakCanary detects memory leak results as follows:
insert image description here

The solution to this kind of leakage is to change the inner class to a static inner class and no longer hold a reference to MainActivity. The modified code is as follows:

public class MainActivity extends AppCompatActivity {
    
    
  private static Config mConfig;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //模拟内存泄露
    if (mConfig == null) {
    
    
      mConfig = new Config();
      mConfig.setSize(18);
      mConfig.setTitle("老九门");
    }
    finish();
  }
 
 
  @Override
  protected void onDestroy() {
    
    
    super.onDestroy();
    LApplication.getRefWatcher().watch(this);
  }
 
  static class Config {
    
    
    private int size;
    private String title;
 
    public int getSize() {
    
    
      return size;
    }
 
    public void setSize(int size) {
    
    
      this.size = size;
    }
 
    public String getTitle() {
    
    
      return title;
    }
 
    public void setTitle(String title) {
    
    
      this.title = title;
    }
  }
}
5.) Memory leak caused by WebView

In the current development, the Hybrid development method will be used more or less, so we will use WebView to host Html web pages, as follows:

public class MainActivity5 extends AppCompatActivity {
    
    
  private WebView mWebView;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_web);
    mWebView = (WebView) findViewById(R.id.web);
    mWebView.loadUrl("http://www.cnblogs.com/whoislcj/p/5720202.html");
  }
 
 
  @Override
  protected void onDestroy() {
    
    
    super.onDestroy();
    LApplication.getRefWatcher().watch(this);
  }
 
}

xml layout file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/activity_main"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical">
 
  <WebView
    android:id="@+id/web"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
</LinearLayout>

When WebView parses a web page, it will apply for Native heap memory to save page elements. When the page is more complex, it will take up a lot of memory. If the page contains images, the memory usage will be even worse. And when opening a new page, in order to roll back quickly, the memory occupied by the previous page will not be released. Sometimes browsing more than a dozen web pages will take up hundreds of megabytes of memory. In this way, when many web pages are loaded, the system will be overwhelmed, and eventually the application will be forced to close, that is, the application will crash or restart. Calling the following code in onDestroy when the Activity is closed has no effect.

private void destroyWebView() {
    
    
    if (mWebView != null) {
    
    
      mLinearLayout.removeView(mWebView);
      mWebView.pauseTimers();
      mWebView.removeAllViews();
      mWebView.destroy();
      mWebView = null;
    }
  }
First look at the results detected by LeakCanary as follows:

insert image description here
How to solve it? This has checked a lot of information, one of which is to use getApplicationgContext as a parameter to build a WebView, and then dynamically add it to a ViewGroup, and finally call the function of destroying the webView when exiting. Although it also achieves the effect of preventing memory overflow, but when some web pages pop up a dialog box that needs to remember the password, there will be an error Unable to add window - token null is not for an application, so the solution adopted here is to put the Activity (or Service) that uses WebView in the in a separate process
.
Then, after detecting that the application occupies too much memory and may be killed by the system or the Activity (or Service) where it is located ends, call android.os.Process.killProcess(android.os.Process.myPid()); to actively kill the process. Since the memory allocation of the system is based on the process, after the process is closed, the system will automatically reclaim all the memory.

The modified code is as follows:

public class MainActivity5 extends AppCompatActivity {
    
    
  private WebView mWebView;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_web);
    mWebView = (WebView) findViewById(R.id.web);
    mWebView.loadUrl("http://www.cnblogs.com/whoislcj/p/5720202.html");
  }
 
  @Override
  protected void onDestroy() {
    
    
    destroyWebView();
    android.os.Process.killProcess(android.os.Process.myPid());
    super.onDestroy();
    LApplication.getRefWatcher().watch(this);
 
  }
 
  private void destroyWebView() {
    
    
    if (mWebView != null) {
    
    
      mWebView.pauseTimers();
      mWebView.removeAllViews();
      mWebView.destroy();
      mWebView = null;
    }
  }
 
}

The corresponding activity configuration in the manifest is as follows:

<activity
  android:name=".MainActivity5"
  android:process="com.whoislcj.webview"/>
6.) Memory leaks caused by unclosed resources

For the use of resources such as BroadcastReceiver, ContentObserver, File, Cursor, Stream, Bitmap, etc., they should be closed or logged out in time when the Activity is destroyed, otherwise these resources will not be recycled, resulting in memory leaks. For example, the code to obtain the image address of the media library must be called at the end of the query

Cursor's close method prevents memory leaks.

 String columns[] = new String[]{
    
    
                MediaStore.Images.Media.DATA, 
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.TITLE,
                MediaStore.Images.Media.DISPLAY_NAME
        };
        Cursor cursor = this.getContentResolver().query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 
                columns, 
                null, null, null);
        if (cursor != null) {
    
    
            int photoIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
            //显示每张图片的地址,但是首先要判断一下,Cursor是否有值
            while (cursor.moveToNext()) {
    
    
                String photoPath = cursor.getString(photoIndex); //这里获取到的就是图片存储的位置信息
                Log.e("LeakCanary", "photoPath---->" + photoPath);
            }
            cursor.close();
        }
    }

Summarize:

The above are the memory leaks and solutions detected by LeakCanary. I hope it will be helpful to everyone's study.

Guess you like

Origin blog.csdn.net/u010351988/article/details/115518382