The Activity Recreating Issue During Configuration Change
When configuration change occurs in an Android device, e.g. rotating the screen from landscape to portrait mode, the Activity will be destroyed and recreated. This could introduce some issues if some tasks inside Activity are not completed during the configuration change. For instance a worker thread may still be running in background and leaking the memory during the configuration change. We assume that the worker thread here in discussion ties to Activity and will communicate back to Activity instance when it completes its task.
Let's take a look at following Activity code:
public class MainActivity extends Activity { private static final String TAG = MainActivity.class.getSimpleName(); private static int instanceCount = 0; private Handler handler; private Thread thread; private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); instanceCount++; Log.d(TAG, "onCreate()"); textView = (TextView)findViewById(R.id.textView1); textView.setText("Activity Instance " + String.valueOf(instanceCount)); handler = new Handler() { @Override public void handleMessage(Message msg) { Log.d(TAG, "Handler thread - " + getThreadInfo()); } }; thread = new Thread(new Runnable() { @Override public void run() { Log.d(TAG, "Worker thread - " + getThreadInfo()); try { int count = 10; while(count-- > 0) { // pause 10 seconds Thread.sleep(1000); } Log.d(TAG, "Worker thread sendMmessage to handler"); handler.sendEmptyMessage(0); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); } @Override protected void onDestroy() { Log.d(TAG, "onDestroy()"); super.onDestroy(); } private static String getThreadInfo() { Thread currentThread = Thread.currentThread(); String info = String.format("%1$s ID: %2$d Priority: %3$s", currentThread.getName(), currentThread.getId(), currentThread.getPriority()); return info; } }
A separate thread sleeps for 10 seconds to simulate a long-run task, then updates a UI view by a handler. If the the screen is rotated within 10 seconds, the activity will be recreated, so as a new thread and a new handler. However the old thread is still running in background, consuming resource and leaking the memory. The old Activity object will not be garbage collected at the time of destroy since the handler and thread are referencing it. The view switches from "Activity Instance 1" to "Activity Instance 2", and the LogCat shows:
Disabling Dangling Thread
The easiest method to resolve the issue is set a flag when the activity is destroyed to control the stale thread:
public class MainActivity extends Activity { private boolean stopThread = false; //... @Override protected void onCreate(Bundle savedInstanceState) { //... thread = new Thread(new Runnable() { @Override public void run() { Log.d(TAG, "Worker thread - " + getThreadInfo()); try { int count = 10; while(count-- > 0 && !stopThread) { // pause 10 seconds Thread.sleep(1000); } if (!stopThread) { Log.d(TAG, "Worker thread sendMmessage to handler"); handler.sendEmptyMessage(0); } } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); } } @Override protected void onDestroy() { Log.d(TAG, "onDestroy()"); super.onDestroy(); stopThread = true; handler.removeCallbacksAndMessages(null); } //... }The LogCat logs:
Now the first worker thread is cancelled along with its partially completed task. To save the work by first thread, we can use onSaveInstanceState() callback to store the partial result, so later the second worker thread can use it as an initial start point, as described in this post.
Using Static Thread Object
The solution above is not perfect: multiple thread instances created during configuration change which is inefficient and expensive. We can use static thread variable to maintain one thread instance:
public class MainActivity extends Activity { private static final String TAG = MainActivity.class.getSimpleName(); private static int instanceCount = 0; private static WorkerThread thread; private TextView textView; private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { Log.d(TAG, "Handler thread - " + getThreadInfo()); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); instanceCount++; Log.d(TAG, "onCreate()"); textView = (TextView)findViewById(R.id.textView1); textView.setText("Activity Instance " + String.valueOf(instanceCount)); if (savedInstanceState != null && thread != null && thread.isAlive()) { thread.setHandler(handler); } else { thread = new WorkerThread(handler); thread.start(); } } @Override protected void onDestroy() { Log.d(TAG, "onDestroy()"); super.onDestroy(); handler.removeCallbacksAndMessages(null); if (thread.isAlive()) { thread.setHandler(null); } } private static String getThreadInfo() { Thread currentThread = Thread.currentThread(); String info = String.format("%1$s ID: %2$d Priority: %3$s", currentThread.getName(), currentThread.getId(), currentThread.getPriority()); return info; } private static class WorkerThread extends Thread { private Handler handler; public WorkerThread(Handler handler) { super(); this.handler = handler; } public void setHandler(Handler handler) { this.handler = handler; } @Override public void run() { Log.d(TAG, "Worker thread - " + getThreadInfo()); try { int count = 10; while (count-- > 0) { // pause 10 seconds Thread.sleep(1000); } if (handler != null) { Log.d(TAG, "Worker thread sendMmessage to handler"); handler.sendEmptyMessage(0); } } catch (InterruptedException e) { e.printStackTrace(); } } } }Notice the extended WorkerThread class is also static to avoid memory leak, as in Java non-static inner and anonymous classes will implicitly hold an reference to their outer class. Now the LogCat logs:
As a side note, be cautious to use static variables within Activity to avoid memory leak. If you have to use the static variables, do not forget to cleanup the resources/references in the Activity.onDestroy() callback.
Using Fragment to Retain Thread
Another option, also the recommended way from Android Developer Guild, is to use Fragment with RetainInstance set to true to retain one instance of thread. The worker thread is wrapped into the non-UI Fragment:
public class ThreadFragment extends Fragment { private static final String TAG = ThreadFragment.class.getSimpleName(); private Handler handler; private Thread thread; private boolean stopThread; public ThreadFragment(Handler handler) { this.handler = handler; } public void setHandler(Handler handler) { this.handler = handler; } @Override public void onCreate(Bundle savedInstanceState) { Log.d(TAG, "onCreate()"); super.onCreate(savedInstanceState); setRetainInstance(true); // retain one Fragment instance in configuration change thread = new Thread(new Runnable() { @Override public void run() { Log.d(TAG, "Worker thread - " + MainActivity.getThreadInfo()); try { int count = 10; while(count-- > 0 && !stopThread) { // pause 10 seconds Thread.sleep(1000); } if (handler != null) { Log.d(TAG, "Worker thread sendMmessage to handler"); handler.sendEmptyMessage(0); } } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); } @Override public void onDestroy() { Log.d(TAG, "onDestroy()"); super.onDestroy(); handler = null; stopThread = true; } }
The Fragment feature was added from Android 3.0 Honeycomb. For older versions you need to include the Android Support package (android-support-v4.jar) to get the Fragment work. With Fragment setup, the main activity will dynamically create or activate existence of ThreadFragment:
public class MainActivity extends Activity { private static final String TAG = MainActivity.class.getSimpleName(); private static int instanceCount = 0; private ThreadFragment fragment; private TextView textView; private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { Log.d(TAG, "Handler thread - " + getThreadInfo()); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); instanceCount++; Log.d(TAG, "onCreate()"); textView = (TextView)findViewById(R.id.textView1); textView.setText("Activity Instance " + String.valueOf(instanceCount)); FragmentManager fm = getFragmentManager(); fragment = (ThreadFragment) fm.findFragmentByTag("thread"); if (fragment == null) { fragment = new ThreadFragment(handler); fm.beginTransaction().add(fragment, "thread").commit(); } else { // retained across configuration changes fragment.setHandler(handler); } } @Override protected void onDestroy() { Log.d(TAG, "onDestroy()"); super.onDestroy(); fragment.setHandler(handler); handler.removeCallbacksAndMessages(null); } public static String getThreadInfo() { Thread currentThread = Thread.currentThread(); String info = String.format("%1$s ID: %2$d Priority: %3$s", currentThread.getName(), currentThread.getId(), currentThread.getPriority()); return info; } }The LogCat result:
Thread Safety
The code snippets demoed about are not thread-safe. To make the code thread-safe, we can set the variable as volatile and wrap the setting inside a synchronized method so that only one thread updates the values at any time:
public class ThreadFragment extends Fragment { //... private volatile Handler handler; private volatile boolean stopThread; //... public void setHandler(Handler handler) { synchronized( this.handler ) { this.handler = handler; } if (handler == null){ requestStop(); } } public synchronized void requestStop() { stopThread = true; } thread = new Thread(new Runnable() { @Override public void run() { //... synchronized (handler) { if (handler != null) { //... handler.sendEmptyMessage(0); } } } } @Override public void onDestroy() { //... requestStop(); handler = null; } //... }
Above code avoids the scenarios like the work thread goes into the block after checking handler is not null, but right at that moment the handler is set to null by the main thread. However I am not so sure if such implementation is necessary. Unlike server side services that may be invoked by multiple callers at the same time, Android apps run locally so this kind of race conditions would rarely occur.