运用单例模式、建造者模式和策略模式实现异步加载Android联系人资料

来源:互联网 发布:复杂网络节点中心性 编辑:程序博客网 时间:2024/05/21 19:27

学完设计模式很久了,最近又在看Android联系人提供程序的官方文档,于是就想实现一个方便的联系人管理程序demo,而联系人管理程序demo的核心就是要实现一个异步加载联系人资料的类,于是就有了下文。

完整的Demo可以点击这里点击这里下载。

实现异步加载联系人的需求

联系人结构

Android的联系人提供程序是一个强大而又灵活的 Android 组件,用于管理设备上有关联系人数据的中央存储库。因此,为了支持其强大的功能,其数据库的表结构就比较复杂了。其结构如下:
contact_structure
对应实际中的例子如下图:
xiaoming

该结构由三个表组成,通过外码联系起来,如下是三个表的官方描述:

  • ContactsContract.Contacts

    表示不同联系人的行,基于聚合的原始联系人行。

  • ContactsContract.RawContacts

    包含联系人数据摘要的行,针对特定用户帐户和类型。

  • ContactsContract.Data

    包含原始联系人详细信息(例如电子邮件地址或电话号码)的行。

其中RawContacts表中每一行的contact_id列的值都与其所属联系人id是对应的,也就是ContactsRawContacts是一对多的关系。同理,RawContactsData也是一对多关系。于是构成了这样的层次结构:
Contact -> RawConacts -> Datas

我们一般用的是下图这样的结构,很少把其他帐号的联系人放进联系人提供程序中,毕竟其它帐号的联系人只要打开客户端就可以了。例如,想发信息给小明的QQ帐号,一般是直接打开QQ,然后找到小明的QQ帐号来发信息,而不是在联系人那里找。
这里写图片描述

联系人层次结构导致的问题

正是因为这种关系,我们打开联系人程序时,一般是只显示联系人的名字(因为联系人的名字可以从RawContacts表中直接获得,不用从Data表中获得)。而看不到该联系人更多的详细信息,因为要看到详细信息就要点击该ListView项,然后跳转到查看详细信息的Activity中去查看,详细信息的Activity中的数据是通过查询Data表来获取的。下图显示了这种不方便的联系人程序:
list_activity
这种查看数据的方式十分麻烦。一、不能直接看到联系人的部分数据。二、如果上图的的联系人1联系人2是两个同名的人,就要点击进去查看其手机号码才能区分不同的两个人。

因此,我希望上图中的左边具有下图的结构:
asn

需要异步处理的原因

  1. 基于Cursor的查询是一项耗时的操作,如果在主线程中进行大量查询数据库的操作,就会阻塞UI,导致用户体验不好。
  2. 如果每个ListView项对应的联系人资料都发送一次查询请求,那么在快速滚动的时候将会产生大量的Cursor,而Cursor就像输入输出流一样,是有限的系统资源。故不要产生太多的Cursor,这可以通过在异步处理中的线程池来控制。把请求放在一个等待队列中,然后用线程池一个个地执行请求。比如,设置一个4条线程的线程池来执行请求,那么同一时刻产生的Cursor就只有4个,未处理的请求将会在等待队列中等待。

    实现异步加载类

    类简介

    要实现上面所说的异步加载,需要设计下面的一个主类、一个携带参数的辅助类和一个表示策略的接口。

    • ContactLoader

      运用单例模式设计的枚举类,保证了只有一个类的实例,并且是线程同步的。里面包括处理异步请求的线程池,以及等待队列。任一线程均可通过该类的loadContacts()方法发送请求任务。
      该方法的签名如下:

      public void loadContacts(final TextView[] textViews, final QueryHandler queryHandler, final ContactTask contactTask)

      其中的TextView数组是异步加载完后显示结果的TextView数组,在上面的假设中,就是要显示联系人电话号码的TextView,在这种情况下,数组只有一个元素。
      剩下的两个参数是辅助类实例和策略类接口实例,用于携带参数和实现策略。

    • QueryHandler

      携带查询要用到的参数,包括Context,Uri, projections, selection,selectionArgs等等。
      由于构成此类的参数过多,并且只有Context,Uri,projections是必要的,其它都是可选的,因此特别适合用建造都模式来进行设计。

    • ContactTask

      用于传递策略的接口,该接口只有一个方法。如下:
      public interface ContactTask {
      void apply(TextView[] textView, Cursor cursor);
      }

    下面就这二个类和一个接口是如何搭配使用的进行简单的说明。

类的使用

下面是ListView的Adatper中getView()方法使用异步加载的一个片段,可以帮助理解这二个类和一个接口是如何配合使用的。

//查询UriUri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI,Uri.encode(getItem(position).getId() + ""));uri = Uri.withAppendedPath(uri, ContactsContract.Contacts.Entity.CONTENT_DIRECTORY);//要查询的列String[] projections = {ContactsContract.Contacts.Entity.DATA1};//查询的条件,在这里是查询联系人的手机号码String select = "mimetype_id" + "=5";//创建携带参数的辅助类QueryHandler.Builder builder = new QueryHandler.Builder(                    getActivity().getApplicationContext(), uri, projections);builder.setSelection(select, null).setId(getItem(position).getId() + "");QueryHandler queryHandler = builder.build();//开始异步加载CONTACT_LOADER.loadContacts(new TextView[]{tvPhone}, queryHandler, new ContactTask() {//通过匿名内部类的形式来传递策略     @Override     public void apply(TextView[] textViews, Cursor cursor) {             if (cursor != null) {                    if (cursor.getCount() > 0) {                          cursor.moveToNext();                          textView[0].setText(cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.Entity.DATA1)));                          cursor.close();                    } else {                          cursor.close();                    }              }   }});

以上代码实现了对每个联系人手机号码的查询,其中通过实现ContactTask接口来传递一个具体的策略,其中的apply()方法是在查询完成后,由ContactLoader调用的,调用时,把查询到的Cursor和要显示结果的TextView数组传进这个方法,然后为TextView设置显示结果。

接下来先把QueryHandler和ContactTask的完整代码贴出来,然后再详细讲解ContactLoader的实现原理。

辅助类和策略接口的代码

  • QueryHandler类
package timeshatter.contactmanager;import android.content.Context;import android.net.Uri;/** * Created by timeshatter on 16-7-27. */public final class QueryHandler {    private final Context context;    private final Uri uri;    private final String[] projections;    private final String selection;    private final String[] selectionArgs;    private final String sortOrder;    private final String id;    private QueryHandler(Builder builder) {        this.context = builder.context;        this.uri = builder.uri;        this.projections = builder.projections;        this.selection = builder.selection;        this.selectionArgs = builder.selectionArgs;        this.sortOrder = builder.sortOrder;        this.id = builder.id;    }    public static class Builder {        private Context context;        private final Uri uri;        private final String[] projections;        private String selection;        private String[] selectionArgs;        private String sortOrder;        private String id;        public Builder(Context context,Uri uri, String[] projections) {            this.context = context;            this.uri = uri;            this.projections = projections;        }    //为了防止设置selection时忘记设置selectionArgs,所以这里一起设置了        public Builder setSelection(String selection,String[] selectionArgs) {            this.selection = selection;            this.selectionArgs = selectionArgs;            return this;        }        public Builder setSortOrder(String sortOrder) {            this.sortOrder = sortOrder;            return this;        }        public Builder setId(String id) {            this.id = id;            return this;        }        public QueryHandler build() {            return new QueryHandler(this);        }    }    public Context getContext() {        return context;    }    public Uri getUri() {        return uri;    }    public String[] getProjections() {        return projections;    }    public String[] getSelectionArgs() {        return selectionArgs;    }    public String getSelection() {        return selection;    }    public String getSortOrder() {        return sortOrder;    }    public String getId() {        return id;    }}
  • ContactTask
/** * Created by timeshatter on 16-7-27. */public interface ContactTask {    /**     * 处理完后,请务必调用传入的Cursor的close()函数来关闭资源     * @param textView     * @param cursor     */    void apply(TextView[] textView, Cursor cursor);}

接下来介绍ContactLoader的实现原理

ContactLoader实现原理

ContactLoader有以下几个关键成员

  • taskQueue

    存放查询任务的任务队列,用一个LinkedList来存放任务,其中的任务都Runnable对象。
    可以比喻成等待处理的快递件

  • threadPool

    线程池对象,执行任务队列中的Runnable对象。
    可以比喻成快递件的机器。

  • taskThread

    后台取任务的线程,以后进先出的方式来从taskQueue中取任务给线程池执行。
    可以比喻成取快递件给机器处理的服务人员。

这三个成员之间的关系可以通过下图来理解:
flow
上图介绍了三个主要成员之间的关系,方便理解下面的ContactLoader中的实现细节,下面贴上ContactLoader的代码:

ContactLoader实现代码

package timeshatter.contactmanager;import android.content.Context;import android.database.Cursor;import android.os.Handler;import android.os.Looper;import android.os.Message;import android.widget.TextView;import java.util.LinkedList;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;/** * Created by TimeShatter on 16-7-27. */public enum ContactLoader {    /**      *唯一的枚举常量      */    INSTANCE(4);    /**     * 任务队列     */    private final LinkedList<Runnable> taskQueue;    /**     * 线程池     */    private  ExecutorService threadPool;    /**     * 这个信号量的数量和我们加载图片的线程个数一致; 每取一个任务去执行,我们会让信号量减一;每完成一个任务,     * 会让信号量+1,再去取任务;目的是当我们的任务到来时,如果此时在没有空闲线程,     * 任务则一直添加到TaskQueue中,而不会直接添加到线程池的等待队列中去,当线程完成任务,     * 可以根据策略去TaskQueue中去取任务,只有这样, 后进选出才有意义。     */    private volatile Semaphore threadCountSemaphore;    /**     * 为TextView设置值的Handler     */    private final Handler UiHandler;    /**     * 从taskQueue取任务给线程池的Handler     */    private Handler taskHandler;    /**      *控制taskHandler初始化的信号量      */    private volatile Semaphore handlerSemaphore = new Semaphore(0);    /**     * 运行taskHandler的线程     */    private final Thread taskThread;    ContactLoader(int threadCount) {        taskThread = new Thread() {            @Override            public void run() {                Looper.prepare();                taskHandler = new Handler() {//任务线程中的handler                    @Override                    public void handleMessage(Message msg) {                        threadPool.execute(getTask());                        try {                            threadCountSemaphore.acquire();// 控制线程池中等待执行的线程的数量                        } catch (InterruptedException e) {                            e.printStackTrace();                        }                    }                };                handlerSemaphore.release();//初始化完,释放一个信号量                Looper.loop();//无限循环取出Message来给taskHandler处理            }        };        taskThread.start();        UiHandler = new Handler() {//主线程中的hanlder,用于更新UI            @Override            public void handleMessage(Message msg) {                ContactHolder contactHolder = (ContactHolder) msg.obj;                ContactTask contactTask = contactHolder.contactTask;                TextView[] textViews = contactHolder.textViews;                Cursor cursor = contactHolder.cursor;                String id = contactHolder.id;                if(textViews[0].getTag().toString().equals(id)){//防止显示错乱                    contactTask.apply(textViews,cursor);//调用策略接口处理查询结果                }else{                    cursor.close();                }            }        };        threadPool = Executors.newFixedThreadPool(threadCount);        threadCountSemaphore = new Semaphore(threadCount);        taskQueue = new LinkedList<>();    }    private static class ContactHolder {        ContactTask contactTask;        TextView[] textViews;        Cursor cursor;        String id;    }    public void loadContacts(final TextView[] textViews, final QueryHandler queryHandler, final ContactTask contactTask) {        /**         * 为当前显示查询结果的TextView设置最新的id,用于查询完后,设置TextView时做匹配         * 如果匹配不成功则不设置该TextView,防止由于上下滑动过快而导致显示错位。         * 如当为第一个ListView项查询数据时,会向线程池添加任务,但如果此任务还没开始执行,用户就向下滑动,         * 此时第一个ListView项会加载一个新的任务。由后进先出原则,该新任务将比之前还没执行的任务先执行。         * 当新任务为第一个ListView项设置查询结果后,旧任务将继续执行。旧任务又将为第一个ListView项设置之前的查询结果         */        textViews[0].setTag(queryHandler.getId());//设置tag,防止显示错乱        addTask(new Runnable() {            @Override            public void run() {                Context context = queryHandler.getContext();                Cursor cursor = context.getContentResolver().query(queryHandler.getUri(),                        queryHandler.getProjections(),queryHandler.getSelection(),                        queryHandler.getSelectionArgs(),queryHandler.getSortOrder());                ContactHolder holder = new ContactHolder();                holder.textViews = textViews;                holder.contactTask = contactTask;                holder.cursor = cursor;                holder.id = queryHandler.getId();                Message msg = Message.obtain();                msg.obj = holder;                UiHandler.sendMessage(msg);//查询完了,向UiHanlder发送结果,更新UI                threadCountSemaphore.release();//每完成一个任务,释放一个信号量            }        });    }    //获取任务    private synchronized void addTask(Runnable runnable){        if(taskHandler == null) {//线程中的handler有可能还没初始化完            try {                handlerSemaphore.acquire();//阻塞,直到初始化完            } catch (InterruptedException e) {                e.printStackTrace();            }        }        taskQueue.addLast(runnable);        taskHandler.sendEmptyMessage(0);    }    private synchronized Runnable getTask() {        return taskQueue.removeLast();    }}

至此ContactLoader介绍结束了。

总结

总的来说,异步处理框架主要包括ContactLoader,QueryHandler和ContactTask三个成员。其中ContactLoader是主要的加载类,通过单例模式来实现;QueryHandler是携带参数的辅助类,通过建造者模式来实现;ContactTask是策略类,体现了策略模式。

他们的用法也很简单,只要把查询要用到的参数封装进QueryHandler中。然后实现一个自定义的ContackTask,在其中的apply()方法中写入对查询结果的处理。然后把这两个实例传进ContactLoader的loadContact()方法就OK了。

0 0
原创粉丝点击