Android支持单词提示搜索框的网络请求策略

在Android应用中,为了提供实时的搜索提示,文章介绍了如何优化网络请求策略。从最初的每次输入触发请求,导致高并发和延迟,到定时器方案减少服务器压力,再到500ms等待延迟发送请求,最后采用Android Handler优化线程和同步问题,逐步提升用户体验并减轻服务器负担。代码示例展示了如何实现这种优化。

项目里有这么一个需求,如果用户进入一个搜索页面,页面顶端有一个搜索框,用户在输入的同时,底下同步的展示相关搜索建议。大约是这个样子:


大体逻辑如下:

实现功能:在输入的同时根据输入的文字立即给出结果,服务器端使用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接口成功


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值