كيف تسارعت معالجة الصور على أندرويد 15 مرة

كيفية تحسين معالجة الصور في وقت التشغيل عندما يكون من الضروري إنشاء 6 صور ، كل منها يتكون من 15 إلى 16 ملف بتنسيق PNG متراكبة بشكل تسلسلي ، دون تلقي OutOfMemoryException في الطريق؟


الصورة


عند تطوير تطبيقي للحيوانات الأليفة ، واجهت مشكلة معالجة الصور. لم أستطع توفير Googlecases جيدًا لـ Google ، لذلك اضطررت إلى السير على أشعل النار وأبتكر دراجة بلدي.
أثناء التطوير أيضًا ، كانت هناك عملية انتقال من Java إلى Kotlin ، لذلك سيتم ترجمة الشفرة في مرحلة ما.


التحدي


تطبيق للتدريب في صالة الألعاب الرياضية. من الضروري بناء خريطة للعمل العضلي بناءً على نتائج التدريب في وقت تشغيل التطبيق.
اثنين من الجنسين: M و G. النظر في الخيار M ، لأنه بالنسبة لكل شيء M هو نفسه.
ينبغي بناء 6 صور في وقت واحد: 3 فترات (جلسة تدريب واحدة ، أسبوعيًا ، شهريًا) × 2 مشاهدة (أمامية ، خلفية)


الصورة


تتكون كل صورة من 15 صورة لمجموعات العضلات للحصول على منظر أمامي و 14 صورة للرؤية الخلفية. بالإضافة إلى 1 صورة للمؤسسة (الرأس واليدين والقدمين). إجمالاً ، لجمع المنظر الأمامي ، تحتاج إلى تركيب 16 صورة ، من الخلف - 15.


فقط 23 مجموعة عضلية لكلا الجانبين (لمن لديهم 15 + 14! = 23 ، تفسير صغير - بعض العضلات "مرئية" على كلا الجانبين).


الخوارزمية في التقريب الأول:


  1. بناءً على البيانات من التدريبات المكتملة ، تم بناء HashMap <String ، Float> ، String هو اسم مجموعة العضلات ، Float هي درجة التحميل من 0 إلى 10.
  2. يتم إعادة طلاء كل من العضلات الـ 23 بالألوان من 0 (غير متورطة) إلى 10 (كحد أقصى).
  3. تراكب صور العضلات طلاؤها في صورتين (الأمامي والخلفي).
  4. نحن حفظ جميع الصور 6.

الصورة


لتخزين 31 (16 + 15) صورة بحجم 1500 × 1500 بكسل مع وضع 24 بت ، 31x1500x1500x24bit = مطلوب 199 ميغابايت من ذاكرة الوصول العشوائي. عندما تتجاوز ~ 30-40 ميغابايت ، تحصل على OutOfMemoryException. في المقابل ، لا يمكنك تحميل جميع الصور من الموارد في نفس الوقت ، لأنك تحتاج إلى تحرير الموارد حتى لا تتلقى الاستقبال. هذا يعني أنك تحتاج إلى تراكب الصور بالتتابع. الخوارزمية تتحول إلى ما يلي:


بناءً على البيانات من التدريبات المكتملة ، تم بناء HashMap <String ، Float> ، String - muscle ، Float - مستوى التحميل من 0 إلى 10.


دورة لكل من الصور 6:


  1. حصلت على المورد BitmapFactory.decodeResource ().
  2. يتم إعادة طلاء كل من العضلات الـ 23 بالألوان من 0 (غير متورطة) إلى 10 (كحد أقصى).
  3. تراكب صور العضلات طلاؤها على قماش واحد.
  4. تحرير Bitmap.recycle () مورد.

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


private class BitmapMusclesTask extends AsyncTask<Void, Void, DoubleMusclesBitmaps> { private final WeakReference<HashMap<String, Float>> musclesMap; BitmapMusclesTask(HashMap<String, Float> musclesMap) { this.musclesMap = new WeakReference<>(musclesMap); } @Override protected DoubleMusclesBitmaps doInBackground(Void... voids) { DoubleMusclesBitmaps bitmaps = new DoubleMusclesBitmaps(); bitmaps.bitmapBack = createBitmapMuscles(musclesMap.get(), false); bitmaps.bitmapFront = createBitmapMuscles(musclesMap.get(), true); return bitmaps; } @Override protected void onPostExecute(DoubleMusclesBitmaps bitmaps) { super.onPostExecute(bitmaps); Uri uriBack = saveBitmap(bitmaps.bitmapBack); Uri uriFront = saveBitmap(bitmaps.bitmapFront); bitmaps.bitmapBack.recycle(); bitmaps.bitmapFront.recycle(); if (listener != null) listener.onUpdate(uriFront, uriBack); } } public class DoubleMusclesBitmaps { public Bitmap bitmapFront; public Bitmap bitmapBack; } 

فئة DoubleMusclesBitmaps الإضافية مطلوبة فقط لإرجاع متغيرين للصور النقطية: الرؤية الأمامية والرؤية الخلفية. التطلع إلى الأمام ، يتم استبدال فئة Java DoubleMusclesBitmaps بـ Pair <Bitmap ، Bitmap> في Kotlin.


رسم


الألوان colours.xml في موارد القيم.


 <?xml version="1.0" encoding="utf-8"?> <resources> <color name="muscles_color0">#BBBBBB</color> <color name="muscles_color1">#ffb5cf</color> <color name="muscles_color2">#fda9c6</color> <color name="muscles_color3">#fa9cbe</color> <color name="muscles_color4">#f890b5</color> <color name="muscles_color5">#f583ac</color> <color name="muscles_color6">#f377a4</color> <color name="muscles_color7">#f06a9b</color> <color name="muscles_color8">#ee5e92</color> <color name="muscles_color9">#eb518a</color> <color name="muscles_color10">#e94581</color> </resources> 

إنشاء عرض واحد


 public Bitmap createBitmapMuscles(HashMap<String, Float> musclesMap, Boolean isFront) { Bitmap musclesBitmap = Bitmap.createBitmap(1500, 1500, Bitmap.Config.ARGB_8888); Canvas resultCanvas = new Canvas(musclesBitmap); for (HashMap.Entry entry : musclesMap.entrySet()) { int color = Math.round((float) entry.getValue()); //         color = context.getResources().getColor(context.getResources() .getIdentifier("muscles_color" + color, "color", context.getPackageName())); drawMuscleElement(resultCanvas, entry.getKey(), color); } return musclesBitmap; } 

تراكب العضلات واحدة


 private void drawMuscleElement(Canvas resultCanvas, String drawableName, @ColorInt int color) { PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN; Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); Bitmap bitmapDst = BitmapFactory.decodeResource(context.getResources(), context.getResources().getIdentifier(drawableName, "drawable", context.getPackageName())); bitmapDst = Bitmap.createScaledBitmap(bitmapDst, 1500, 1500, true); paint.setColorFilter(new PorterDuffColorFilter(color, mode)); resultCanvas.drawBitmap(bitmapDst, 0, 0, paint); bitmapDst.recycle();//  } 

نبدأ في توليد 3 أزواج من الصور.


 private BitmapMusclesTask taskLast; private BitmapMusclesTask taskWeek; private BitmapMusclesTask taskMonth; private void startImageGenerating(){ taskLast = new BitmapMusclesTask(mapLast); taskLast.execute(); taskWeek = new BitmapMusclesTask(mapWeek); taskWeek.execute(); taskMonth = new BitmapMusclesTask(mapMonth); taskMonth.execute(); } 

نبدأ startImageGenerating ():


 > start 1549350950177 > finish 1549350959490 diff=9313 ms 

تجدر الإشارة إلى أن موارد القراءة تستغرق الكثير من الوقت. لكل زوج من الصور ، يتم فك تشفير 29 ملفًا من ملفات PNG من الموارد. في حالتي ، من بين التكلفة الإجمالية لإنشاء الصور ، تنفق الدالة BitmapFactory.decodeResource () حوالي 75٪ من الوقت: 6960 مللي ثانية تقريبًا.


سلبيات:


  1. أحصل على OutOfMemoryException من وقت لآخر.
  2. تستغرق المعالجة أكثر من 9 ثوانٍ ، وهذا على المحاكي (!) في الهاتف "العادي" (منجم قديم) وصل إلى 20 ثانية.
  3. AsyncTask مع كل تسرب [الذاكرة] الناتجة.

الايجابيات:
مع احتمال (1-OutOfMemoryException) يتم رسم الصور.


AsyncTask في IntentService


لترك AsyncTask ، تقرر التبديل إلى IntentServie ، حيث تم تنفيذ مهمة إنشاء الصور. بعد اكتمال الخدمة ، إذا كانت خدمة BroadcastReceiver قيد التشغيل ، فسنحصل على Uri لجميع الصور الستة التي تم إنشاؤها ، وإلا فقد تم حفظ الصور ببساطة بحيث في المرة التالية التي يفتح فيها المستخدم التطبيق ، لن تكون هناك حاجة لانتظار عملية الإنشاء. في الوقت نفسه ، لم يتغير وقت التشغيل ، ولكن مع حدوث ناقص واحد - تم اكتشاف تسرب الذاكرة ، كان هناك اثنين من السلبيات الأخرى.


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


طرق تحسين المخطط التفصيلي:


  1. معالجة الصور.
  2. مضيفا LruCache.

معالجة الصور


جميع مصادر PNG المصدر هي 1500 × 1500 بكسل. اختزلهم إلى 1080 × 1080.
كما ترى في الصورة الثانية ، جميع المصادر مربعة ، والعضلات في مكانها ، وتشغل وحدات البكسل الحقيقية المفيدة مساحة صغيرة. حقيقة أن جميع مجموعات العضلات موجودة بالفعل هي مناسبة للمبرمج ، ولكنها ليست منطقية للأداء. نقطع (نقطع) الفائض في جميع أكواد المصدر ، نسجل الموضع (س ، ص) لكل مجموعة عضلية من أجل تطبيقها لاحقًا في المكان المناسب.


في النهج الأول ، تم طلاؤها جميع الصور 29 من مجموعات العضلات وتثبيتها على القاعدة. وشملت القاعدة فقط الرأس واليدين وأجزاء من الساقين. نغير الأساس: الآن يشمل ، بالإضافة إلى الرأس والذراعين والساقين ، جميع مجموعات العضلات الأخرى. نحن نرسم كل شيء باللون الرمادي color_muscle0. هذا سيسمح بعدم إعادة الطلاء وعدم فرض مجموعات العضلات التي لم تشارك.


الآن جميع المصادر تبدو مثل هذا:


الصورة


Lrucache


بعد معالجة إضافية للصور الأصلية ، بدأ البعض في شغل القليل من الذاكرة ، مما أدى إلى التفكير في إعادة (عدم تحريرها بعد كل تراكب باستخدام طريقة .recycle ()) باستخدام LruCache. ننشئ فئة لتخزين الصور المصدر ، والتي تفترض في وقت واحد وظيفة القراءة من الموارد:


 class LruCacheBitmap(val context: Context) { private val lruCache: LruCache<String, Bitmap> init { val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() val cacheSize = maxMemory / 4 lruCache = object : LruCache<String, Bitmap>(cacheSize) { override fun sizeOf(key: String, bitmap: Bitmap): Int { return bitmap.byteCount } } } fun getBitmap(drawableName: String): Bitmap? { return if (lruCache.get(drawableName) != null) lruCache.get(drawableName) else decodeMuscleFile(drawableName) } fun clearAll() { lruCache.evictAll() } private fun decodeMuscleFile(drawableName: String): Bitmap? { val bitmap = BitmapFactory.decodeResource(context.resources, context.resources.getIdentifier(drawableName, "drawable", context.packageName)) if (bitmap != null) { lruCache.put(drawableName, bitmap) } return bitmap } } 

يتم إعداد الصور ، يتم تحسين فك تشفير الموارد.
لن نناقش الانتقال السلس من Java إلى Kotlin ، لكن حدث ذلك.


Coroutines


تعمل التعليمة البرمجية التي تستخدم IntentService ، ولكن لا يمكن استدعاء قابلية قراءة التعليمات البرمجية مع عمليات الاسترجاعات.


أضف رغبة في إلقاء نظرة على أعمال كوتلين في العمل. نضيف فهمًا أنه بعد مرور بضعة أشهر سيكون من الجيد قراءة الكود المتزامن الخاص بك بدلاً من البحث عن مكان لإرجاع ملفات Uri للصور التي تم إنشاؤها.


أيضًا ، دفع تسارع معالجة الصور الفكرة إلى استخدام الميزة في العديد من الأماكن الجديدة في التطبيق ، لا سيما في وصف التمارين ، وليس فقط بعد التدريب.


 private val errorHandler = CoroutineExceptionHandler { _, e -> e.printStackTrace()} private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.Main + job + errorHandler) private var uries: HashMap<String, Uri?> = HashMap() fun startImageGenerating() = scope.launch { ... val imgMuscle = ImgMuscle() uries = withContext(Dispatchers.IO) { imgMuscle.createMuscleImages() } ... } 

الحزمة القياسية من errorHandler ، الوظيفة والنطاق هي coroutine الكشفية مع معالج الأخطاء إذا كسر coroutine.


uries - HashMap ، التي تخزن 6 صور في حد ذاتها لإخراجها لاحقًا إلى واجهة المستخدم:
uries ["last_back"] = أوري؟
uries ["last_front"] = أوري؟
uries ["week_back"] = أوري؟
uries ["week_front"] = أوري؟
uries ["month_back"] = أوري؟
uries ["month_front"] = أوري؟


 class ImgMuscle { val lruBitmap: LruCacheBitmap suspend fun createMuscleImages(): HashMap<String, Uri?> { return suspendCoroutine { continuation -> val resultUries = HashMap<String, Uri?>() ... //    continuation.resume(resultUries) } } } 

نقيس وقت المعالجة.


 >start 1549400719844 >finish 1549400720440 diff=596 ms 

من 9313 مللي ثانية ، انخفضت المعالجة إلى 596 مللي ثانية.


إذا كانت لديك أفكار لتحسين إضافي - فلا تتردد في التعليق.

Source: https://habr.com/ru/post/ar439596/


All Articles