0

I have a file sharing app, that transmits files over network.

In order to make user selects files properly, i list all the files available on the device and to make a better UX, i get thumbnails of videos and images in OnBindViewHolder. Since there are arbitrary number of photos and videos on a device, this results in loading huge number of thumbnails, and given its a recyclerview i expected that it won't use much memory, but apparently it does. I tried calling bitmap.recycle in onViewRecycled since some views might also have a refrence to the thumbnail. Tried using glide but didn't go beyond glide.with(context).load(bitmap).into(holder.imageview).

Also when i finish the activity, some bitmaps might persist (checked in memory profiler)

But i want to begin with reducing memory usage while activity is still in foreground.

I'm missing something?

Edit also i have to say that the Activity is using a tablayout, that have 5 tabs for 5 different MimeTypes

so here is the page adapter


public class SectionsPagerAdapter extends FragmentStateAdapter {

    public static final int[]              TAB_TITLES = new int[]{R.string.Text_files, R.string.audio_files, R.string.image_files, R.string.video_files/*, R.string.unknown_files*/,R.string.apk_files};
    public static final int[]              SEARCH_TITLE = new int[]{R.string.search_res};
    private final boolean isSearching;
    private ArrayList<FileItemsModel> searchResModel;
    private ArrayList<FileItemsModel> txtModel;
    private ArrayList<FileItemsModel> audioModel;
    private ArrayList<FileItemsModel> videoModel;
    private ArrayList<FileItemsModel> imageModel;
    private ArrayList<FileItemsModel> apkModel;
    //private ArrayList<FileItemsModel> unknownModel;



    public SectionsPagerAdapter(FragmentActivity fm, ArrayList<FileItemsModel> txtModel, ArrayList<FileItemsModel> audioModel,
                                ArrayList<FileItemsModel> videoModel, ArrayList<FileItemsModel> imageModel, ArrayList<FileItemsModel> apkModel/*, ArrayList<FileItemsModel> unknownModel*/) {
        super(fm);
        this.apkModel   = apkModel;
        this.txtModel   = txtModel;
        this.audioModel = audioModel;
        this.videoModel = videoModel;
        this.imageModel = imageModel;
        //this.unknownModel = unknownModel;
        isSearching = false;
    }

    public SectionsPagerAdapter(FragmentActivity fm, ArrayList<FileItemsModel> searchResModel) {
        super(fm);
        isSearching = true;
        this.searchResModel = searchResModel;
    }

    @Override
    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
        super.onDetachedFromRecyclerView(recyclerView);
        searchResModel = txtModel = audioModel = videoModel = imageModel = apkModel = null;
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        if (!isSearching) {
            switch (position) {
                case 0:
                    return PlaceholderFragment.newInstance(txtModel);
                case 1:
                    return PlaceholderFragment.newInstance(audioModel);
                case 2:
                    return PlaceholderFragment.newInstance(imageModel);
                case 3:
                    return PlaceholderFragment.newInstance(videoModel);
                case 4:
                    return PlaceholderFragment.newInstance(apkModel);
                /*case 5:
                    return PlaceholderFragment.newInstance(unknownModel);*/
                default:
                    throw new RuntimeException("invalid tab index " + position);
            }
        }

        return PlaceholderFragment.newInstance(searchResModel);

    }

    @Override
    public int getItemCount() {
        if (!isSearching)
            return TAB_TITLES.length;

        return SEARCH_TITLE.length;
    }
}

Fragment that holds the recyclerview

    public class PlaceholderFragment extends Fragment implements FileItemsAdapter.ItemClickListener{

    private static final String MODEL_KEY = "MODEL_KEY";
    private ArrayList<FileItemsModel> model;
    private FragmentFileSelectorBinding binding;
    private FileItemsAdapter adapter;
    private RecyclerView recyclerGrid;
    private FileSelectorActivity fileActivity;


    public static PlaceholderFragment newInstance(ArrayList<FileItemsModel> model) {
        PlaceholderFragment fragment = new PlaceholderFragment();
        Bundle bundle = new Bundle();
        bundle.putParcelableArrayList(MODEL_KEY, model);
        fragment.setArguments(bundle);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        fileActivity = (FileSelectorActivity) getActivity();
        if (getArguments() != null)
            model = getArguments().getParcelableArrayList(MODEL_KEY);
    }

    @Override
    public View onCreateView(
            @NonNull LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {

        binding = FragmentFileSelectorBinding.inflate(inflater, container, false);
        View root = binding.getRoot();
        recyclerGrid =  binding.FilesGridList;
        recyclerGrid.setLayoutManager(new GridLayoutManager(getContext(),3));
        adapter = new FileItemsAdapter(model);
        adapter.setClickListener(this);
        recyclerGrid.setAdapter(adapter);

        return root;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
        model = null;
        fileActivity = null;
        recyclerGrid.removeAllViews();
        recyclerGrid = null;
        adapter.setClickListener(null);
        adapter = null;
    }

    @Override
    public void onItemClick(View view, int position, ArrayList<FileItemsModel> list) {
        if(fileActivity.ChosenFiles.contains(list.get(position).getPath()))
            fileActivity.ChosenFiles.remove(list.get(position).getPath());
        else
            fileActivity.ChosenFiles.add(list.get(position).getPath());

        view.setAlpha(view.getAlpha() == 1 ? 0.5f : 1);
        list.get(position).setIsSelected(view.getAlpha() != 1);
    }
}

My adapter

    public class FileItemsAdapter extends RecyclerView.Adapter<FileSelectorViewHolder> {

    private static final String[] queryRes = new String[] { MediaStore.MediaColumns._ID };
    private static final int thumbType = MediaStore.Images.Thumbnails.MICRO_KIND;
    private static final Uri imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    private static final Uri videoUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
    private static final String queryFilter = MediaStore.MediaColumns.DATA + "=?";
    private final ArrayList<FileItemsModel> FileItemsModelArrayList;
    private ItemClickListener listener;
    public FileItemsAdapter(ArrayList<FileItemsModel> FileItemsModelArrayList)
    {
        super();
        this.FileItemsModelArrayList = FileItemsModelArrayList;
    }

    @Override
    public int getItemCount() {
        return FileItemsModelArrayList.size();
    }

    @NonNull
    @Override
    public FileSelectorViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View item = inflater.inflate(R.layout.file_item,parent,false);
        return new FileSelectorViewHolder(item, listener,FileItemsModelArrayList);
    }


    @Override
    public void onBindViewHolder(@NonNull FileSelectorViewHolder holder, int position) {

        //TODO: sometimes item is null.
         FileItemsModel item = FileItemsModelArrayList.get(position);
         if(item != null) {
             holder.linearLayout.setAlpha(item.getIsSelected() ? 0.5f : 1);
             holder.fileInfo.setText(item.getFileName());
             String minSdk = item.getMinSdk();
             if (minSdk != null)
                 holder.minSdk.setText(minSdk);

             setFileIcon(item, holder);
         }

        item = null;
    }


    @Override
    public void onViewRecycled(@NonNull FileSelectorViewHolder holder) {
        super.onViewRecycled(holder);
       // Glide.with(holder.itemView.getContext()).clear(holder.fileImg);
        if(holder.fileBitmap != null)
        {
            holder.fileBitmap.recycle();
            holder.fileBitmap = null;
        }

        holder.fileImg.setImageBitmap(null);
        holder.itemView.setOnClickListener(null);
        this.listener = null;
    }

    @Override
    public void onViewDetachedFromWindow(@NonNull FileSelectorViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        if(holder.fileBitmap != null)
        {
            holder.fileBitmap.recycle();
            holder.fileBitmap = null;
        }
        holder.fileImg.setImageBitmap(null);
        holder.itemView.setOnClickListener(null);
        this.listener = null;

    }

    public void setClickListener(@Nullable ItemClickListener itemClickListener) {
        this.listener = itemClickListener;
    }

    public interface ItemClickListener {
        void onItemClick(View view, int position, ArrayList<FileItemsModel> list);
    }


    private Bitmap getThumbnail(ContentResolver contentResolver,FileItemsModel item) {

      
        
        String mimeType = item.getMimeType();
        Cursor ca = contentResolver.query(mimeType.startsWith("video") ? videoUri : imageUri,queryRes, queryFilter, new String[] {item.getPath()}, null);


        if (ca != null && ca.moveToFirst()) {
            int id = ca.getInt(ca.getColumnIndex(MediaStore.MediaColumns._ID));
            ca.close();
            return  mimeType.startsWith("video") ? MediaStore.Video.Thumbnails.getThumbnail(contentResolver, id,thumbType , null )
                    : MediaStore.Images.Thumbnails.getThumbnail(contentResolver, id,thumbType , null );
        }

        ca.close();
        return null;
    }



    public void setFileIcon(FileItemsModel item, @NonNull FileSelectorViewHolder holder)
    {
        ContentResolver contentResolver = item.getContentResolver();
        if(contentResolver != null) {
            holder.fileBitmap = getThumbnail(contentResolver, item);

            if (holder.fileBitmap != null) {
                holder.fileImg.setImageBitmap(holder.fileBitmap);
                return;
            }
        }

        holder.fileImg.setImageDrawable(FileSelectorActivity.getFileDrawable(item.getMimeType()));
    }


}

my ViewHolder


    public class FileSelectorViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

    TextView fileInfo ;
    TextView minSdk;
    ImageView fileImg;
    LinearLayout linearLayout;
    Bitmap fileBitmap;
    FileItemsAdapter.ItemClickListener listener;
    private final ArrayList<FileItemsModel> FileItemsModelArrayList;

    public FileSelectorViewHolder(View item, FileItemsAdapter.ItemClickListener listener, ArrayList<FileItemsModel> FileItemsModelArrayList)
    {
        super(item);
        this.minSdk           = item.findViewById(R.id.minSdk);
        this.fileInfo       = item.findViewById(R.id.FileName);
        this.fileImg       = item.findViewById(R.id.FileImg);
        this.linearLayout = item.findViewById(R.id.FileContainer);
        item.setOnClickListener(this);
        this.listener = listener;
        this.FileItemsModelArrayList = FileItemsModelArrayList;

    }


    @Override
    public void onClick(View view) {
        if(listener != null) listener.onItemClick(view,getAdapterPosition(), FileItemsModelArrayList);

    }
}
Amr Ehab
  • 1
  • 1

1 Answers1

0

Edit: I miss read, you tried the Glide library. But you haven't tried using a disk cache strategy on your call.

Something like:

Glide.with(fragment).load(url).diskCacheStrategy(DiskCacheStrategy.ALL).into(imageView);

You can read about Glide caching here:

https://bumptech.github.io/glide/doc/caching.html#disk-cache-strategies

You can also preload the images into a cache if you want them to load instantly when you display them:

Preload multiple images with Glide

Sean Blahovici
  • 5,350
  • 4
  • 28
  • 38
  • My issue with glide wasn't loading images/bitmaps, in fact i managed to display the thumbnails i wanted and to provide a placeholder vector if no thumbnail is available in the OS. The thing is when i tried glide, i expected it would reduce memory usage (not referring to caching thumbnails). I thought it would recycle bitmaps already on the heap when some viewholders aren't visible. But i didn't notice even a slight drop in memory usage in comparison to just loading and assigning bitmaps directly to imageviews. I want to reduce RAM/heap usage. – Amr Ehab Apr 19 '22 at 21:55