تحميل الصور النقطية الكبيرة بكفاءة

ملاحظة: هناك العديد من المكتبات التي تتّبع أفضل الممارسات لتحميل الصور. يمكنك استخدام هذه المكتبات في تطبيقك لتحميل الصور بأكثر الطرق تحسينًا. ونقترح استخدام مكتبة Glide التي تُحمّل الصور وتعرضها في أسرع وقت ممكن. وتشمل مكتبات تحميل الصور الشائعة الأخرى Picasso من Square وCoil من Instacart وFresco من Facebook. تبسّط هذه المكتبات معظم المهام المعقدة المرتبطة بالصور النقطية وأنواع أخرى من الصور في Android.

تظهر الصور بجميع الأشكال والأحجام. وفي كثير من الحالات، تكون هذه العناوين أكبر مما هو مطلوب في واجهة المستخدم النموذجية (UI). على سبيل المثال، يعرض تطبيق "معرض الصور" الخاص بالنظام صورًا تم التقاطها باستخدام كاميرا أجهزة Android الخاصة بك والتي تكون عادةً دقة أعلى بكثير من كثافة شاشة جهازك.

بما أنك تعمل بذاكرة محدودة، تحتاج، بصورة مثالية، إلى تحميل نسخة ذات دقة أقل في الذاكرة. يجب أن يتطابق الإصدار المنخفض الدقة مع حجم مكون واجهة المستخدم الذي يعرضه. لا تقدم الصورة ذات الدقة الأعلى أي فائدة مرئية، ولكنها تظل تستهلك ذاكرة ثمينة وتتحمل أعباء إضافية في الأداء بسبب التوسع السريع.

يرشدك هذا الدرس إلى طريقة فك ترميز الصور النقطية الكبيرة بدون تجاوز الحد الأقصى للذاكرة لكل تطبيق من خلال تحميل نسخة أصغر مستندة إلى عيّنات فرعية في الذاكرة.

قراءة أبعاد الصورة النقطية ونوعها

توفّر الفئة BitmapFactory طُرقًا متعدّدة لفك الترميز (decodeByteArray() وdecodeFile() وdecodeResource() وما إلى ذلك) لإنشاء Bitmap من مصادر مختلفة. اختَر أنسب طريقة لفك الترميز استنادًا إلى مصدر بيانات صورتك. وتحاول هذه الطرق تخصيص ذاكرة للصورة النقطية التي تم إنشاؤها، وبالتالي يمكن أن تؤدي بسهولة إلى استثناء OutOfMemory. يتضمّن كل نوع من طرق فك الترميز توقيعات إضافية تتيح لك تحديد خيارات فك الترميز من خلال الفئة BitmapFactory.Options. يؤدي ضبط السمة inJustDecodeBounds على true مع فك ترميز الترميز إلى تجنُّب تخصيص الذاكرة، حيث يتم عرض null لكائن الصورة النقطية مع ضبط outWidth وoutHeight وoutMimeType. يتيح لك هذا الأسلوب قراءة أبعاد بيانات الصورة ونوعها قبل إنشاء (وتخصيص الذاكرة) للصورة النقطية.

Kotlin

val options = BitmapFactory.Options().apply {
    inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, R.id.myimage, options)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
val imageType: String = options.outMimeType

Java

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

لتجنّب استثناءات java.lang.OutOfMemory، يجب التحقّق من أبعاد الصورة النقطية قبل فك ترميزها، إلا إذا كنت تثق في المصدر كي يوفّر لك بيانات صور بحجم يمكن توقّعه وتتناسب بشكل مريح مع الذاكرة المتاحة.

تحميل نسخة مصغّرة إلى الذاكرة

الآن بعد أن أصبحت أبعاد الصورة معروفة، يمكن استخدامها لتحديد ما إذا كان ينبغي تحميل الصورة الكاملة في الذاكرة أو إذا كان ينبغي تحميل نسخة مستندة إلى عيّنات فرعية بدلاً من ذلك. فيما يلي بعض العوامل التي يجب مراعاتها:

  • الاستخدام المقدَّر للذاكرة لتحميل الصورة الكاملة في الذاكرة
  • حجم الذاكرة التي تريد الالتزام بها لتحميل هذه الصورة مع مراعاة أي متطلبات أخرى للذاكرة لتطبيقك.
  • يشير إلى أبعاد العنصر ImageView المستهدف أو مكوِّن واجهة المستخدم الذي سيتم تحميل الصورة إليه.
  • حجم شاشة الجهاز الحالي وكثافته

لا يستحق مثلاً تحميل صورة بحجم 1024x768 بكسل في الذاكرة إذا تم عرضها في النهاية ضمن صورة مصغّرة بحجم 128x96 بكسل في ImageView.

لتوجيه برنامج فك الترميز إلى إنشاء عيّنة فرعية من الصورة، وتحميل نسخة أصغر في الذاكرة، اضبط inSampleSize على true في عنصر BitmapFactory.Options. على سبيل المثال، إنّ الصورة ذات الدقة 2048×1536 والتي تم فك ترميزها باستخدام inSampleSize من 4 تكون صورة نقطية بحجم 512×384 تقريبًا. يستخدم تحميل هذا في الذاكرة 0.75 ميغابايت بدلاً من 12 ميغابايت للصورة الكاملة (بافتراض ضبط الصورة النقطية لـ ARGB_8888). وفي ما يلي طريقة لاحتساب قيمة حجم العينة التي تساوي اثنين استنادًا إلى العرض والارتفاع المستهدفَين:

Kotlin

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    // Raw height and width of image
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1

    if (height > reqHeight || width > reqWidth) {

        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }

    return inSampleSize
}

Java

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    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;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) >= reqHeight
                && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

ملاحظة: يتم احتساب قوة تساوي قيمة اثنتين لأنّ برنامج فك الترميز يستخدم قيمة نهائية من خلال التقريب إلى أقرب قيمة لاثنتين، وفقًا لمستندات inSampleSize.

لاستخدام هذه الطريقة، يجب أولاً فك الترميز مع ضبط inJustDecodeBounds على true، وتمرير الخيارات ثم فك الترميز مرة أخرى باستخدام قيمة inSampleSize الجديدة وضبط inJustDecodeBounds على false:

Kotlin

fun decodeSampledBitmapFromResource(
        res: Resources,
        resId: Int,
        reqWidth: Int,
        reqHeight: Int
): Bitmap {
    // First decode with inJustDecodeBounds=true to check dimensions
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeResource(res, resId, this)

        // Calculate inSampleSize
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)

        // Decode bitmap with inSampleSize set
        inJustDecodeBounds = false

        BitmapFactory.decodeResource(res, resId, this)
    }
}

Java

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

تسهِّل هذه الطريقة تحميل صورة نقطية ذات حجم كبير عشوائيًا في ImageView تعرض صورة مصغّرة بحجم 100x100 بكسل، كما هو موضّح في المثال التالي:

Kotlin

imageView.setImageBitmap(
        decodeSampledBitmapFromResource(resources, R.id.myimage, 100, 100)
)

Java

imageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

يمكنك اتّباع عملية مشابهة لفك ترميز الصور النقطية من مصادر أخرى من خلال استبدال طريقة BitmapFactory.decode* المناسبة حسب الحاجة.