zoukankan      html  css  js  c++  java
  • 使用具体解释及源代码解析Android中的Adapter、BaseAdapter、ArrayAdapter、SimpleAdapter和SimpleCursorAdapter

    Adapter相当于一个数据源,能够给AdapterView提供数据。并依据数据创建相应的UI。能够通过调用AdapterView的setAdapter方法使得AdapterView将Adapter作为数据源。

    常见的AdapterView的子类有ListView、GridView、Spinner和ExpandableListView等。

    本文就以ListView为例解说各种常见的Adapter的使用。

    下面是Adapter相关类的关系图:
    这里写图片描写叙述


    Adapter接口

    Adapter源代码链接例如以下:
    https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/Adapter.java

    Adapter接口定义了例如以下方法:

    • public abstract void registerDataSetObserver (DataSetObserver observer)
      Adapter表示一个数据源,这个数据源是有可能发生变化的,比方添加了数据、删除了数据、改动了数据,当数据发生变化的时候,它要通知相应的AdapterView做出相应的改变。为了实现这个功能。Adapter使用了观察者模式,Adapter本身相当于被观察的对象,AdapterView相当于观察者,通过调用registerDataSetObserver方法,给Adapter注冊观察者。

    • public abstract void unregisterDataSetObserver (DataSetObserver observer)
      通过调用unregisterDataSetObserver方法,反注冊观察者。

    • public abstract int getCount ()
      返回Adapter中数据的数量。

    • public abstract Object getItem (int position)
      Adapter中的数据相似于数组,里面每一项就是相应一条数据,每条数据都有一个索引位置,即position,依据position能够获取Adapter中相应的数据项。

    • public abstract long getItemId (int position)
      获取指定position数据项的id,通常情况下会将position作为id。

      在Adapter中,相对来说,position使用比id使用频率更高。

    • public abstract boolean hasStableIds ()
      hasStableIds表示当数据源发生了变化的时候。原有数据项的id会不会发生变化。假设返回true表示Id不变,返回false表示可能会变化。

      Android所提供的Adapter的子类(包括直接子类和间接子类)的hasStableIds方法都返回false。

    • public abstract View getView (int position, View convertView, ViewGroup parent)
      getView是Adapter中一个非常重要的方法,该方法会依据数据项的索引为AdapterView创建相应的UI项。


    ListAdapter接口

    ListAdapter接口继承自Adapter接口,ListAdapter源代码链接例如以下:
    https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/ListAdapter.java

    ListAdapter能够作为AbsListView的数据源。AbsListView的子类有ListView、GridView和ExpandableListView。

    ListAdapter相比Adapter新增了areAllItemsEnabled和isEnabled两个方法。


    SpinnerAdapter接口

    SpinnerAdapter接口继承自Adapter接口。SpinnerAdapter源代码链接例如以下:
    https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/SpinnerAdapter.java

    SpinnerAdapter能够作为AbsSpinner的数据源。AbsSpinner的子类有Gallery, Spinner和AppCompatSpinner。

    相比Adapter。SpinnerAdapter中新增了getDropDownView方法。该方法与Adapter接口中定义的getView方法相似,该方法主要是供AbsSpinner调用,用于生成Spinner下拉弹出区域的UI。在SpinnerAdapter的子类BaseAdapter中。getDropDownView方法默认直接调用了getView方法。

    ArrayAdapter和SimpleAdapter都重写了getDropDownView方法。这两个类中的getDropDownView方法与其getView的方法都调用了createViewFromResource方法。所以这两个类中方法getView与方法getDropDownView代码基本一致。

    CursorAdapter也重写了getView与getDropDownView方法。尽管这两个方法没有使用公共代码。可是这两个方法代码逻辑一致。
    综上。我们可知当我们在覆写getDropDownView方法时。应该尽量使其与getView的代码逻辑一致。


    BaseAdapter抽象类

    BaseAdapter是抽象类,事实上现了ListAdapter接口和SpinnerAdapter接口,其源代码链接例如以下:
    https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/BaseAdapter.java

    BaseAdapter主要实现了下面功能:
    - BaseAdapter实现了观察者模式,Adapter接口定义了方法registerDataSetObserver和unregisterDataSetObserver。BaseAdapter中维护了一个DataSetObservable类型的变量mDataSetObservable,并实现了方法registerDataSetObserver和unregisterDataSetObserver。

    • BaseAdapter重写了getDropDownView方法。其调用了getView方法。例如以下所看到的:

      public View getDropDownView(int position, View convertView, ViewGroup parent) {
          return getView(position, convertView, parent);
      }
    • 覆写其它一些方法,设置了默认值,比方覆写hasStableIds方法,使其默认返回false


    ArrayAdapter类

    类ArrayAdapter继承并实现了BaseAdapter抽象类,其源代码链接例如以下:
    https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/ArrayAdapter.java

    ArrayAdapter是最简单的Adapter。AdapterView会将ArrayAdapter中的数据项调用toString()方法,作为文本显示出来。

    ArrayAdapter的使用代码例如以下所看到的:

    package com.ispring.adapter;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.View;
    import android.widget.AdapterView;
    import android.widget.ArrayAdapter;
    import android.widget.ListView;
    import android.widget.Toast;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    
    public class MainActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            ListView listView = (ListView)findViewById(R.id.listView);
            String[] values = {"iPhone","小米","三星","华为","中兴","联想","黑莓","魅族"};
            //List<String> list = Arrays.asList(values);
            //Arrays.asList(values)返回的是一个仅仅读的List。不能进行add和remove
            //new ArrayList<>(Arrays.asList(values))则是一个可写的List。能够进行add和remove
            List<String> list = new ArrayList<>(Arrays.asList(values));
            final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, list);
            listView.setAdapter(adapter);
            //单击item之后,删除相应的item
            listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    String item = adapter.getItem(position);
                    adapter.remove(item);
                    Toast.makeText(MainActivity.this, item, Toast.LENGTH_SHORT).show();
                }
            });
        }
    }
    

    界面例如以下所看到的:
    这里写图片描写叙述

    我们绑定了ListView的OnItemClickListener事件,当单击当中一项的时候就会通过ArrayAdapter的remove()方法删除相应项。

    ArrayAdapter有下面几个构造函数:

    public ArrayAdapter(Context context, @LayoutRes int resource) {
            this(context, resource, 0, new ArrayList<T>());
        }
    
    public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId) {
            this(context, resource, textViewResourceId, new ArrayList<T>());
        }
    
    public ArrayAdapter(Context context, @LayoutRes int resource, @NonNull T[] objects) {
            this(context, resource, 0, Arrays.asList(objects));
        }
    
    public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId,
                @NonNull T[] objects) {
            this(context, resource, textViewResourceId, Arrays.asList(objects));
        }
    
    public ArrayAdapter(Context context, @LayoutRes int resource, @NonNull List<T> objects) {
            this(context, resource, 0, objects);
        }
    
    public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId,
                @NonNull List<T> objects) {
            mContext = context;
            mInflater = LayoutInflater.from(context);
            mResource = mDropDownResource = resource;
            mObjects = objects;
            mFieldId = textViewResourceId;
        }

    上面几个构造函数事实上最后都调用了最后构造函数,有几点须要说明一下:

    • 上面的构造函数中涉及到了resource和textViewResourceId这两个參数,resource肯定是数据项相应的UI的layout文件,那textViewResourceId是什么呢?我们之前提到,ArrayAdapter是用来让AdapterView显示文本项的。所以其数据项须要显示在一个TextView上。假设resource本身就是以<TextView>作为根结点,那么textViewResourceId设置为0就可以。假设resource所相应的layout不是以<TextView>作为根结点。那么该layout中也必须要有一个<TextView>节点。textViewResourceId即时该<TextView>的id。

      字段mResource存储了resource的值,字段mFieldId存储了textViewResourceId。

    • ArrayAdapter的构造函数既能够接收List作为数据源,又能够接收一个数组作为数据源,假设传入的是一个数组。那么在构造函数中也会通过Arrays.asList()将数组转换成list,终于用mObjects存储该list。

    ArrayAdapter重写了getCount、getItem、getItemId、getView与getDropDownView。当中getView与getDropDownView这两个方法都调用了createViewFromResource方法,其源代码例如以下所看到的:

    private View createViewFromResource(LayoutInflater inflater, int position, View convertView,
                ViewGroup parent, int resource) {
            View view;
            TextView text;
    
            if (convertView == null) {
                view = inflater.inflate(resource, parent, false);
            } else {
                view = convertView;
            }
    
            try {
                if (mFieldId == 0) {
                    //  If no custom field is assigned, assume the whole resource is a TextView
                    text = (TextView) view;
                } else {
                    //  Otherwise, find the TextView field within the layout
                    text = (TextView) view.findViewById(mFieldId);
                }
            } catch (ClassCastException e) {
                Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
                throw new IllegalStateException(
                        "ArrayAdapter requires the resource ID to be a TextView", e);
            }
    
            T item = getItem(position);
            if (item instanceof CharSequence) {
                text.setText((CharSequence)item);
            } else {
                text.setText(item.toString());
            }
    
            return view;
        }

    该方法的主要逻辑是查找TextView。并给TextView设置文本值,其逻辑例如以下所看到的:

    1. 假设在ArrayAdapter的构造函数中没有设置mFieldId的值或者mFieldId的值为0,那么就将整个View作为TextView。

    2. 假设在ArrayAdapter的构造函数中设置了mFieldId的值,那么就会调用(TextView)view.findViewById(mFieldId)查找TextView。

    ArrayAdapter还添加了add、addAll、insert、remove、clear等方法,当调用了这些方法时,数据会发生变化,ArrayAdapter就会在这些方法里面调用notifyDataSetChanged方法,比方remove源代码例如以下所看到的:

    public void remove(T object) {
            synchronized (mLock) {
                if (mOriginalValues != null) {
                    mOriginalValues.remove(object);
                } else {
                    mObjects.remove(object);
                }
            }
            if (mNotifyOnChange) notifyDataSetChanged();
        }

    假设我们在一个for循环中多次调用add方法加入数据,那么默认会多次触发notifyDataSetChanged方法的运行,因为每次notifyDataSetChanged方法运行后,AdapterView都会又一次渲染UI,所以多次触发notifyDataSetChanged方法运行会导致效率比較低。最好的办法是在所有数据变化完毕后。我们自己调用notifyDataSetChanged方法。

    为此。ArrayAdapter内部提供了一个boolean类型的变量mNotifyOnChange,默认值为true,每次调用add、addAll、insert、remove、clear等方法,都会先推断mNotifyOnChange的值,仅仅有当mNotifyOnChange为true。才会运行notifyDataSetChanged方法。我们能够通过调用setNotifyOnChange方法将mNotifyOnChange设置为false,然后在for循环中多次调用add方法。这样不会触发notifyDataSetChanged方法,在运行完for循环之后,我们自己再调用notifyDataSetChanged方法。

    另一点须要说明的是,假设我们将一个数组作为数据源传递给ArrayAdapter,那么当调用ArrayAdapter的add、addAll、insert、remove、clear等写操作的方法时就会抛出异常java.lang.UnsupportedOperationException。

    这是为什么呢?

    我们之前在上面提到。假设在构造函数中传入数组。会调用Arrays.asList()将数组转换成List,并赋值给字段mObjects存储。

    可是Arrays.asList()返回的List是仅仅读的,不能够进行add、remove等写操作。Arrays.asList()返回的List是事实上是一个java.util.AbstractList对象。其add、remove方法的默认实现就是抛出异常,详见博文《为什么Java里的Arrays.asList不能用add和remove方法?》

    同理,假设我们传入了一个仅仅读的List对象给ArrayAdapter的构造函数。那么在调用add、addAll、insert、remove、clear等写操作的方法时也会抛出异常。

    在上面的样例中,我们运行了一下方法:

    List<String> list = new ArrayList<>(Arrays.asList(values));

    我们将Arrays.asList(values)得到的仅仅读的list作为參数实例化了一个ArrayList。新得到的ArrayList是可写的,所以我们将它作为參数传递给ArrayAdapter之后,能够正常调用其remove()方法。

    所以,在使用ArrayAdapter的时候。最好传给构造函数一个可写的List。这样才干正常使用ArrayAdapter的写操作方法。


    SimpleAdapter类

    类SimpleAdapter继承并实现了BaseAdapter抽象类,其源代码链接例如以下:
    https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/SimpleAdapter.java

    SimpleAdaper的作用是方便地将数据与XML文件定义的各种View绑定起来,从而创建复杂的UI。

    SimpleAdapter的使用代码例如以下所看到的:

    package com.ispring.adapter;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.widget.ListView;
    import android.widget.SimpleAdapter;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    
    public class MainActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            ListView listView = (ListView)findViewById(R.id.listView);
    
            final String[] names = {"Windows","Mac OS","Linux","Android","Chrome OS"};
    
            final String[] descriptions = {
                    "Windows是微软公司的操作系统",
                    "Mac OS是苹果公司的操作系统",
                    "Linux是开源免费操作系统",
                    "Android是Google公司的智能手机操作系统",
                    "Chrome OS是Google公司的Web操作系统"
            };
    
            final int[] icons = {
                    R.drawable.windows,
                    R.drawable.mac,
                    R.drawable.linux,
                    R.drawable.android,
                    R.drawable.chrome
            };
    
            List<Map<String, Object>> list = new ArrayList<>();
    
            for(int i = 0; i < names.length; i++){
                HashMap<String, Object> map = new HashMap<>();
                map.put("name", names[i]);
                map.put("description", descriptions[i]);
                map.put("icon", icons[i]);
                list.add(map);
            }
    
    
            //每一个数据项相应一个Map,from表示的是Map中key的数组
            String[] from = {"name", "description", "icon"};
    
            //数据项Map中的每一个key都在layout中有相应的View,
            //to表示数据项相应的View的ID数组
            int[] to = {R.id.name, R.id.description, R.id.icon};
    
            //R.layout.item表示数据项UI所相应的layout文件
            SimpleAdapter adapter = new SimpleAdapter(this, list, R.layout.item, from, to);
    
            listView.setAdapter(adapter);
        }
    }

    当中R.layout.item表示数据项UI所相应的layout文件。例如以下所看到的:

    <?

    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="wrap_content" android:orientation="horizontal" android:paddingTop="5dp" android:paddingBottom="5dp"> <ImageView android:id="@+id/icon" android:layout_width="100dp" android:layout_height="wrap_content" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:paddingLeft="10dp"> <TextView android:id="@+id/name" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="@dimen/defaultFontSize" /> <TextView android:id="@+id/description" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="@dimen/defaultFontSize" android:layout_marginTop="10dp"/> </LinearLayout> </LinearLayout>

    界面例如以下所看到的:
    这里写图片描写叙述

    SimpleAdapter仅仅有一个构造函数,签名例如以下所看到的:

    public SimpleAdapter (Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to)
    • data表示的是List数据源,当中List中的元素都是Map类型,而且Map的key是String类型,Map的value能够是随意类型,我们一般使用HashMap<String, Object>作为List中的数据项。

    • resource表示数据项UI所相应的layout文件,在本例中即R.layout.item。在本例中,每条数据项都要包括图片、名称、描写叙述三条信息,所以我们在item.xml中定义了一个ImageView表示图片,两个TextView分别表示名称和描写叙述,而且都设置了ID值。

    • 每一个数据项相应一个Map。from表示的是Map中key的数组。

    • 数据项Map中的每一个key都在layout中有相应的View。to表示数据项相应的View的ID数组。

    你可能会好奇SimpleAdapter是怎么把数据和layout自己主动关联起来的呢?

    SimpleAdapter实现了下面方法:getCount、getItem、getItemId、getView和getDropDownView。当中getView和getDropDownView都调用了createViewFromResource方法。其源代码例如以下所看到的:

    private View createViewFromResource(LayoutInflater inflater, int position, View convertView,
                ViewGroup parent, int resource) {
            View v;
            if (convertView == null) {
                v = inflater.inflate(resource, parent, false);
            } else {
                v = convertView;
            }
    
            bindView(position, v);
    
            return v;
        }

    在createViewFromResource中。会调用bindView方法,bindView方法的作用就是将数据项与相应的View绑定起来,从而使得View在界面上展现出数据内容。
    bindView的源代码例如以下所看到的:

    private void bindView(int position, View view) {
            final Map dataSet = mData.get(position);
            if (dataSet == null) {
                return;
            }
    
            final ViewBinder binder = mViewBinder;
            final String[] from = mFrom;
            final int[] to = mTo;
            final int count = to.length;
    
            //在for循环中遍历一条数据项中所有的view
            for (int i = 0; i < count; i++) {           
                final View v = view.findViewById(to[i]);
                if (v != null) {
                    final Object data = dataSet.get(from[i]);
                    String text = data == null ? "" : data.toString();
                    if (text == null) {
                        text = "";
                    }
    
                    boolean bound = false;
                    if (binder != null) {
                        //首先尝试用binder对View和data进行绑定
                        bound = binder.setViewValue(v, data, text);
                    }
    
                    //假设binder不存在或binder没有绑定成功,SimpleAdapter会尝试进行自己主动绑定
                    if (!bound) {
                        if (v instanceof Checkable) {
                            if (data instanceof Boolean) {
                                ((Checkable) v).setChecked((Boolean) data);
                            } else if (v instanceof TextView) {
                                // Note: keep the instanceof TextView check at the bottom of these
                                // ifs since a lot of views are TextViews (e.g. CheckBoxes).
                                setViewText((TextView) v, text);
                            } else {
                                throw new IllegalStateException(v.getClass().getName() +
                                        " should be bound to a Boolean, not a " +
                                        (data == null ?

    "<unknown type>" : data.getClass())); } } else if (v instanceof TextView) { // Note: keep the instanceof TextView check at the bottom of these // ifs since a lot of views are TextViews (e.g. CheckBoxes). setViewText((TextView) v, text); } else if (v instanceof ImageView) { if (data instanceof Integer) { setViewImage((ImageView) v, (Integer) data); } else { setViewImage((ImageView) v, text); } } else { throw new IllegalStateException(v.getClass().getName() + " is not a " + " view that can be bounds by this SimpleAdapter"); } } } } }

    bindView方法会遍历to数组中View的id。然后通过view.findViewById()方法找到相应的View。

    那怎么将数据和View绑定起来呢?

    主要分两步:

    1. 首先通过ViewBinder实现开发人员自己绑定数据
      SimpleAdapter中内部有一个ViewBinder类型的成员变量mViewBinder,通过SipmleAdater的setViewBinder方法能够对其赋值。mViewBinder的默认值是null。

      ViewBinder是SimpleAdapter的一个内部接口,其定义了setViewValue方法。我们能够定义一个对象,实现ViewBinder接口的setViewValue方法。然后通过setViewBinder赋值给mViewBinder。

      在bindView方法中,会首先推断mViewBinder存不存在,假设存在就调用mViewBinder的setViewValue方法,该方法会返回一个boolean值,假设返回true表示开发人员自己已经成功将数据和View绑定起来了。
      bound值为true。后面就不会再运行其它逻辑。

    2. 假设开发人员没有自己绑定数据(这是常见的情形),那么SimpleAdapter会自己尝试去绑定数据

      详细来说。假设mViewBinder不存在或者mViewBinder的setViewValue方法返回false,那么bound值为false,这时候Android就会依照自己的逻辑尽量去将数据和View进行绑定。


    SimpleCursorAdapter类

    类SimpleCursorAdapter的继承路线是:
    SimpleCursorAdapter->ResourceCursorAdapter->CursorAdapter->BaseAdapter

    CursorAdapter代码较多。ResourceCursorAdapter代码较少。SimpleCursorAdapter新增的代码主要是实现数据与View的绑定。涉及到内部类ViewBinder和bindView方法。

    CursorAdapter源代码链接例如以下:
    https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/CursorAdapter.java

    ResourceCursorAdapter源代码链接例如以下:
    https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/ResourceCursorAdapter.java

    SimpleCursorAdapter源代码链接例如以下:
    https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/SimpleCursorAdapter.java

    类SimpleCursorAdapter的主要作用是将Cursor作为数据源创建Adapter。

    Android的ApiDemos提供了SimpleCursorAdapter基本使用的演示样例。链接例如以下所看到的:
    https://github.com/android/platform_development/blob/master/samples/ApiDemos/src/com/example/android/apis/view/List2.java

    相应的代码例如以下:

    package com.example.android.apis.view;
    
    import android.app.ListActivity;
    import android.database.Cursor;
    import android.provider.ContactsContract.Contacts;
    import android.os.Bundle;
    import android.widget.ListAdapter;
    import android.widget.SimpleCursorAdapter;
    
    /**
     * A list view example where the
     * data comes from a cursor.
     */
    public class List2 extends ListActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            // Get a cursor with all people
            Cursor c = getContentResolver().query(Contacts.CONTENT_URI,
                    CONTACT_PROJECTION, null, null, null);
            startManagingCursor(c);
    
            ListAdapter adapter = new SimpleCursorAdapter(this,
                    // Use a template that displays a text view
                    android.R.layout.simple_list_item_1,
                    // Give the cursor to the list adatper
                    c,
                    // Map the NAME column in the people database to...
                    new String[] {Contacts.DISPLAY_NAME},
                    // The "text1" view defined in the XML template
                    new int[] {android.R.id.text1});
            setListAdapter(adapter);
        }
    
        private static final String[] CONTACT_PROJECTION = new String[] {
            Contacts._ID,
            Contacts.DISPLAY_NAME
        };
    }

    上面的样例演示了使用SimpleCursorAdapter显示手机通讯录中的联系人。通过ContentResolver的query方法得到了Cursor。然后将Cursor作为參数传递给了SimpleCursorAdapter,然后将其作为ListView的adapter。

    可是,上面的样例有缺陷:

    • getContentResolver().query()在主线程上运行了ContentResolver的query()方法,但该方法是个耗时方法,非常可能导致应用程序无响应,出现ANR现象。

    • 用startManagingCursor()方法管理Cursor的生命周期,使Cursor的生命周期与Activity的生命周期相相应。详细例如以下:

      • 当Activity处于stopped状态的时候,会自己主动调用Cursor的deactive()方法
      • 当Activity从stopped状态变为started状态的时候。其又会自己主动调用该Cursor的requery()方法又一次查询数据
      • 当Activity销毁的时候,该Cursor对象也会自己主动关闭
      • 当Activity configuration发生变化的时候(比方手机的横屏竖屏来回切换等)。Activity会重新启动,在重新启动的时候Cursor也会又一次运行requery()方法

      通过上面的描写叙述,看起来startManagingCursor()非常智能。而且貌似非常完美地帮我们处理了Cursor的生命周期,可是我们须要注意的是,当Activity从onStop()转变到onStart()的时候,其会又一次运行Cursor的requery()方法,可是该方法的运行时运行在主线程上的,而且Cursor的requery()方法也是耗时方法,该方法非常有可能堵塞UI线程。导致ANR现象。而且在Activity重新启动的时候。也会导致Cursor运行requery()方法,进一步添加应用出现无响应的情况,即ANR。

    综上,上面获取Cursor并管理Cursor生命周期的代码非常可能堵塞UI线程。导致ANR。为了解决问题,Android推荐使用CursorLoader和LoaderManager异步载入Cursor。并自己主动管理Cursor。详细可參见我的另一篇博文《Android中Loader及LoaderManager的使用(附源代码下载)》

    上面的博文中的演示样例演示的是怎样用CursorLoader和SimpleAdapter显示并过滤手机中的联系人。下面就把演示样例代码贴一下:

    package com.ispring.loaderdemo;
    
    import android.app.Activity;
    import android.app.LoaderManager;
    import android.content.CursorLoader;
    import android.content.Loader;
    import android.database.Cursor;
    import android.net.Uri;
    import android.os.Bundle;
    import android.provider.ContactsContract;
    import android.text.Editable;
    import android.text.TextWatcher;
    import android.view.KeyEvent;
    import android.widget.EditText;
    import android.widget.ListView;
    import android.widget.SimpleCursorAdapter;
    import android.widget.TextView;
    
    
    public class MainActivity extends Activity implements LoaderManager.LoaderCallbacks<Cursor>, TextWatcher {
    
        private EditText editText = null;
    
        private ListView listView = null;
    
        private SimpleCursorAdapter adapter = null;
    
    
        private final int CURSOR_LOADER_ID = 1;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            //绑定编辑框的文本变化事件
            editText = (EditText)findViewById(R.id.editText);
            editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
                @Override
                public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                    return false;
                }
            });
            editText.addTextChangedListener(this);
    
            //获取ListView
            listView = (ListView)findViewById(R.id.listView);
    
            //创建Adapter
            adapter = new SimpleCursorAdapter(
                    this,
                    android.R.layout.simple_list_item_2,
                    null,
                    new String[]{ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.Contacts.CONTACT_STATUS},
                    new int[]{android.R.id.text1, android.R.id.text2},
                    0);
            listView.setAdapter(adapter);
    
            //查询所有联系人
            Bundle args = new Bundle();
            args.putString("filter", null);
            LoaderManager lm = getLoaderManager();
            lm.initLoader(CURSOR_LOADER_ID, args, this);
        }
    
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }
    
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
        }
    
        @Override
        public void afterTextChanged(Editable s) {
            String filter = editText.getText().toString();
            Bundle args = new Bundle();
            args.putString("filter", filter);
            LoaderManager lm = getLoaderManager();
            lm.restartLoader(CURSOR_LOADER_ID, args, this);
        }
    
        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    
            Uri uri;
    
            String filter = args != null ?

    args.getString("filter") : null; if(filter != null){ //依据用户指定的filter过滤显示 uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, Uri.encode(filter)); }else{ //显示所有 uri = ContactsContract.Contacts.CONTENT_URI; } String[] projection = new String[]{ ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.Contacts.CONTACT_STATUS }; String selection = "((" + ContactsContract.Contacts.DISPLAY_NAME + " NOTNULL) AND "+ "(" + ContactsContract.Contacts.HAS_PHONE_NUMBER + " =1) AND "+ "(" + ContactsContract.Contacts.DISPLAY_NAME + " != ''))"; String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; return new CursorLoader(this, uri, projection, selection, null, sortOrder); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { adapter.swapCursor(data); } @Override public void onLoaderReset(Loader<Cursor> loader) { adapter.swapCursor(null); } }

    界面例如以下所看到的:
    这里写图片描写叙述

    所以。SimpleCursorAdapter和CursorLoader、LoaderManager一起使用是最佳实践,总的来说分为下面几步:

    1. 创建一个SimpleCursorAdapter的实例,将Cursor作为null值传入,即初始化的时候Cursor还不存在。并相应设置from数组和to数组,这与SimpleAdapter中from、to相似。

    2. 调用LoaderManager的initLoader方法,在LoaderCallbacks的onCreateLoader方法中new一个CursorLoader,去异步载入Cursor。

    3. 在LoaderCallbacks的onLoadFinished回调方法中获得Cursor对象。然后调用SimpleCursorAdapter的swapCursor方法将cursor赋值给Adapter。

    4. 在LoaderCallbacks的onLoaderReset回调方法中,将null作为參数传递给SimpleCursorAdapter的swapCursor方法。

    须要说明的是,LoaderManager会管理Cursor的生命周期。我们无需也不能调用Cursor对象的close方法,LoaderManager会自己调用。

    SimpleCursorAdapter有两个构造函数。第一个构造函数签名例如以下所看到的:

    SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to)

    该构造函数从API Level 1開始就有了,该构造函数会导致在某些情况下,多次在UI线程上查询Cursor。这可能会导致程序无响应。从而出现ANR现象,所以该构造函数从API Level 11開始就被废弃了。因为从API Level 11開始,SimpleCursorAdapter新增了一个构造函数。签名例如以下所看到的:

    public SimpleCursorAdapter (Context context, int layout, Cursor c, String[] from, int[] to, int flags)

    相比于第一个构造函数,新增的构造函数多了一个int类型的flags參数。因为SimpleCursorAdapter是继承自CursorAdapter的。所以该flags參数终于传递给了CursorAdapter的构造函数。

    flags是个标记位,能够取值0或FLAG_AUTO_REQUERY 或FLAG_REGISTER_CONTENT_OBSERVER或二者的逻辑或值,Android官方推荐的值是0,FLAG_AUTO_REQUERY 和FLAG_REGISTER_CONTENT_OBSERVER的作用在下面会说明。

    CursorAdapter的构造函数终于会调用内部的init方法。init方法源代码例如以下所看到的:

    void init(Context context, Cursor c, int flags) {
            if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) {
                //假设flag含有FLAG_AUTO_REQUERY标记位,那么强制给其也设置FLAG_REGISTER_CONTENT_OBSERVER标记位
                flags |= FLAG_REGISTER_CONTENT_OBSERVER;
                mAutoRequery = true;
            } else {
                mAutoRequery = false;
            }
            boolean cursorPresent = c != null;
            mCursor = c;
            mDataValid = cursorPresent;
            mContext = context;
            mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
            if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
                //假设flags具有FLAG_REGISTER_CONTENT_OBSERVER标记位,那么就注冊ChangeObserver和DataSetObserver
                mChangeObserver = new ChangeObserver();
                mDataSetObserver = new MyDataSetObserver();
            } else {
                mChangeObserver = null;
                mDataSetObserver = null;
            }
    
            if (cursorPresent) {
                if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
                if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
            }
        }

    假设设置了FLAG_AUTO_REQUERY标记位,那么当数据发出内容变化的通知的时候,就会运行cursor的requery()方法,而且在UI线程上运行,这会导致程序响应体验变差,甚至ANR。所以Android不建议使用该标记位。假设flag含有FLAG_AUTO_REQUERY标记位。mAutoRequery的值就为true,而且此时CursorAdapter强制给其也设置FLAG_REGISTER_CONTENT_OBSERVER标记位。

    假设设置了FLAG_REGISTER_CONTENT_OBSERVER标记位,那么会注冊监听器监听数据内容的变化。详细来说,会实例化ChangeObserver和MyDataSetObserver 。并分别调用mCursor的registerContentObserver和registerDataSetObserver注冊监听器。

    ChangeObserver和MyDataSetObserver都是CursorAdapter的内部类,下面讲一下二者的作用与差别。

    • ChangeObserver
      ChangeObserver继承自ContentObserver,其源代码例如以下所看到的:

      private class ChangeObserver extends ContentObserver {
          public ChangeObserver() {
              super(new Handler());
          }
      
          @Override
          public boolean deliverSelfNotifications() {
              return true;
          }
      
          @Override
          public void onChange(boolean selfChange) {
              onContentChanged();
          }
      }

      当数据库中的内容发生变化时,会触发ContentObserver的onChange方法的运行,这是一个提前通知。相当于数据库告诉Cursor说,我的内容变化了。ContentObserver的onChange方法中又运行了CursorAdapter的onContentChanged方法。源代码例如以下所看到的:

      protected void onContentChanged() {
          if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
              if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
              mDataValid = mCursor.requery();
          }
      }

      我们发现,在CursorAdapter的onContentChanged方法中,会推断是否自己主动requery,假设之前设置过FLAG_AUTO_REQUERY标记位。那么mAutoRequery 就为true。此时会运行mCursor的requery()方法。

    • MyDataSetObserver
      MyDataSetObserver继承自DataSetObserver,其源代码例如以下所看到的:

      private class MyDataSetObserver extends DataSetObserver {
          @Override
          public void onChanged() {
              mDataValid = true;
              notifyDataSetChanged();
          }
      
          @Override
          public void onInvalidated() {
              mDataValid = false;
              notifyDataSetInvalidated();
          }
      }

      当mCursor运行了完了requery()方法时,就会触发DataSetObserver的onChanged方法的运行。此时表示mCursor所代表的数据内容已经发生了变化,此时调用CursorAdapter的notifyDataSetChanged()方法,告知AdapterView数据内容更新了。

      当MCursor运行完了deactivate()或close()时,就会触发DataSetObserver的onInvalidated方法的运行。此时表示mCursor所代表的数据内容已经无效,不可用了。

      此时会调用CursorAdapter的notifyDataSetInvalidated()方法。告知AdapterView数据无效了。

      也就是说,DataSetObserver是一个后置通知,表示mCursor所代表的数据内容已经完毕了变化才会触发DataSetObserver 响应方法的运行。

      很多其它关于ContentObserver 和DataSetObserver 的差别。大家能够參考博客园上的一篇文章《Android CursorAdapter的监听事件》

    CursorAdapter重写了getCount、getItem、getItemId、hasStableIds、getView和getDropDownView方法,我们依次看一下这几个方法。

    • getCount
      源代码例如以下所看到的:

      public int getCount() {
          if (mDataValid && mCursor != null) {
              return mCursor.getCount();
          } else {
              return 0;
          }
      }

      其将mCursor的getCount作为返回值。

    • getItem

      源代码例如以下所看到的:

      public Object getItem(int position) {
          if (mDataValid && mCursor != null) {
              mCursor.moveToPosition(position);
              return mCursor;
          } else {
              return null;
          }
      }

      其调用了mCursor的moveToPosition方法,将mCursor移动到指定的位置。然后将mCursor返回。

    • getItemId
      其源代码例如以下所看到的:

      public long getItemId(int position) {
          if (mDataValid && mCursor != null) {
              if (mCursor.moveToPosition(position)) {
                  return mCursor.getLong(mRowIDColumn);
              } else {
                  return 0;
              }
          } else {
              return 0;
          }
      }

      首先也是调用了mCursor的moveToPosition方法。将mCursor移动到指定的位置,然后通过调用mCursor.getLong(mRowIDColumn),得到该条记录的行号,将其作为itemId返回。

    • hasStableIds
      其源代码例如以下所看到的:

      public boolean hasStableIds() {
          return true;
      }

      因为mCursor中每条记录的rowId都是固定的,所以CursorAdapter是具有稳定ID的,所以hasStableIds方法返回true。

    • getView
      getDropDownView的代码逻辑与getView差点儿相同,我们仅仅看getView就可以。getView源代码例如以下所看到的:

      public View getView(int position, View convertView, ViewGroup parent) {
          if (!mDataValid) {
              throw new IllegalStateException("this should only be called when the cursor is valid");
          }
          if (!mCursor.moveToPosition(position)) {
              throw new IllegalStateException("couldn't move cursor to position " + position);
          }
          View v;
          if (convertView == null) {
              v = newView(mContext, mCursor, parent);
          } else {
              v = convertView;
          }
          bindView(v, mContext, mCursor);
          return v;
      }

      首先mCursor会通过moveToPosition方法将游标移动到指定的position。然后,假设convertView为null就调用newView方法创建View。只是CursorAdapter的newView是个抽象方法,其子类ResourceCursorAdapter实现了newView方法,事实上就是简单调用mInflater.inflate(mLayout, parent, false)。


      最后会调用bindView方法,只是在CursorView中。bindView是个抽象方法,SimpleCursorAdapter实现了bindView方法,其代码逻辑与SimpleAdapter的代码逻辑基本一致。在此不再赘述。

    CursorAdapter中有一个比較重要的方法swapCursor,通过该方法能够切换Cursor。其源代码例如以下所看到的:

    public Cursor swapCursor(Cursor newCursor) {
            if (newCursor == mCursor) {
                return null;
            }
            Cursor oldCursor = mCursor;
            if (oldCursor != null) {
                //删除oldCursor的监听器
                if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
                if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
            }
            mCursor = newCursor;
            if (newCursor != null) {
                //为newCursor注冊监听器
                if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
                if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
                mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
                mDataValid = true;
                // notify the observers about the new cursor
                notifyDataSetChanged();
            } else {
                mRowIDColumn = -1;
                mDataValid = false;
                // notify the observers about the lack of a data set
                notifyDataSetInvalidated();
            }
            return oldCursor;
        }

    该方法主要具有下面逻辑:

    • 假设oldCursor不为null。那么尝试删除oldCursor 的数据监听器。

    • 假设newCursor不为null,那么尝试注冊newCursor 的数据监听器,然后运行notifyDataSetChanged()方法,表示数据更新了。

    • 假设newCursor 为null,那么运行notifyDataSetInvalidated()方法,表示数据无效了。

    swapCursor方法会将oldCursor作为返回值返回。

    大家能够看到。swapCursor方法并没有运行oldCursor的close方法。事实上CursorAdapter还提供了一个changeCursor方法,其源代码例如以下所看到的:

    public void changeCursor(Cursor cursor) {
            Cursor old = swapCursor(cursor);
            if (old != null) {
                old.close();
            }
        }

    changeCursor事实上内部也还是调用了swapCursor方法。仅仅只是会对返回的oldCursor运行close()方法。当我们在使用CursorLoader、LoaderManager的时候,不要调用CursorAdapter的changeCursor方法。因为CursorLoader和LoaderManager会自己主动管理Cursor的生命周期。会在合适的实际调用oldCursor的close方法,假设在使用CursorLoader时自己调用了changeCursor方法,会导致程序崩溃。

    本文比較长,感谢大家耐心读完,希望对大家有所帮助。

    相关阅读:
    我的Android博文整理汇总
    源代码解析ListView中的RecycleBin机制
    Android中Loader及LoaderManager的使用(附源代码下载)
    深入源代码解析Android中Loader、AsyncTaskLoader、CursorLoader、LoaderManager

  • 相关阅读:
    HDFS架构原理
    Hadoop集群搭建
    解决8080端口号占用问题
    基于SSM的Maven项目(Redis和Mysql)配置文件整合
    maven 集成SSM项目配置文件模版
    初识Spring笔记
    初识Mybatis一些总结
    将对数据库的增删改查封装为方法
    10分钟安装Elasticsearch
    ThreadLocal详解
  • 原文地址:https://www.cnblogs.com/yutingliuyl/p/7068616.html
Copyright © 2011-2022 走看看