A practical approach
Preface
Lifecycle of your application is managed by the Android itself. Sometimes you dont have control over when and how the system interrupts your application. Like when screen rotation happens, a call is received, low memory condition or etc. Consequently, you have to proactively handle these occurrences. In this article I want to discuss how you can avoid memory leak when screen orientation happens. All the snippets of code are from my earlier post Location Finder.
Scenario
As you know, long running operations must not be executed on the UI thread. That’s why we have AsyncTask. In my older post Location Finder 1, I have an AsyncTask for querying data from Google Places. Constructor of the RequestHandler class accepts the activity itself. This is because I need to have Context for loading resources and the activity as a call back mechanism to update the UI in the main thread. By passing this reference the Android is not able to garbage collect this activity when screen rotation happens (In case of screen rotation the activity is destroyed and created again) our RequestHandler which is an AsyncTask have a reference to the activity so it wont get garbage collected.
1. There are some design changes since the date of the post.
Possible Approach
Now, lets look at an approach that avoids memory leak. We define 2 methods in our RequestHandler (AsyncTask) as detach and attach. These 2 methods have the responsibility of setting the reference of the activity to null (detaching) and setting back the reference (attaching). When should we detach the activity? We can detach it in the onStop() method as below. You might ask why not detaching in onDestroy() method? Because the system might sometimes kill your app without calling onDestroy(). You can look at the whole code at this address.
Here are part of the codes from each class involved.
GMapActivity: Is the activity class from which the AsyncTask is executed.
TaskComplete: Is a call back interface which our GMapActivity implements.
RequestHandler: Is the AsyncTask responsible for our long running operation.
GMapActivity class:
.
.
.
protected void onStop() {
//Always call the super first
super.onStop();
//This is our AsyncTask detaching.
if(requestHandler != null){
requestHandler.detach();
}
//This is another class which like our AsyncTask has received a reference of our activity as well.
//Same mechanism applies here
if(itemizedoverlay != null){
itemizedoverlay.detach();
}
//you can forget about this line for this example
locationManager.removeUpdates(this);
// We dismiss any ProgressDialog. Failing to do so will result in memory leak as well
// since we have passed the activity to this dialog in onCreate().
if(queryProgressBar != null){
queryProgressBar.dismiss();
}
}
.
.
.
TaskComplete interface:
public interface TaskComplete <T> {
public void onComplete(T t);
public void updateProgress(int progress);
}
RequestHandler class:
.
.
.
//Class level private fields
//Implemented interface reference
private TaskComplete <List<OverlayItem>> callBack;
//Forget about this line for this example
private String selectedRadius;
//Forget about this line for this example
private String pointOfInterest;
//Context needed for loading resources. No need to worry about this in this example
private Context context;
public RequestHandler(TaskComplete <List<OverlayItem>> taskComplete, String selectedRadius, String pointOfInterest) {
this.callBack = taskComplete;
this.context = ((Activity)callBack).getApplicationContext();
this.selectedRadius = selectedRadius;
this.pointOfInterest = pointOfInterest;
}
public void detach() {
callBack = null;
}
public void attach(TaskComplete <List<OverlayItem>> taskComplete) {
this.callBack = taskComplete;
}
.
.
.
Although we could have defined our task as an inner class I've decided to make it separate. Beware non-static inner classes have an implicit reference to the enclosing class. As a consequence, this reference might prevent the activity from being garbage collected causing memory leak.
Oh, We have detached the activity how about attaching it back. Ok, We want to attach the activity back to the AsyncTask. We said that current activity will be destroyed and recreated again. Before This happens we have to keep a reference to our RequestHandler(AsyncTask). We achieve this by creating a new State class and overriding onRetainNonConfigurationInstance() method in our Activity class. This method returns an object. We return a new instance of State. In this class we put our RequestHandler(AsyncTask) and any other detached objects. This method is called between onStop() and onDestroy(). In onStop() we already detached the activity and now are ready to returning the State of our application.
public Object onRetainNonConfigurationInstance() {
State state = null;
if(requestHandler != null && itemizedoverlay != null){
state = new State(requestHandler, itemizedoverlay, currentPositionOvrItem);
}
return state;
}
When onCreate() is called by the system again after the new activity has been created we have to attach back our AsyncTask. You can follow part of the code below.
public void onCreate(Bundle savedInstanceState) {
.
.
.
//The data returned by this method is accessible in onCreate() and onStart() methods.
if(getLastNonConfigurationInstance() != null){
((State) getLastNonConfigurationInstance()).getRequestHandler().attach(this);
requestHandler = ((State) getLastNonConfigurationInstance()).getRequestHandler();
//Forget about the lines below for this article
((State) getLastNonConfigurationInstance()).getItemizedOverlay().attach(this);
itemizedoverlay = ((State) getLastNonConfigurationInstance())
.getItemizedOverlay();
currentPositionOvrItem = ((State)getLastNonConfigurationInstance())
.getCurrentPositionOvrItem();
redrawOverlayItems();
}
.
.
.
}
By taking this approach you can avoid memory leaks and also preserve the state of your application while screen rotation happens.
1. There are some design changes since the date of the post.
Possible Approach
Now, lets look at an approach that avoids memory leak. We define 2 methods in our RequestHandler (AsyncTask) as detach and attach. These 2 methods have the responsibility of setting the reference of the activity to null (detaching) and setting back the reference (attaching). When should we detach the activity? We can detach it in the onStop() method as below. You might ask why not detaching in onDestroy() method? Because the system might sometimes kill your app without calling onDestroy(). You can look at the whole code at this address.
Here are part of the codes from each class involved.
GMapActivity: Is the activity class from which the AsyncTask is executed.
TaskComplete: Is a call back interface which our GMapActivity implements.
RequestHandler: Is the AsyncTask responsible for our long running operation.
GMapActivity class:
.
.
.
protected void onStop() {
//Always call the super first
super.onStop();
//This is our AsyncTask detaching.
if(requestHandler != null){
requestHandler.detach();
}
//This is another class which like our AsyncTask has received a reference of our activity as well.
//Same mechanism applies here
if(itemizedoverlay != null){
itemizedoverlay.detach();
}
//you can forget about this line for this example
locationManager.removeUpdates(this);
// We dismiss any ProgressDialog. Failing to do so will result in memory leak as well
// since we have passed the activity to this dialog in onCreate().
if(queryProgressBar != null){
queryProgressBar.dismiss();
}
}
.
.
.
TaskComplete interface:
public interface TaskComplete <T> {
public void onComplete(T t);
public void updateProgress(int progress);
}
RequestHandler class:
.
.
.
//Class level private fields
//Implemented interface reference
private TaskComplete <List<OverlayItem>> callBack;
//Forget about this line for this example
private String selectedRadius;
//Forget about this line for this example
private String pointOfInterest;
//Context needed for loading resources. No need to worry about this in this example
private Context context;
public RequestHandler(TaskComplete <List<OverlayItem>> taskComplete, String selectedRadius, String pointOfInterest) {
this.callBack = taskComplete;
this.context = ((Activity)callBack).getApplicationContext();
this.selectedRadius = selectedRadius;
this.pointOfInterest = pointOfInterest;
}
public void detach() {
callBack = null;
}
public void attach(TaskComplete <List<OverlayItem>> taskComplete) {
this.callBack = taskComplete;
}
.
.
.
Although we could have defined our task as an inner class I've decided to make it separate. Beware non-static inner classes have an implicit reference to the enclosing class. As a consequence, this reference might prevent the activity from being garbage collected causing memory leak.
Oh, We have detached the activity how about attaching it back. Ok, We want to attach the activity back to the AsyncTask. We said that current activity will be destroyed and recreated again. Before This happens we have to keep a reference to our RequestHandler(AsyncTask). We achieve this by creating a new State class and overriding onRetainNonConfigurationInstance() method in our Activity class. This method returns an object. We return a new instance of State. In this class we put our RequestHandler(AsyncTask) and any other detached objects. This method is called between onStop() and onDestroy(). In onStop() we already detached the activity and now are ready to returning the State of our application.
public Object onRetainNonConfigurationInstance() {
State state = null;
if(requestHandler != null && itemizedoverlay != null){
state = new State(requestHandler, itemizedoverlay, currentPositionOvrItem);
}
return state;
}
When onCreate() is called by the system again after the new activity has been created we have to attach back our AsyncTask. You can follow part of the code below.
public void onCreate(Bundle savedInstanceState) {
.
.
.
//The data returned by this method is accessible in onCreate() and onStart() methods.
if(getLastNonConfigurationInstance() != null){
((State) getLastNonConfigurationInstance()).getRequestHandler().attach(this);
requestHandler = ((State) getLastNonConfigurationInstance()).getRequestHandler();
//Forget about the lines below for this article
((State) getLastNonConfigurationInstance()).getItemizedOverlay().attach(this);
itemizedoverlay = ((State) getLastNonConfigurationInstance())
.getItemizedOverlay();
currentPositionOvrItem = ((State)getLastNonConfigurationInstance())
.getCurrentPositionOvrItem();
redrawOverlayItems();
}
.
.
.
}
By taking this approach you can avoid memory leaks and also preserve the state of your application while screen rotation happens.
good tutorial on Android Online Training
ReplyDelete