Android:在getView()中执行异步操作的最佳实践



请不要关闭这个,IMHO这是一个不错的,可能有用的编程问题。


拜托,我读了很多东西,我很困惑,因为我读到了不同的观点和方法。

问题如下:

AdaptergetView()中,我需要执行一些异步操作,比如检查网络上的信息,并在此基础上更新视图。

我使用了以下方法:

每次调用getView()时,我都会启动Thread

但我的做法赢得了很多批评:

https://stackoverflow.com/a/28484345/1815311

https://stackoverflow.com/a/28484335/1815311

https://stackoverflow.com/a/28484351/1815311

public View getView(int position, View convertView, ViewGroup parent) { 
    ViewHolder holder;            
    if (convertView == null) {                
        //...         
    } 
    else {                
        //...
    }     
    Thread th= new Thread(new Runnable() {
           @Override
           public void run() {
                mActivity.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        CheckSomeInfoOverTheInternet(url, new myCallback {

                            @Override
                            public void onSuccess() {
                            holder.textview.setText("OK");
                            }
                            @Override
                            public void onFailre() {
                            holder.textview.setText("NOT OK!!!!");
                            }    
                        });
                    }
                });

           }
       });
    th.start();
    return convertView;
}  

请问做这种事的最佳做法是什么


请注意,我不是在寻找在getView()中执行网络请求的解决方案,而是如何根据异步调用的结果更新视图。

这绝对不是在ListView中更新信息的好方法。getView方法应该简单地根据已有的数据创建视图。它当然不应该运行任何程序来获取更多信息。

我能给你的最好的建议是事先获取数据。提取数据,更新Adapter连接的ArrayList,然后调用adapter.notifyDataSetChanged()。这将重新绘制您的所有信息。

一次提取所有数据,而不是分成小部分。这是最好、最合理的方法。

编辑

我认为这是一个有趣的问题,值得一些"规范"的解决方案

谷歌I/O 2013:P我建议你从2013年开始看这个谷歌I/O。他们在那里巧妙地解释了很多这些东西。你所有的问题都会在那里得到回答。这是正典。

我用过这里的沃尔利图书馆。如果你阅读这些文档,你会发现Volley是在后台线程上运行的。因此,无需实现异步任务。由于其他人已经介绍了使用Threads的问题,我就不谈这些了。让我直接进入代码:)

每当我的列表视图或网格视图或任何其他视图依赖于网络信息时,以下内容对我很有用:

  1. 创建一个接口:WebInfoUpdateReceiver.java

    public interface WebInfoUpdateReceiver {
        public void receive(Foo [] fooItems);
    }
    
  2. 创建一个类来下载东西:Downloader.java

    public class Downloader {
        private Context mContext;
        private WebInfoUpdateReceiver mReceiver;
        public Downloader(WebInfoUpdateReceiver receiver) {
           mReceiver = receiver;
        }
        public void downloadStuff() {
        MyStringRequest request = new MyStringRequest(Request.Method.GET,requestUrl, new Response.Listener<String>() {
        @Override
        public void onResponse(String response) {
           // parse the response into Foo[]
            mReceiver.update(foo);
                }
            }
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
        }
    });
    RequestQueue queue = Volley.newRequestQueue(mContext);
    queue.add(request);
        }
    }
    
  3. 现在让您的活动实现接口:

    public class Activity extends Activity implements WebInfoUpdateReceiver {
    public void receive(Foo [] fooItems) {
         // convert the array  into arrayList
        adapter.insert(arraylist);
        adapter.notifyDataSetChanged();
    }
      }
    

这有几个近似值。尽管你所做的确实不合适。

  1. 异步任务

    • 这里的线程池是在内部完成的,所以您不需要为此而烦恼
    • 这是一种更干净的方法来解决您的问题,而不是生成单独的线程
    • 如果您的用户在API调用期间更改了屏幕,您也可以取消调用
    • 您必须启用notifyDatasetChanged()
    • 您需要覆盖极少数的功能才能实现所需的功能
  2. 异步任务加载器

    • 它为您提供了更多的控制,但您失去了几个隐式定义的函数
    • 您需要更多的知识来使用它,并且应该精通LoaderManager、Loader等类
    • 变化是自我触发假设您要更改基础数据集,这些更改将自动触发并为您的UI提供更改
  3. 处理程序和线程

    • 这比你目前的估计高出一倍,但提供了更多的好处
    • 您可以抽象线程创建,并提供一个处理程序来处理对所有id的调用
    • 您可以对线程和传递的消息进行排队
    • 如果屏幕发生变化,您可以删除回调和消息

总之,您当前方法的主要缺点是:-是在需要进行更改时丢失上下文。-显式创建多线程

虽然后者是一个主要问题,但更"用户关注"的问题将是第一个问题。

基于您所需的控制以及您在android回调和线程管理方面的专业知识,还有其他几种方法。但(据我说)这三个是最合适的。

PS:所有这些方法的共同点是,

public View getView(int position, View convertView, ViewGroup     parent) { 
    ViewHolder holder;            
    if (convertView == null) {                
        //...         
    } 
    else {                
        //...
    }     
    //execute a task for your given id where task could be:
    //1. AsyncTask
    //2. AsyncTaskLoader
    //3. Handlers and thread
    //call notifyDataSetChanged() in all the cases,


    return convertView;
}
 @Override
 public void notifyDataSetChanged() {
        super.notifyDataSetChanged();
        //do any tasks which you feel are required
 } 

PPS:您还可以查看DataSetObserver来再次自动化您的需求。

您可以使用这样的东西:

   public View getView(int position, View convertView,
        ViewGroup parent) {
    ViewHolder holder;
    ...
    holder.position = position;
    new ThumbnailTask(position, holder)
            .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
    return convertView;
}
private static class ThumbnailTask extends AsyncTask {
    private int mPosition;
    private ViewHolder mHolder;
    public ThumbnailTask(int position, ViewHolder holder) {
        mPosition = position;
        mHolder = holder;
    }
    @Override
    protected Cursor doInBackground(Void... arg0) {
        // Download bitmap here
    }
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (mHolder.position == mPosition) {
            mHolder.thumbnail.setImageBitmap(bitmap);
        }
    }
}
private static class ViewHolder {
    public ImageView thumbnail;
    public int position;
}

此外,为了获得更好的性能,您需要为ListView适配器添加交互意识,这样它就不会在ListView上的一个投掷手势之后每行触发任何异步操作——这意味着滚动速度太快,甚至启动任何异步操作都没有意义。滚动停止或即将停止时,就是您想要开始实际显示每行的重内容的时候。

这里有一个很好的例子:https://code.google.com/p/shelves/

Volley库似乎已经抽象出了一些常用的模式。。。

为了更好地控制网络请求和缓存,您可以查看:http://developer.android.com/training/volley/simple.html#send

就像在架构图中一样,在发出请求时,它会查找缓存,在未命中的情况下,尝试网络请求并将其添加到缓存中以供以后使用,此外,您似乎可以提供适合您需要的自定义回退重试请求,并在需要时处理/使缓存无效。

为了获得深入的参考,您可以查看-https://android.googlesource.com/platform/frameworks/volley/+/master/src/main/java/com/android/stully

IMHO,最佳做法是不在getView()中执行异步操作。Adapter类只应该将对象转换为视图,并且应该尽可能简单。

如果启动引用新创建的视图的异步任务(或线程),如果该任务在视图不再显示在屏幕上后完成,或者由于滚动而被适配器回收,则可能会遇到麻烦。

您应该考虑在适配器之外执行异步操作。启动操作并在操作完成后更新列表。异步操作完成后,可以更新整个列表或某些元素。

操作本身可以在活动的AsyncTask或专用服务中完成。避免创建新线程,因为创建线程的成本很高,而且如果由于某种原因过于频繁地调用它,可能会耗尽内存。AsyncTasks使用线程池,因此不会出现此问题。

因此,退一步,完全抽象地看待这一点,您所问的问题似乎是如何管理一些可能会影响列表视图项外观的长期运行的操作(网络或其他)。

考虑到这种假设,您将面临两个主要问题:

  1. 线程的启动成本相对较高
  2. getView()返回的视图被回收,当用户滚动列表时可以更改"项"

问题1可以通过使用ThreadPoolExecutor(或您喜欢的任何其他机制)来解决。这个想法是线程也被回收,所以它们不需要太多时间来启动。

问题2稍微复杂一些。当视图即将被回收时,您可以取消线程操作(有几种方法可以做到这一点),但您需要决定是否可以接受失去精力(用户可能会在屏幕上滚动回您的视图,您必须重新开始)。您可以将长时间运行任务的结果存储在列表项(或与列表项相关的某些数据持有者)中,但需要注意内存不足(最近使用最少的缓存或lru缓存有时在这里很有用)。这将允许您继续努力,并且只有在列表视图仍在屏幕上时才更新列表视图。项目数据中的标志可用于指示您已经拥有数据,不再加载。

对不起,我现在没有足够的时间详述。对我来说现在是睡觉时间:-)

祝你好运。如果我有时间,我会再写一些。谨致问候,CJ

最新更新