PDA

Orijinalini görmek için tıklayınız : Bitmap Nesnelerini Etkili Bir Şekilde Görüntüleme (Bölüm I) - Android


ULAGA
26. November 2014, 08:27 PM
Bölüm I – Yüksek Çözünürlüklü Bitmap Nesnelerini Görüntüleme

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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) kaynağının çözünürlüğü düşürüldüğünde bellekte daha az yer kaplar. Bitmap ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) kaynaklarının bellek kullanımını bir örnekle daha anlaşılabilir bir hale getirebiliriz.

Örneğin elimizde 2 adet Bitmap ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) kaynağı olsun. Biri 640×480 (VGA) çözünürlüğünde diğeri de 5 megapiksel çözünürlüğünde olsun. Android Bitmap ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) kaynaklarına Subsampling işlemini uygulamadan önce BitmapFactory ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) nesnesinden bahsetmek istiyorum. Bu nesne birçok şekilde Bitmap ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) sınıfının alt sınıfı olan BitmapFactory.Options ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) sınıfını kullanacağız. BitmapFactory.Options ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) sınıfının inSampleSize ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) oluştururken BitmapFactory.Options ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) sınıfının inSampleSize ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) ö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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) bilgi ve seçeneklerini barındıran BitmapFactory.Options ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) nesnesini alır. Bu parametre Bitmap ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) değeri hesaplanacaktır. Ancak yukarıdaki işlevi kendi uygulamalarımda denediğimde şöyle bir şey farkettim. Ortaya çıkan inSampleSize ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) değeri 2’nin bir sonraki kuvveti olması gerekiyor. Zira Bitmap ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) seçeneklerinde inSampleSize ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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 ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) nesnesini de elde ettiğimize göre bu Bitmap ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) nesnesini arayüzümüzdeki herhangi bir ImageView ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!]) 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(decodeSampledBitmapFromR esource(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.

Kaynaklar :
Loading Large Bitmaps Efficiently | Android Developers ([Yanlızca Üyeler Görebilir.Üye Olmanız Gerekli !!!])