-
2022-04-19 11:01:35
加载和使用缩小的位图(对于非常模糊的图像)
永远不要使用完整大小的位图。图像越大,需要模糊的越多,模糊半径也需要越高,通常,模糊半径越高,算法所需的时间就越长。
缩小位图的两种方式
1.位图options缩小
BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 8; Bitmap blurTemplate = BitmapFactory.decodeResource(getResources(), bitmap, options);
加载8 位图,因此只有原始图像的 1/64。
inSampleSize可以根据需要修改
,但需要满足条件 2^n (2,4,8,...) ,以避免因缩放而降低质量。2.位图Scale缩小
Bitmap bitmap = ...; float BITMAP_SCALE = 0.4f; int width = Math.round(bitmap .getWidth() * BITMAP_SCALE); int height = Math.round(bitmap .getHeight() * BITMAP_SCALE); // 将缩小后的图片做为预渲染的图片 Bitmap newBitmap = Bitmap.createScaledBitmap(bitmap , width, height, false);
使用渲染脚本模糊
Renderscript 提供
了一个高斯模糊滤镜ScriptIntrinsicBlur。它具有良好的视觉质量,使得位图在 Android上获得的更快速度。Google 声称“通常比多线程 C 实现快 2-3 倍,通常比 Java 实现快 10 倍以上”
。Renderscript 非常复杂(使用最快的处理设备(GPU、ISP 等)等),在理论上,通过测试和其他开发人员的报告,不可盲目地使用 Renderscript,因为硬件/驱动程序碎片似乎会导致某些设备出现问题。// 图片缩放比例(即模糊度) private static final float BITMAP_SCALE = 0.4f; /** * @param context 上下文对象 * @param srcBitmap 需要模糊的图片 * @return 模糊处理后的Bitmap */ public static Bitmap blurBitmap(Context context, Bitmap srcBitmap, float blurRadius) { // 计算图片缩小后的长宽 int width = Math.round(srcBitmap.getWidth() * BITMAP_SCALE); int height = Math.round(srcBitmap.getHeight() * BITMAP_SCALE); // 将缩小后的图片做为预渲染的图片 Bitmap inputBitmap = Bitmap.createScaledBitmap(srcBitmap, width, height, false); // 创建一张渲染后的输出图片 Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap); // 创建RenderScript内核对象 RenderScript rs = RenderScript.create(context); // 创建一个模糊效果的RenderScript的工具对象 ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); // 由于RenderScript并没有使用VM来分配内存,所以需要使用Allocation类来创建和分配内存空间 // 创建Allocation对象的时候其实内存是空的,需要使用copyTo()将数据填充进去 Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap); Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap); // 设置渲染的模糊程度, 25f是最大模糊度 blurScript.setRadius(blurRadius); // 设置blurScript对象的输入内存 blurScript.setInput(tmpIn); // 将输出数据保存到输出内存中 blurScript.forEach(tmpOut); // 将数据填充到Allocation中 tmpOut.copyTo(outputBitmap); rs.destroy(); return outputBitmap; }
android { ... defaultConfig { ... renderscriptTargetApi 19 renderscriptSupportModeEnabled true } }
尽可能重用位图(如果优先:性能 > 内存占用)如果需要多个模糊来进行实时模糊或类似操作,并且内存允许它不会多次从可绘制对象中加载位图,而是将其“缓存”在成员变量中。在这种情况下,请始终尝试使用相同的变量,以将垃圾收集降至最低。
使用RGB点模糊
java代码(比脚本模糊慢,但是开原程度高)
public static Bitmap fastBlur(Bitmap sentBitmap, float scale, int radius) { int width = Math.round(sentBitmap.getWidth() * scale); int height = Math.round(sentBitmap.getHeight() * scale); sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false); Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); if (radius < 1) { return null; } int w = bitmap.getWidth(); int h = bitmap.getHeight(); int[] pix = new int[w * h]; bitmap.getPixels(pix, 0, w, 0, 0, w, h); int wm = w - 1; int hm = h - 1; int wh = w * h; int div = radius + radius + 1; int[] r = new int[wh]; int[] g = new int[wh]; int[] b = new int[wh]; int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; int[] vmin = new int[Math.max(w, h)]; int divsum = (div + 1) >> 1; divsum *= divsum; int[] dv = new int[256 * divsum]; for (i = 0; i < 256 * divsum; i++) { dv[i] = (i / divsum); } yw = yi = 0; int[][] stack = new int[div][3]; int stackpointer; int stackstart; int[] sir; int rbs; int r1 = radius + 1; int routsum, goutsum, boutsum; int rinsum, ginsum, binsum; for (y = 0; y < h; y++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; for (i = -radius; i <= radius; i++) { p = pix[yi + Math.min(wm, Math.max(i, 0))]; sir = stack[i + radius]; sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff); rbs = r1 - Math.abs(i); rsum += sir[0] * rbs; gsum += sir[1] * rbs; bsum += sir[2] * rbs; if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; } } stackpointer = radius; for (x = 0; x < w; x++) { r[yi] = dv[rsum]; g[yi] = dv[gsum]; b[yi] = dv[bsum]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; sir = stack[stackstart % div]; routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]; if (y == 0) { vmin[x] = Math.min(x + radius + 1, wm); } p = pix[yw + vmin[x]]; sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff); rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; sir = stack[(stackpointer) % div]; routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]; yi++; } yw += w; } for (x = 0; x < w; x++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; yp = -radius * w; for (i = -radius; i <= radius; i++) { yi = Math.max(0, yp) + x; sir = stack[i + radius]; sir[0] = r[yi]; sir[1] = g[yi]; sir[2] = b[yi]; rbs = r1 - Math.abs(i); rsum += r[yi] * rbs; gsum += g[yi] * rbs; bsum += b[yi] * rbs; if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; } if (i < hm) { yp += w; } } yi = x; stackpointer = radius; for (y = 0; y < h; y++) { pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; sir = stack[stackstart % div]; routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]; if (x == 0) { vmin[y] = Math.min(y + r1, hm) * w; } p = x + vmin[y]; sir[0] = r[p]; sir[1] = g[p]; sir[2] = b[p]; rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; sir = stack[stackpointer]; routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]; yi += w; } } bitmap.setPixels(pix, 0, w, 0, 0, w, h); return bitmap; }
JNI代码(性能没有测试)
#include <jni.h> #include <string.h> #include <math.h> #include <stdio.h> #include <android/log.h> #include <android/bitmap.h> #define LOG_TAG "libbitmaputils" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) typedef struct { uint8_t red; uint8_t green; uint8_t blue; uint8_t alpha; } rgba; JNIEXPORT void JNICALL Java_com_insert_your_package_ClassName_functionToBlur(JNIEnv* env, jobject obj, jobject bitmapIn, jobject bitmapOut, jint radius) { LOGI("Blurring bitmap..."); // Properties AndroidBitmapInfo infoIn; void* pixelsIn; AndroidBitmapInfo infoOut; void* pixelsOut; int ret; // Get image info if ((ret = AndroidBitmap_getInfo(env, bitmapIn, &infoIn)) < 0 || (ret = AndroidBitmap_getInfo(env, bitmapOut, &infoOut)) < 0) { LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret); return; } // Check image if (infoIn.format != ANDROID_BITMAP_FORMAT_RGBA_8888 || infoOut.format != ANDROID_BITMAP_FORMAT_RGBA_8888) { LOGE("Bitmap format is not RGBA_8888!"); LOGE("==> %d %d", infoIn.format, infoOut.format); return; } // Lock all images if ((ret = AndroidBitmap_lockPixels(env, bitmapIn, &pixelsIn)) < 0 || (ret = AndroidBitmap_lockPixels(env, bitmapOut, &pixelsOut)) < 0) { LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret); } int h = infoIn.height; int w = infoIn.width; LOGI("Image size is: %i %i", w, h); rgba* input = (rgba*) pixelsIn; rgba* output = (rgba*) pixelsOut; int wm = w - 1; int hm = h - 1; int wh = w * h; int whMax = max(w, h); int div = radius + radius + 1; int r[wh]; int g[wh]; int b[wh]; int rsum, gsum, bsum, x, y, i, yp, yi, yw; rgba p; int vmin[whMax]; int divsum = (div + 1) >> 1; divsum *= divsum; int dv[256 * divsum]; for (i = 0; i < 256 * divsum; i++) { dv[i] = (i / divsum); } yw = yi = 0; int stack[div][3]; int stackpointer; int stackstart; int rbs; int ir; int ip; int r1 = radius + 1; int routsum, goutsum, boutsum; int rinsum, ginsum, binsum; for (y = 0; y < h; y++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; for (i = -radius; i <= radius; i++) { p = input[yi + min(wm, max(i, 0))]; ir = i + radius; // same as sir stack[ir][0] = p.red; stack[ir][1] = p.green; stack[ir][2] = p.blue; rbs = r1 - abs(i); rsum += stack[ir][0] * rbs; gsum += stack[ir][1] * rbs; bsum += stack[ir][2] * rbs; if (i > 0) { rinsum += stack[ir][0]; ginsum += stack[ir][1]; binsum += stack[ir][2]; } else { routsum += stack[ir][0]; goutsum += stack[ir][1]; boutsum += stack[ir][2]; } } stackpointer = radius; for (x = 0; x < w; x++) { r[yi] = dv[rsum]; g[yi] = dv[gsum]; b[yi] = dv[bsum]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; ir = stackstart % div; // same as sir routsum -= stack[ir][0]; goutsum -= stack[ir][1]; boutsum -= stack[ir][2]; if (y == 0) { vmin[x] = min(x + radius + 1, wm); } p = input[yw + vmin[x]]; stack[ir][0] = p.red; stack[ir][1] = p.green; stack[ir][2] = p.blue; rinsum += stack[ir][0]; ginsum += stack[ir][1]; binsum += stack[ir][2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; ir = (stackpointer) % div; // same as sir routsum += stack[ir][0]; goutsum += stack[ir][1]; boutsum += stack[ir][2]; rinsum -= stack[ir][0]; ginsum -= stack[ir][1]; binsum -= stack[ir][2]; yi++; } yw += w; } for (x = 0; x < w; x++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; yp = -radius * w; for (i = -radius; i <= radius; i++) { yi = max(0, yp) + x; ir = i + radius; // same as sir stack[ir][0] = r[yi]; stack[ir][1] = g[yi]; stack[ir][2] = b[yi]; rbs = r1 - abs(i); rsum += r[yi] * rbs; gsum += g[yi] * rbs; bsum += b[yi] * rbs; if (i > 0) { rinsum += stack[ir][0]; ginsum += stack[ir][1]; binsum += stack[ir][2]; } else { routsum += stack[ir][0]; goutsum += stack[ir][1]; boutsum += stack[ir][2]; } if (i < hm) { yp += w; } } yi = x; stackpointer = radius; for (y = 0; y < h; y++) { output[yi].red = dv[rsum]; output[yi].green = dv[gsum]; output[yi].blue = dv[bsum]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; ir = stackstart % div; // same as sir routsum -= stack[ir][0]; goutsum -= stack[ir][1]; boutsum -= stack[ir][2]; if (x == 0) vmin[y] = min(y + r1, hm) * w; ip = x + vmin[y]; stack[ir][0] = r[ip]; stack[ir][1] = g[ip]; stack[ir][2] = b[ip]; rinsum += stack[ir][0]; ginsum += stack[ir][1]; binsum += stack[ir][2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; ir = stackpointer; // same as sir routsum += stack[ir][0]; goutsum += stack[ir][1]; boutsum += stack[ir][2]; rinsum -= stack[ir][0]; ginsum -= stack[ir][1]; binsum -= stack[ir][2]; yi += w; } } // Unlocks everything AndroidBitmap_unlockPixels(env, bitmapIn); AndroidBitmap_unlockPixels(env, bitmapOut); LOGI ("Bitmap blurred."); } int min(int a, int b) { return a > b ? b : a; } int max(int a, int b) { return a > b ? a : b; }
更多相关内容 -
Android 说说Bitmap那些事
2022-03-07 17:20:28过了一个年,发现自己懈怠,没怎么去写博客了,项目中遇到的问题也很想把它写出来,但是都没有付诸行动,最近重构完项目的一些烂代码,闲下来时也是时候把...Android加载图片的对象就是我们老生常谈的Bitmap了,Bit.Android 说说Bitmap那些事
前言
过了一个年,发现自己懈怠,没怎么去写博客了,项目中遇到的问题也很想把它写出来,但是都没有付诸行动,最近重构完项目的一些烂代码,闲下来时也是时候把项目中遇到的问题分享给大家。
好了,唠叨说完,今天主要说下图片压缩那些事,在Android开发中,我们无可避免地都会和图片打交道,其中图片压缩就是我们比较常见和棘手的问题,处理过程中需要注意失真和内存的问题:图片马赛克了,业务或测试就找上门了;Android大量位图(Bitmap)加载导致内存溢出。现在我们谈谈Bitmap一些基本概念。
Bitmap存储格式
Android加载图片的对象就是我们老生常谈的Bitmap了,Bitmap是位图,是由像素点组成的,那它是如何存储在内存中的呢?Bitmap像素点有几种存储方式,对应Bitmap.Config中的枚举值:
- ALPHA_8:
只保存透明度,不保存颜色。每个像素存储为单个半透明(alpha)通道,1个像素点占1个字节,不常用。创建此类型图后,无法在上面画颜色。 - RGB_565:
只存储色值,不存储透明度(不支持alpha通道),默认不透明,RGB分别占5、6、5位,一个像素点占用16位2个字节。当使用不需要高色彩保真度的不透明位图时,此配置比较适合。 - ARGB_4444:
ARGB各用4位存储,1个像素点16位占2个字节。此类型的图片配置导致画质质量较差,建议使用ARGB_8888。 - ARGB_8888:
ARGB各用8位存储,1个像素点32位占4个字节。每个通道(RGB和alpha为半透明)以8位精度(256个可能值)存储,配置灵活,图片也很清晰,应尽可能使用此种方式,缺点比较占内存。 - RGBA_F16:
每个通道(RGB和半透明的alpha)存储为半精度浮点值。每个像素存储在8个字节上。它非常适合用于广色域宽屏和HDR(高动态范围的图片),它所占用的内存是最高的,因此显示的效果也非常好(API26以上才能用)。 - HARDWARE:
硬件位图,其像素数据是存储在显存中,并对图片仅在屏幕上绘制的场景做了优化。简而言之,它把图片的内存只在GPU上存一份,而不需要应用本身的副本,这样的话,理论上通过Hardware方式加载一张图片,内存占用可以比原来少一半,因为像素数据是在显存中的,在有些场景下访问像素数据会发生异常,详见硬件位图。
Bitmap内存计算方法
前面我们讲了Bitmap的几种方式及其像素点所占字节,那一张图片Bitmap在内存中的大小是多少呢?这里我们以像素为500*313的jpg格式图片为例:
这里我们看到了文件的大小34.1KB,那么将这张图片加载到内存中大小是多少呢?Android的BitmapFactory给我们提供了几种加载图片的方式:BitmapFactory.decodeResource()
:从资源文件中通过id加载bitmapBitmapFactory.decodeFile()
:传入文件路径加载,比如加载sd卡中的文件BitmapFactory.decodeStream()
:从输入流中加载图片BitmapFactory.decodeByteArray()
:从byte数组中加载图片
它们有很多重载函数,具体可去看源码,Bitmap对象的创建过程就不说了,网上也有很多介绍,现在我们一般都会将图片资源放到drawable-xxhdpi目录下,然后调用decodeResource()加载Bitmap对象,根据网上相关资料,采用ARGB_8888(一个像素点32B,即4字节)格式存储的这张图片内存大小(非文件存储大小) = 原宽×原高×像素点所占字节数:
500* 313* (32/8)B = 626000B = 0.6MB
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); Log.e(TAG, "原始大小: " + SampleUtils.getBitmapSize(bitmap)); BitmapFactory.Options options1 = new BitmapFactory.Options(); options1.inPreferredConfig = Bitmap.Config.RGB_565; // options1.inDensity = 160; // options1.inScaled = false; Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.test, options1); Log.e(TAG, "RGB_565: " + SampleUtils.getBitmapSize(bitmap1) + " inTargetDensity=" + bitmap1.getDensity() + " width=" + bitmap1.getWidth() + " height=" + bitmap1.getHeight() + " totalSize=" + bitmap1.getWidth() * bitmap1.getHeight() * 2); BitmapFactory.Options options2 = new BitmapFactory.Options(); options2.inPreferredConfig = Bitmap.Config.ARGB_8888; // options2.inDensity = 160; // options2.inScaled = false; Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.test, options2); Log.e(TAG, "ARGB_8888: " + SampleUtils.getBitmapSize(bitmap2) + " inTargetDensity=" + bitmap2.getDensity() + " width=" + bitmap2.getWidth() + " height=" + bitmap2.getHeight() + " totalSize=" + bitmap2.getWidth() * bitmap2.getHeight() * 4);
获取Bitmap大小:
/** * 得到bitmap的大小 */ public static int getBitmapSize(Bitmap bitmap) { //API 19 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { return bitmap.getAllocationByteCount(); } //API 12 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { return bitmap.getByteCount(); } //在低版本中用一行的字节x高度 return bitmap.getRowBytes() * bitmap.getHeight(); }
上面打印出来的log日志如下:
从日志中推断出- BitmapFactory.Options.inPreferredConfig默认参数应该是Bitmap.Config.ARGB_8888,一般情况下,这个参数就是我们设置的需要存储像素的格式,所以是可以通过设置它的参数来减少内存,但是也会有不符合配置的情况,详见Android inpreferredconfig参数分析;
- 一般情况下,图片内存大小 = 原宽×原高×像素所占字节数
本着严谨的态度,我们再做一个测试,将图片放到drawable目录下,打印日志结果:
将上面两幅图整理比较,在dpi为480的设备下,加载分辨率为500*313的jpg格式图片:存储格式 drawable 目录 width(px) height(px) 像素点字节数(B) 内存大小 RGB_565 drawable 1500 939 2 2817000(2.68MB) RGB_565 drawable-xxhdpi 500 313 2 313000(0.30MB) ARGB_8888 drawable 1500 939 4 5634000(5.37MB) ARGB_8888 drawable-xxhdpi 500 313 4 626000(0.60) 看到这里,在同种存储格式,不同的drawable目录下,图片的分辨率(宽*高)不一样,drawable目录下的图片加载到内存中宽和高都变为原来的3倍,分辨率变成原来的9倍,所以内存也就变成原来的9倍,由此我们可以猜测图片内存大小与图片存放drawable目录有关,通过查看 decodeResource() 源码,如果没有Options参数传入会生成默认的,最终调用 decodeResourceStream() 方法:
/** * Decode a new Bitmap from an InputStream. This InputStream was obtained from * resources, which we pass to be able to scale the bitmap accordingly. */ public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) { if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { //可理解为图片放置在drawable对应的dpi final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { //手机的dpi opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); }
这里说明 decodeResourceStream() 内部做了对Bitmap的密度适配,然后再调用 decodeStream(),Bitmap的decode过程实际上是在native层完成的,跟踪到BitmapFactory.cpp#nativeDecodeStream(),相关缩放代码如下:
if (env->GetBooleanField(options, gOptions_scaledFieldID)) { const int density = env->GetIntField(options, gOptions_densityFieldID); const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID); const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID); if (density != 0 && targetDensity != 0 && density != screenDensity) { scale = (float) targetDensity / density; } } ... int scaledWidth = decoded->width(); int scaledHeight = decoded->height(); if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) { scaledWidth = int(scaledWidth * scale + 0.5f); scaledHeight = int(scaledHeight * scale + 0.5f); } ... if (willScale) { const float sx = scaledWidth / float(decoded->width()); const float sy = scaledHeight / float(decoded->height()); bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight); bitmap->allocPixels(&javaAllocator, NULL); bitmap->eraseColor(0); SkPaint paint; paint.setFilterBitmap(true); SkCanvas canvas(*bitmap); canvas.scale(sx, sy); canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint); }
一目了然了,缩放值scale的由来:
scale = (float) targetDensity / density;
这里列出不同drawable目录所对应的设备dpi值:
不同目录 drawable drawable-ldpi drawable-mdpi drawable-hdpi drawable-xhdpi drawable-xxhdpi 对应设备的dpi 160 120 160 240 320 480 结合上面两个表格和源码分析,初步得出Android在加载res目录下的资源图片时,会根据不同drawable目录下的图片做一次分辨率转换,转换规则:
- 新图的高度 = 原图高度 * (设备的 dpi / 目录对应的 dpi )
- 新图的宽度 = 原图宽度 * (设备的 dpi / 目录对应的 dpi )
按照我们初步的理论,将表格中存储格式为RGB_565两行数据代入:
- drawable对应160dpi,新图高度:1500 = 500 * (480/160) ;新图宽度: 939 = 313 * (480/160)。
- drawable-xxhdpi对应480dpi,新图高度:500 = 500 * (480/480) ;新图宽度: 313 = 313 * (480/480)。
如果想要进一步验证该结论,可采用dpi为240的测试机进行控制变量的实验,这里就不做比较了,所以我们说前面图片:
内存大小 = 原宽×原高×像素点所占字节数
是不太对的,它还与设备的 dpi 和不同的资源目录有关,具体体现在位于res不同资源目录中的图片,当加载进内存时,会先经过一次分辨率的转换,然后再计算大小。 最终计算方式:
Bitmap内存占用 ≈ 原宽 × 原高× (设备dpi/资源目录对应dpi)^2 × 像素点所占字节数
既然分析了res中图片加载到内存的计算方法,那其它资源图片加载到内存中是不是同样的计算方法呢?现在我们不妨来分析下 decodeFile() 方法,其源码如下:public static Bitmap decodeFile(String pathName, Options opts) { validate(opts); Bitmap bm = null; InputStream stream = null; try { stream = new FileInputStream(pathName); bm = decodeStream(stream, null, opts); } catch (Exception e) { Log.e("BitmapFactory", "Unable to decode stream: " + e); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { } } } return bm; }
内部根据文件路径创建FileInputStream,最终和 decodeResource() 方法一样调用 decodeStream() 方法进解码图片,不同之处在于并没有进行分辨率的转换,所以图片内存大小的计算方法应该为我们最初的公式:
内存大小 = 原宽×原高×像素点所占字节数
下面测试一下(先申请存储权限):File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "/TestPath/test.jpg"); BitmapFactory.Options options3 = new BitmapFactory.Options(); options3.inPreferredConfig = Bitmap.Config.ARGB_8888; Log.e(TAG, "ARGB_8888: " + SampleUtils.getBitmapSize(bitmap3) + " inTargetDensity=" + bitmap3.getDensity() + " width=" + bitmap3.getWidth() + " height=" + bitmap3.getHeight() + " totalSize=" + bitmap3.getWidth() * bitmap3.getHeight() * 4);
结果如下:
这里只做了一个实验,你也可以用另一张图片重复此操作,结果都是一样的:
内存大小 = 原宽×原高×像素点所占字节数
除此之外,其它方式加载到内存的计算方法也是一样的,如网路资源(本质上也是下载到手机储存)、assert目录、SD卡资源等。所以这里我们得出结论:
只有res内的图片资源会进行分辨率的转换,采用新的分辨率去计算内存大小,而其它资源用的是原图分辨率去计算内存大小
前面长篇大论主要是为了总结这几点:- 对于非res目录下的图片资源,如本地文件图片,网络图片等,Bitmap内存占用 ≈ 原宽 × 原高× × 像素点所占字节数
- 对于res目录下的不同drawable目录下图片资源,Bitmap内存占用 ≈ 原宽 × 原高× (设备dpi/资源目录对应dpi)^2 × 像素点所占字节数
这里,不得不说下BitmapFactory.Options类的一些参数及其意义:
类型 参数 意义 boolean inJustDecodeBounds 如果为true,解码后不会返回Bitmap对象,但Bitmap宽高将返回到options.outWidth与options.outHeight中;反之返回。主要用于只需获取解码后的Bitmap的大小而不会将bitmap载入内存,浪费内存空间。 boolean inMutable 为true,代表返回可变属性的Bitmap,反之不可变 boolean inPreferredConfig 根据指定的Config来进行解码,如:Bitmap.Config.RGB_565等 boolean inSampleSize 如果值大于1,在解码过程中将按比例返回占更小内存的Bitmap。例如值为2,则对宽高进行缩放一半。这个值很有用,大多数图片压缩都有用到。 boolean inScaled 如果为true,且inDesity与inTargetDensity都不为0,那么在加载过程中将会根据inTargetDensityl来缩放,在drawn中不依靠于图片自身的缩放属性。 boolean inDensity Bitmap自身的密度 ,默认为图片所在drawable目录对应的dpi boolean inTargetDensity Bitmap drawn过程中使用的密度,默认采用当前设备的dpi,同inScreenDensity
图片文件存储格式
上面我们讲了图片加载到内存大小的计算方法,现在我们来看看图片文件存储大小,常见的图片文件几种格式:JPEG(JPG)、PNG、WEBP。
我们可以将它们理解为图片的容器,它们是经过相对应的压缩算法将原图每个像素点信息转换用另一种数据格式表示,以此达到压缩目的,从而减少图片文件大小。
总而言之,这几种格式就是不同的压缩算法,对应Bitmap.CompressFormat:生成的图片使用指定的图片存储格式- Bitmap.CompressFormat.JPEG:
采用JPEG压缩算法,是一种有损压缩格式,会在压缩过程中改变图像原本质量,画质越差,对原来的图片质量损伤越大,但是得到的文件比较小,而且JPEG不支持透明度,当遇到透明度像素时,会以黑色背景填充。 - Bitmap.CompressFormat.PNG:
采用PNG算法,是一种支持透明度的无损压缩格式,拥有丰富的颜色显示效果,即使在压缩情况下也能做到不降低图像质量。 - Bitmap.CompressFormat.WEBP:
WEBP是一种同时提供了有损压缩和无损压缩的图片文件格式,在14<=api<=17时,WEBP是一种有损压缩格式,而且不支持透明度,在api18以后WEBP是一种无损压缩格式,而且支持透明度,有损压缩时,在质量相同的情况下,WEBP格式的图片体积比JPEG小40%,但是编码时间比JPEG长8倍。在无损压缩时,无损的WEBP图片比PNG压缩小26%,但是WEBP的压缩时间是PNG格式压缩时间的5倍。
更多详细可参考 Bitmap.CompressFormat
图片压缩方法
前面我们讲了如何计算bitmap在内存的大小,例如我们要从网络上下载一张1920*1080分辨率的图片采用ARGB_8888模式显示,那这张图片所占内存大小:
1920*1080*4B = 7.91MB
一张图片竟然要占用这么大的内存,手机内存是有限的,如果我们不加以控制,那加载几十张又会是什么样场景,最后的结果肯定是OOM闪退了,这显然是无法接受的,所以我们更加要小心翼翼地处理这些图片。
根据前面所说的及相关计算方式,Bitmap的内存优化方法主要是图片存放在合适的drawable目录下、采取合适的存储格式去降低像素点所占字节数、降低图片的分辨率以及Bitmap的复用和缓存,即:- 将图片存放在合适的drawable目录下
- 减少每个像素点大小
- 降低分辨率
- 复用和缓存
依据中间两点,就有了下面这几种压缩方法
RGB_565压缩
这是通过设置像素点占用的内存大小来达到压缩的效果,一般不建议使用ARGB_4444,因为画质实在是敢看,如果对透明度没有要求,建议可以改成RGB_565,相比ARGB_8888将节省一半的内存开销。
/** * RGB565压缩 * * @param context 上下文 * @param id 图片资源id * @return 压缩后的图片Bitmap */ public static Bitmap compressByRGB565(Context context, int id) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; return BitmapFactory.decodeResource(context.getResources(), id, options); }
质量压缩
保持像素的前提下改变图片的位深及透明度,通过抹除某点附件相近像素,达到降低质量、压缩文件的目的。加载Bitmap的内存并不会减少,文件会变小,用用于服务器上传图片的压缩或保存本地图片文件。
/** * 质量压缩 * * @param bmp 图片位图 * @param quality 质量参数0-100,100为不压缩,PNG为无损压缩,此参数对Bitmap.CompressFormat.PNG无效 * @param file 保存压缩后的图片文件 */ public static void qualityCompress(Bitmap bmp, int quality, File file) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); // 把压缩后的数据存放到bos中 bmp.compress(Bitmap.CompressFormat.JPEG, quality, bos); try { FileOutputStream fos = new FileOutputStream(file); fos.write(bos.toByteArray()); fos.flush(); fos.close(); } catch (Exception e) { e.printStackTrace(); } }
尺寸压缩(缩放压缩)
改变图片的尺寸,即压缩图片宽度和高度的像素点,从上面的计算公式我们可以得知,这样会降低图片bitmap内存的占用,从而一定程度上减少OOM的概率。但这里我们需要注意,如果压缩比太大,也会由于像素点降低导致图片失真严重,最后图片由高清变成了马赛克。
/** * 缩放压缩 * * @param bmp 图片位图 * @param radio 缩放比例,值越大,图片尺寸越小 * @param file 保存压缩后的图片文件 */ public static void scaleCompress(Bitmap bmp, int radio, File file) { //设置缩放比 Bitmap result = Bitmap.createBitmap(bmp.getWidth() / radio, bmp.getHeight() / radio, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(result); RectF rectF = new RectF(0, 0, bmp.getWidth() * 1.0f / radio, bmp.getHeight() * 1.0f / radio); //将原图画在缩放之后的矩形上 canvas.drawBitmap(bmp, null, rectF, null); ByteArrayOutputStream bos = new ByteArrayOutputStream(); // 把压缩后的数据存放到bos中 bmp.compress(Bitmap.CompressFormat.JPEG, 100, bos); try { FileOutputStream fos = new FileOutputStream(file); fos.write(bos.toByteArray()); fos.flush(); fos.close(); } catch (Exception e) { e.printStackTrace(); } }
采样率压缩
其实采样率压缩和尺寸压缩原理是一样的,都是通过减少图片尺寸,压缩图片宽度和高度的像素点,这里充分利用了Options类里的参数设置(可参考上面表格):
- inSampleSize:采样率,为整数,且为2的n次幂,n可以为0,即采样率为1,处理后的图片尺寸与原图一致。当采样率为2时,即宽、高均为原来的1/2,像素则为原来的1/4,其占有内存也为原来的1/4。当设置的采样率小于1时,其效果与1一样。当设置的inSampleSize大于1,不为2的指数时,系统会向下取一个最接近2的指数的值。
- inJustDecodeBounds:当设置为true时,BitmapFactory只会解析图片的原始宽/高信息,不会真正的去加载图片,这一设置堪称绝了。
/** * 采样率压缩 * * @param context 上下文 * @param id 图片资源id * @param destW 目标宽大小 * @param destH 目标高大小 * @return 压缩后的图片Bitmap */ public static Bitmap sampleSizeCompress(Context context, int id, int destW, int destH) { Bitmap bm = null; int inSampleSize = 1; //第一次采样 BitmapFactory.Options options = new BitmapFactory.Options(); //该属性设置为true只会加载图片的宽高、类型信息,并不会加载图片具体的像素点 options.inJustDecodeBounds = true; bm = BitmapFactory.decodeResource(context.getResources(), id, options); Log.e(TAG, "sampleSizeCompress--压缩之前图片的宽:" + options.outWidth + "--压缩之前图片的高:" + options.outHeight + "--压缩之前图片大小:" + options.outWidth * options.outHeight * 4 / 1024 + "kb"); int iWidth = options.outWidth; int iHeight = options.outHeight; //对缩放比例进行调整,直到宽和高符合我们要求为止 while (iWidth > destW || iHeight > destH) { //如果宽高的任意一方的缩放比例没有达到要求,都继续增大缩放比例 //inSampleSize应该为2的n次幂,如果给inSampleSize设置的数字不是2的n次幂,那么系统会就近取 //宽高均为原图的宽高的1/2 内存约为原来的1/4 inSampleSize = inSampleSize * 2; iWidth = iWidth / inSampleSize; iHeight = iHeight / inSampleSize; } //二次采样开始 //二次采样时我需要将图片完整加载出来显示,inJustDecodeBounds属性要设置为false options.inJustDecodeBounds = false; options.inSampleSize = inSampleSize; //设置像素颜色信息,默认Bitmap.Config.ARGB_8888 //bitmapFactoryOptions.inPreferredConfig = Bitmap.Config.RGB_565; bm = BitmapFactory.decodeResource(context.getResources(), id, options); Log.e(TAG, "sampleSizeCompress--图片的宽:" + bm.getWidth() + "--图片的高:" + bm.getHeight() + "--图片大小:" + bm.getWidth() * bm.getHeight() * 4 / 1024 + "kb"); //返回压缩后的照片 return bm; }
采样率压缩好处是不会先将大图片读入内存,大大减少了内存的使用,也不必考虑将大图片读入内存后的释放事宜,不足的地方是inSampleSize参数因为是整数,不能很好地保证图片的质量。
图片压缩的过程可能会比较耗时,请不要放到主线程执行,可以使用线程池,然后再将结果回调给主线程即可,这里就不写代码了。当然,这几种压缩方法,你也可以结合使用,充分利用它们各自的优点才是好出路,如质量压缩并不会减少图片转换为bitmap时的内存消耗,为避免出现OOM,我们建议先进行合适的尺寸压缩,然后再进一步进行质量压缩。
现在比较出名的三方压缩库就是号称最接近微信朋友圈图片压缩的 Luban了,它里面最核心的东西就是采样率的算法,感兴趣的同学可以去看看。
微信分享问题
压缩方法介绍完后,这里给大家分享一个案例:微信分享图片的大小,为什么有时候微信分享不成功呢?官方文档说分享图片限制10M,实际结果我1M的图片就分享不出去了。
首先看了下微信分享文档,理清一些事情:微信图片分享是有两种的,一是本地图片路径的分享(10M),二是图片字节流分享,这个是通过intent开启活动分享给微信的,既然涉及到意图,Android开发应该知道intent传递数据是有限制的吧,官方文档说是只能传递1M的数据,但是综合各种复杂情况和手机机型等,512KB才是一个比较通用的值。我们处理的方式就是通过循环质量压缩将图片压缩到512KB下,这里的512KB指的是文件大小,而不是前面的Bitmap占用内存大小,不要搞混了。/** * 循环质量压缩便于支持微信分享 * * 注意:微信限制分享图片10M以下,但是微信分享图片会涉及到启动一个新活动,新活动涉及到Intent传值,而Intent传值有大小限制(不同机型不一样), * 所以为了适配所有机型,此处图片大小应低于512KB * PS:如果要传不压缩的大图可以采用imagePath的方式,先把图片先保存在本地(涉及存储权限),然后直接传递图片的地址。 * * @param bitmap 需要压缩的图片Bitmap * @param callBack 回调压缩后的图片字节数组 */ public static void asyncCompressImageForWXShare(final Bitmap bitmap, final OnBitmap2ByteCallBack callBack) { ThreadUtils.getCachedPool().execute(new Runnable() { @Override public void run() { ByteArrayOutputStream byteAOStream = new ByteArrayOutputStream(); // 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到byteAOStream中 bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteAOStream); byte[] bytes = byteAOStream.toByteArray(); int options = 100; // 循环判断如果压缩后图片是否大于512KB,大于继续压缩 while (bytes.length / 1024 > 512 && options >= 10) { // 每次都减少10 options -= 10; if (options < 0) { options = 0; } // 重置byteAOStream,不然会累加 byteAOStream.reset(); // 把压缩后的数据存放到byteAOStream中 bitmap.compress(Bitmap.CompressFormat.JPEG, options, byteAOStream); // 将流转换成字节数组 bytes = byteAOStream.toByteArray(); } Log.e(TAG, "微信分享图片字节数组大小:" + bytes.length); final byte[] finalBytes = bytes; ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { callBack.onBitmap2ByteCallBack(finalBytes); } }); } }); } /** * 异步转换图片字节流的回调 */ public interface OnBitmap2ByteCallBack { void onBitmap2ByteCallBack(byte[] imageBytes); }
这里的线程池引入的依赖是
implementation 'com.blankj:utilcode:1.30.6'
总结
本篇文章,我们主要讲了Bitmap一些重要的概念、压缩方法及其内存优化等等,以上的理论都是基于最原始的加载图片方式,并没有涉及到三方强大的图片处理库,如Glide、fresco等,这些库对于图片的加载、内存肯定是做了很好地优化了,我们也不用花费太多的精力去处理这些问题了,但是我们有必要去了解最基本的东西,整体有个认识,最好也是看看优秀的开源库是怎么处理这些问题,这对我们的能力绝对是一大截的提升。
最后,我想说的是,业务千奇百怪,问题各种各样,不同的人会有不同的情况,我们只需要从中找到一些蛛丝马迹,再结合自己的思考,我相信问题终会得到解决。因为自己最近一直在和图片Bitmap打交道,所以查阅了各种资料,积累了下,再结合自己遇到的问题,才写出这篇文章。
好了,如果讲得不对的地方或有什么疑惑,欢迎评论区留言,也请大家不要舍不得自己的赞👍哦!! - ALPHA_8:
-
Android:安卓学习笔记之Bitmap的简单理解和使用
2022-01-14 10:49:13Android Bitmap的简单理解和使用Android Bitmap一.Bitmap的定义二.Bitmap的格式2.1 存储格式2.2 压缩格式三.Bitmap创建方法3.1 BitmapFactory3.1.1、 Bitmap.Options类3.2 Bitmap静态方法3.3 创建Bitmap的总结四....Android Bitmap的简单理解和使用
Android Bitmap
一.Bitmap的定义
Bitmap是Android系统中的图像处理的最重要类之一。用它可以获取图像文件信息,进行图像剪切、旋转、缩放等操作,并可以指定格式保存图像文件。
Bitmap相关的使用主要有两种:
- 1.给ImageView设置背景
- 2.当做画布来使用
分别对应下面两个方法
imageView.setImageBitmap(Bitmap bm); Canvas canvas = new Canvas(Bitmap bm)
二.Bitmap的格式
我们知道Bitmap是位图,是由像素点组成的,这就涉及到两个问题,
- 第一:如何存储每个像素点?
- 第二:怎么压缩像素点?
Bitmap 中有两个内部枚举类:
Config
和CompressFormat
,Config
是用来设置颜色配置信息的,CompressFormat
是用来设置压缩方式的
2.1 存储格式
2.2 压缩格式
我们不妨来计算一下,如果一张和手机屏幕大小一样的Bitmap图片,采用ARGB_8888格式存储需要多大的内存!
按照1024*768的屏幕大小来计算,每个像素需要32位也就是4个字节,
result = 1024*768*32B=25165824B=24MB
一张手机屏幕大小的Bitmap图片竟然要24M? 那就不奇怪我的app为什么一直闪退了,只不过用for循环创建了几十个用在滑动列表里面。
所以我们必须要对图片进行压缩呀,压缩格式使用枚举类Bitmap.CompressFormat
Bitmap.CompressFormat.JPEG:采用JPEG压缩算法,是一种有损压缩格式,会在压缩过程中改变图像原本质量,画质越差,对原来的图片质量损伤越大,但是得到的文件比较小,而且JPEG不支持透明度,当遇到透明度像素时,会以黑色背景填充。
Bitmap.CompressFormat.PNG:采用PNG算法,是一种支持透明度的无损压缩格式。
Bitmap.CompressFormat.WEBP:WEBP是一种同时提供了有损压缩和无损压缩的图片文件格式,在14<=api<=17时,WEBP是一种有损压缩格式,而且不支持透明度,在api18以后WEBP是一种无损压缩格式,而且支持透明度,有损压缩时,在质量相同的情况下,WEBP格式的图片体积比JPEG小40%,但是编码时间比JPEG长8倍。在无损压缩时,无损的WEBP图片比PNG压缩小26%,但是WEBP的压缩时间是PNG格式压缩时间的5倍。
三.Bitmap创建方法
我们如何创建一个 Bitamap 对象呢?Google 给我们提供了两种方式:
- 1、Bitmap 的静态方法 createBitmap(XX)
- 2、BitmapFactory 的 decodeXX 系列静态方法
3.1 BitmapFactory
BitmapFactory提供了多种创建bitmap的静态方法
decodeFile、 decodeResource、decodeStream和decodeByteArray
,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile
和decodeResource
又间接调用了decodeStream
方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native 方法。3.1.1、 Bitmap.Options类
如何高效地加载bitmap?
通过
BitmapFactory.Options
按一定的采样率
来加载缩小后的图片,将缩小后的图片在ImageView
中显示,这样就会降低内存占用从而在一定程度上避免OOM,提高了Bitmap 加载时的性能。采样率解释:
BitmapFactory提供的加载图片的四类方法都支持BitmapFactory.Options
参数
通过BitmapFactory.Options
来缩放图片,主要用到了inSampleSize
参数,即采样率
。- 当
inSampleSize
为1时,采样后的图片大小为图片的原始大小; - 当
inSampleSize
大于1时,比如为2,那么采样后的图片其宽/高均为原图大小的1/2,而像素数为原图的1/4,其占有的内存大小也为原图的1/4。
拿一张1024×1024像素的图片来说,假定采用ARGB8888格式存储,那么它占有的内存为1024×1024×4即4MB,如果inSampleSize为2,那么采样后的图片其内存占用只有512×512×4,即1MB。
采样率同时作用于宽/高,这将导致缩放后的图片大小以采样率的2次方形式递减,即缩放比例为
1/ (inSampleSize的2次方)
- 比如inSampleSize为4,那么缩放比例就是1/16。
- 有一种特殊情况,那就是当inSampleSize 小于1时,其作用相当于1,即无缩放效果。
参数说明:
举例说明try { FileInputStream fis = new FileInputStream(filePath); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; // 设置inJustDecodeBounds为true后,再使用decodeFile()等方法,并不会真正的分配空间, //即解码出来的Bitmap为null,但是可计算出原始图片的宽度和高度,即options.outWidth和options.outHeight BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options); float srcWidth = options.outWidth; float srcHeight = options.outHeight; int inSampleSize = 1; if (srcHeight > height || srcWidth > width) { if (srcWidth > srcHeight) { inSampleSize = Math.round(srcHeight / height); } else { inSampleSize = Math.round(srcWidth / width); } } options.inJustDecodeBounds = false; options.inSampleSize = inSampleSize; return BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options); } catch (Exception e) { e.printStackTrace(); }
过程说明:
- (1) 将
BitmapFactory.Options
的inJustDecodeBounds
参数设为true
并加载图片。该参数为true
时,BitmapFactory
只会解析图片的原始宽高信息,不会去真正加载图片,同时获取到的信息和图片的位置与程序运行的设备有关 - (2) 从
BitmapFactory.Options
中取出图片的原始宽高信息,它们对应于outWidth
和outHeight
参数。 - (3) 根据采样率的规则并结合目标
View
的所需大小计算出采样率inSampleSize
. - (4) 将
BitmapFactory.Options
的inJustDecodeBounds
参数设为false
, 然后重新加载图片。
3.2 Bitmap静态方法
//width和height是长和宽单位px,config是存储格式 static Bitmap createBitmap(int width , int height Bitmap.Config config) // 根据一幅图像创建一份一模一样的实例 static Bitmap createBitmap(Bitmap bm) //截取一幅bitmap,起点是(x,y),width和height分别对应宽高 static Bitmap createBitmap(Bitmap bm,int x,int y,int width,int height) //比上面的裁剪函数多了两个参数,Matrix:给裁剪后的图像添加矩阵 boolean filter:是否给图像添加滤波效果 static Bitmap createBitmap(Bitmap bm,int x,int y,int width,int height,Matrix m,boolean filter); //用于缩放bitmap,dstWidth和dstHeight分别是目标宽高 createScaledBitmap(Bitmap bm,int dstWidth,int dstHeight,boolean filter)
这些方法大致可以分为三类:
- 1、根据已有的Bitmap来创建新Bitmap
/** * 通过矩阵的方式,返回原始 Bitmap 中的一个不可变子集。新 Bitmap 可能返回的就是原始的 Bitmap,也可能还是复制出来的。 * 新 Bitmap 与原始 Bitmap 具有相同的密度(density)和颜色空间; * * @param source 原始 Bitmap * @param x 在原始 Bitmap 中 x方向的其起始坐标(你可能只需要原始 Bitmap x方向上的一部分) * @param y 在原始 Bitmap 中 y方向的其起始坐标(你可能只需要原始 Bitmap y方向上的一部分) * @param width 需要返回 Bitmap 的宽度(px)(如果超过原始Bitmap宽度会报错) * @param height 需要返回 Bitmap 的高度(px)(如果超过原始Bitmap高度会报错) * @param m Matrix类型,表示需要做的变换操作 * @param filter 是否需要过滤,只有 matrix 变换不只有平移操作才有效 */ public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height, @Nullable Matrix m, boolean filter)
- 2、通过像素点数组创建空的Bitmap
/** * * 返回具有指定宽度和高度的不可变位图,每个像素值设置为colors数组中的对应值。 * 其初始密度由给定的确定DisplayMetrics。新创建的位图位于sRGB 颜色空间中。 * @param display 显示将显示此位图的显示的度量标准 * @param colors 用于初始化像素的sRGB数组 * @param offset 颜色数组中第一个颜色之前要跳过的值的数量 * @param stride 行之间数组中的颜色数(必须> = width或<= -width) * @param width 位图的宽度 * @param height 位图的高度 * @param config 要创建的位图配置。如果配置不支持每像素alpha(例如RGB_565), * 那么colors []中的alpha字节将被忽略(假设为FF) */ public static Bitmap createBitmap(@NonNull DisplayMetrics display, @NonNull @ColorInt int[] colors, int offset, int stride, int width, int height, @NonNull Config config)
- 3、 创建缩放的Bitmap
/** * 对Bitmap进行缩放,缩放成宽 dstWidth、高 dstHeight 的新Bitmap */ public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,boolean filter)
3.3 创建Bitmap的总结
- 1.加载图像可以使用BitmapFactory和Bitmap.create系列方法
- 2.可以通过Options实现缩放图片,获取图片信息,配置缩放比例等功能
- 3.如果需要裁剪或者缩放图片,只能使用create系列函数
- 4.注意加载和创建bitmap事通过try catch捕捉OOM异常
四.常见函数
4.1 函数及其参数
copy(Config config,boolean isMutable) //根据原图像创建一个副本,但可以指定副本的像素存储格式 //参数含义。 // config:像素在内存中的存储格式,但可以指定副本的像素存储格式 // boolean isMutable:新建的bitmap是否可以修改其中的像素值 extractAlpha() //主要作用是从bitmap中获取Alpha值,生成一幅只有Alpha值得图像,存储格式是ALPHA_8 getByteCount()//获取bitmap的字节数 recycle()://不用的bitmap必须要及时回收,以免造成oom isRecycled()//判断bitmap是否被回收,被收回不可使用会造成crash
综合案例演示
String items[] = {"copy","extractAlpha 1","extractAlpha 2","bitmap大小","recycle","isRecycled()"}; ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item,items); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { switch (position){ case 0: //copy Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.photo); Bitmap copy = bm.copy(Bitmap.Config.ARGB_8888, true); imageView.setImageBitmap(copy); bm.recycle(); break; case 1: //extractAlpha 不带参数 Bitmap bp = BitmapFactory.decodeResource(getResources(), R.drawable.photo); Bitmap alpha = bp.extractAlpha(); imageView.setImageBitmap(alpha); bp.recycle(); break; case 2: //extractAlpha 带参数 Bitmap bp1 = BitmapFactory.decodeResource(getResources(), R.drawable.photo); Paint paint = new Paint(); BlurMaskFilter blurMaskFilter = new BlurMaskFilter(6, BlurMaskFilter.Blur.NORMAL); paint.setMaskFilter(blurMaskFilter); int[] offsetXY = new int[2]; Bitmap alpha1 = bp1.extractAlpha(paint, offsetXY); imageView.setImageBitmap(alpha1); break; case 3: //获取bitmap大小 Bitmap b = BitmapFactory.decodeResource(getResources(), R.drawable.photo); Toast.makeText(getApplicationContext(), "图片大小为:"+b.getByteCount()+"字节", Toast.LENGTH_SHORT).show(); break; case 4: //回收bitmap Bitmap b1 = BitmapFactory.decodeResource(getResources(), R.drawable.photo); b1.recycle(); if(b1.isRecycled()){ Toast.makeText(getApplicationContext(), "已经被回收", Toast.LENGTH_SHORT).show(); } //isRecycled()判断是否被回收 break; } } @Override public void onNothingSelected(AdapterView<?> parent) { } });
4.2 常用操作
1、裁剪、缩放、旋转、移动
Matrix matrix = new Matrix(); // 缩放 matrix.postScale(0.8f, 0.9f); // 左旋,参数为正则向右旋 matrix.postRotate(-45); // 平移, 在上一次修改的基础上进行再次修改 set 每次操作都是最新的 会覆盖上次的操作 matrix.postTranslate(100, 80); // 裁剪并执行以上操作 Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);
2、保存与释放
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); File file = new File(getFilesDir(),"test.jpg"); if(file.exists()){ file.delete(); } try { FileOutputStream outputStream=new FileOutputStream(file); bitmap.compress(Bitmap.CompressFormat.JPEG,90,outputStream); outputStream.flush(); outputStream.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } //释放bitmap的资源,这是一个不可逆转的操作 bitmap.recycle();
3、图片压缩
public static Bitmap compressImage(Bitmap image) { if (image == null) { return null; } ByteArrayOutputStream baos = null; try { baos = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.JPEG, 100, baos); byte[] bytes = baos.toByteArray(); ByteArrayInputStream isBm = new ByteArrayInputStream(bytes); Bitmap bitmap = BitmapFactory.decodeStream(isBm); return bitmap; } catch (OutOfMemoryError e) { e.printStackTrace(); } finally { try { if (baos != null) { baos.close(); } } catch (IOException e) { e.printStackTrace(); } } return null; }
五.常见问题
5.1 Bitmap与Canvas,View,Drawable的关系
5.2 使用Bitmap如何造成内存溢出的?
个人认为,Bitmap容易造成内存溢出是由于位图较大,一张屏幕大小的ARGB_8888存储格式的图片竟然有24M,如果有几个这种量级的图片在内存中,并且没有及时回收,那会非常容易造成OOM
5.3怎么解决或者避免Bitmap内存溢出?
- 1.我们可以对位图进行压缩,压缩手段有PNG,JPEG,WEBP
- 2.对不使用的Bitmap一定要及时回收。
- 3.在创建Bitmap时使用try catch步骤OOM异常,使程序更健壮,即使发生了OOM也不会闪退,造成不好的使用体验.
5.4 Bitmap与Drawable的转换
5.4.1 Drawable转换成Bitmap
public static Bitmap drawableToBitmap(Drawable drawable) { // 取 drawable 的长宽 int w = drawable.getIntrinsicWidth(); int h = drawable.getIntrinsicHeight(); // 取 drawable 的颜色格式 Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; // 建立对应 bitmap Bitmap bitmap = Bitmap.createBitmap(w, h, config); // 建立对应 bitmap 的画布 Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, w, h); // 把 drawable 内容画到画布中 drawable.draw(canvas); return bitmap; }
5.4.2 Bitmap转换成Drawable
Bitmap bm=Bitmap.createBitmap(xxx); BitmapDrawable bd= new BitmapDrawable(getResource(), bm);
参考
1、那些关于Bitmap图片资源优化的小事
2、Bitmap史上最详细全解
3、Android Bitmap详解
4、Android Bitmap的内存大小是如何计算的?
5、Android Bitmap 详解:关于 Bitamp 你所要知道的一切 -
Bitmap类01_浅探
2021-11-21 23:25:57Bitmap类解析 Bitmap类是对图像进行处理的类,可以获取图像信息,进行图像颜色变换等操作 在安卓中,Bitmap指的是一张图片,可以是 .png 或者 .jpg 等其他常见的图像格式 参考API: Bitmap | Android Developers ...Bitmap类解析
Bitmap类是对图像进行处理的类,可以获取图像信息,进行图像颜色变换等操作
在安卓中,Bitmap指的是一张图片,可以是
.png
或者.jpg
等其他常见的图像格式参考API:
Bitmap - Android中文版 - API参考文档 (apiref.com)
如何使用Bitmap加载图片
需要使用
BitmapFactory
类提供的四个方法:decodeFile()
:从文件中加载Bitmap对象decodeResource()
:从资源中加载Bitmap对象decodeStream()
:从输入流中加载Bitmap对象decodeByteArray()
:从字节数组中加载Bitmap对象使用举例:
从资源中加载一张命名为 test 的图片,再加载到
ImageView
控件中Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); imageView.setImageBitmap(bitmap);
Bitmap颜色配置
Bitmap类中有两个枚举类,其中一个为Config类,用于配置颜色信息:
解析:
Bitmap.Config.ALPHA_8
:颜色信息只由透明度组成,占8位。Bitmap.Config.ARGB_4444
:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位。Bitmap.Config.ARGB_8888
:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置。Bitmap.Config.RGB_565
:颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位。通常我们优化Bitmap时,当需要做性能优化或者防止OOM,通常会使用
RGB_565
这个配置,因为ALPHA_8
只有透明度,显示一般图片没有意义,而ARGB_4444
显示图片不清楚,ARGB_8888
则占用内存最多。测试一下:
主要代码如下:
// 加载原图 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); imageView.setImageBitmap(bitmap); // 测试其他颜色模式 Bitmap bitmap1 = bitmap.copy(Bitmap.Config.ALPHA_8, true); Bitmap bitmap2 = bitmap.copy(Bitmap.Config.ARGB_4444, true); Bitmap bitmap3 = bitmap.copy(Bitmap.Config.ARGB_8888, true); Bitmap bitmap4 = bitmap.copy(Bitmap.Config.RGB_565, true); imageView1.setImageBitmap(bitmap1); imageView2.setImageBitmap(bitmap2); imageView3.setImageBitmap(bitmap3); imageView4.setImageBitmap(bitmap4);
运行后点击按钮效果如下:
(可能由于图片原因,或者显示器等问题,似乎除了ALPHA_8都没啥区别)
Bitmap压缩方式配置
除了
Config
类的另一个枚举类则是CompressFormat
,用于配置压缩方式:解析:
Bitmap.CompressFormat.JPEG
:表示以JPEG
压缩算法进行图像压缩,压缩后的格式可以是.jpg
或者.jpeg
,是一种有损压缩。Bitmap.CompressFormat.PNG
:表示以PNG
压缩算法进行图像压缩,压缩后的格式可以是.png
,是一种无损压缩。Bitmap.CompressFormat.WEBP
:表示以WebP
压缩算法进行图像压缩,压缩后的格式可以是.webp
,是一种有损压缩。关于三种格式的优劣,可以在自行搜索,或者参考Webp 相对于 PNG、JPG 有什么优势? - 知乎 (zhihu.com)
使用Bitmap的方法操作图片
裁剪:
使用
createBitmap(Bitmap source, int x, int y, int width, int height)
方法可以对Bitmap位图进行裁剪其中x,y为裁剪时,x和y轴开始的第一个像素
width 和 height 则是剪切时的宽度和高度
(ps:x+width要小于source的宽度,y+height同理)
使用:
主要代码:
TextView textView = findViewById(R.id.text1); ImageView imageView1 = findViewById(R.id.image_1); ImageView imageView2 = findViewById(R.id.image_2); ImageView imageView3 = findViewById(R.id.image_3); Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test); Bitmap bitmap1 = Bitmap.createBitmap(bitmap, 0, 0, 100, 100); Bitmap bitmap2 = Bitmap.createBitmap(bitmap, 0, 0, 530, 530); // 530也可以使用bitmap.getHeight()和bitmap.getWidth()代替 String out = "Height = " + bitmap.getHeight() + " and Width = " + bitmap.getWidth(); textView.setText(out); imageView1.setImageBitmap(bitmap); imageView2.setImageBitmap(bitmap1); imageView3.setImageBitmap(bitmap2);
效果呈现:
缩放、旋转、移动:
通过
createBitmap(Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter)
方法,不但能实现裁剪还能够实现如缩放等其他效果
其中,Matrix类代表一个矩阵对象,对其的简述可以参考本文下方的Matrix类简析
而参数 filter 为 true 时表示 source 会被过滤,仅仅当 m 操作不仅包含移动操作,还包含别的操作时才适用。
使用:
主要代码:
Matrix matrix = new Matrix(); // 向左旋转45度,参数为正时表示向右旋转 matrix.postRotate(-45); Bitmap bitmap3 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); imageView4.setImageBitmap(bitmap3);
效果呈现:
图像的保存与释放
请参考之后关于缓存的文章
BitmapFactory类
对BitmapFactory类的使用主要是上述的四个方法加载图片
BitmapFactory.decodeResource 加载的图片可能会经过缩放,该缩放目前是放在 java 层做的,效率比较低,而且需要消耗 java 层的内存。因此,如果大量使用该接口加载图片,容易导致OOM错误
BitmapFactory.decodeStream 不会对所加载的图片进行缩放,相比之下占用内存少,效率更高。
这两个接口各有用处,如果对性能要求较高,则应该使用 decodeStream;如果对性能要求不高,且需要 Android 自带的图片自适应缩放功能,则可以使用 decodeResource。
BitmapFactory.option类
BitmapFactory.Options 类是 BitmapFactory 对图片进行解码时使用的一个配置参数类,其中定义了一系列的public成员变量,每个成员变量代表一个配置参数。
参考API:
BitmapFactory.Options | Android Developers
BitmapFactory.Options - Android中文版 - API参考文档 (apiref.com)
Matrix类简述
(此处的坐标轴) Matrix类本身不能对图片进行操作,但是可以结合其他的API实现图片的旋转平移等效果
Matrix类中包含一个3X3的矩阵,对图片的操作分为四种:
(1)
Translate
:平移变换方法为
setTranslate()
,该方法接收两个浮点数作为参数,分别表示x和y轴上移动的数量第一个参数若为整数则表示在x轴上向右移动相应距离,第二个参数同理
(2)
Scale
:缩放变换方法为
setScale()
接收两个浮点数作为参数,分别表示在每个轴上所产生的缩放量第一个参数是x轴的缩放比例,而第二个参数是y轴的缩放比例
(3)
Rotate
:旋转变换方法之一为
setRotate()
接收一个浮点数表示旋转的角度也可以在角度后再传入两个参数作为旋转的中心点
默认围绕点(0,0),正数将顺时针旋转图像,而负数将逆时针旋转图像
使用举例:
其中默认点是图像的左上角,如:
Matrix matrix = new Matrix(); matrix.setRotate(15);
另外,也可以使用旋转的角度及围绕的旋转点作为参数调用setRotate方法。选择图像的中心点作为旋转点,如:
matrix.setRotate(15, bmp.getWidth()/2, bmp.getHeight()/2);
(4)
Skew
:错切变换在数学上又称为
Shear mapping
(可译为“剪切变换”)或者Transvection
(缩并),是一种比较特殊的线性变换错切变换的效果就是让所有点的 x 坐标(或者 y 坐标)保持不变,而对应的 y 坐标(或者 x 坐标)则按比例发生平移
平移的大小和该点到 x 轴(或 y 轴)的垂直距离成正比
错切变换,属于等面积变换,即一个形状在错切变换的前后,其面积是相等的
而在Android的API中对于每一种变换都提供了三种操作方式:
(1)set(用于设置Matrix中的值)
(2)post(后乘,根据矩阵的原理,相当于左乘)
(3)pre(先乘,相当于矩阵中的右乘)
默认时,这四种变换都是围绕(0,0)点变换的,当然可以自定义围绕的中心点,通常围绕中心点。
参考API:
Matrix - Android中文版 - API参考文档 (apiref.com)
本文参考文章:
Android Bitmap最全面详解 - 掘金 (juejin.cn)
玩转Android Bitmap - 简书 (jianshu.com)
聊聊 Bitmap 的一些知识点 - 掘金 (juejin.cn)
-
RoaringBitmap的原理与应用,看这个就够了
2021-08-16 16:23:32RoaringBitmap的原理与应用,看这个就够了 针对RoaringBitmap的实现思路,container的类型及读取性能进行介绍,并给出JAVA使用示例 -
Bitmap类源文件
2013-02-02 22:56:51java实现的Bitmap类 用于自动生成Bitmap文件头和填充调色板等,目前只支持8位256色的Bitmap -
android绘图之Canvas和Bitmap结合
2021-09-18 21:19:18Rect和RectF Matrix Canvas Bitmap -
【拿走不谢】大数据高效查询神器--bitmap
2022-06-07 11:54:351.提升 hive 中精确去重性能,代替hive 中的 count(distinct uuid); 2.节省 hive 存储 ,使用 ...3.提供在 hive 中 bitmap 的灵活运算 ,比如:交集、并集、差集运算 ,计算后的 bitmap 也可以直接写入 hive; ... -
将Bitmap转换为Byte[]
2014-03-28 07:26:35一个在VC中将Bitmap转换为Byte[]的小例子。 -
bitmap与mat互转方便opencv操作
2021-11-16 17:04:48亲测可用,达到的效果是在java层中输入bitmap,在c++层里将bitmap转为mat,然后用opencv来进行图像处理,最后将处理好的mat再转为bitmap给到java层。 -
Android 高效显示Bitmap图片
2020-09-24 10:11:08Android 高效显示Bitmap图片 本文会介绍一些处理与加载Bitmap对象的常用方法,这些技术能够使得程序的UI不会被阻塞,并且可以避免程序超出内存限制。如果我们不注意这些,Bitmap会迅速的消耗掉可用内存从而导致... -
SparkSQL & ClickHouse RoaringBitmap使用实践
2020-12-10 21:47:42文章目录简介ClickHouse简介RoaringBitmap(RBM)原理ClickHouse中使用RBM存在的问题RoaringBitmap(RBM)定制序列化实现ClickHouse中RoaringBitmap的结构解析Spark中RoaringBitmap的实现定制RBM序列化方式以兼容... -
【数据结构】BitMap
2021-03-27 19:40:47目录 一、BitMap介绍 二、BitMap应用场景 1、查询统计、定位查询,排序,去重 2、取两个集合的交集,并集等 三、BitMap的实现 1、自己动手实现BitMap 2、JDK中实现的BitMap —— BitSet 集合 3、谷歌实现的BitMap ... -
Bitmap详解(中)之像素级操作
2021-04-06 19:00:54在Android SDK中,图像的像素读写能够通过getPixel与setPixel两个Bitmap的API实现。 1.1 getPixel读取像素 Bitmap API读取像素的代码例如以下: int pixel = bitmap.getPixel(col, row);// ARGB int red = Color.red... -
Bitmap详细讲解
2020-03-08 16:23:35Bitmap描述: 每一张图片都是有无数点组成,我们取其中足够数量的点(可以形成视觉连贯性,以便形成近似真正图片的视觉效果),这些点就是像素点。比如某张图片大小为:1080*1920 ,它的像素数就为1080*1920个。这些... -
clickhouse之bitmap
2021-08-27 00:24:421. bitmap知多少 2. clickhouse中的bitmap函数 3. clickhouse+bitmap的使用场景 -
Android Canvas.drawBitmap 绘制bitmap
2022-02-18 16:03:16Android Canvas.drawBitmap 绘制bitmap -
Android view转Bitmap(将布局文件转成Bitmap)&& Bitmap保存到本地相册
2021-11-24 14:49:06文章目录一、view转Bitmap1、使用LayoutInflater导入布局2、测量计算view大小3、把view转成图片Bitmap二、将Bitmap保存到本地相册三、代码实现demo 一、view转Bitmap 1、使用LayoutInflater导入布局 View view = ... -
Android Bitmap详解及Bitmap的内存优化
2018-05-28 20:05:36一、Bitmap:Bitmap是Android系统中的图像处理的最重要类之一。用它可以获取图像文件信息,进行图像剪切、旋转、缩放等操作,并可以指定格式保存图像文件。常用方法:public void recycle() // 回收位图占用的内存... -
程序猿成长之路之Redis(6)-- redis数据结构之bitmap类型介绍
2022-03-06 12:45:14Bitmap 又称位图,是redis中的一种数据结构,它的出现对于redis来说具有什么意义,它具有哪些作用,它又是如何使用的?它的实现机制又是什么?下面就让我们来走进bitmap的世界。 Bitmap介绍 什么是bitmap? 在文字... -
Android Bitmap 全面详解
2020-11-04 18:33:45每一个 Android App 中都会使用到 Bitmap,它也是程序中内存消耗的大户,当 Bitmap 使用内存超过可用空间,则会报 OOM。 因此如何正确使用也是 Android 工程师的重点关注内容。 Bitmap 占用内存分析 Bitmap 用来描述... -
byte转Bitmap
2021-11-05 15:25:37public static Bitmap byteToBitmap(byte[] imgByte) { InputStream input = null; Bitmap bitmap = null; BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 1; input = ... -
【Android】Bitmap史上最详细全解
2020-05-19 20:33:17深度解剖Bitmap一.Bitmap的相关使用二.Bitmap的格式2.1 存储格式2.2 压缩格式三.Bitmap创建方法3.1 Bitmap.Options3.2 BitmapFactory3.3 Bitmap静态方法3.4 创建Bitmap的总结四.常见函数4.1 函数及其参数4.2 综合... -
YUV 数据和 Bitmap 互相转换
2021-12-17 19:13:13YUV 数据和 Bitmap 互相转换 -
Android bitmap 转RGB数组(三通道)、RGB数组(单、三通道)转回Bitmap、bitmap Resize 512
2020-01-07 11:11:46import android.graphics.Bitmap; import android.graphics.Matrix; /** * bitmap and rgb bytes dual transfer * * @author guochao * @version 1.0 * @since 2019/12/12 */ public class ... -
深入理解Android Bitmap的各种操作
2018-12-15 16:10:27在 Android 开发中,经常和 Bitmap 打交道,不知道你是否真正理解 Bitmap?接下来让我们一起走进 Bitmap 的世界。 一、Bitmap 1.1 Bitmap的创建 在 Bitmap 类中, Bitmap 构造方法默认权限,而且这个类也是 final... -
Redis的Bitmap使用
2022-02-09 22:58:40Redis的Bitmap 在日常开发过程中,经常会有一些 bool 类型数据需要存取。比如记录用户一年内签到的次数,签了是 1,没签是 0。如果使用 key-value 来存储,那么每个用户都要记录 365 次,当用户成百上亿时,需要的... -
Android 超清大尺寸图片压缩转Base64中卡顿/速度优化问题整理(在子线程压缩Bitmap卡的主线程进度条走不动...
2022-04-04 13:58:30*/ private BitmapManager() { } /** * 图片合成 * * @param bitmap 位图1 * @param mark 位图2 * @return Bitmap */ public static Bitmap createBitmap(Bitmap bitmap, Bitmap mark) { int w = bitmap.getWidth()... -
Android自定义控件(八)——详解创建bitmap的方式
2019-11-28 14:28:02本文目录什么是BitmapBitmap格式它是如何存储每个像素点的如何进行压缩防止OOM创建Bitmap 什么是Bitmap Bitmap是绘图中非常重要的概念,在我们前面自定义的所有View中,他们的画布Canvas说到底都其实是Bitmap,... -
【Android 内存优化】Bitmap 内存缓存 ( Bitmap 内存复用 | 弱引用 | 引用队列 | 针对不同 Android 版本...
2020-07-02 15:59:52一、Bitmap 复用池、 二、弱引用 Bitmap 内存释放、 三、从 Bitmap 复用池中获取对应可以被复用的 Bitmap 对象、 1、Android 2.3.3(API 级别 10)及以下的版本、 2、Android 4.4(API 级别 19)以下的版本、 2、在 ...