I have a Fragment with RecyclerView, which holds items with ImageView (basically like gallery app). Images are displayed using async task to do the work in separate thread. Images are displayed from base64 encoded string. Images are also cached with lrucache.
Problem
Everything works fine until i rotate the device 3rd or 4th time. The device crashes with out of memory error in onSaveInstanceState method.
Question
Any ideas how to prevent the OutOfMemory error? Thanks in advance
Code
Activity
public class TabsActivity extends BaseActivity implements ViewPager.OnPageChangeListener,
ActivityActions, TabLayout.OnTabSelectedListener {
private static final String TAG = TabsActivity.class.getSimpleName();
private static final String STATE_TAB_LAYOUT = "STATE_TAB_LAYOUT";
private static final String STATE_TOOLBAR = "STATE_TOOLBAR";
public static final String ACTION_TASKS = "ACTION_TASKS";
public static final String ACTION_MESSAGES = "ACTION_MESSAGES";
public static final int REQUEST_TASK_UPDATE = 1;
public static final int REQUEST_MESSAGE_UPDATE = 2;
private Toolbar toolbar;
private TabLayout tabLayout;
private ViewPager viewPager;
private FloatingActionButton createButton;
private PrefsManager prefsManager;
private ViewPagerAdapter adapter;
private LocalBroadcastManager broadcastManager;
private NotificationReceiver notificationReceiver;
private FragmentManager.OnBackStackChangedListener backStackChangedListener;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tabs);
prefsManager = PrefsManager.getInstance(this);
notificationReceiver = new NotificationReceiver();
backStackChangedListener = new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
onSettingsFragmentStateChanged(false);
}
}
};
tabLayout = (TabLayout) findViewById(R.id.tab_layout);
toolbar = (Toolbar) findViewById(R.id.toolbar);
viewPager = (ViewPager) findViewById(R.id.view_pager);
createButton = (FloatingActionButton) findViewById(R.id.floating_action_button);
createButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
KeyboardUtils.hideSoftwareInput(TabsActivity.this);
if (adapter.getCurrentTab(viewPager.getCurrentItem()) == Tab.TASKS) {
createNewTask();
} else {
createNewConversation();
}
}
});
setSupportActionBar(toolbar);
toolbar.setNavigationIcon(R.drawable.ic_action_back);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
final String action = getIntent().getAction();
adapter = new ViewPagerAdapter(getSupportFragmentManager());
adapter.setAdapterListener(new ExtendedPagerAdapter.AdapterListener() {
@Override
public void onAdapterInstantiated() {
if (savedInstanceState == null && action != null) {
if (action.equals(ACTION_TASKS)) {
onPageSelected(viewPager.getCurrentItem());
} else {
viewPager.setCurrentItem(1);
}
}
}
});
viewPager.addOnPageChangeListener(this);
viewPager.setAdapter(adapter);
tabLayout.setupWithViewPager(viewPager);
tabLayout.setOnTabSelectedListener(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_TASK_UPDATE) {
if (resultCode == Activity.RESULT_OK) {
((TasksFragment) adapter.getFragment(Tab.TASKS)).onTasksUpdated();
}
} else if (requestCode == REQUEST_MESSAGE_UPDATE) {
TabFragment tabFragment = adapter.getFragment(Tab.MESSAGES);
if (resultCode == Activity.RESULT_OK) {
if (tabFragment != null) {
((MessagesFragment) tabFragment).onNewMessagesReceived();
}
} else {
((MessagesFragment) tabFragment).initData();
}
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
protected void onResume() {
broadcastManager = LocalBroadcastManager.getInstance(this);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BroadcastConfig.ACTION_NEW_MESSAGE);
intentFilter.addAction(BroadcastConfig.ACTION_USER_STATUS);
broadcastManager.registerReceiver(notificationReceiver, intentFilter);
getSupportFragmentManager().addOnBackStackChangedListener(backStackChangedListener);
super.onResume();
}
@Override
protected void onPause() {
broadcastManager.unregisterReceiver(notificationReceiver);
getSupportFragmentManager().removeOnBackStackChangedListener(backStackChangedListener);
super.onPause();
}
@Override
protected void onRestoreInstanceState(Bundle inState) {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(inState.getString(STATE_TOOLBAR));
}
if (!inState.getBoolean(STATE_TAB_LAYOUT)) {
onSettingsFragmentStateChanged(true);
}
super.onRestoreInstanceState(inState);
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean(STATE_TAB_LAYOUT, tabLayout.getVisibility() == View.VISIBLE);
outState.putString(STATE_TOOLBAR, toolbar.getTitle().toString());
super.onSaveInstanceState(outState);
}
@Override
public void onBackPressed() {
MenuItem menuItem = toolbar.getMenu().findItem(R.id.action_search);
if (menuItem != null && !((SearchView) menuItem.getActionView()).isIconified()) {
((SearchView) menuItem.getActionView()).onActionViewCollapsed();
return;
}
if (popSupportBackStack(SettingsFragment.class.getSimpleName())) {
return;
}
setResult(RESULT_OK);
finish();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_contacts, menu);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
adapter.getFragment(viewPager.getCurrentItem()).onSearchPhraseChanged(newText);
return true;
}
});
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
onSettingsClick();
break;
case R.id.action_refresh:
onRefreshClick();
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onPageSelected(int position) {
if (adapter.isInstantiated()) {
onToolbarTitleChanged(adapter.getPageTitle(position).toString());
onToolbarSubtitleChanged(UserStatus.NONE);
}
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (adapter.getCurrentTab(tab.getPosition()).getFragmentTitle()
== Tab.ATTACHMENT_HISTORY.getFragmentTitle()) {
createButton.hide();
}
viewPager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(final TabLayout.Tab tab) {
createButton.hide(new FloatingActionButton.OnVisibilityChangedListener() {
@Override
public void onHidden(FloatingActionButton fab) {
fab.show();
}
});
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
@Override
public void requestDisplayDetails(Intent intent, int requestCode) {
startActivityForResult(intent, requestCode);
}
@Override
public void refreshTasks() {
adapter.getFragment(Tab.TASKS).onRefresh();
}
@Override
public void refreshMessages() {
adapter.getFragment(Tab.MESSAGES).onRefresh();
}
@Override
public boolean isNetworkAvailable() {
return checkNetworkAvailability();
}
private void createNewTask() {
startActivityForResult(new Intent(this, CreateTaskActivity.class), REQUEST_TASK_UPDATE);
}
private void createNewConversation() {
MessagesFragment fragment = (MessagesFragment) adapter.getFragment(Tab.MESSAGES);
startActivityForResult(new Intent(TabsActivity.this, MessageDetailsActivity.class)
.setAction(MessageDetailsActivity.ACTION_CREATE_MESSAGE)
.putExtra(MessageDetailsActivity.EXTRA_EXIST_CONV,
fragment.getCreatedConversations()), REQUEST_MESSAGE_UPDATE);
}
private void setTabLayoutVisible(boolean visible) {
int visibility = visible ? View.VISIBLE : View.GONE;
tabLayout.setVisibility(visibility);
}
private void onRefreshClick() {
if (checkNetworkAvailability()) {
adapter.getFragment(viewPager.getCurrentItem()).onRefresh();
}
}
private void onSettingsClick() {
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container, new SettingsFragment(), SettingsFragment.class.getSimpleName())
.addToBackStack(SettingsFragment.class.getSimpleName())
.commit();
onSettingsFragmentStateChanged(true);
}
private void onSettingsFragmentStateChanged(boolean visibleState) {
if (visibleState) {
isFragmentDialog = true;
onToolbarTitleChanged(getString(R.string.title_settings));
setTabLayoutVisible(false);
createButton.hide();
} else {
onPageSelected(viewPager.getCurrentItem());
setTabLayoutVisible(true);
if (adapter.getCurrentTab(viewPager.getCurrentItem()) != Tab.ATTACHMENT_HISTORY) {
createButton.show();
}
}
}
public void onToolbarTitleChanged(String title) {
toolbar.setTitle(title);
}
public void onToolbarSubtitleChanged(UserStatus userStatus) {
if (userStatus != null) {
toolbar.setSubtitle(userStatus.getText());
toolbar.setSubtitleTextColor(userStatus.getColor());
}
}
public void onLogoutConfirmed() {
HttpRequestManager.logout(prefsManager.getPhpSessId(), App.getPhoneId(this), prefsManager.getUserId(),
new HttpCallback<LogoutResponse>() {
@Override
public void onResponse(LogoutResponse logoutResponse) {
prefsManager.reset();
DBManager.delete(TabsActivity.this, DeleteTask.DeleteType.ALL, new Callback<Boolean>() {
@Override
public void onResponseReceived(Boolean params) {
startActivity(new Intent(TabsActivity.this, LoginActivity.class)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK));
finish();
}
});
}
});
}
private class NotificationReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(BroadcastConfig.ACTION_NEW_MESSAGE) || action.equals(BroadcastConfig.ACTION_USER_STATUS)) {
TabFragment childFragment = adapter.getFragment(Tab.MESSAGES);
if (childFragment != null) {
((MessagesFragment) childFragment).onNewMessagesReceived();
}
}
}
}
private class ViewPagerAdapter extends ExtendedPagerAdapter {
private final List<Tab> tabs = Tab.getAllTabs();
private final List<TabFragment> fragments = new ArrayList<>();
public ViewPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
fragments.add((TabFragment) super.instantiateItem(container, position));
return fragments.get(fragments.size() - 1);
}
@Override
public Fragment getItem(int position) {
Log.e(TAG, "CreatingFragment: " + tabs.get(position).getFragmentClass().getCanonicalName());
return Fragment.instantiate(TabsActivity.this, tabs.get(position).getFragmentClass().getCanonicalName());
}
@Override
public CharSequence getPageTitle(int position) {
return getString(tabs.get(position).getFragmentTitle());
}
@Override
public int getCount() {
return tabs.size();
}
public Tab getCurrentTab(int position) {
return tabs.get(position);
}
public TabFragment getFragment(int position) {
if (getCount() > position) {
return fragments.get(position);
}
return null;
}
public TabFragment getFragment(Tab tab) {
for (TabFragment tabFragment : fragments) {
if (tab.getFragmentClass() == tabFragment.getClass()) {
return tabFragment;
}
}
return null;
}
}
}
Fragment
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
swipeRefresh = (SwipeRefresh) inflater.inflate(R.layout.fragment_recycler_view, container, false);
galleryView = (RecyclerView) swipeRefresh.findViewById(R.id.recycler_view);
return swipeRefresh;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
swipeRefresh.setOnRefreshListener(this);
galleryView.setHasFixedSize(true);
galleryView.setLayoutManager(new GridLayoutManager(getContext(), getSpanCount()));
galleryView.setAdapter(adapter = new Adapter(attachments));
if (savedInstanceState == null) {
onRefresh();
} else {
onRestoreInstanceState(savedInstanceState);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
try {
outState.putString(STATE_ATTACHMENTS, Json.fromObject(attachments)); //Crashes here after 3rd or 4th rotate
} catch (JsonProcessingException e) {
Log.e(TAG, "Error while saving attachments", e);
}
super.onSaveInstanceState(outState);
}
@Override
public void onSearchPhraseChanged(String phrase) {
}
@Override
public void onRefresh() {
swipeRefresh.setRefreshing(true);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
initSampleData();
}
}, 3000);
}
private void onRestoreInstanceState(Bundle savedInstanceState) {
try {
attachments = Json.toCollection(savedInstanceState.getString(STATE_ATTACHMENTS),
ArrayList.class, Attachment.class);
adapter.notifyDataSetChanged();
} catch (IOException e) {
Log.e(TAG, "Error while restoring attachments", e);
}
}
private void initSampleData() {
swipeRefresh.setRefreshing(false);
String image = "";
File path = new File(Environment.getExternalStorageDirectory(), "image.txt");
byte[] bytes = new byte[(int) path.length()];
try {
FileInputStream fileInputStream = new FileInputStream(path);
fileInputStream.read(bytes);
image = new String(bytes);
} catch (FileNotFoundException e) {
Log.e(TAG, "Error while finding file to read from", e);
} catch (IOException e) {
Log.e(TAG, "Error while writing from file to string", e);
}
Attachment attachment = new Attachment(image);
attachments.clear();
for (int i = 0; i < 100; i++) {
attachment.setFileName(String.valueOf(i));
attachments.add(attachment);
}
adapter.notifyDataSetChanged();
}
private int getSpanCount() {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
float width = displayMetrics.widthPixels / displayMetrics.density;
float height = displayMetrics.heightPixels / displayMetrics.density;
int size;
if (Math.min(width, height) >= 600) {
size = Math.round(width / THUMBNAIL_SIZE_TABLET);
} else {
size = Math.round(width / THUMBNAIL_SIZE_PHONE);
}
return size < 7 ? size : 6;
}
private class ViewHolder extends BaseHolder<Attachment> implements View.OnClickListener {
private ImageView imageView;
public ViewHolder(ViewGroup viewGroup, int layoutRes) {
super(viewGroup, layoutRes);
imageView = (ImageView) itemView;
}
@Override
public void bind(Attachment attachment) {
itemView.setOnClickListener(this);
if (attachment.getImage() != null && !attachment.getImage().isEmpty()) {
DisplayThumbnailRequest.loadBitmap(attachment, imageView);
}
}
@Override
public void onClick(View v) {
// TODO: 2016-01-15 Open image in fullscreen
}
}
private class Adapter extends RecyclerView.Adapter<ViewHolder> {
private List<Attachment> attachments;
public Adapter(List<Attachment> attachments) {
this.attachments = attachments;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(parent, R.layout.adapter_item_attachment_history);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(attachments.get(position));
}
@Override
public int getItemCount() {
return attachments.size();
}
}
AsyncTask
public class DisplayThumbnailRequest extends AsyncHttpTask<Attachment, Void, Bitmap> {
private static final String TAG = DisplayThumbnailRequest.class.getSimpleName();
private WeakReference<ImageView> imageViewRef;
private Attachment attachment;
public DisplayThumbnailRequest(ImageView imageView) {
this.imageViewRef = new WeakReference<>(imageView);
}
@Override
protected void onPreExecute() {
if (imageViewRef != null) {
ImageView imageView = imageViewRef.get();
if (imageView != null) {
if (imageView.getVisibility() != View.VISIBLE) {
imageView.setVisibility(View.VISIBLE);
}
imageView.setImageResource(R.mipmap.ic_launcher);
}
}
}
@Override
protected Bitmap doInBackground(Attachment... params) {
attachment = params[0];
Bitmap bitmap = BitmapCache.getBitmap(attachment.getFileName());
if (bitmap == null) {
bitmap = BitmapUtils.fromBase64(attachment.getImage());
BitmapCache.addBitmap(attachment.getFileName(), bitmap);
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewRef != null && bitmap != null) {
ImageView imageView = imageViewRef.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
@Override
protected void onResponseReceived() {
}
public Attachment getAttachment() {
return attachment;
}
public static void loadBitmap(Attachment attachment, ImageView imageView) {
if (cancelDownloadRequest(attachment, imageView)) {
DisplayThumbnailRequest request = new DisplayThumbnailRequest(imageView);
AsyncBitmapDrawable drawable = new AsyncBitmapDrawable(App.getRes(), null, request);
imageView.setImageDrawable(drawable);
request.execute(attachment);
}
}
private static boolean cancelDownloadRequest(Attachment attachment, ImageView imageView) {
DisplayThumbnailRequest request = getDownloadTask(imageView);
if (request != null) {
String filePath = request.getAttachment().getFileName();
if (filePath == null || filePath.isEmpty() || !filePath.equals(attachment.getFileName())) {
request.cancel(true);
} else {
return false;
}
}
return true;
}
private static DisplayThumbnailRequest getDownloadTask(ImageView imageView) {
if (imageView != null) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncBitmapDrawable) {
return ((AsyncBitmapDrawable) drawable).getDisplayThumbnailRequest();
}
}
return null;
}
}
BitmapCache
public class BitmapCache {
private static final String TAG = BitmapCache.class.getSimpleName();
private static final int MAX_MEMORY = (int)((Runtime.getRuntime().maxMemory() / 1024) / 4);
private static LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(MAX_MEMORY) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount() / 1024;
}
};
public static void addBitmap(String key, Bitmap bitmap) {
if (getBitmap(key) == null) {
lruCache.put(key, bitmap);
}
}
public static Bitmap getBitmap(String key) {
return lruCache.get(key);
}
}
BitmapUtils.fromBase64
public static Bitmap fromBase64(String string) {
if (string != null && !string.isEmpty()) {
byte[] decodedString = Base64.decode(string, Base64.DEFAULT);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length, options);
options.inSampleSize = getInSampleSize(options, Metrics.dp2px(100), Metrics.dp2px(100));
options.inJustDecodeBounds = false;
return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length, options);
}
return null;
}
EDIT
I tried reducing arraylist size which is saved in onSavedInstanceState from 100 to 50 and OOM error is not being displayed (despite how many times you rotate the device). Can it be, that saved instance is too long (if it list has 100 items) and it floods the memory?