当需要创建6张图像(每个图像由15-16个PNG顺序覆盖)而又没有在途中收到OutOfMemoryException时,如何在运行时优化图像处理?

在开发宠物应用程序时,我遇到了图像处理问题。 我无法为Google提供优质的Google Cases,因此我不得不靠耙子走路,自己发明自行车。
同样在开发过程中,从Java迁移到Kotlin,因此代码将在某个时候进行翻译。
挑战赛
在体育馆训练的申请。 必须基于应用程序运行时中的训练结果来构建肌肉工作图。
有两个性别:M和G。考虑选项M,因为对于M来说,所有事物都是相同的。
应同时制作6张图像:3个时段(每周一次,每月一次,一次培训)x 2次观看(正面,背面)

每个这样的图像包括15个肌肉组图像(正视图)和14个后视图像。 加上1张粉底图像(头部,手和脚)。 总体上,要收集前视图,您需要从背面叠加16张图像-15。
两侧只有23个肌肉群(对于15 + 14!= 23的肌肉群,这是一个很小的解释-两侧的某些肌肉都是“可见的”)。
一阶近似算法:
- 根据完成的锻炼的数据,构建HashMap <String,Float>,String是肌肉组的名称,Float是从0到10的负重程度。
- 将23块肌肉中的每一个都重新涂上颜色(从0(不涉及)到10(最大负载))。
- 在两个图像(正面,背面)上覆盖重新绘制的肌肉图像。
- 我们保存所有6张图像。

要以24位模式存储31(16 + 15)个大小为1500x1500 px的图像,则需要31x1500x1500x24bit = 199 MB的RAM。 当您超过〜30-40 MB时,您将收到OutOfMemoryException。 相应地,您不能同时从资源加载所有图像,因为您需要释放资源以不接收接收信息。 这意味着您需要顺序覆盖图像。 该算法转换为以下内容:
根据完成的锻炼中的数据,构建HashMap <String,Float>,String-肌肉,Float-负载级别(从0到10)。
6个图像中每个图像的循环:
- 获得了BitmapFactory.decodeResource()资源。
- 将23块肌肉中的每一个都重新涂上颜色(从0(不涉及)到10(最大负载))。
- 在一张画布上覆盖重新绘制的肌肉图像。
- 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即可返回两个Bitmap变量:前视图和后视图。 展望未来,Kotlin中的Double <Bitmap,Bitmap>将替换DoubleMusclesBitmaps Java类。
画图
在值资源中为colors.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());
单层肌肉覆盖
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毫秒。
缺点:
- 我不时收到OutOfMemoryException。
- 处理过程需要9秒钟以上,并且在模拟器上(!)。在“ average”(旧矿井)电话中,它达到了20秒。
- AsyncTask与所有导致的[内存]泄漏。
优点:
以概率(1-OutOfMemoryException)绘制图像。
IntentService中的AsyncTask
为了离开AsyncTask,决定切换到IntentServie,在其中执行创建图像的任务。 服务完成后,如果BroadcastReceiver正在运行,我们将获取所有生成的六个图像的Uri,否则将仅保存图像,以便下次用户打开应用程序时,无需等待创建过程。 同时,操作时间没有改变,但是减去了一个负号(算出内存泄漏),还有两个负号。
当然,强迫用户期望在如此长的时间内创建图像是不可能的。 需要优化。
概述优化方式:
- 图像处理。
- 添加LruCache。
影像处理
所有源PNG资源的大小均为1500x1500像素。 将其降低为1080x1080。
如您在第二张照片中所见,所有源代码都是正方形的,肌肉就位了,真正有用的像素占据了很小的面积。 所有肌肉组都已经就位的事实对于程序员来说很方便,但是对于表现却不合理。 我们裁剪(剪切)所有源代码中的多余部分,记录每个肌肉组的位置(x,y),以便随后将其应用于正确的位置。
在第一种方法中,将所有29组肌肉图像重新粉刷并叠加在底座上。 底座仅包括头部,手和腿部。 我们改变了基础:现在,除了头部,手臂和腿部之外,它还包括所有其他肌肉组。 我们将所有内容都涂成灰色color_muscle0。 这将允许不重新粉刷和不施加不参与的那些肌肉群。
现在所有来源看起来像这样:

Lrucache
在对原始图像进行了额外的处理之后,一些图像开始占用一些内存,这导致了使用LruCache进行重用(在每次使用.recycle()方法覆盖后不释放它们)的想法。 我们创建一个用于存储源图像的类,该类同时承担读取资源的功能:
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的平稳过渡,但是确实发生了。
协程
使用IntentService的代码可以工作,但是带有回调的代码的可读性不能称为愉悦。
渴望了解工作中的Kotlin协程。 我们增加了一个理解,即几个月后,与您寻找返回生成的图像的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,job和scope捆绑是scout coroutine和错误处理程序。
uries-HashMap,HashMap本身存储6张图像,以便随后输出到UI:
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?>() ...
我们测量处理时间。
>start 1549400719844 >finish 1549400720440 diff=596 ms
从9313毫秒减少到596毫秒。
如果您有其他优化的想法,请随时发表评论。