android-db-commons icon indicating copy to clipboard operation
android-db-commons copied to clipboard

Cursor doesn't close after using lazy()

Open ultraon opened this issue 10 years ago • 15 comments

While using lazy() collection based on FluentCursor, Cursor doesn't close. I get such messages in LogCat oftenly. Actually Cursor has own close in finalize() method, but Android Group recommends that we should explicitely close Cursor after usage.

ultraon avatar Sep 15 '14 15:09 ultraon

Hey,

There's no lazy() method on the FluentCursor so I assume you are talking about creating ComposedCursorLoader, am I right? Can you show me how are you exactly using it?

Thanks.

henieek avatar Sep 15 '14 15:09 henieek

Hey! Of course, I had in mind ComposedCursorLoader with methos lazy(). After that I use ArrayAdapter with the resulting collection. This is sample of my code

@Override
    protected Loader<List<Group>> createGroupsLoader() {
        final Member member = loginManager.getMember();
        final String ownerGuid = null != member ? member.getId() : "";
        return CursorLoaderBuilder.forUri(FStoreProvider.contentUri(FStore.GroupTable.CONTENT_URI))
                .projection(microOrm.getProjection(Group.class))
                .orderBy(FStore.GroupTable.NAME)
                .transformRow(microOrm.getFunctionFor(Group.class))
                .lazy()
                .transform(new Function<List<Group>, List<Group>>() { //run on backgrhoun thread
                    final INetworkManager networkManager = App.produce(INetworkManager.class);
                    @Override
                    public List<Group> apply(final List<Group> groups) {
                        for (Group group : groups) {
                            if (!TextUtils.isEmpty(ownerGuid) && ownerGuid.equals(group.getOwnerId())) {
                                try {
                                    group.setEventCount(networkManager.getCountRequestsForGroupId(group.getGuid()).getCount());
                                } catch (Exception e) {
                                    Log.w(TAG, e);
                                }
                            }
                        }
                        return groups;
                    }
                })
                .build(getActivity());
    }

ultraon avatar Sep 16 '14 15:09 ultraon

I have run the following code without any issues:

public class MyActivity extends FragmentActivity implements LoaderCallbacks<List<String>> {

  private ArrayAdapter<String> mAdapter;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_list);

    mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1);
    ((ListView) findViewById(android.R.id.list)).setAdapter(mAdapter);

    StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
        .detectLeakedSqlLiteObjects()
        .penaltyLog()
        .build());

    getSupportLoaderManager().initLoader(0, null, this);
  }

  @Override
  public Loader<List<String>> onCreateLoader(int id, Bundle args) {
    return CursorLoaderBuilder
        .forUri(Contacts.CONTENT_URI)
        .transformRow(getColumn(Contacts.DISPLAY_NAME).asString())
        .lazy()
        .transform(Functions.<List<String>>identity())
        .build(this);
  }

  @Override
  public void onLoadFinished(Loader<List<String>> loader, List<String> data) {
    mAdapter.clear();
    mAdapter.addAll(data);
  }

  @Override
  public void onLoaderReset(Loader<List<String>> loader) {
    mAdapter.clear();
  }
}

Maybe the problem is caused by something else. I do not recognize the createGroupsLoader method. Is it a part of your code base or some 3rd party library?

chalup avatar Sep 16 '14 20:09 chalup

Hey, i get messages from LogCat about not closing cursor after usage. It doesn't call crash on my Application, but it may be problem with memory on many queries with many cursors, Cursor has close() method in finalize(), but sometimes finalize() doesn't call by system, as far as i know.

09-17 13:06:58.209  25820-26157/com.ultraon.navygroups E/CursorLeakDetecter﹕ PossibleCursorLeak:content://com.ultraon.navygroups.providers.store/group_table,QueryCounter:5
    android.database.sqlite.DatabaseObjectNotClosedException: Application did not close the cursor or database object that was opened here
            at android.content.ContentResolver.query(ContentResolver.java:399)
            at android.content.ContentResolver.query(ContentResolver.java:316)
            at com.getbase.android.db.loaders.ComposedCursorLoader.loadCursorInBackground(ComposedCursorLoader.java:73)
            at com.getbase.android.db.loaders.ComposedCursorLoader.loadInBackground(ComposedCursorLoader.java:59)
            at android.support.v4.content.AsyncTaskLoader.onLoadInBackground(AsyncTaskLoader.java:242)
            at android.support.v4.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:51)
            at android.support.v4.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:40)
            at android.support.v4.content.ModernAsyncTask$2.call(ModernAsyncTask.java:123)
            at java.util.concurrent.FutureTask.run(FutureTask.java:234)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
            at java.lang.Thread.run(Thread.java:838)
09-17 13:06:58.250  25820-26155/com.ultraon.navygroups E/CursorLeakDetecter﹕ PossibleCursorLeak:content://com.ultraon.navygroups.providers.store/group_table,QueryCounter:6
    android.database.sqlite.DatabaseObjectNotClosedException: Application did not close the cursor or database object that was opened here
            at android.content.ContentResolver.query(ContentResolver.java:399)
            at android.content.ContentResolver.query(ContentResolver.java:316)
            at com.getbase.android.db.loaders.ComposedCursorLoader.loadCursorInBackground(ComposedCursorLoader.java:73)
            at com.getbase.android.db.loaders.ComposedCursorLoader.loadInBackground(ComposedCursorLoader.java:59)
            at android.support.v4.content.AsyncTaskLoader.onLoadInBackground(AsyncTaskLoader.java:242)
            at android.support.v4.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:51)
            at android.support.v4.content.AsyncTaskLoader$LoadTask.doInBackground(AsyncTaskLoader.java:40)
            at android.support.v4.content.ModernAsyncTask$2.call(ModernAsyncTask.java:123)
            at java.util.concurrent.FutureTask.run(FutureTask.java:234)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
            at java.lang.Thread.run(Thread.java:838)

ultraon avatar Sep 17 '14 10:09 ultraon

@ultraon Looks weird. How are you initializing this loader? Can you post some wider code sample, showing how you are using ComposedCursorLoader within the Fragment or Activity?

henieek avatar Sep 17 '14 10:09 henieek

ok, this is my code BaseGroupsFragment.java:

package com.ultraon.navygroups.fragments;

import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.View;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.Toast;

import com.telly.groundy.Groundy;
import com.telly.groundy.annotations.OnCancel;
import com.telly.groundy.annotations.OnFailure;
import com.telly.groundy.annotations.OnSuccess;
import com.ultraon.navygroups.R;
import com.ultraon.navygroups.adapters.GroupsAdapter;
import com.ultraon.navygroups.managers.ILoginManager;
import com.ultraon.navygroups.network.models.Member;
import com.ultraon.navygroups.store.models.Group;
import com.ultraon.navygroups.tasks.GetGroupsTask;

import org.chalup.microorm.MicroOrm;

import java.util.List;

import javax.inject.Inject;

import butterknife.InjectView;

/**
 * Created by vpopov on 9/13/2014.
 */
public abstract class BaseGroupsFragment extends BaseListFragment implements SwipeRefreshLayout.OnRefreshListener {
    protected static final String TAG = "myApp";
    protected final LoaderManager.LoaderCallbacks<List<Group>> groupsLoaderCallback = new LoaderManager.LoaderCallbacks<List<Group>>() {
        @Override
        public Loader<List<Group>> onCreateLoader(final int i, final Bundle bundle) {
            return createGroupsLoader();
        }
        @Override
        public void onLoadFinished(final Loader<List<Group>> listLoader, final List<Group> groups) {
            onDataLoaded(groups);
        }
        @Override
        public void onLoaderReset(final Loader<List<Group>> listLoader) {
        }
    };
    @InjectView(R.id.swiperefresh)
    SwipeRefreshLayout swipeRefreshLayout;
    @InjectView(android.R.id.list)
    ListView listView;
    @Inject
    MicroOrm microOrm;
    @Inject
    ILoginManager loginManager;

    @Override
    public void onViewCreated(final View view, final Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        swipeRefreshLayout.setOnRefreshListener(this);
        initRefreshData();
        initLoader(getLoaderManager(), false);
    }

    protected abstract Loader<List<Group>> createGroupsLoader();

    protected abstract void initLoader(LoaderManager lm, boolean restart);

    protected ListAdapter createAdapter(List<Group> groups) {
        final Member member = loginManager.getMember();
        return new GroupsAdapter(getActivity(), R.layout.groups_list_item, groups, null != member ? member.getId() : "");
    }

    @Override
    public void onRefresh() {
        initRefreshData();
    }

    protected void initRefreshData() {
        if (!swipeRefreshLayout.isRefreshing()) swipeRefreshLayout.setRefreshing(true);
        //refresh data - task on server get
        Groundy.create(GetGroupsTask.class).callback(new Object() {
            @OnSuccess(GetGroupsTask.class)
            public void onSuccess() {
                initLoader(getLoaderManager(), true);
                if (!isResumed()) return;
                swipeRefreshLayout.setRefreshing(false);
                Toast.makeText(getActivity(), "Groups is updated", Toast.LENGTH_SHORT).show();
            }

            @OnCancel(GetGroupsTask.class)
            @OnFailure(GetGroupsTask.class)
            public void onFailure() {
                if (!isResumed()) return;
                swipeRefreshLayout.setRefreshing(false);
                Toast.makeText(getActivity(), "Groups updating was failed", Toast.LENGTH_SHORT).show();
            }
        }).queueUsing(getActivity());
    }

    protected void onDataLoaded(final List<Group> groups) {
        setListAdapter(createAdapter(groups));
    }
}

MyGroupsFragment.java:

package com.ultraon.navygroups.fragments;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.Toast;

import com.getbase.android.db.loaders.CursorLoaderBuilder;
import com.google.common.collect.Lists;
import com.ultraon.navygroups.R;
import com.ultraon.navygroups.activities.DetailsChatActivity;
import com.ultraon.navygroups.activities.GroupsActivity;
import com.ultraon.navygroups.network.models.Member;
import com.ultraon.navygroups.store.FStore;
import com.ultraon.navygroups.store.FStoreProvider;
import com.ultraon.navygroups.store.models.Group;

import java.util.List;

import butterknife.OnClick;

/**
 * Created by vpopov on 9/13/2014.
 */
public class MyGroupsFragment extends BaseGroupsFragment {

    private boolean needRefresh;

    public interface ICallback {
        void onGroupsClicked(View view);
    }

    private static final int MY_GROUPS_LOADER_ID = R.id.my_groups_loader_id;

    private ICallback callback;

    public static MyGroupsFragment newInstance() {
        final MyGroupsFragment fragment = new MyGroupsFragment();
        Bundle args = new Bundle();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onAttach(final Activity activity) {
        super.onAttach(activity);
        if (activity instanceof ICallback) {
            callback = (ICallback) activity;
        }
    }

    @Override
    public void onDestroyView() {
        callback = null;
        super.onDestroyView();
    }

    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
        return inflater.inflate(R.layout.my_groups_layout, container, false);
    }

    @Override
    public void onResume() {
        super.onResume();
        if (needRefresh) {
            needRefresh = false;
            initLoader(getLoaderManager(), true);
        }
    }

    @Override
    protected Loader<List<Group>> createGroupsLoader() {
        final Member member = loginManager.getMember();
        List<String> groups = null;
        if (null != member) groups = member.getGroups();
        if (null == groups) groups = Lists.newArrayList();

        return CursorLoaderBuilder.forUri(FStoreProvider.contentUri(FStore.GroupTable.CONTENT_URI))
                .projection(microOrm.getProjection(Group.class))
                .whereIn(FStore.GroupTable.GUID, groups)
                .orderBy(FStore.GroupTable.NAME)
                .transformRow(microOrm.getFunctionFor(Group.class))
                .lazy()
                .build(getActivity());
    }

    @Override
    protected void initLoader(final LoaderManager lm, final boolean restart) {
        if (restart) {
            lm.restartLoader(MY_GROUPS_LOADER_ID, null, groupsLoaderCallback);
        } else {
            lm.initLoader(MY_GROUPS_LOADER_ID, null, groupsLoaderCallback);
        }
    }

    @OnClick(R.id.v_add_group)
    public void onAddGroupBtnClicked(final View view) {
        if (null != callback) {
            callback.onGroupsClicked(view);
            needRefresh = true;
        }
    }

    @Override
    public void onListItemClick(final ListView l, final View v, final int position, final long id) {
        //Toast.makeText(getActivity(), position + " item clicked", Toast.LENGTH_SHORT).show();
        Group group = (Group) getListAdapter().getItem(position);
        DetailsChatActivity.startActivity(getActivity(), group);
    }
}

GroupsAdapter.java:

package com.ultraon.navygroups.adapters;

import android.content.Context;
import android.graphics.Color;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import com.ultraon.navygroups.R;
import com.ultraon.navygroups.store.models.Group;

import java.util.List;

import butterknife.ButterKnife;
import butterknife.InjectView;

/**
* Created by vpopov on 9/13/2014.
*/
public class GroupsAdapter extends ArrayAdapter<Group> {
    private final int layoutId;
    private final String ownerGuid;

    public GroupsAdapter(@NonNull final Context context, @LayoutRes int layoutId, List<Group> groups, @NonNull final String ownerGuid) {
        super(context, layoutId, groups);
        this.layoutId = layoutId;
        this.ownerGuid = ownerGuid;
    }

    @Override
    public View getView(final int position, final View convertView, final ViewGroup parent) {
        ViewHolder viewHolder;
        View rootView = convertView;
        if (null == rootView) {
            final LayoutInflater inflater = LayoutInflater.from(getContext());
            rootView = inflater.inflate(layoutId, null);
            viewHolder = new ViewHolder(rootView);
            rootView.setTag(viewHolder);
        }

        viewHolder = (ViewHolder) rootView.getTag();

        Group model = getItem(position);

        final int color = ownerGuid.equals(model.getOwnerId()) ? Color.LTGRAY : Color.TRANSPARENT;
        viewHolder.root.setBackgroundColor(color);
        viewHolder.title.setText(model.getName());

        final int eventCount = model.getEventCount();
        if (0 == eventCount) {
            viewHolder.count.setVisibility(View.INVISIBLE);
        } else {
            viewHolder.count.setVisibility(View.VISIBLE);
        }
        viewHolder.count.setText(String.valueOf(eventCount));

        return rootView;
    }

    public static class ViewHolder {
        @InjectView(R.id.tv_title_group)
        TextView title;
        @InjectView(R.id.tv_count_messages)
        TextView count;
        @InjectView(R.id.root)
        View root;

        public ViewHolder(final View rootView) {
            ButterKnife.inject(this, rootView);
        }
    }
}

ultraon avatar Sep 17 '14 13:09 ultraon

@ultraon Ok, fragment doesn't look like a cause. Are you sure that you are not opening another Cursor (other than the one you return) within your ContentProvider.query?

henieek avatar Sep 17 '14 13:09 henieek

@partition 100% sure! Would you tell me, where you call cursor.close() in "ListWrapperCursor" (in connection to lazy() method), because i didn't see this call. So when i close my fragment, i lose the refference on Fragment, and all data erases by GC, so Adapter with "ListWrapperCursor" killed by GC, but Cursor doesn't get explicit call for close() method.

ultraon avatar Sep 17 '14 14:09 ultraon

@ultraon Cursors aren't closed by LazyCursorList, they are managed by ComposedCursorLoader and closed in releaseResources call.

Getting back to your problem - do you observe the leak in 100% of cases, or only from time to time?

chalup avatar Sep 17 '14 19:09 chalup

@chalup I get this messages time to time, when close Activity that manages appropriate Loader (ComposedCursorLoader)

ultraon avatar Sep 18 '14 13:09 ultraon

@chalup I can reproduce this error constantly when i start Activity, initLoader(), then i call restartLoader() and finish() Activity quickly. I think there is problem with onAbandone() method (maybe we should call releaseResources() method) - first idea. And second Idea - in place where we call releaseCursor(cursorsForResults.remove(result)) maybe some data is not cleaned from cursorsForResults, and we can petentially have leak.


  @Override
  protected void onAbandon() {
    mObserver.setEnabled(false);
    unregisterAdditionalUris();
  }

@Override
  protected void releaseResources(T result) {
    releaseCursor(cursorsForResults.remove(result));
  }

ultraon avatar Sep 18 '14 13:09 ultraon

onAbandon is definitely not a place to release resources - you should keep the already loaded data until the 2nd instance of loader created after restartLoader() delivers its data.

I'll look into it. It seems that your transform call performs a network call, which might take a while. Is that correct?

chalup avatar Sep 18 '14 18:09 chalup

I still can't figure out how this can happen. I have few more questions that might help me pinpoint this issue:

  1. Which version of android-db-commons do you use?
  2. Can you try to reproduce it without the lazy() call?
  3. Is the queried Uri notified of some changes by contentResolver.notifyChange() calls during the bug reproduction scenario?

chalup avatar Sep 30 '14 07:09 chalup

Hi. I have the same issue but without lazy(), when use

@Override
    public void onResume() {
        super.onResume();
        //hack for loader in ViewPager fragment
        getActivity().getWindow().getDecorView().post(new Runnable() {
            @Override
            public void run() {
                LoaderHelper<T> loaderHelper = getLoaderHelper();
                loaderHelper.initLoader(LoaderFragment.this, getArgs(), LoaderFragment.this);
            }
        });
    }

in my page fragment with FragmentStatePagerAdapter.

pkozlovskiy avatar Dec 29 '14 13:12 pkozlovskiy

Thanks for the code sample, I'll try to reproduce (and fix) this issue soon.

chalup avatar Dec 30 '14 21:12 chalup