您的位置:首页 > 移动开发 > Android开发

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

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

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

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

联系人结构

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



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



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

ContactsContract.Contacts

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

ContactsContract.RawContacts

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

ContactsContract.Data

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

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

Contact -> RawConacts -> Datas

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



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

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



这种查看数据的方式十分麻烦。一、不能直接看到联系人的部分数据。二、如果上图的的联系人1联系人2是两个同名的人,就要点击进去查看其手机号码才能区分不同的两个人。

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



需要异步处理的原因

基于Cursor的查询是一项耗时的操作,如果在主线程中进行大量查询数据库的操作,就会阻塞UI,导致用户体验不好。

如果每个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中取任务给线程池执行。

可以比喻成取快递件给机器处理的服务人员。

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



上图介绍了三个主要成员之间的关系,方便理解下面的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了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息