【问题标题】:Changing device orientation crashes Activity when using AutoCompleteTextView and CursorAdapter使用 AutoCompleteTextView 和 CursorAdapter 时更改设备方向会导致 Activity 崩溃
【发布时间】:2012-03-16 17:42:23
【问题描述】:

情况

我有一个带有 AutoCompleteTextView 的 Activity。在您键入时,AutoCompleteTextView 会从您的联系人中找到匹配的姓名并将其显示在列表中。如果在显示此列表时更改设备方向,则 Activity 崩溃(源代码后提供的错误消息)。

注意事项

我正在为 ICS 4.0.3 开发并在 Nexus S 设备上进行测试。我正在尝试遵循使用 LoaderManager 生成和管理游标的最佳实践。我的理解是 LoaderManager 应该在方向更改时保留光标数据 (http://developer.android.com/guide/topics/fundamentals/loaders.html#callback),但似乎并非如此。

因为当过滤约束太小而无法使用时,CursorAdapter 想让我返回原始的、未过滤的光标,所以我是:

  1. 将光标保存为 Activity 的静态变量,以便在不满足过滤条件时使用(抱歉术语不正确。我是 Java 新手)。
  2. 防止 CursorAdapter 在替换游标后关闭游标,除非它通过与保存的游标进行比较来确认它不是原始游标。

问题似乎是在方向更改后调用了 onLoadFinished LoaderManager 回调,但它传递的光标(原始光标?)在重新定位期间关闭。

如果我通过将以下内容添加到清单中的 activity 声明中来配置我的 Activity 以自行管理方向更改:

android:configChanges="orientation|screenSize"

保存的原始光标应在方向更改时保留(对吗?)。虽然应用程序没有崩溃,但出现了另一个相关问题:

  • 如果我键入几个字母,更改设备方向,然后开始删除字母,一旦我减少到 1 个或 0 个字母,LogCat 会警告我在光标关闭后我正在尝试访问它。

在这种情况下,我原来的光标似乎也消失了。我猜应用程序没有崩溃,因为当我的活动被配置为管理方向更改时,没有调用 onLoadFinished 回调

我的问题

  1. 我是否正确地假设我的光标在设备方向改变时被破坏?
  2. 如何在设备方向改变时保留光标和/或其数据?

源代码

查看 - home.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <AutoCompleteTextView
        android:id="@+id/newPlayer_edit"
        android:inputType="text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:hint="Contact"
        android:singleLine="true" >
        <requestFocus />
    </AutoCompleteTextView>

</LinearLayout>

活动 - Home.java

public class Home extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {

// Constants
private static final String TAG = "HOME";
private static final boolean DEBUG = true;
public static final int LOADER_CONTACTS_CURSOR = 1;

// Variables
private AdapterContacts adapter;
public static Cursor originalCursor = null;


/**
 * Overrides
 */

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Set the view
    setContentView(R.layout.home);

    // Initialize CursorAdapter
    adapter = new AdapterContacts(this, null, 0);

    // Attach CursorAdapter to AutoCompleteTextView field
    AutoCompleteTextView field = (AutoCompleteTextView) findViewById(R.id.newPlayer_edit);
    field.setAdapter(adapter);

    // Initialize Cursor using LoaderManager
    LoaderManager.enableDebugLogging(true);
    getLoaderManager().initLoader(LOADER_CONTACTS_CURSOR, null, this);
}

@Override
public void onDestroy() {
    if (DEBUG) Log.i(TAG, "Destroying activity");
    super.onDestroy();
}

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    if (DEBUG) Log.i(TAG, "Loader Callback: creating loader");
    return new CursorLoader(this, ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    if (DEBUG) Log.i(TAG, "Loader Callback: load finished");
    // If no cursor has been loaded before, reserve this cursor as the original
    // It will be returned by the CursorAdapter when the filter constraint is null 
    if (originalCursor == null) {
        originalCursor = cursor;
    }

    // add the cursor to the adapter
    adapter.swapCursor(cursor);
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    if (DEBUG) Log.i(TAG, "Loader Callback: resetting");
    adapter.swapCursor(null);
}
}

CursorAdapter - AdapterContacts.java

public class AdapterContacts extends CursorAdapter {

// Constants
private static final String TAG = "AdapterContacts";
private static final boolean DEBUG = true;

// Variables
private TextView mName;
private ContentResolver mContent;

/**
 * Constructor
 */
public AdapterContacts(Context context, Cursor c, int flags) {
    super(context, c, flags);
    mContent = context.getContentResolver();
}

/**
 * Overrides
 */

@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
    // Inflate the views that create each row of the dropdown list
    final LayoutInflater inflater = LayoutInflater.from(context);
    final LinearLayout ret = new LinearLayout(context);
    ret.setOrientation(LinearLayout.VERTICAL);

    mName = (TextView) inflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
    ret.addView(mName, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));

    /*
    int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    mName.setText(cursor.getString(nameIdx));
    */

    return ret;
}

@Override
public void bindView(View view, Context context, Cursor cursor) {
    // Fill the dropdown row with data from the cursor
    int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    String name = cursor.getString(nameIdx);
    ((TextView) ((LinearLayout) view).getChildAt(0)).setText(name);
}

@Override
public String convertToString(Cursor cursor) {
    // Convert the dropdown list entry that the user clicked on
    // into a string that will fill the AutoCompleteTextView
    int nameCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
    return cursor.getString(nameCol);
}

@Override
public void changeCursor(Cursor newCursor) {
    // Because a LoaderManager is used to initialize the originalCursor
    // changeCursor (which closes cursors be default when they're released)
    // is overridden to use swapCursor (which doesn't close cursors). 
    Cursor oldCursor = swapCursor(newCursor);

    // Any swapped out cursors that are not the original cursor must 
    // then be closed manually.
    if (oldCursor != Home.originalCursor) {
        oldCursor.close();
    }
}

@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
    // If their is a constraint, generate and return a new cursor
    if (constraint != null) {
        // I'd love to use a LoaderManager here too,
        // but haven't quite figured out the best way.
        if (DEBUG) Log.i(TAG, "Constraint is not null: " + constraint.toString());
        Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, constraint.toString());
        return mContent.query(uri, null, null, null, null);
    }

    // If no constraint, return the originalCursor
    if (DEBUG) Log.i(TAG, "Constraint is null");
    return Home.originalCursor;
}
}

错误信息

03-16 10:39:34.839: E/AndroidRuntime(22097): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.myapp.basic/com.myapp.basic.Home}: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1956)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1981)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3351)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.access$700(ActivityThread.java:123)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1151)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.os.Handler.dispatchMessage(Handler.java:99)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.os.Looper.loop(Looper.java:137)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.main(ActivityThread.java:4424)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at java.lang.reflect.Method.invokeNative(Native Method)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at java.lang.reflect.Method.invoke(Method.java:511)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at dalvik.system.NativeStart.main(Native Method)
03-16 10:39:34.839: E/AndroidRuntime(22097): Caused by: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:75)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.BulkCursorToCursorAdaptor.getColumnNames(BulkCursorToCursorAdaptor.java:170)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.AbstractCursor.getColumnIndex(AbstractCursor.java:248)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.AbstractCursor.getColumnIndexOrThrow(AbstractCursor.java:265)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.database.CursorWrapper.getColumnIndexOrThrow(CursorWrapper.java:78)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.widget.CursorAdapter.swapCursor(CursorAdapter.java:338)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.myapp.basic.Home.onLoadFinished(Home.java:70)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at com.myapp.basic.Home.onLoadFinished(Home.java:1)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl$LoaderInfo.callOnLoadFinished(LoaderManager.java:438)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl$LoaderInfo.finishRetain(LoaderManager.java:309)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.LoaderManagerImpl.finishRetain(LoaderManager.java:765)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.Activity.performStart(Activity.java:4485)
03-16 10:39:34.839: E/AndroidRuntime(22097):    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1929)
03-16 10:39:34.839: E/AndroidRuntime(22097):    ... 12 more

警告消息 - 当 Activity 配置为自行管理方向更改时

03-16 10:47:50.804: W/Filter(22739): An exception occured during performFiltering()!
03-16 10:47:50.804: W/Filter(22739): android.database.StaleDataException: Attempted to access a cursor after it has been closed.
03-16 10:47:50.804: W/Filter(22739):    at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:75)
03-16 10:47:50.804: W/Filter(22739):    at android.database.BulkCursorToCursorAdaptor.getCount(BulkCursorToCursorAdaptor.java:81)
03-16 10:47:50.804: W/Filter(22739):    at android.database.CursorWrapper.getCount(CursorWrapper.java:57)
03-16 10:47:50.804: W/Filter(22739):    at android.widget.CursorFilter.performFiltering(CursorFilter.java:53)
03-16 10:47:50.804: W/Filter(22739):    at android.widget.Filter$RequestHandler.handleMessage(Filter.java:234)
03-16 10:47:50.804: W/Filter(22739):    at android.os.Handler.dispatchMessage(Handler.java:99)
03-16 10:47:50.804: W/Filter(22739):    at android.os.Looper.loop(Looper.java:137)
03-16 10:47:50.804: W/Filter(22739):    at android.os.HandlerThread.run(HandlerThread.java:60)

【问题讨论】:

    标签: java android


    【解决方案1】:

    我发现我的问题的解决方案(解决方案)分为两部分:

    1. 我无法继续在我的 CursorAdapter 中手动生成游标。我不得不开始使用 Loader Manager
    2. 我不需要挂在任何光标上。

    注意:对于任何关注的人,我在运行此代码时仍然会遇到一些错误,但这不是致命的,而且它似乎与游标无关,所以我没有解决它在这里。

    在 CursorAdapter 中生成游标

    最大的复杂性是CursorAdapter 中的runQueryOnBackgroundThread 方法要求返回一个游标。使用 LoaderManager 时,在异步回调之前您不会接触到光标,这有缺点:

    1. 您无法在runQueryOnBackgroundThread 方法中生成和检索光标。
    2. 您无法提前生成光标,因为runQueryOnBackgroundThread 是使用新过滤约束调用的第一个方法。
    3. runQueryOnBackgroundThread 方法将其光标交给changeCursor 方法,该方法关闭已更改的光标(在使用 LoaderManager/CursorLoader 时我们不会这样做),所以我们不想无论如何都要遵循该工作流程。

    默认情况下,CursorAdapter 的 runQueryOnBackgroundThread 方法只调用 CursorAdapter 的 FilterQueryProvider 的 runQuery 方法(如果已定义)。我选择定义 FilterQueryProvider 而不是覆盖 runQueryOnBackgroundThread 方法有几个原因:

    • FilterQueryProvider 可以从实例化 CursorAdapter 的 Activity 中定义,并且从 Activity 中使用 LoaderManager 比在 CursorAdapter 中使用它更容易。
    • 如果可能的话,我更愿意让代码做它应该做的事情。

    注意:runQuery 方法仍然需要返回一个游标,所以我们不能回避这个问题。

    我决定在我的 FilterQueryProvider 的 runQuery 方法中生成一个虚拟光标。然后,由于该虚拟光标将被移交给 CursorAdapter 的 changeCursor 方法,因此我覆盖了 changeCursor 以简单地关闭它传递的每个光标。

    runQuery 方法还启动包含过滤约束的异步 LoaderManager 调用。然后 LoaderManager 回调负责交换生成的新游标。

    注意:理想情况下,我想您可以覆盖调用 runQueryOnBackgroundThread 的函数,并让它进行异步 LoaderManager 调用,但我不知道那是什么。

    依靠光标

    我试图区分未过滤的游标和已过滤的游标,当过滤约束为空时,可以使用未过滤的游标。在无数次阅读Android 3.0 - what are the advantages of using LoaderManager instances exactly? 之后,我意识到接受的答案是使用相同的 CursorLoader 来生成所有光标。

    我决定在需要时只生成一个新的未过滤光标,而不是试图挂在原始的未过滤光标上。 onCreateLoader LoaderManager 回调变得有点复杂(但更像我看到的示例),onLoadFinished 回调变得更加简单(就像我看到的示例)。

    源代码

    活动 - home.java

    public class Home extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {
    
    // Constants
    private static final String TAG = "Home";
    private static final boolean DEBUG = true;
    public static final int LOADER_CONTACTS_CURSOR = 1;
    
    // Variables
    private AdapterContacts adapter;
    
    
    
    /**
     * Overrides
     */
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        // Set the view
        setContentView(R.layout.home);
    
        // Initialize CursorAdapter
        adapter = new AdapterContacts(this, null, 0);
        final LoaderManager.LoaderCallbacks<Cursor> iFace = this;
        adapter.setFilterQueryProvider(new FilterQueryProvider() {
            public Cursor runQuery(CharSequence constraint) {
                if (constraint != null) {
                    Bundle bundle = new Bundle();
                    bundle.putCharSequence("constraint", constraint);
                    getLoaderManager().restartLoader(Home.LOADER_CONTACTS_CURSOR, bundle, iFace);
                } else {
                    getLoaderManager().restartLoader(Home.LOADER_CONTACTS_CURSOR, null, iFace);
                }
                return getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
            }
        });
    
        // Attach CursorAdapter to AutoCompleteTextView field
        AutoCompleteTextView field = (AutoCompleteTextView) findViewById(R.id.newPlayer_edit);
        field.setAdapter(adapter);
    
        // Initialize Cursor using LoaderManagers
        LoaderManager.enableDebugLogging(true);
        getLoaderManager().initLoader(LOADER_CONTACTS_CURSOR, null, this);
    }
    
    @Override
    public void onDestroy() {
        if (DEBUG) Log.i(TAG, "Destroying activity");
        super.onDestroy();
    }
    
    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        if (DEBUG) Log.i(TAG, "Loader Callback: creating loader");
        Uri baseUri;
    
        if (args != null) {
            CharSequence constraint = args.getCharSequence("constraint");
            if (DEBUG) Log.i(TAG, "Constraint: " + constraint.toString());
            baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, Uri.encode(constraint.toString()));
        } else {
            if (DEBUG) Log.i(TAG, "No Constraint");
            baseUri = ContactsContract.Contacts.CONTENT_URI;
        }
        return new CursorLoader(this, baseUri, null, null, null, null);
    }
    
    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        if (DEBUG) Log.i(TAG, "Loader Callback: load finished");
        adapter.swapCursor(cursor);
    }
    
    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        if (DEBUG) Log.i(TAG, "Loader Callback: resetting");
        adapter.swapCursor(null);
    }
    }
    

    CursorAdapter - AdapterContacts.java

    public class AdapterContacts extends CursorAdapter {
    
    // Constants
    private static final String TAG = "AdapterContacts";
    private static final boolean DEBUG = true;
    
    // Variables
    private TextView mName;
    
    /**
     * Constructor
     */
    public AdapterContacts(Context context, Cursor c, int flags) {
        super(context, c, flags);
    }
    
    /**
     * Overrides
     */
    
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        // Inflate the views that create each row of the dropdown list
        final LayoutInflater inflater = LayoutInflater.from(context);
        final LinearLayout ret = new LinearLayout(context);
        ret.setOrientation(LinearLayout.VERTICAL);
    
        mName = (TextView) inflater.inflate(android.R.layout.simple_dropdown_item_1line, parent, false);
        ret.addView(mName, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
    
        /*
        int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
        mName.setText(cursor.getString(nameIdx));
        */
    
        return ret;
    }
    
    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        // Fill the dropdown row with data from the cursor
        int nameIdx = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
        String name = cursor.getString(nameIdx);
        ((TextView) ((LinearLayout) view).getChildAt(0)).setText(name);
    }
    
    @Override
    public String convertToString(Cursor cursor) {
        // Convert the dropdown list entry that the user clicked on
        // into a string that will fill the AutoCompleteTextView
        int nameCol = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
        return cursor.getString(nameCol);
    }
    
    @Override
    public void changeCursor(Cursor newCursor) {
        newCursor.close();
    }
    }
    

    【讨论】:

      【解决方案2】:

      如果您使用Loader 来管理您的AdapterCursor,那么您需要不惜一切代价避免使用adapter.filter()。由于适配器内的filter() 期望返回Cursor,这是不可能的,因为加载器是异步的并且在后台线程中运行。

      如何用加载器替换adapter.filter()

      1. 在 onSelect/Item/TextInput 侦听器中,将选定/输入值存储在类变量中。

      2. 将对myAdapter.filter()的所有调用替换为myLoadManager.restartLoader(...)

      3. 在您的 onCreateLoader() 方法中,使用由于步骤 1 而存储的选择/输入值,动态生成您的 sql/cursor 查询并运行它。

      这就是事件的顺序:

      1. 用户选择一个微调器下拉菜单并选择“美国”。

      2. 您将“USA”存储在一个类变量中,然后调用myLoadManager.restartLoader(...)

      3. 负载管理器销毁之前的负载管理器并创建一个新的调用onCreateLoader() 的负载管理器,其中包含您的自动生成查询代码。

      4. 新加载程序运行更新后的查询并通知您的适配器刷新和重绘其数据。

      为什么要使用Loaders

      1. 基于慢速数据对非阻塞 UI 更新进行免费/更轻松的异步管理。比AsyncTasks 容易得多。

      2. 游标的空闲内存管理。将在重新查询或活动关闭期间自动清理/关闭Cursors

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-05-03
        • 1970-01-01
        • 2016-03-29
        • 1970-01-01
        • 2015-04-17
        • 2018-04-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多