项目里有这么一个需求,如果用户进入一个搜索页面,页面顶端有一个搜索框,用户在输入的同时,底下同步的展示相关搜索建议。大约是这个样子:
大体逻辑如下:
实现功能:在输入的同时根据输入的文字立即给出结果,服务器端使用solaris做内存上的缓存。
关键地方在于:
1、用户输入完毕之后要尽可能快的响应
2、尽可能的为搜索服务器降低压力
3、将网络带宽资源最大化的用在用户真正需要的搜索结果上
注:本文只讨论客户端的优化,服务器搜索优化暂不涉及
这个功能出现了很久,直到最近才开始修改,并且连改三个版本
第一版:监听EditTextView的TextChanged事件,每次当用户在搜索框输入或者删除一个字符,就用当前输入框里的文字发起一次搜索请求,并在请求结束后的回调函数中,判断之前请求的文字和当前输入框里文字是否相同,如果相同就显示
优点:实时性强,用户输入完毕之后立刻发出请求,不需等待
缺点:由于http请求线程池有限,允许的并发量很小,也许新请求的关键字需要排队,也就是等之前关键字搜索完毕之后再进行请求,实际上用户等待的时间会很长。并且给搜索服务器带来了极大的压力
第二版:设置一个定时器,每1.5s检测一下搜索框中的内容,如果内容发生改变,就发起一次Http请求,并通过OkHttp将之前全部的关键字请求都取消掉,减少并发。
优点:减少了并发Http请求的队列等待,为服务器减少了压力
缺点:1.5秒的扫描间隔比较长,用户需要多付出无谓的1.5s等待时间,极大的降低了用户体验
第三版:启动一个子线程进行维护,用户每输入一个字母后等待500ms,如果500ms内用户没有输入其他字符,就搜索当前搜索框里的文字,如果500ms以内用户又继续输入了文字,就重新开始500ms的等待
第四版:策略与第三版相同,但是不使用子线程进行维护,而是使用Android原生的Handler和MessageQueue队列进行维护,减少线程的内存开销和线程之间的同步问题
下面搬上源代码:
第一版源码:简单实现TextChanged监听器
headerEditText.addTextChangedListener(new TextWatcher() {//EditText的文字改变监听事件
@Override
public void onTextChanged(CharSequence constraint, int start, int before, int count) {
//发起Http请求
}
@Override
public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
}
@Override
public void afterTextChanged(Editable s) {
}
});
第二版源码:在第一版的基础上加入了TimerTask实现定时扫描
String searchKeyWord;//用于在500ms内缓存用户输入的关键字
String lastSearchWord;//上次搜索成功的关键字
/**
* 用于取消没有执行完的搜索请求
* @return
*/
@Override
public String getHttpTaskKey() {
return this.getClass().getName();
}
class RestaurantSuggestionTask extends TimerTask {
@Override
public void run() {
JLogUtils.i("AlexHttp","1.5s等待结束,当前的关键字"+searchKeyWord);
if(searchKeyWord==lastSearchWord)return;//如果500ms以内,用户没有写过别的字,就不发起新的搜索
lastSearchWord = searchKeyWord;
//清除掉之前没有完成的搜索请求
if(searchFindActivity!=null)HttpTaskHandler.getInstance().removeTask(searchFindActivity.getClass().getName());
getAllSearchTextFilter(searchKeyWord);
//发起Http请求
}
}
Timer searchTimer = null;
private void getAllSearchTextFilter(final String keyword) {
JLogUtils.i("MartinFilter", "getAllSearchTextFilter keyword=>" + keyword);
searchKeyWord = keyword;
if(searchTimer!=null)return;
searchTimer = new Timer();
try {searchTimer.scheduleAtFixedRate(new RestaurantSuggestionTask(),0,1500);} catch (Exception e) {JLogUtils.i("AlexHttp","搜索餐馆出现异常"+keyword,e);}
}
@Override
public void onStop() {
super.onStop();
//停止搜索关键字监听器
if(searchTimer!=null)searchTimer.cancel();
searchTimer = null;
}
第三版源码:去掉定时更新的方案,采用500ms自动延迟,并使用子线程维护
String searchKeyWord;//用于在500ms内缓存用户输入的关键字
String wordSearching;//正在搜索的关键字
/**
* 用于取消没有执行完的搜索请求
* @return
*/
@Override
public String getHttpTaskKey() {
return this.getClass().getName();
}
class SearchThread extends Thread{
String keyWord;
private int waitTime = 500;
Handler handler = new Handler();
public void pushKeyWord(String keyWord){
this.keyWord = keyWord;
waitTime = 500;
}
@Override
public void run() {
super.run();
while(waitTime > 0){//逐步倒计时,如果有新的关键字,就恢复500ms等待
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
waitTime-=100;
}
if(searchFindActivity!=null)HttpTaskHandler.getInstance().removeTask(searchFindActivity.getClass().getName());
handler.post(new Runnable() {
@Override
public void run() {
TMPSearchFindSuggestionFilterKeywordsEntity filterKeywordsEntity = getLatestKeyFilterDeletePreviousFilters();
//此处发起Http请求
searchThread = null;
}
});
}
}
SearchThread searchThread = null;
private void getAllSearchTextFilter(final String keyword) {
JLogUtils.i("MartinFilter", "getAllSearchTextFilter keyword=>" + keyword);
//清除掉之前没有完成的搜索请求
if(keyword==null)return;
if(searchFindActivity!=null && !keyword.equals(wordSearching))HttpTaskHandler.getInstance().removeTask(searchFindActivity.getClass().getName());
searchKeyWord = keyword;
if(searchThread!=null){
searchThread.pushKeyWord(keyword);
return;
}
searchThread = new SearchThread();
searchThread.start();
}
@Override
public void onStop() {
super.onStop();
//停止搜索关键字监听器
if(searchThread!=null)searchThread.interrupt();
searchThread = null;
}
第四版源码:不使用子线程进行维护,而使用Handler减少子线程的建立
这里的方案是,在Fragment中有一个Runnable实例,它的Run方法就是请求Http的代码,每次在搜索框输入文字的时候,就用Handler先把之前放到MessageQueue的Runnable移除,然后把新的Runnable放进去(其实是同一个实例),分别调用的是handler.removeTask(Runnable)和handler.postDelay(Runnable,500);这样就实现了通过一个队列去管理输入文字的搜索。
如果用户输入的比较慢,假设700ms才输入一个字符,那么在新字符输入的过程中,所有旧的没有锁完的关键字的请求会被通过OkHttp cancel掉
SearchRunnable searchRunnable = new SearchRunnable();
class SearchRunnable implements Runnable{
String keyWord;
Handler handler = new Handler();
public void pushKeyWord(String keyWord){
this.keyWord = keyWord;
handler.removeCallbacks(this);
handler.postDelayed(this,500);
}
@Override
public void run() {
TMPSearchFindSuggestionFilterKeywordsEntity filterKeywordsEntity = getLatestKeyFilterDeletePreviousFilters();
//此处发起Http请求
wordSearching = searchKeyWord;
}
}
private void getAllSearchTextFilter(final String keyword) {
JLogUtils.i("MartinFilter", "getAllSearchTextFilter keyword=>" + keyword);
//清除掉之前没有完成的搜索请求
if(keyword==null)return;
if(searchFindActivity!=null && !keyword.equals(wordSearching))HttpTaskHandler.getInstance().removeTask(searchFindActivity.getClass().getName());
searchKeyWord = keyword;
searchRunnable.pushKeyWord(keyword);
}
@Override
public void onStop() {
super.onStop();
}
看看实际操作打印出来的Log
07-11 11:56:08.061 28267-28267/? I/AlexHttp: 收到信息的关键字c
07-11 11:56:08.218 28267-28267/? I/AlexHttp: 收到信息的关键字ch
07-11 11:56:08.541 28267-28267/? I/AlexHttp: 收到信息的关键字chi
07-11 11:56:08.873 28267-28267/? I/AlexHttp: 收到信息的关键字chin
07-11 11:56:09.112 28267-28267/? I/AlexHttp: 收到信息的关键字chine
07-11 11:56:09.321 28267-28267/? I/AlexHttp: 收到信息的关键字chines
07-11 11:56:09.528 28267-28267/? I/AlexHttp: 收到信息的关键字chinese
07-11 11:56:10.029 28267-28267/? I/AlexHttp: 搜索餐馆提示的接口=>http://staging.xxx.com:8033/search/filtergroupedname?v=2.6.7&client=1&cityID=2&filterWord=chinese
07-11 11:56:10.029 28267-28267/? I/AlexHttp: 准备访问get接口http://staging.xxx.com:8033/search/filtergroupedname?v=2.6.7&client=1&cityID=2&filterWord=chinese
07-11 11:56:10.467 28267-28267/? I/AlexHttp: 请求完毕,准备释放内存
07-11 11:56:10.467 28267-28267/? I/AlexHttp: 访问get接口成功