Kategoriler
Android

Bitmap Nesnelerini Etkili Bir Şekilde Görüntüleme – Android

Kimi zaman, geliştirdiğimiz Android uygulamalarında, yüksek çözünürlükte resimler görüntülemeyi isteyebiliriz.

Ancak Android geliştiricilerinin bir çoğu bu işlemi, uygulamalarında herhangi bir kaynaktan çektiği resmi direk olarak ImageView denetimine yüklemek ile gerçekleştirir. Türkiye’deki Android ile ilgili bir çok kitapta da, resim yükleme işleminin bu şekilde anlatıldığını gördüm. Eğer yüksek çözünürlüklü resimler, bu şekilde gösterilir ise büyük ihtimalle şu hata meydana gelecektir:

java.lang.OutOfMemoryError: ...

Şunu unutmayın! Yukarıdaki hata, o an geliştirme yaptığınız kendi cihanızda vermese bile başka bir cihazda vermesi yüksek ihtimaldir. Zira Google Play’i sık ziyaret edenlerdenseniz, bazı uygulamaların altında; “Uygulama bende hata verdi, cihazım şu …” diye bir çok yoruma rastlamışsınızdır. Bu tür hataların alınmasının sebebi, Android işletim sisteminin bellek yönetimi ile ilgili kurallarından kaynaklanıyor. Zira Android işletim sistemi, taşınabilir cihazlara yönelik geliştirildiği için uygulamaların, sistem belleğini etkili bir şekilde kullanması gerekir. Yüksek çözünürlük resimler ise bellek tüketimi açısından oldukça açgözlüdürler.

Bu sorunu aşmak için evrensel olarak bilinen SubSampling denilen bir yöntem kullanılmakta. Bu yöntem, bir bitmap kaynağının tümünün gösterilmesi yerine çözünürlüğünün düşürülerek gösterilmesi ile gerçekleştiriliyor. Bitmap kaynağının çözünürlüğü düşürüldüğünde, bellekte daha az yer kaplar. Bitmap kaynaklarının, bellek kullanımını bir örnekle daha anlaşılabilir bir hale getirebiliriz.

Örneğin elimizde 2 adet Bitmap kaynağı olsun. Biri 640×480 (VGA) çözünürlüğünde, diğeri de 5 megapiksel çözünürlüğünde olsun. Android, Bitmap kaynaklarını 4 farklı biçimde bellekte tutar: ALPHA_8, ARGB_4444, ARGB_8888 ve RGB_565 biçimleri. Bunların içinden en çok kullanılanı ARGB_8888 biçimidir. ARGB_8888 biçiminde her bir piksel için bellekte 4-Byte yer ayrılır. RGB bilgisi için 3Byte, Alpha (transparanlık) bilgisi için de 1Byte. Bu bilgilerin doğrultusunda;
VGA için; 640*480=307200 => 307200*4=1228800-Byte (1.2MB)
5MP için; 2592*1944=5038848 => 5038848*4=20155392-Byte (19.2MB)

VGA bitmap, bellekte 1.2MB yer kaplarken 5MP bitmap 19.2MB yer kaplayacaktır. Hele de 24MP bir resim yüklemeye çalıştığımızda, bu yer 96MB olabiliyor. Taşınabilir bir cihaz için gerçektende çok değerli bir bellek alanı. Android, uygulamalara her bir işlem (process) için belli bir bellek alanı sunar. Elbette bu ayrılan bellek miktarı da cihazdan cihaza değişir. İşte resim yükleme işlemi, kendisine ayrılan bellek miktarını aştığında uygulamayı hata verdirerek çökertir.

Bitmap kaynaklarına Subsampling işlemini uygulamadan önce BitmapFactory nesnesinden bahsetmek istiyorum. Bu nesne, birçok şekilde Bitmap nesnesi oluşturabilir. Örneğin proje paketinde bulunan bir kaynaktan, cihaz belleğindeki bir dosyadan, tanımlanmış bir streamden ya da bir Byte dizisinden. Subsampling işlemi için BitmapFactory sınıfının, alt sınıfı olan BitmapFactory.Options sınıfını kullanacağız. BitmapFactory.Options sınıfının inSampleSize adında bir özelliği var. Bu özellik; 1, 2, 4, 8, 16, … diye 2’nin kuvvetlerine göre işlem yapar. Değer olarak 6 girilse dahi 4 değeri üzerinden işlem yapar. Örneğin, Bitmap oluştururken BitmapFactory.Options sınıfının inSampleSize özelliğini 8 yaparsak, 2592×1944 çözünürlüğündeki bir resmi 648×486 çözünürlüğüne düşürür. Yani genişlik ve yüksekliği 1/8 şeklinde azaltır. İşte bu işleme SubSampling işlemi denir.

Peki bu inSampleSize değerini nasıl elde edebiliriz? Bu değeri elde etmek için öncelikle elimizdeki Bitmap kaynağının çözünürlüğünü, hangi çözünürlüğe düşürmemiz gerektiğini bilmemiz gerekiyor. Örneğin elimizdeki cihazın Nexus 4 olduğunu düşünelim. Bu cihazın çözünürlüğü dikey olarak 768×1280 pikseldir. 5MP’lik bir resmi 2592×1944 çözünürlüğünde göstermenin hiçbir anlamı yok sanırım, bu ekranda. O yüzden bu resmi, dikey ekranda göstereceğimiz zaman genişliği en fazla 768 piksel olacak şekilde değiştirebiliriz. inSampleSize değerini etkin bir şekilde elde edebilmek için Anroid Developers sayfasında yayınlanan örnek bir kod var:

public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Yüklenecek olan resmin orijinal yükseklik ve genişliği
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;
 
    if (height > reqHeight || width > reqWidth) {
 
        final int halfHeight = height / 2;
        final int halfWidth = width / 2;
 
        // 2'nin kuvveti olacak şekilde en büyük inSampleSize değerini hesaplar.
        // Hesaplanan çözünürlük, istenilen çözünürlükten büyük olduğu sürece 2'nin kuvvetini arttırır.
        while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }
 
    return inSampleSize;
}

Yukarıdaki işlev, ilk parametre olarak bir Bitmap bilgi ve seçeneklerini barındıran BitmapFactory.Options nesnesini alır. Bu parametre, Bitmap kaynağının orijinal çözünürlüğünü elde etmek için kullanılacaktır. 2. ve 3. parametre ise istenilen genişlik ve yükseklik değerleridir. Bu çözünürlük değerlerine göre inSampleSize değeri hesaplanacaktır. Ancak yukarıdaki işlevi kendi uygulamalarımda denediğimde, şöyle bir şey farkettim. Ortaya çıkan inSampleSize değeri, 2’nin bir sonraki kuvveti olması gerekiyor. Zira Bitmap seçeneklerinde inSampleSize değerini örneğin 4 olarak girdiğimde resmin yükseklik ve genişlik değerlerini 2’ye bölüyor. Bu yüzden, bir Bitmap kaynağını istediğim çözünürlüğe düşürememiş oluyorum. Bunun teknik sebebini şuan için bilmiyorum. O yüzden yukarıdaki işlevi, çok daha duyarlı bir hale getirdim. Böylelikle istenilen çözünürlük için en uygun inSampleSize değerini bulan aşağıdaki işlevi yazdım.

/**
 * Bitmap'in orijinal boyut bilgisini ve çevirilmesi istenilen boyut
 * bilgisini alarak gerekli olan inSampleSize değerini hesaplar.
 * 
 * @param _Options
 *            Bitmap bilgilerini barındırır.
 * @param reqWidth
 *            İstenilen çözünürlüğün genişlik değeri.
 * @param reqHeight
 *            İstenilen çözünürlüğün yükseklik değeri.
 * @return Hesaplanmış inSampleSize değeri. (2'nin kuvvetleri olabilir)
 */
public static int calculateInSampleSize(BitmapFactory.Options _Options, int reqWidth, int reqHeight) {
	int inSampleSize = 1;
	// Bitmap kaynağının genişliği ya da yüksekliği, istenilen genişlik ya
	// da yükseklikten büyük olduğu sürece inSampleSize değeri
	// hesaplanıyor...
	if (_Options.outWidth > reqWidth || _Options.outHeight > reqHeight) {
		inSampleSize = 2;
		int calculatedWidth = _Options.outWidth / inSampleSize;
		int calculatedHeight = _Options.outHeight / inSampleSize;
		// inSampleSize değeri (2'nin kuvveti olarak) hesaplanır.
		// Hesaplanan çözünürlük, (reqWidth + (reqWidth / 2)) değerinden
		// büyük olduğu sürece 2'nin kuvveti artırılır.
		while (calculatedWidth > (reqWidth + (reqWidth / 2)) && calculatedHeight > (reqHeight + (reqHeight / 2))) {
			inSampleSize *= 2;
			calculatedWidth = _Options.outWidth / (inSampleSize / 2);
			calculatedHeight = _Options.outHeight / (inSampleSize / 2);
		}
		int estimatedWidth = _Options.outWidth / inSampleSize;
		int estimatedHeight = _Options.outHeight / inSampleSize;
		// Güncel inSampleSize değeri ile oluşturulacak olan yeni çözünürlük
		// değerleri halen yüksek ise inSampleSize değeri 2 ile çarpılır.
		// Bunun sebebi; Bitmap oluşturucusunun, yeni oluşturulacağı Bitmap
		// için orijinal Bitmap çözünürlüğünü, inSampleSize değerinin
		// yarısına bölüyor olmasıdır.
		if (reqWidth < (estimatedWidth + (estimatedWidth / 2)) && reqHeight < (estimatedHeight + (estimatedHeight / 2)))
			inSampleSize *= 2;
	}
	return inSampleSize;
}

inSampleSize değerinin nasıl elde edileceğini öğrendiğimize göre artık bu değerin nerede nasıl kullanılacağını öğrenmenin vakti geldi. Seçtiğimiz bir resmi arayüzdeki herhangi bir ImageView denetimine atmadan önce seçilen resmi, elde ettimiz inSampleSize değeri ile SubSampling işlemine tabi tutmamız gerekiyor. Bunun için aşağıdaki işlevi kullanabilirsiniz.

/**
 * Alınan kaynak ile istenilen çözünürlüğe uygun olarak yeni bir Bitmap
 * nesnesi oluşturur.
 * 
 * @param _Resources
 *            İçerisinde Image verisi barındıran kaynaklar nesnesi.
 * @param resID
 *            SubSample'ı alınacak olan kaynağın ID'si.
 * @param reqWidth
 *            SubSampling işlemi için gerekli olan genişlik değeri.
 * @param reqHeight
 *            SubSampling işlemi için gerekli olan yükseklik değeri.
 * @return SubSampling işlemine tabi tutulmuş olan yeni Bitmap nesnesi.
 */
static Bitmap decodeSampledBitmapFromResource(Resources _Resources, int resID, int reqWidth, int reqHeight) {
	final BitmapFactory.Options _Options = new BitmapFactory.Options();
	// Sadece kaynağa ait temel bilgilerin alınması sağlanıyor...
	_Options.inJustDecodeBounds = true;
	// inJustDecodeBounds değeri true olarak ayarlandığı için aşağıdaki
	// Bitmap için kaynak çözücüsü sadece verilen kaynağın bilgilerini
	// oluşturur. Geriye Bitmap döndermez.
	BitmapFactory.decodeResource(_Resources, resID, _Options);
	// inSampleSize değeri hesaplanıyor...
	_Options.inSampleSize = calculateInSampleSize(_Options, reqWidth, reqHeight);
	// inSampleSize değeri artık bulunduğu için çözülecek olan kaynaktan
	// Bitmap nesnesi oluşturulması da sağlanıyor...
	_Options.inJustDecodeBounds = false;
	// Hesaplanmış yeni inSampleSize değeri ile verilen kaynaktan Bitmap
	// nesnesi oluşturuluyor...
	return BitmapFactory.decodeResource(_Resources, resID, _Options);
}

Artık istenilen çözünürlüğe getirilmiş olan yeni Bitmap nesnesini de elde ettiğimize göre bu Bitmap nesnesini arayüzümüzdeki herhangi bir ImageView denetimine yükleme vakti geldi. Aşağıdaki kodu kullanarak bu işlemi de gerçekleştirebilirsiniz.

ImageView _ImageView = (ImageView) findViewById(R.id.ivImage);
_ImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.drawable.img_large, 800, 480));

Böylelikle çok yüksek çözünürlüklü bir resmi 800×480 çözünürlüğünde olan bir cihaz için uygun hale getirmiş olduk.


Geldik bir yazının daha sonuna! Bu yazı ile ilgili olarak herhangi bir bildirimde bulunmak için konu altına yorum yazabilir ya da [email protected] adresine e-posta gönderebilirsiniz.

Kaynaklar:

  • https://developer.android.com/topic/performance/graphics/load-bitmap

“Bitmap Nesnelerini Etkili Bir Şekilde Görüntüleme – Android” için 4 yanıt

Hocam çok teşekkürler. Yalnız şunu sormak istiyorum. Diyelim ki 1000×1000 resmimizi bu kod ile 250×250’ye düşürdük.. Kendimiz photoshoptan size değerlerini 250×250 yaparsak bellek açısından bir farkı olur mu?

Olmaz. Ancak buradaki amaç, elimizdeki yüksek çözünürlüklü bir resmi istediğimiz ölçülerde göstermek. Çünkü yüksek çözünürlüklü bir resmi, düşük çözünürlüklü bir ekranda, yüksek çözünürlüklü olarak göstermenin bir anlamı olmaz. Ancak o aynı yüksek çözünürlüklü resmi, yüksek çözünürlüklü bir ekranda yüksek çözünürlüklü olarak da gösterebiliriz.

Bir cevap yazın