android-db-commons
android-db-commons copied to clipboard
Cursor doesn't close after using lazy()
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.
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.
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());
}
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?
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 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?
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 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?
@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 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 I get this messages time to time, when close Activity that manages appropriate Loader (ComposedCursorLoader)
@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));
}
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?
I still can't figure out how this can happen. I have few more questions that might help me pinpoint this issue:
- Which version of android-db-commons do you use?
- Can you try to reproduce it without the
lazy()
call? - Is the queried Uri notified of some changes by
contentResolver.notifyChange()
calls during the bug reproduction scenario?
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.
Thanks for the code sample, I'll try to reproduce (and fix) this issue soon.