0

Occasionally, the application I am building will run out of memory or receiving an error relating to bytes (Failed to allocate a 2320024 byte allocation with 125240 free bytes and 122KB until OOM, for example).

The app is very image heavy and relies on converting byte[] to bitmap (not generally recommended I know - but a necessary evil for this particular project). I keep receiving constant messages about "doing too much on the main thread." When it crashes, the source can be traced to my gridAdapter class, and sometimes my buildImage class (which uses gridAdapter)

I realize the proper approach would be to run the processes on another thread, but I keep running into difficulties. When I tried to run the "fillGrid()" and "fillPartialGrid()" methods on another thread - nothing appears when I run the application. When I try to use a new thread within the getView method of gridAdapter, I only end up with half an image. Furthermore, I have even managed to receive an error with it pointing to a location where I am using a separate thread (in buildImage).

If anyone could offer some advice on how to improve performance, I would be very grateful.

buildImage

    public class buildImage extends AppCompatActivity {
    private SimpleCursorAdapter adapter;
private ImageButton back;
    ImageView buildContainer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setContentView(R.layout.activity_build_image);
        super.onCreate(savedInstanceState);
        buildContainer = (ImageView) findViewById(R.id.fullSizeImage);
        back = (ImageButton) findViewById(R.id.backButton);



        back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                finish();
            }
        }, 60 * 2* 1000L);
    }

    protected void onStart() {
        super.onStart();

        Intent i = getIntent();
        if (i.hasExtra("position"))

        {
           final int position = i.getExtras().getInt("position");
            int lto = i.getExtras().getInt("lto");
            int location = i.getExtras().getInt("location");
            final gridAdapter adapter = new gridAdapter(this, lto, location);




            new Thread(new Runnable() {
                @Override
                public void run() {
                    final Bitmap buildImage = BitmapFactory.decodeByteArray(adapter.buildImages.get(position), 0, adapter.buildImages.get(position).length);
                    buildContainer.post(new Runnable() {
                        @Override
                        public void run() {
                            buildContainer.setImageBitmap(buildImage);
                        }
                    });
                }
            }).start();


        } else

        {


            byte[] imageId = i.getByteArrayExtra("imageID");
            Bitmap buildImage = BitmapFactory.decodeByteArray(imageId, 0, imageId.length);
            buildContainer.setImageBitmap(buildImage);

        }
    }
    }

gridAdapter

public class gridAdapter extends BaseAdapter {
    private Context mContext;
    private int isLto;
    private int board;
    public Cursor items;

public ArrayList<String> itemNames = new ArrayList<String>();

  public ArrayList<byte[]> icons = new ArrayList<byte[]>();
public ArrayList<byte[]> buildImages = new ArrayList<byte[]>();




    public gridAdapter(Context context, int lto, int location) {
mContext = context;
    isLto = lto;
        board = location;

                fillGrid();


    }

    public gridAdapter(Context context, int location, boolean homePage) {
        mContext = context;
        isLto = 1;
        board = location;

                fillPartialGrid();


    }

    private void fillGrid() {
        databaseHelper dbHelper = new databaseHelper(mContext);
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        items = db.rawQuery("SELECT " + databaseHelper.ITEM_NAME + ", " + databaseHelper.ICON + ", " + databaseHelper.IMAGE + " FROM " + databaseHelper.TABLE_NAME + " where " + databaseHelper.LTO + " = " + isLto + " and " + databaseHelper.LOCATION + " = " + board + " ORDER BY " + databaseHelper.ITEM_NAME + " ASC", null);
        items.moveToFirst();

int counter = 0;

        while(!items.isAfterLast()) {

            itemNames.add(items.getString(items.getColumnIndex(databaseHelper.ITEM_NAME)));


icons.add(items.getBlob(items.getColumnIndex(databaseHelper.ICON)));
             buildImages.add(items.getBlob(items.getColumnIndex(databaseHelper.IMAGE)));



            counter++;
            items.moveToNext();
        }
        items.close();
        db.close();
dbHelper.database.close();

    }

    private void fillPartialGrid() {
        databaseHelper dbHelper = new databaseHelper(mContext);
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        items = db.rawQuery("SELECT " + databaseHelper.ITEM_NAME + ", " + databaseHelper.ICON + ", " + databaseHelper.IMAGE + " FROM " + databaseHelper.TABLE_NAME + " where " + databaseHelper.LTO + " = " + isLto + " and " + databaseHelper.LOCATION + " = " + board +  " ORDER BY " + databaseHelper.ITEM_NAME + " ASC LIMIT 3", null);
        items.moveToFirst();
        int counter = 0;
        while(!items.isAfterLast()) {  itemNames.add(items.getString(items.getColumnIndex(databaseHelper.ITEM_NAME)));


            icons.add(items.getBlob(items.getColumnIndex(databaseHelper.ICON)));
            buildImages.add(items.getBlob(items.getColumnIndex(databaseHelper.IMAGE)));



            counter++;
            items.moveToNext();
        }
        items.close();

        db.close();
        dbHelper.database.close();
    }
    @Override
    public int getCount() {
return itemNames.size();
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public Object getItem(int position) {
        return buildImages.get(position);
    }

    @Override
    public View getView(int position, View view, ViewGroup parent) {
        LayoutInflater layout = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        view = layout.inflate(R.layout.grid_item, parent, false);

        TextView gridItemName = (TextView)view.findViewById(R.id.gridItemName);


        ImageView iconContainer = (ImageView)view.findViewById(R.id.gridItemIcon);

        gridItemName.setText(itemNames.get(position));

Bitmap iconBitmap = BitmapFactory.decodeByteArray(icons.get(position), 0, icons.get(position).length);
iconContainer.setImageBitmap(iconBitmap);


        return view;
    }
}

and an example of another activity that uses gridAdapter and leads to buildImage

public class lunchStandard extends AppCompatActivity {
    private String release = Build.VERSION.RELEASE;
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch(item.getItemId()) {
            case R.id.configure_icon:

                Intent i = new Intent(this, preferences.class);
                startActivity(i);
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }
private  int discriminator;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_lunch_standard);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        toolbar.setClipChildren(false);
        toolbar.setNavigationIcon(R.drawable.back);
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent i = new Intent(lunchStandard.this, launch_activity.class);
                startActivity(i);
            }
        });
                GridView grid = (GridView) findViewById(R.id.lunchStandardGrid);
        SharedPreferences preferences = getSharedPreferences("config", MODE_PRIVATE);


        if(preferences.getInt("Location", 2) == 3) {
            discriminator = 3;
        } else if(preferences.getInt("Location", 2) == 2){
            discriminator = 2;
        }
        if(discriminator == 2) {
            setTitle("Main Boards Builds " + release);
        } else if (discriminator == 3) {
            setTitle("Speciality Builds " + release);
        }


        gridAdapter adapter = new gridAdapter(this, 0, discriminator);
        grid.setAdapter(adapter);


        grid.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Intent intent = new Intent(lunchStandard.this, buildImage.class);
                intent.putExtra("location", discriminator);
                intent.putExtra("lto", 0);
                intent.putExtra("position", position);
                startActivity(intent);
            }
        });

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent i = new Intent(lunchStandard.this, midamcorp.com.burgerkingapp.preferences.class);
                startActivity(i);
            }
        });

    }

}
KellyM
  • 2,472
  • 6
  • 46
  • 90

2 Answers2

0

I was able to to identify a few slowdowns and memory intensive areas in the code that you provided. I'll try to list the problems where you can get the biggest gains.

  • Doing a SQL query on the main thread and iterating through it - In general, doing a single SQL query won't slow you down by much but since you are iterating through it in fillGrid(), the time to render the activity increases linearly with the number of results returned. The general solution to this is to use LoaderManager/CursorAdapter for your grid view.
  • Using a bitmap without checking first the size - This one always gets me. Android memory heap size is very limited on some devices and bitmaps can quickly take up the entire memory if you're not careful. One common tactic is to check the size of the bitmap before using it and downsize it if necessary. Please see other answers, such as Strange out of memory issue while loading an image to a Bitmap object, for help on this matter.
  • Doing the byte[] to bitmap transformation - If you're able to avoid this, you will also get performance gains from moving to storing your images on filestorage rather than in the SQL database as a blob. You'll also be able to leverage many great third-party image libraries such as Fresco, Glide, Picasso, etc... that handle all the heavy lifting for you (loading images off the main thread, downsizing/resizing, network requests, loading indicators, etc...)
Community
  • 1
  • 1
charleschenster
  • 252
  • 3
  • 8
0

Why don't you use a image loading libraries like Glide and let it take care of the loading process? and you will never get OOM error.

Its pretty easy to setup, just compile this dependency in your gradle.build

compile 'com.github.bumptech.glide:glide:3.7.0'

and then all you need to do is

 Glide
.with(Context)
.load(adapter.buildImages.get(position))
.into(buildContainer);

I have worked on multiple application which dealt with images and this is the best approach to avoid OOMi know of.

Hope it helps

Atiq
  • 14,435
  • 6
  • 54
  • 69