Performance issue downloading images for RecyclerView

家住魔仙堡 提交于 2019-12-02 07:53:55

问题


I have a ListView with a custom adapter. Every row has an ImageView that I render using a Bitmap, but my current code blocks the UI thread as I am using get() after executing my AsyncTask that downloads the bitmaps. I would like to change my code and access the imageViews in the onPostExecute() or something similar. So that the rows already display without waiting for all sprites to load.

Adapter class (download is triggered here)

public class PokemonAdapter extends ArrayAdapter<PokemonPOJO> implements View.OnClickListener{

private ArrayList<PokemonPOJO> dataSet;
Context mContext;
private int lastPosition = -1;

// View lookup cache
private static class ViewHolder {
    TextView txtName;
    TextView txtCP;
    TextView txtGenderShiny;
    ImageView sprite;
    Button btnDelete;
}

public PokemonAdapter(ArrayList<PokemonPOJO> data, Context context) {
    super(context, R.layout.row_pokemon, data);
    this.dataSet = data;
    this.mContext=context;
}

@Override
public void onClick(View v) {

    int position=(Integer) v.getTag();
    Object object= getItem(position);
    PokemonPOJO dataModel=(PokemonPOJO)object;

    switch (v.getId())
    {
        case R.id.btn_delete:
            FirebaseDatabase.getInstance().getReference("pokemons").child(dataModel.getUid()).removeValue();
            Toast.makeText(getContext(), "Pokemon removed!", Toast.LENGTH_SHORT).show();
            this.remove(dataModel);
            break;
    }
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // Get the data item for this position
    PokemonPOJO dataModel = getItem(position);
    // Check if an existing view is being reused, otherwise inflate the view
    ViewHolder viewHolder; // view lookup cache stored in tag

    final View result;

    if (convertView == null) {

        viewHolder = new ViewHolder();
        LayoutInflater inflater = LayoutInflater.from(getContext());
        convertView = inflater.inflate(R.layout.row_pokemon, parent, false);
        viewHolder.txtName = (TextView) convertView.findViewById(R.id.text_name);
        viewHolder.txtCP = (TextView) convertView.findViewById(R.id.text_cp);
        viewHolder.txtGenderShiny = (TextView) convertView.findViewById(R.id.text_gendershiny);
        viewHolder.sprite = (ImageView) convertView.findViewById(R.id.img_sprite);
        viewHolder.btnDelete = (Button)convertView.findViewById(R.id.btn_delete);
        result=convertView;

        convertView.setTag(viewHolder);
    } else {
        viewHolder = (ViewHolder) convertView.getTag();
        result=convertView;
    }

    lastPosition = position;

    viewHolder.txtName.setText(dataModel.getName());
    viewHolder.txtCP.setText("CP: " + Integer.toString(dataModel.getCP()));
    viewHolder.txtGenderShiny.setText(dataModel.getGender() + (dataModel.isShiny() ? " (Shiny)" : ""));
    viewHolder.btnDelete.setOnClickListener(this);

    try {
        Bitmap bm = new DownloadImageTask().execute(dataModel.getSpriteUrl()).get();
        viewHolder.sprite.setImageBitmap(bm);
    } catch (Exception e) {
        e.printStackTrace();
    }
    viewHolder.btnDelete.setTag(position);

    // Return the completed view to render on screen
    return convertView;
}

private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {

    @Override
    protected Bitmap doInBackground(String... urls) {
        String urldisplay = urls[0];
        Bitmap bm = null;
        try {
            InputStream in = new java.net.URL(urldisplay).openStream();
            bm = BitmapFactory.decodeStream(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bm;
    }

}

Fragment with ListView

public class MyPokemonFragment extends Fragment {

    private FirebaseAuth auth;
    private DatabaseReference pokemonDb;
    private TextView text_noPokemon;
    private ListView listViewPokemon;
    private static PokemonAdapter adapter;
    private populateListViewTask populateListView;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_mypokemon,null);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {

        super.onViewCreated(view, savedInstanceState);
        auth = FirebaseAuth.getInstance();

        listViewPokemon = view.findViewById(R.id.list_pokemon);
        text_noPokemon= view.findViewById(R.id.text_noPokemon);

        Query getUserPokemon = FirebaseDatabase.getInstance().getReference("pokemons").orderByChild("userUid").equalTo(auth.getCurrentUser().getUid());
        getUserPokemon.addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot snapshot) {

                if(!snapshot.hasChildren()) {
                    text_noPokemon.setText("You have not added any Pokémon yet.");
                }
                else {
                    TreeMap<String, Pokemon> pokemons = new TreeMap<>();
                    for (DataSnapshot pokemon : snapshot.getChildren()) {
                        pokemons.put(pokemon.getKey(), pokemon.getValue(Pokemon.class));
                    }
                    populateListView = new populateListViewTask();
                    populateListView.execute(pokemons);
                }
            }

            @Override
            public void onCancelled(DatabaseError databaseError) { }
        });

    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if(populateListView != null && populateListView.getStatus() == AsyncTask.Status.RUNNING)
            populateListView.cancel(true);
    }

    private class populateListViewTask extends AsyncTask<TreeMap<String, Pokemon>, Void, ArrayList<PokemonPOJO>> {

        @Override
        protected ArrayList<PokemonPOJO> doInBackground(TreeMap<String, Pokemon>... maps) {

            ArrayList<PokemonPOJO> pojos = new ArrayList<>();
            HttpURLConnection connection = null;
            BufferedReader reader = null;

            Iterator it = maps[0].entrySet().iterator();
            while(it.hasNext()) {
                Map.Entry pair = (Map.Entry)it.next();
                Pokemon p = (Pokemon)pair.getValue();
                try {
                    URL url = new URL("https://pokeapi.co/api/v2/pokemon/" + p.getPokedexNr() + "/");
                    connection = (HttpURLConnection) url.openConnection();
                    connection.connect();
                    InputStream stream = connection.getInputStream();
                    reader = new BufferedReader(new InputStreamReader(stream));
                    StringBuffer buffer = new StringBuffer();
                    String line = "";
                    while ((line = reader.readLine()) != null) {
                        buffer.append(line + "\n");
                    }

                    JSONObject j = new JSONObject(buffer.toString());
                    String name = j.getString("name");
                    String spriteUrl = (p.isShiny() ? j.getJSONObject("sprites").getString("front_shiny") : j.getJSONObject("sprites").getString("front_default"));

                    PokemonPOJO pojo = new PokemonPOJO((String)pair.getKey(), p.getPokedexNr(), name, spriteUrl, p.isShiny(), p.getGender(), p.getCP());
                    pojos.add(pojo);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    connection.disconnect();
                    try {
                        if (reader != null)
                            reader.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return pojos;
        }

        @Override
        protected void onPostExecute (ArrayList < PokemonPOJO > pojos) {
            adapter = new PokemonAdapter(pojos, getContext());
            listViewPokemon.setAdapter(adapter);
        }
    }
}

Pokemon row XML

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/img_sprite"
            android:layout_width="96dp"
            android:layout_height="96dp"
            android:scaleType="fitCenter" />

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="left|center_vertical"
            android:orientation="vertical">

            <TextView
                android:id="@+id/text_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textColor="@android:color/black"
                android:textSize="20sp"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/text_cp"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="18sp" />

            <TextView
                android:id="@+id/text_gendershiny"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="18sp" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="end"
            android:orientation="vertical">

            <Button
                android:id="@+id/btn_delete"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:backgroundTint="@color/colorPrimary"
                android:text="DELETE"
                android:textColor="#ffffff"
                android:textSize="16sp"
                android:textStyle="bold" />
        </LinearLayout>

    </LinearLayout>

</android.support.v7.widget.CardView>

回答1:


You are having performance issues because you are calling the get() method on your AsyncTask. The get() method basically causes the main thread to wait until the code in the AsyncTask completes execution before the main thread continues executing other instructions. Why Google added this method is curious to say the least. So do this to fix your code.

Create a new Java class file. Name the file "DownloadImageTask" and add this code:

public interface DownloadImageListener {
    void onCompletedImageDownload(Bitmap bm);
}


public class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {

    private static final String TAG = DownloadImageTask.class.getSimpleName();

    private DownloadImageListener mListener;
    private String imageUrl = "";

    public DownloadImageTask(String imageUrl, DownloadImageListener listener){
        this.imageUrl = imageUrl;
        this.mListener = listener;
    }


    @Override
    protected Bitmap doInBackground(String... urls) {
        Bitmap bm = null;
        try {
            InputStream in = new java.net.URL(imageUrl).openStream();
            bm = BitmapFactory.decodeStream(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bm;
    }

    protected void onPostExecute(Bitmap bm) {
        mListener.onCompletedImageDownload(bm);
    }
}


If you have any issues adding the public interface to the "DownloadImageTask" Java file just create a separate Java file name "DownloadImageListener" and put the interface code in there.


Set your code to query the AsyncTask.
Change the Adapter code inside your getView() from this:

try {
    Bitmap bm = new DownloadImageTask().execute(dataModel.getSpriteUrl()).get();
    viewHolder.sprite.setImageBitmap(bm);
} catch (Exception e) {
    e.printStackTrace();
}

to this:

try {
    DownloadImageListener listener = new DownloadImageListener() {
        @Override
        public void onCompletedImageDownload(Bitmap bm) {
            if(bm != null){
                viewHolder.sprite.setImageBitmap(bm);
            }
        }
    };

    String imageUrl = dataModel.getSpriteUrl();

    DownloadImageTask downloadImageTask = new DownloadImageTask(imageUrl, listener);
    downloadImageTask.execute();

} catch (Exception e) {
    Log.e(TAG, e.getMessage());
}


This allows your AsyncTask to execute and when the Bitmap is returned the listener is triggered in the onPostExecute() method sending the Bitmap to your ListView in the onCompletedImageDownload() callback method.

Additional Info:
To improve performance even further you could create a caching model to save and retrieve images from the device if you have already downloaded them in the past. But that requires some really advanced techniques--and gets really tricky when images you wish to download might change from time to time.



来源:https://stackoverflow.com/questions/53905844/performance-issue-downloading-images-for-recyclerview

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!