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


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

联系人结构

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

android 异步延迟执行_android


对应实际中的例子如下图:

android 异步延迟执行_联系人_02

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

  • ContactsContract.Contacts

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

  • ContactsContract.RawContacts

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

  • ContactsContract.Data

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

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

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

android 异步延迟执行_联系人_03

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

正是因为这种关系,我们打开联系人程序时,一般是只显示联系人的名字(因为联系人的名字可以从RawContacts表中直接获得,不用从Data表中获得)。而看不到该联系人更多的详细信息,因为要看到详细信息就要点击该ListView项,然后跳转到查看详细信息的Activity中去查看,详细信息的Activity中的数据是通过查询Data表来获取的。下图显示了这种不方便的联系人程序:

android 异步延迟执行_android 异步延迟执行_04


这种查看数据的方式十分麻烦。一、不能直接看到联系人的部分数据。二、如果上图的的联系人1联系人2是两个同名的人,就要点击进去查看其手机号码才能区分不同的两个人。因此,我希望上图中的左边具有下图的结构:

android 异步延迟执行_异步_05

需要异步处理的原因

  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()方法使用异步加载的一个片段,可以帮助理解这二个类和一个接口是如何配合使用的。

//查询Uri
Uri 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中取任务给线程池执行。
可以比喻成取快递件给机器处理的服务人员。

这三个成员之间的关系可以通过下图来理解:

android 异步延迟执行_android 异步延迟执行_06


上图介绍了三个主要成员之间的关系,方便理解下面的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 = contact;
                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;
                 = 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了。