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

Android: best practice to perform asynchronous operations in getView()

请不要关闭它,恕我直言,这是一个不错的编程问题,可能很有用。


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

问题如下:

AdaptergetView() 中,我需要执行一些异步操作,例如在 Web 上检查队形,并根据该操作更新视图。

我使用了以下方法:

每次调用 getView() 我都会启动一个 Thread

但是我的方法为我赢得了很多批评:

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()。这将重绘您的所有信息。

一次提取所有数据 - 不要分成小部分。这是最好和最合理的方法。

对此有几种方法。 虽然你这样做确实不合适

  1. AsyncTask

    • 这里的线程池是在内部完成的,所以你不需要费心
    • 它是解决问题的更简洁的方法,而不是产生单独的线程。
    • 如果您的用户在您 API 通话期间更改屏幕,您也可以 cancel the call
    • 您必须启用 notifyDatasetChanged()
    • 您只需覆盖很少的函数即可实现所需的功能。
  2. AsyncTaskLoader

    • 它给了你更多的控制权,但是你失去了几个隐式定义的函数
    • 您需要更多知识才能使用它,并且应该精通 类,例如 LoaderManager,Loader
    • 改变是自触发的 假设您要更改基础数据集,这些更改会自动触发并为您的 UI.
    • 提供更改
  3. Handlers 和线程

    • 这比您当前的方法高一级,但提供更多好处
    • 您可以抽象线程创建并提供一个处理程序来处理对您所有 ID 的调用。
    • 您可以对线程和传递的消息进行排队。
    • 如果屏幕发生变化,您可以remove callbacks and messages

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

虽然后者是主要问题,但第一个问题"user-noticeable"更多。

根据您需要的控制以及您在 android 回调和线程 management.But 方面的专业知识,有并且可能有其他几种方法(根据我的说法)这三种方法最合适。

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

就像在架构图中一样,在发出请求时,它会查找缓存,如果未命中,则会尝试网络请求并将其添加到缓存中以备后用, 此外,您似乎可以提供适合您需要的自定义后退重试请求,并在需要时 handle/invalidate 缓存。

如需深入参考,您可以查看 - https://android.googlesource.com/platform/frameworks/volley/+/master/src/main/java/com/android/volley

恕我直言,最佳做法是不要在 getView() 中执行异步操作。您的适配器 class 只能将对象转换为视图,您应该尽可能简单明了。

如果您启动一个引用新创建的视图的异步任务(或线程),如果任务在视图不再显示在屏幕上或者它被适配器回收后完成,您可能 运行 会遇到麻烦作为滚动的结果。

您应该考虑在适配器外部执行异步操作。开始操作并在完成后更新列表。您可以在异步操作完成后更新整个列表或部分元素。

操作本身可以在 activity 的 AsyncTask 或专用服务中完成。避免创建新的线程,因为创建线程的成本很高,而且如果由于某种原因你调用得太频繁,你可能 运行 内存不足。 AsyncTasks 使用线程池,因此您不会遇到这个问题。

因此,退后一步,完全抽象地看待这个问题,您问的问题似乎与如何管理一些可能需要很长时间的 运行 操作(网络或其他方式)有关影响列表视图项的外观。

鉴于该假设,您面临两个主要问题:

  1. 线程的启动成本相对较高
  2. getView() 返回的视图被回收并且可以在用户滚动列表时改变"item"

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

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

抱歉,我现在没有足够的时间来详细介绍。我该睡觉了:-)

祝你好运。有空我再写几篇。 亲切的问候, CJ

编辑

I think this is an interesting question, worth some kind of "canonical" solution

Google I/O 2013 :P 我建议你看这个 Google I/O 从 2013 年开始。他们在那里清楚地解释了很多这些东西。您的所有问题都将在那里得到解答。是佳能

我在这里使用了 Volley 库。如果您阅读文档,那么您会看到 Volley 在后台线程上运行。所以不需要实现你的异步任务。由于其他人已经涵盖了使用 Threads 的问题,因此我不会谈论这些。让我直接进入代码:)

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

  1. 创建接口:WebInfoUpdateReceiver.java

    public interface WebInfoUpdateReceiver {
    
        public void receive(Foo [] fooItems);
    
    }
    
  2. 创建一个 class 来下载东西: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. 现在让你的activity实现接口:

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