Android ListView 详解及其优化

一、基本原理

ListView 是一个显示一列可滚动项目的视图组。 系统使用 Adapter 自动将列表项目插入列表,适配器从来源(例如数组或数据库查询)提取内容,并将每个项目结果转换为视图放置到列表中。

列表数据的显示需要 4 个元素:

  1. ListView 视图组;

  2. 用来把数据映射到 ListView 上的 Adapter;

  3. 需要展示的数据集;

  4. 数据展示的 View 模版。

ListView 只负责加载和管理视图,核心实现是由 Adapter 类完成。

在 Adapter 里主要实现这四个函数:

获取数据的个数
获取 position 位置的数据
获取 position 位置的数据 id,一般直接返回 position 即可
获取 position 位置上的每项数据(Item View)视图

在 getView 函数中用户必须构建 Item View,然后将该 position 位置上数据绑定到 Item View。

1. 视图复用机制

Android 采用视图复用的形式避免创建过多的 Item View,提升性能,降低内存占有率。

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = null;
        //有视图缓存,复用视图
        if (convertView != null) {
            view = convertView;
        } else {
            //重新加载视图
        }
        //进行数据绑定
        //返回Item View
        return view;
    }

position 表示该视图是第几项数据,convertView 表示缓存的 Item View,parent 表示该 Item View 的父视图,对于 ListView 来说这个 parent 就是 ListView 本身。

数据量较大时,ListView 不会构建全部 Item View,ListView 只会构建足够铺满屏幕所需的 Item View 个数,例如 8 个 Item View 足够铺满屏幕,数据项有 1000 个,那么 Item View 可以只产生 8 个,即反复利用容器,变换里面的数据,这样可以节约内存,提高效率。

2. Adapter(适配器)模式

适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。

共有两类适配器模式:

  • 对象适配器模式
    在这种适配器模式中,适配器容纳一个它包裹的类的实例。在这种情况下,适配器调用被包裹对象的物理实体。

  • 类适配器模式
    这种适配器模式下,适配器继承自已实现的类(一般多重继承)。

ListView 要想使用 getView() 方法,但是不同的数据,不同的需求,就会有不同的 getView() 结果,所以 getView() 必须是可复写的,那么就可以想到用适配器模式。

ListView 里面包含了一个 ListAdapter 的成员变量,实际上是 ListView 继承了 AbsListView,ListAdapter 变量是在 AbsListView 中声明的。

BaseAdapter 是实现了 ListAdapter 的,于是,我们自定义的 xxxAdapter 继承了 BaseAdapter,其实就相当于间接实现了 ListAdapter,然后我们就可以复写 getView() 方法。

ListView 就可以通过调用 setAdapter(ListAdapter adapter) 方法,将自定义的 Adapter 传进去,调用我们复写后的方法,从而得到想要的结果。

3. 观察者模式

Adapter 内部有一个可观察者类,ListView 作为它的其中一个观察者。

ListView 的 setAdapter() 方法:

    @Override
    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        resetList();
        //清空视图缓存mRecycler
        mRecycler.clear();

        if (mAdapter != null) {
            ......
            mDataSetObserver = new AdapterDataSetObserver();
            //将 mDataSetObserver 注册到 adapter 中
            mAdapter.registerDataSetObserver(mDataSetObserver);
            ......
        } else {
            ......
        }

        requestLayout();
    }

BaseAdapter 部分代码:

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {
    private final DataSetObservable mDataSetObservable = new DataSetObservable();

    public boolean hasStableIds() {
        return false;
    }
    
    public void registerDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.registerObserver(observer);
    }

    public void unregisterDataSetObserver(DataSetObserver observer) {
        mDataSetObservable.unregisterObserver(observer);
    }

注册观测者实际上是调用了 DataSetObservable 对应的函数。DataSetObservable 拥有一个观察者集合,当可观察者发生变更时,就会通知观察者做出相应的处理。

public abstract class Observable<T> {
    /**
     * The list of observers.  An observer can be in the list at most
     * once and will never be null.
     */
    protected final ArrayList<T> mObservers = new ArrayList<T>();

    public void registerObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            mObservers.add(observer);
        }
    }

Adapter 的数据源发生变化时,我们会调用 Adapter 的
notifyDataSetChanged() 函数,在该函数中又会调用
DataSetObservable 对象的 notifyChanged() 函数通知所有的观察者数据发生了变化,使观察者进行相应的操作。

public class DataSetObservable extends Observable<DataSetObserver> {

    public void notifyChanged() {
        synchronized(mObservers) {

            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

对于 ListView 来说,这个观察者就是 AdapterDataSetObserver 对象,该类声明在 AdapterView 类中,也是 ListView 中的一个父类。

    class AdapterDataSetObserver extends DataSetObserver {

        private Parcelable mInstanceState = null;

        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            //获取元素个数 
            mItemCount = getAdapter().getCount();

            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                AdapterView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
                rememberSyncState();
            }
            checkFocus();
            //重新布局
            requestLayout();
        }

onChanged() 函数中,实际调用的是 AdapterView 或者 ViewGroup 类中的属性或者函数完成功能。因此,AdapterDataSetObserver 只是在外层做了一下包装,真正的核心功能是 ListView,确切地说应该是 AdapterView。因此,源码里使用 AdapterDataSetObserver 对象作为观察者并不违反 ListView 作为真正观察者的意图。

二、处理点击事件

可以通过实现 AdapterView.OnItemClickListener 来响应 AdapterView 中每一项上的点击事件。 例如:

// Create a message handling object as an anonymous class.
private OnItemClickListener mMessageClickedHandler = new OnItemClickListener() {
    public void onItemClick(AdapterView parent, View v, int position, long id) {
        // Do something in response to the click
    }
};

listView.setOnItemClickListener(mMessageClickedHandler);

三、其他

GridView 与 ListView 相似, 同样继承 AbsListViewAbsListView 又是 AdapterView 的子类,所以原理机制基本相同,不同的是,GridView 通过网格布局形式展示。

同一个 Adapter 可以设置给多个的列表视图,数据会同时显示且根据数据变化而变化,这些列表视图都作为该 Adapter 的观察者,这可能也是为什么要使用观察者模式的一个原因。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容