运用单例模式、建造者模式和策略模式实现异步加载Android联系人资料
2016-07-31 11:58
381 查看
学完设计模式很久了,最近又在看Android联系人提供程序的官方文档,于是就想实现一个方便的联系人管理程序demo,而联系人管理程序demo的核心就是要实现一个异步加载联系人资料的类,于是就有了下文。
完整的Demo可以点击这里点击这里下载。
对应实际中的例子如下图:
该结构由三个表组成,通过外码联系起来,如下是三个表的官方描述:
ContactsContract.Contacts 表
表示不同联系人的行,基于聚合的原始联系人行。
ContactsContract.RawContacts 表
包含联系人数据摘要的行,针对特定用户帐户和类型。
ContactsContract.Data 表
包含原始联系人详细信息(例如电子邮件地址或电话号码)的行。
其中RawContacts表中每一行的contact_id列的值都与其所属联系人id是对应的,也就是Contacts和RawContacts是一对多的关系。同理,RawContacts与Data也是一对多关系。于是构成了这样的层次结构:
Contact -> RawConacts -> Datas
我们一般用的是下图这样的结构,很少把其他帐号的联系人放进联系人提供程序中,毕竟其它帐号的联系人只要打开客户端就可以了。例如,想发信息给小明的QQ帐号,一般是直接打开QQ,然后找到小明的QQ帐号来发信息,而不是在联系人那里找。
这种查看数据的方式十分麻烦。一、不能直接看到联系人的部分数据。二、如果上图的的联系人1和联系人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);
}
下面就这二个类和一个接口是如何搭配使用的进行简单的说明。
以上代码实现了对每个联系人手机号码的查询,其中通过实现ContactTask接口来传递一个具体的策略,其中的apply()方法是在查询完成后,由ContactLoader调用的,调用时,把查询到的Cursor和要显示结果的TextView数组传进这个方法,然后为TextView设置显示结果。
接下来先把QueryHandler和ContactTask的完整代码贴出来,然后再详细讲解ContactLoader的实现原理。
ContactTask
接下来介绍ContactLoader的实现原理
taskQueue
存放查询任务的任务队列,用一个LinkedList来存放任务,其中的任务都Runnable对象。
可以比喻成等待处理的快递件
threadPool
线程池对象,执行任务队列中的Runnable对象。
可以比喻成快递件的机器。
taskThread
后台取任务的线程,以后进先出的方式来从taskQueue中取任务给线程池执行。
可以比喻成取快递件给机器处理的服务人员。
这三个成员之间的关系可以通过下图来理解:
上图介绍了三个主要成员之间的关系,方便理解下面的ContactLoader中的实现细节,下面贴上ContactLoader的代码:
至此ContactLoader介绍结束了。
他们的用法也很简单,只要把查询要用到的参数封装进QueryHandler中。然后实现一个自定义的ContackTask,在其中的apply()方法中写入对查询结果的处理。然后把这两个实例传进ContactLoader的loadContact()方法就OK了。
完整的Demo可以点击这里点击这里下载。
实现异步加载联系人的需求
联系人结构
Android的联系人提供程序是一个强大而又灵活的 Android 组件,用于管理设备上有关联系人数据的中央存储库。因此,为了支持其强大的功能,其数据库的表结构就比较复杂了。其结构如下:对应实际中的例子如下图:
该结构由三个表组成,通过外码联系起来,如下是三个表的官方描述:
ContactsContract.Contacts 表
表示不同联系人的行,基于聚合的原始联系人行。
ContactsContract.RawContacts 表
包含联系人数据摘要的行,针对特定用户帐户和类型。
ContactsContract.Data 表
包含原始联系人详细信息(例如电子邮件地址或电话号码)的行。
其中RawContacts表中每一行的contact_id列的值都与其所属联系人id是对应的,也就是Contacts和RawContacts是一对多的关系。同理,RawContacts与Data也是一对多关系。于是构成了这样的层次结构:
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了。
相关文章推荐
- Android第三方异步网路加载库AsyncHttpClient内部实现缓存策略了吗?
- handler加线程模式实现android应用的异步加载
- Android第三方异步网路加载库AsyncHttpClient内部实现缓存策略了吗?
- Android第三方异步网路加载库AsyncHttpClient内部实现缓存策略了吗? 转载请注明出处:http://blog.csdn.net/zhangphil Android第三方异步网
- Android实现异步加载图片
- 转载:Android实现ListView异步加载图片
- android 实现listView异步加载图片
- 实例讲解Android中如何实现图片的异步加载功能
- Android实现ListView异步加载图片
- 转载 Android实现ListView异步加载图片
- Android实现ListView异步加载图片
- Android实现ListView异步加载图片
- 实例讲解Android中如何实现图片的异步加载功能
- Android中实现图片的异步加载学习笔记
- [Silverlight入门系列]Prism中TreeView真正实现MVVM模式和Expanded发生时异步动态加载子节点(WCFRiaService)
- 实例讲解Android中如何实现图片的异步加载功能
- 实例讲解Android中如何实现图片的异步加载功能
- Android实现ListView异步加载图片
- Android 如何实现ListView异步加载网络图片
- Android实现ListView异步加载图片