用于图像获取的 AsyncTask 不断被调用

AsyncTask for image fetching keeps getting called

我有一个用于保存学生名单的小应用程序。

添加新学生记录时,随机图像 url(字符串)是从我自己的 10 个地址池中选择的,使用 AsyncTask 在图像视图上获取和设置。

我正在为列表使用自定义 CursorAdapter,并使用自定义 SQLiteOpenHelper 数据库来处理数据库(包含 idnamegrade , image url str).

我正在使用 AsyncTask 从互联网上获取图像

我的问题是我的 AsyncTask 不断被调用,在每次点击屏幕时,获取之前已经获取的相同图像。

我想我错误地使用了我的 AsyncTask(通过 bindView),但不确定。

我的目标 是每行只获取一次图像

主要活动:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    /* Fields for adding new student to the list */
    private EditText mEtName;
    private EditText mEtGrade;
    private ListView mLvStudents;

    /* Our DB model to store student objects */
    private SqlDbHelper mDB;

    /* Custom SQL-Adapter to connect our SQL DB to the ListView */
    private SQLAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /* Init fields & needed views */
        mEtName = findViewById(R.id.et_name);
        mEtGrade = findViewById(R.id.et_grade);
        mLvStudents = findViewById(R.id.lv_students);
        mDB = new SqlDbHelper(getApplicationContext());
        mAdapter = new SQLAdapter(this, mDB.getAllRows(), false);

        /* Set click listeners and adapter to our list */
        mLvStudents.setAdapter(mAdapter);
        findViewById(R.id.button_add).setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        final String name = mEtName.getText().toString();
        final int gradeInt = AidUtils.getGradeInt(mEtGrade.getText().toString());

        mDB.addStudent(name, gradeInt, AidUtils.randImageUrl());
        mAdapter.changeCursor(mDB.getAllRows());
        mEtName.setText("");
        mEtGrade.setText("");
    }
}

SQL适配器:

final class SQLAdapter extends CursorAdapter {
    private LayoutInflater mInflater;

    public SQLAdapter(Activity context, Cursor c, boolean autoRequery) {
        super(context, c, autoRequery);
        mInflater = LayoutInflater.from(context);
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
        return mInflater.inflate(R.layout.lv_line, viewGroup, false);
    }

    @Override
    public void bindView(final View view, Context context, Cursor cursor) {
        /* Set name */
        ((TextView)view.findViewById(R.id.tv_name)).setText(
                cursor.getString(cursor.getColumnIndex(SqlDbHelper.KEY_NAME)));

        /* Set the image URL for it and fetch the image */
        final String imageUrlStr = cursor.getString(cursor.getColumnIndex(SqlDbHelper.KEY_IMG));
        ((TextView)view.findViewById(R.id.tv_image_url)).setText(imageUrlStr);

        new AsyncImageSet(imageUrlStr, (ImageView)view.findViewById(R.id.iv_pic)).execute();

        /* Set grade and color for it */
        final int grade = cursor.getInt(cursor.getColumnIndex(SqlDbHelper.KEY_GRADE));
        ((TextView)view.findViewById(R.id.tv_grade)).setText(String.valueOf(grade));
    }
}

SqlDbHelper:

final class SqlDbHelper extends SQLiteOpenHelper {
    private static final String TAG = "SqlDbHelper";

    /* Database version */
    public static final int VERSION = 1;

    /* Relevant string names, keys represent columns */
    public static final String DB_NAME = "StudentsDB";
    public static final String TABLE_NAME = "students";
    public static final String KEY_ID = "_id";
    public static final String KEY_NAME = "Name";
    public static final String KEY_GRADE = "Grade";
    public static final String KEY_IMG = "Image";

    public SqlDbHelper(Context context) {
        super(context, DB_NAME, null, VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        StringBuilder createQuery = new StringBuilder();
        createQuery.append("CREATE TABLE " + TABLE_NAME + " (")
                .append(KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,")
                .append(KEY_NAME + " TEXT,")
                .append(KEY_GRADE + " INT,")
                .append(KEY_IMG + " TEXT")
                .append(")");

        Log.d(TAG, "Create table query: " + createQuery.toString());
        sqLiteDatabase.execSQL(createQuery.toString());
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {}

    public void addStudent(final String name, final int grade, final String imageUrl) {
        ContentValues cv = new ContentValues();
        cv.put(KEY_NAME, name);
        cv.put(KEY_GRADE, grade);
        cv.put(KEY_IMG, imageUrl);
        getWritableDatabase().insert(TABLE_NAME, null, cv);
    }

    public Cursor getAllRows() {
        return (getReadableDatabase().
                rawQuery("SELECT * FROM " + TABLE_NAME, null));
    }
}

AsyncImageSet:

public class AsyncImageSet extends AsyncTask<Void, Void, Bitmap> {
    private String mImageUrl;
    private ImageView mImageView;

    public AsyncImageSet(String imageUrl, ImageView imageView) {
        mImageUrl = imageUrl;
        mImageView = imageView;
    }

    @Override
    protected Bitmap doInBackground(Void... voids) {
        Log.v("AsyncImageSet", "New Async Task launched!");
        Bitmap image = null;
        try {
            image = AidUtils.getBitmapFromUrl(AidUtils.buildUrl(mImageUrl));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            return image;
        }
    }

    @Override
    protected void onPostExecute(Bitmap image) {
        if(image != null) {
            mImageView.setImageBitmap(image);
        }
    }
}

我做错了什么?

谢谢

不是从 URL 获取位图,而是使用 Picasso 将相同的 URL 加载到图像视图中。

代替

new AsyncImageSet(imageUrlStr, (ImageView)view.findViewById(R.id.iv_pic)).execute();

使用 Picasso 库加载图像,例如:

Picasso.with(context).load(imageUrlStr).into(imageView);

有关毕加索设置,请参阅 here

What Am I doing wrong here?

您不能简单地创建一个新的 AsyncTask 并在 bindView() 中执行它。每次 ListView 的新行进入屏幕时都会调用该方法(也可以在其他情况下调用),因此当用户上下滚动列表时,您将创建 很多 的 AsyncTask 实例。

处理这个问题的正确方法是执行一个 AsyncTask 来获取图像 只有 如果没有一个 AsyncTask 运行 已经为你的图像 url正在尝试获取。处理这个问题的最简单方法是在你的适配器中有一个映射来将一个字符串(imageUrl)映射到一个 AsyncTask 实例(它将获取该 imageUrl 指向的图像):

final class SQLAdapter extends CursorAdapter {
    private LayoutInflater mInflater;
    private Map<String, AsyncImageSet> mappings = new HashMap<>();
    //...

然后,在您的 bindView() 方法中使用上面的地图:

//...   
final String imageUrlStr = cursor.getString(cursor.getColumnIndex(SqlDbHelper.KEY_IMG));
// at this point look in our map to see if we didn't already create an AsyncTask for this imageUrl
if (mappings.get(imageUrl) != null) {
// there's a task for this imageUrl already created so we use that
AsyncImageSet task = mappings.get(imageUrl);
task.updateView((ImageView)view.findViewById(R.id.iv_pic));
} else {
// there isn't a task for this imageUrl so create one and execute it(and save it in our mappings)    
AsyncImageSet task = AsyncImageSet(imageUrlStr, (ImageView)view.findViewById(R.id.iv_pic));  
mappings.put(imageUrl, task);
task.execute();
}
((TextView)view.findViewById(R.id.tv_image_url)).setText(imageUrlStr);
//...

您还需要更改 AsyncTask 以添加额外的方法:

public class AsyncImageSet extends AsyncTask<Void, Void, Bitmap> {
    //...
    private Bitmap bitmap; 

    public void updateView(ImageView imageView) {
         mImageView = imageView;
         // if the task is already finished it means the bitmap is 
         // already available
         if (getStatus() == AsyncTask.Status.FINISHED) {
              mImageView.setImageBitmap(bitmpa);
         }
    }

    @Override
    protected void onPostExecute(Bitmap image) {
    if(image != null) {
        bitmap = image;
        mImageView.setImageBitmap(image);
    }
}

这是一个非常简单的实现,它将位图保存在内存中,如果图像很大,这可能不起作用。

理想情况下,正如其他答案所提到的,您应该使用像 Picasso 这样的图像加载库,这将帮助您避免实现自己的缓存系统的许多陷阱。