Bagaimana saya mempercepat pemrosesan gambar di Android 15 kali

Bagaimana cara mengoptimalkan pemrosesan gambar dalam runtime ketika diperlukan untuk membuat 6 gambar, yang masing-masing terdiri dari 15-16 PNG yang secara berurutan dilapis, tanpa menerima OutOfMemoryException dalam perjalanan?


gambar


Saat mengembangkan aplikasi hewan peliharaan saya, saya menemui masalah pemrosesan gambar. Saya tidak bisa memberikan Googlecases yang baik untuk Google, jadi saya harus berjalan menyapu dan menciptakan sepeda sendiri.
Juga selama pengembangan, ada migrasi dari Jawa ke Kotlin, jadi kode akan diterjemahkan di beberapa titik.


Tantangan


Aplikasi untuk pelatihan di gym. Penting untuk membuat peta kerja otot berdasarkan hasil pelatihan dalam runtime aplikasi.
Dua jenis kelamin: M dan G. Pertimbangkan pilihan M, karena untuk M semuanya sama.
6 gambar harus dibangun secara bersamaan: 3 periode (satu sesi pelatihan, per minggu, per bulan) x 2 tampilan (depan, belakang)


gambar


Setiap gambar tersebut terdiri dari 15 gambar kelompok otot untuk tampilan depan dan 14 untuk tampilan belakang. Ditambah 1 gambar foundation (kepala, tangan dan kaki). Secara total, untuk mengumpulkan tampilan depan, 16 gambar harus ditumpangkan, 15 di belakang.


Hanya 23 kelompok otot untuk kedua sisi (untuk mereka dengan 15 + 14! = 23, sedikit penjelasan - beberapa otot “terlihat” di kedua sisi).


Algoritma dalam pendekatan pertama:


  1. Berdasarkan data dari latihan yang diselesaikan, HashMap <String, Float> dibangun, String adalah nama kelompok otot, Float adalah tingkat beban dari 0 hingga 10.
  2. Masing-masing dari 23 otot dicat ulang dalam warna dari 0 (tidak terlibat) hingga 10 (Maks. Load).
  3. Hamparkan gambar otot yang dicat ulang dalam dua gambar (depan, belakang).
  4. Kami menyimpan semua 6 gambar.

gambar


Untuk menyimpan 31 (16 + 15) gambar berukuran 1500x1500 px dengan mode 24-bit, diperlukan 31x1500x1500x24bit = 199 MB RAM. Ketika Anda melebihi ~ 30-40 MB, Anda mendapatkan OutOfMemoryException. Sejalan dengan itu, Anda tidak dapat memuat semua gambar dari sumber pada saat yang bersamaan, karena Anda perlu membebaskan sumber daya untuk tidak menerima penerimaan. Ini berarti Anda perlu overlay gambar secara berurutan. Algoritma berubah menjadi sebagai berikut:


Berdasarkan data dari latihan yang diselesaikan, HashMap <String, Float>, String - muscle, Float - load level mulai 0 hingga 10 dibangun.


Siklus untuk masing-masing dari 6 gambar:


  1. Dapatkan sumber daya BitmapFactory.decodeResource ().
  2. Masing-masing dari 23 otot dicat ulang dalam warna dari 0 (tidak terlibat) hingga 10 (maks. Beban)
  3. Hamparkan gambar otot yang dicat ulang pada satu Kanvas.
  4. Bitmap.recycle () membebaskan sumber daya.

Kami melakukan tugas dalam utas terpisah menggunakan AsyncTask. Di setiap Tugas, dua gambar dibuat secara berurutan: tampilan depan dan tampilan belakang.


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; } 

Kelas tambahan DoubleMusclesBitmaps diperlukan hanya untuk mengembalikan dua variabel Bitmap: tampilan depan dan tampilan belakang. Ke depan, kelas Java DoubleMusclesBitmaps digantikan oleh Pair <Bitmap, Bitmap> di Kotlin.


Menggambar


Warna colors.xml dalam sumber daya nilai.


 <?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> 

Buat satu tampilan


 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; } 

Hamparan otot tunggal


 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();//  } 

Kami memulai pembuatan 3 pasang gambar.


 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(); } 

Kami mulai startImageGenerating ():


 > start 1549350950177 > finish 1549350959490 diff=9313 ms 

Perlu dicatat bahwa sumber daya membaca membutuhkan banyak waktu. Untuk setiap pasangan gambar, 29 file PNG dari sumber daya diterjemahkan. Dalam kasus saya, dari total biaya pembuatan gambar, fungsi BitmapFactory.decodeResource () menghabiskan ~ 75% dari waktu: ~ 6960 ms.


Cons:


  1. Saya mendapatkan OutOfMemoryException dari waktu ke waktu.
  2. Pemrosesan membutuhkan waktu lebih dari 9 detik, dan ini ada pada emulator (!) Di telepon "rata-rata" (tambang lama) mencapai 20 detik.
  3. AsyncTask dengan semua kebocoran [memori] yang dihasilkan.

Pro:
Dengan probabilitas (1-OutOfMemoryException) gambar diambil.


AsyncTask di IntentService


Untuk meninggalkan AsyncTask, diputuskan untuk beralih ke IntentServiCe, di mana tugas membuat gambar dilakukan. Setelah layanan selesai, jika BroadcastReceiver berjalan, kami mendapatkan Uri dari semua enam gambar yang dihasilkan, jika tidak gambar hanya disimpan sehingga saat berikutnya pengguna membuka aplikasi, tidak perlu menunggu proses pembuatan. Pada saat yang sama, waktu operasi tidak berubah, tetapi dengan satu kekurangan - kebocoran memori yang ditemukan, ada dua minus lagi.


Memaksa pengguna untuk mengharapkan kreasi gambar untuk jumlah waktu seperti itu, tentu saja, tidak mungkin. Perlu dioptimalkan.


Cara pengoptimalan garis besar:


  1. Pemrosesan gambar.
  2. Menambahkan LruCache.

Pemrosesan gambar


Semua sumber PNG sumber berukuran 1500x1500 px. Kurangi hingga 1080x1080.
Seperti yang dapat Anda lihat di foto kedua, semua kode sumber berbentuk bujur sangkar, otot-otot berada di tempatnya, dan piksel-piksel berguna yang sebenarnya menempati area kecil. Fakta bahwa semua kelompok otot sudah ada di tempat nyaman untuk programmer, tetapi tidak rasional untuk kinerja. Kami memotong (memotong) kelebihan di semua kode sumber, merekam posisi (x, y) dari masing-masing kelompok otot untuk selanjutnya menerapkannya ke tempat yang tepat.


Dalam pendekatan pertama, semua 29 gambar kelompok otot dicat ulang dan ditumpangkan di pangkalan. Pangkalan hanya mencakup kepala, tangan, dan bagian kaki. Kami mengubah dasar: sekarang ini termasuk, selain kepala, lengan dan kaki, semua kelompok otot lainnya. Kami melukis semuanya dengan color_muscle0 abu-abu. Ini akan memungkinkan tidak mengecat ulang dan tidak memaksakan kelompok-kelompok otot yang tidak terlibat.


Sekarang semua sumber terlihat seperti ini:


gambar


Lrucache


Setelah pemrosesan tambahan dari gambar asli, beberapa mulai mengambil sedikit memori, yang mengarah pada pemikiran untuk menggunakan kembali (tidak membebaskan mereka setelah setiap overlay dengan metode .recycle ()) menggunakan LruCache. Kami membuat kelas untuk menyimpan gambar sumber, yang secara bersamaan mengasumsikan fungsi membaca dari sumber daya:


 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 } } 

Gambar disiapkan, decoding sumber daya dioptimalkan.
Kami tidak akan membahas kelancaran transisi dari Jawa ke Kotlin, tetapi itu memang terjadi.


Coroutine


Kode menggunakan IntentService berfungsi, tetapi keterbacaan kode dengan panggilan balik tidak bisa disebut menyenangkan.


Tambahkan keinginan untuk melihat coroutine Kotlin dalam pekerjaan. Kami menambahkan pemahaman bahwa setelah beberapa bulan akan lebih menyenangkan untuk membaca kode sinkron Anda daripada mencari tempat untuk mengembalikan file Uri dari gambar yang dihasilkan.


Juga, percepatan pemrosesan gambar mendorong gagasan untuk menggunakan fitur di beberapa tempat baru dalam aplikasi, khususnya dalam deskripsi latihan, dan tidak hanya setelah pelatihan.


 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() } ... } 

Bundel standar errorHandler, pekerjaan, dan ruang lingkup adalah scout coroutine dengan penangan kesalahan jika coroutine rusak.


uries - HashMap, yang menyimpan 6 gambar dalam dirinya sendiri untuk output berikutnya ke UI:
uries ["last_back"] = Uri?
uries ["last_front"] = Uri?
uries ["week_back"] = Uri?
uries ["week_front"] = Uri?
uries ["month_back"] = Uri?
uries ["month_front"] = Uri?


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

Kami mengukur waktu pemrosesan.


 >start 1549400719844 >finish 1549400720440 diff=596 ms 

Dari 9313 ms, pemrosesan menurun menjadi 596 ms.


Jika Anda memiliki ide untuk optimasi tambahan - jangan ragu untuk berkomentar.

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


All Articles