SQLite query run on UI thread with ExpandableListV

2019-04-11 16:52发布

问题:

I'm in the progress of developing an Android app for displaying a number of RSS feeds (yes, I know there are many apps like this already). The data to be displayed is backed by a content provider, and I want to be backward compatible with API level 4.

I'm using an ExpandableListView to display the content of three different RSS feeds. The ExpandableListView's adapter is implemented as a sub-class of SimpleCursorTreeAdapter:

private class RssFeedLatestListAdapter extends SimpleCursorTreeAdapter {

    public static final String FEED_NAME_COLUMN = "feedName";

    public RssFeedLatestListAdapter(Context ctx, Cursor groupCursor, int groupLayout,
            String[] groupFrom, int[] groupTo, int childLayout, String[] childFrom,
            int[] childTo) {
        super(ctx, groupCursor, groupLayout, groupLayout, groupFrom, groupTo, childLayout, childFrom, childTo);
    }

    @Override
    protected Cursor getChildrenCursor(final Cursor groupCursor) {
        final String feedName = groupCursor.getString(groupCursor.getColumnIndex(FEED_NAME_COLUMN));
        return managedQuery(LATEST_URI, null,
                FeedItemColumns.CATEGORY + " = ? AND " + FeedItemColumns.FEED + " = ?",
                new String[] { mCategory.getId(), feedName }, null);
    }

    @Override
    protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
        super.bindGroupView(view, context, cursor, isExpanded);

        // Bind group view (impl details not important) ...
    }

    @Override
    protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
        super.bindChildView(view, context, cursor, isLastChild);

        // Bind child view (impl details not important) ...
    }
}

With this setup, all content is loaded as expected. However, the UI hangs/stutters randomly during the loading of the list content. Usually not enough to get an ANR, but still noticeable and very annoying!

In order to debug this issue, I enabled android.os.StrictMode (great new feature/tool btw!), and started up the emulator (on Android 2.3). When the list content is loaded, I get the following StrictMode output:

D/StrictMode(  395): StrictMode policy violation; ~duration=1522 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=23 violation=2
D/StrictMode(  395):  at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:745)
D/StrictMode(  395):  at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:1345)
D/StrictMode(  395):  at android.database.sqlite.SQLiteQueryBuilder.query(SQLiteQueryBuilder.java:330)
D/StrictMode(  395):  at com.mycompany.myapp.provider.RSSFeedProvider.query(RSSFeedProvider.java:128)
D/StrictMode(  395):  at android.content.ContentProvider$Transport.query(ContentProvider.java:187)
D/StrictMode(  395):  at android.content.ContentResolver.query(ContentResolver.java:262)
D/StrictMode(  395):  at android.app.Activity.managedQuery(Activity.java:1550)
D/StrictMode(  395):  at com.mycompany.myapp.activity.MultipleFeedsActivity$RssFeedLatestListAdapter.getChildrenCursor(MultipleFeedsActivity.java:388)
D/StrictMode(  395):  at android.widget.CursorTreeAdapter.getChildrenCursorHelper(CursorTreeAdapter.java:106)
D/StrictMode(  395):  at android.widget.CursorTreeAdapter.getChildrenCount(CursorTreeAdapter.java:178)
D/StrictMode(  395):  at android.widget.ExpandableListConnector.refreshExpGroupMetadataList(ExpandableListConnector.java:561)
D/StrictMode(  395):  at android.widget.ExpandableListConnector.expandGroup(ExpandableListConnector.java:682)
D/StrictMode(  395):  at android.widget.ExpandableListConnector.expandGroup(ExpandableListConnector.java:636)
D/StrictMode(  395):  at android.widget.ExpandableListView.expandGroup(ExpandableListView.java:608)
D/StrictMode(  395):  at com.mycompany.myapp.activity.MultipleFeedsActivity.onReceiveResult(MultipleFeedsActivity.java:335)
D/StrictMode(  395):  at com.mycompany.myapp.service.FeedResultReceiver.onReceiveResult(FeedResultReceiver.java:40)
D/StrictMode(  395):  at android.os.ResultReceiver$MyRunnable.run(ResultReceiver.java:43)
D/StrictMode(  395):  at android.os.Handler.handleCallback(Handler.java:587)
D/StrictMode(  395):  at android.os.Handler.dispatchMessage(Handler.java:92)
D/StrictMode(  395):  at android.os.Looper.loop(Looper.java:123)
D/StrictMode(  395):  at android.app.ActivityThread.main(ActivityThread.java:3647)
D/StrictMode(  395):  at java.lang.reflect.Method.invokeNative(Native Method)
D/StrictMode(  395):  at java.lang.reflect.Method.invoke(Method.java:507)
D/StrictMode(  395):  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)
D/StrictMode(  395):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)
D/StrictMode(  395):  at dalvik.system.NativeStart.main(Native Method)

This seems reasonable! In SimpleCursorTreeAdapter.getChildrenCursor(), the content provider is queried on the UI thread, which of course is a bad idea that will most definitely hang the UI!

I had a look at the JavaDoc (API level 4) of CursorTreeAdapter (a super-class of SimpleCursorTreeAdapter) and for getChildrenCursor() it says:

"If you want to asynchronously query a provider to prevent blocking the UI, it is possible to return null and at a later time call setChildrenCursor(int, Cursor).".

Great! Let's do that! I change my implementation of getChildrenCursor() to start a new task responsible for performing the query and setting the children cursor on the adapter. After having started the task, I just return null in getChildrenCursor():

@Override
protected Cursor getChildrenCursor(final Cursor groupCursor) {
    final String feedName = groupCursor.getString(groupCursor.getColumnIndex(FEED_NAME_COLUMN));
    new RefreshChildrenCursorTask(groupCursor.getPosition()).execute(feedName);
    return null;
}

, where the RefreshChildrenCursorTask is implemented as:

private class RefreshChildrenCursorTask extends AsyncTask<String, Void, Cursor> {

    private int mGroupPosition;

    public RefreshChildrenCursorTask(int groupPosition) {
        this.mGroupPosition = groupPosition;
    }

    @Override
    protected Cursor doInBackground(String... params) {
        String feedName = params[0];
        return managedQuery(LATEST_URI, null,
                FeedItemColumns.CATEGORY + " = ? AND " + FeedItemColumns.FEED + " = ?",
                new String[] { mCategory.getId(), feedName }, null);
        }

        @Override
        protected void onPostExecute(Cursor childrenCursor) {
            mLatestListAdapter.setChildrenCursor(mGroupPosition, childrenCursor);
        }
    }
}

I reinstalled the app on the 2.3 emulator and started it up. It worked like a charm, bye bye non-responsive UI! But the joy didn't last long ... The next thing to do was to run the same code on the target device (in this case a Samsung Galaxy S running Android 2.2). I then got the following Exception during the loading of the list feed content:

E/AndroidRuntime(23191): FATAL EXCEPTION: main
E/AndroidRuntime(23191): java.lang.NullPointerException
E/AndroidRuntime(23191):  at android.widget.SimpleCursorTreeAdapter.initFromColumns(SimpleCursorTreeAdapter.java:194)
E/AndroidRuntime(23191):  at android.widget.SimpleCursorTreeAdapter.initChildrenFromColumns(SimpleCursorTreeAdapter.java:205)
E/AndroidRuntime(23191):  at android.widget.SimpleCursorTreeAdapter.init(SimpleCursorTreeAdapter.java:186)
E/AndroidRuntime(23191):  at android.widget.SimpleCursorTreeAdapter.(SimpleCursorTreeAdapter.java:136)
E/AndroidRuntime(23191):  at com.mycompany.myapp.activity.MultipleFeedsActivity$RssFeedLatestListAdapter.(MultipleFeedsActivity.java:378)
E/AndroidRuntime(23191):  at com.mycompany.myapp.activity.MultipleFeedsActivity$2.run(MultipleFeedsActivity.java:150)
E/AndroidRuntime(23191):  at android.os.Handler.handleCallback(Handler.java:587)
E/AndroidRuntime(23191):  at android.os.Handler.dispatchMessage(Handler.java:92)
E/AndroidRuntime(23191):  at android.os.Looper.loop(Looper.java:123)
E/AndroidRuntime(23191):  at android.app.ActivityThread.main(ActivityThread.java:4627)
E/AndroidRuntime(23191):  at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(23191):  at java.lang.reflect.Method.invoke(Method.java:521)
E/AndroidRuntime(23191):  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:871)
E/AndroidRuntime(23191):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:629)
E/AndroidRuntime(23191):  at dalvik.system.NativeStart.main(Native Method)

A quick look at the source code of SimpleCursorTreeAdapter tells me that there is no way that it can cope with null being returned asynchronously from getChildrenCursor(). They seem to have solved this problem in Android 2.3, but how are you supposed to get around it in earlier versions of Android? Do I really have to write my own implementation of CursorTreeAdapter to be able to refresh the children cursor asynchronously?

Has anybody had the same problem and perhaps even found a (feasible) workaround? If not, could anybody give me a hint on how to proceed!? I would really appreciate any help I can get!

Thanks in advance!

Regards, Jacob

回答1:

If they fixed it in the 2.3 SimpleCursorTreeAdapter, why don't you just download the java file and include it in your project and use that version rather than the one on the phone?

I haven't looked at the code, but as long as they didn't make any new 2.3 API calls, it should work.



回答2:

Thanks for your feedback, mp2526! I apologize for my late response!

That is a great suggestion! However, I really thought that it should be possible to do this (retrieve the children cursors asynchronouosly) in API 8, and that I had misunderstood anything ... Apparently, it isn't possible (at least not with the use of SimpleCursorTreeAdapter)!

Anyway, I made a diff between API 8 and API 9 for SimpleCursorTreeAdapter, and what they have done in Android 2.3 is that the childFrom and groupFrom column ids are now initialized lazily, i.e. no longer in the constructor. Instead, these members are now initialized in the first call to bindView(). That way, there's now time to asynchronously retrieve the children cursors. As there doesn't seem to be any changes to the public API of this class between API 4 and API 9 (except the new method setViewText()), your solution to use the API version 9 of this class should work without changes! I tried it out, and it did!

Thanks a lot, mp2526!