首先,在Android Studio项目中有两种放置图片的资源文件:
drawable资源目录: 不管是jpg、png、还是9.png,都可以放在这里。除此之外,还有像selector这样的xml文件也是可以放在drawable文件夹下面的。
mipmap资源目录: 一般只是用来放置应用程序的icon的。当然,并非是硬性规定,就是想放图片也可以。使用的方式与drawable一样。
Bitmap的使用
Android中提供了BitmapFactory工具类用以创建不同来源的Bitmap。 比如解析资源文件,本地文件,流等方式。 基本流程都很类似,读取目标文件,转换成输入流,调用native方法解析流,虽然Java层代码没有体现,但是可以猜想到,最后native方法解析完成后,必然会通过JNI调用Bitmap的构造函数(Bitmap的构造函数,是一个给native层调用的方法),完成Java层的Bitmap对象创建。
示例:
1 2 3 4 5 6 Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher);ivImage.setImageBitmap(bitmap);
Bitmap占用的内存
Bitmap作为位图,需要读入一张图片每一个像素点的数据,其主要占用的内存也正是这些像素数据。
Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小
单个像素的字节大小 单个像素的字节大小由Bitmap的一个可配置的参数Config(枚举类Config)来决定 注:在Android系统中,默认Bitmap加载图片,使用24位真彩色模式。
Config
占用字节大小(byte)
说明
ALPHA_8
1
单透明通道
RGB_565
2
简易RGB色调
ARGB_4444
4
已废弃
ARGB_8888
4
24位真彩色
RGBA_F16
8
Android 8.0 新增(更丰富的色彩表现HDR
HARDWARE
Special
Android 8.0 新增 (Bitmap直接存储在graphic memory
示例 1 2 3 4 5 6 7 8 9 Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.img);ivImage.setImageBitmap(bitmap); Log.e("TAG" ,bitmap.getWidth()+"--" +bitmap.getHeight()); Log.e("TAG" ,bitmap.getByteCount()+"" ); Log.e("TAG" ,bitmap.getConfig()+"" ); Log.e("TAG" ,bitmap.getDensity()+"" );
可以看到,源图片的尺寸数据,和decode到内存中的Bitmap的横纵像素数量实际上缩小了 3 倍。 通过源码,bitmap最终通过canvas绘制出来,而canvas在绘制之前,有一个scale的操作,scale的值由: scale = (float) targetDensity / density;这一行代码决定。 即缩放的倍率和targetDensity和density相关,而这两个参数都是从传入的options中获取到的。 Options是BitmapFactory中的一个静态内部类,用于配置Bitmap在decode时的一些参数。
inDensity:Bitmap位图自身的密度、分辨率 它与图片存放的资源文件的目录有关,会有不同值:
density | 0.75 | 1 | 1.5 | 2 | 3 | 4 :-:|:-:|:-:|:-:|:-:|:-:|:-:|:-: densityDpi | 0 ~ 120 | 120 ~ 160 | 160 ~ 240 | 240 ~ 320 | 320 ~ 480 | 480 ~ 640 DpiFolder | ldpi | mdpi | hdpi | xhdpi | xxhdpi | xxxhdpi
inTargetDensity: Bitmap最终绘制的目标位置的分辨率(默认情况下,和设备分辨率保持一致,很少手动去赋值)
inScreenDensity: 设备屏幕分辨率(默认情况下,和设备分辨率保持一致,很少手动去赋值)
接下来查看同一张图片在不同资源目录下的参数变化: (测试机参数:华为荣耀7,Android6.0系统,getResources().getDisplayMetrics().density = 3(密度),getResources().getDisplayMetrics().xdpi = 422.03(像素密度),getResources().getDisplayMetrics().ydpi = 424.069(像素密度),默认ARGB_8888)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Log.e("TAG" ,bitmap.getWidth()+"--" +bitmap.getHeight()); Log.e("TAG" ,bitmap.getByteCount()+"" );
由此可见:
同一张图片,放在不同资源目录下,其分辨率会有变化。
bitmap分辨率越高,其解析后的宽高越小,甚至会小于图片原有的尺寸(即缩放),从而内存占用也相应减少。
图片不特别放置任何资源目录时,其默认使用 mdpi 的分辨率。 根据公式 scale = (float) targetDensity / density; 这里的 targetDensity 和设备保持一致为 480 ,mdpi 为 160,所以缩放倍率为 3。
资源目录像素密度和设备像素密度一致时,图片尺寸不会缩放。
在这里,也就是当位于drawable-xxhdpi目录下时。
这里会有个匹配的规则,比如手机屏幕的密度是xxhdpi,那么会先从drawable-xxhdpi目录下寻找资源图片,如果没找到,会优先去更高密度的文件夹下依次寻找,如果还没找到,则会到drawable-nodpi文件夹找这张图,还没有,则到更低密度的文件夹下寻找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。
在宽高设为wrap_content并且图片不是特别大时,在低密度文件中找到时,系统会认为这张图是专门为低密度的设备所设计的,如果直接将这张图在当前的高密度设备上使用就有可能会出现像素过低的情况,于是系统自动帮我们做了这样一个放大操作。反之亦然,如果系统是在drawable-xxxhdpi等更高密度的文件夹下面找到这张图的话,它会认为这张图是为更高密度的设备所设计的,如果直接将这张图在当前设备上使用就有可能会出现像素过高的情况,于是会自动帮我们做一个缩小的操作。
drawable-nodpi文件夹,这个文件夹是一个密度无关的文件夹,放在这里的图片系统就不会对它进行自动缩放,原图片是多大就会实际展示多大。
一般来讲,在开发中也只需要一套图片资源,并优先放在高密度的drawable文件下,比如drawable-xxhdpi目录下,这样也可以节省内存开支。因为一张原图片被缩小了之后显示其实并没有什么副作用,但是一张原图片被放大了之后显示就意味着要占用更多的内存了。因为图片被放大了,像素点也就变多了,而每个像素点都是要占用内存的(所以在上述查看图片在不同资源目录下的参数变化时,并没有drawable-ldpi的数据,因为图片本身已经挺大了,放在ldpi目录下会直接出现OOM)。
因此,关于Bitmap占用内存大小的公式,可以更细化为: Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高 × (设备像素密度/资源目录像素密度)^2 × 每个像素的字节大小 之所以出现平方是因为;横向像素数量 = 图片宽 * (设备像素密度/资源目录像素密度),纵向像素数量 = 图片高× (设备像素密度/资源目录像素密度)。
Bitmap内存优化
Bitmap 的优化主要是通过 BitmapFactory.Options 来根据需要对图片进行采样,采样过程中主要用到了 inSampleSize 参数。
图片占用的内存一般分为”运行时占用的内存”和”存储时本地开销(反映在包大小上)” 关于运行时占用的内存,可以通过以下几种方式来减少内存:
使用低色彩的解析模式,减少单个像素的字节大小。 适用于对色彩丰富程度不高的场景。默认的ARGB8888占用4个字节,若改用RGB565将只占用2字节,大约能减少一半的内存开销,代价是显示的色彩将相对少。
资源文件合理放置,高分辨率图片可以放到高分辨率目录下。 理论上,图片放置的资源目录分辨率越高,其占用内存会越小,但是低分辨率图片会因此被拉伸,显示上出现失真。另一方面,高分辨率图片也意味着其占用的本地储存也变大。
图片缩小,减少尺寸。 理论上根据适用的环境,是可以减少十几倍的内存使用的,它基于这样一个事实:源图片尺寸一般都大于目标需要显示的尺寸,因此可以通过缩放的方式,来减少显示时的图片宽高,从而大大减少占用的内存。
复用和缓存
三级缓存 三级缓存指的是:内存缓存、本地缓存、网络缓存。其各自的特点是内存缓存速度快, 优先读取,本地缓存速度其次, 内存没有该资源信息就去读取本地内存,网络缓存速度较慢(比较对象是内存缓存和本地缓存),假设本地内存也没有,才请求网络获取。
示例 一个图片压缩的小例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 public class MainActivity extends AppCompatActivity { private ImageView ivImage; private ImageView ivCommpressImg; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); initData(); } @Override public void onRequestPermissionsResult (int requestCode, String permissions[], int [] grantResults) { switch (requestCode) { case 11 : { if (grantResults.length > 0 && grantResults[0 ] == PackageManager.PERMISSION_GRANTED) { compress(); } else { } return ; } } } private void initData () { int permissionCheck = ContextCompat.checkSelfPermission(this , Manifest.permission.WRITE_EXTERNAL_STORAGE); Log.e("TAG" ,permissionCheck+"---" ); if (ContextCompat.checkSelfPermission(this , Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.shouldShowRequestPermissionRationale(this , Manifest.permission.WRITE_EXTERNAL_STORAGE)) { compress(); } else { ActivityCompat.requestPermissions(this , new String []{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 11 ); } } Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.img); ivImage.setImageBitmap(bitmap); } private void compress () { Resources res = this .getResources(); BitmapDrawable d = (BitmapDrawable) res.getDrawable(R.drawable.img); Bitmap img = d.getBitmap(); String fn = "image_test.png" ; String path = this .getFilesDir() + File.separator + fn; Log.e("TAG" ,path); try { OutputStream os = new FileOutputStream (path); img.compress(Bitmap.CompressFormat.PNG, 100 , os); os.close(); }catch (Exception e){ Log.e("TAG" , "" , e); } File file = BitmapCompressUtil.compressBitmap(this ,path, 500 ); if (file != null ) { Log.e("TAG" ,"压缩完成:" +file.getPath()); Bitmap bitmap1 = BitmapFactory.decodeFile(file.getPath(), null ); ivCommpressImg.setImageBitmap(bitmap1); } else { Log.e("TAG" ,"file文件为null" ); } } private void initView () { ivImage = findViewById(R.id.iv_img); ivCommpressImg = findViewById(R.id.iv_commpress_img); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 public class BitmapCompressUtil { public static final String PATH = "/pic" ; public static Bitmap compressByResolution (String imgPath, int w, int h) { BitmapFactory.Options opts = new BitmapFactory .Options(); opts.inJustDecodeBounds = true ; BitmapFactory.decodeFile(imgPath, opts); int width = opts.outWidth; int height = opts.outHeight; int widthScale = width / w; int heightScale = height / h; int scale; if (widthScale < heightScale) { scale = widthScale; } else { scale = heightScale; } if (scale < 1 ) { scale = 1 ; } opts.inSampleSize = scale; opts.inJustDecodeBounds = false ; Bitmap bitmap = BitmapFactory.decodeFile(imgPath, opts); return bitmap; } public static File compressBitmap (Context mContext,String srcPath, int ImageSize) { int subtract; Log.e("TAG" ,"图片处理开始.." ); Bitmap bitmap = compressByResolution(srcPath, 1280 , 720 ); if (bitmap == null ) { Log.e("TAG" ,"bitmap 为空" ); return null ; } ByteArrayOutputStream baos = new ByteArrayOutputStream (); int options = 100 ; int angle = readPictureDegree(srcPath); Bitmap bitmapBefore = rotaingImageView(angle, bitmap); bitmapBefore.compress(Bitmap.CompressFormat.JPEG, options, baos); Log.e("TAG" , "图片分辨率压缩后:" + baos.toByteArray().length / 1024 + "KB" ); while (baos.toByteArray().length > ImageSize * 1280 ) { subtract = setSubstractSize(baos.toByteArray().length / 1280 ); baos.reset(); options -= subtract; bitmapBefore.compress(Bitmap.CompressFormat.JPEG, options, baos); Log.e("TAG" ,"图片压缩后:" + baos.toByteArray().length / 1024 + "KB" ); } Log.e("TAG" ,"图片处理完成!" + baos.toByteArray().length / 1280 + "KB" ); String temFile = mContext.getFilesDir() + File.separator; File file = new File (temFile,"index.png" ); boolean saved = false ; try { FileOutputStream fos = new FileOutputStream (file); fos.write(baos.toByteArray()); fos.flush(); fos.close(); saved = true ; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } if (bitmap != null ) { bitmap.recycle(); } return file; } public static int readPictureDegree (String path) { int degree = 0 ; try { ExifInterface exifInterface = new ExifInterface (path); int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: degree = 90 ; break ; case ExifInterface.ORIENTATION_ROTATE_180: degree = 180 ; break ; case ExifInterface.ORIENTATION_ROTATE_270: degree = 270 ; break ; } } catch (IOException e) { e.printStackTrace(); } return degree; } public static Bitmap rotaingImageView (int angle, Bitmap bitmap) { Bitmap returnBm = null ; Matrix matrix = new Matrix (); matrix.postRotate(angle); try { returnBm = Bitmap.createBitmap(bitmap, 0 , 0 , bitmap.getWidth(), bitmap.getHeight(), matrix, true ); } catch (OutOfMemoryError e) { } if (returnBm == null ) { returnBm = bitmap; } if (bitmap != returnBm) { bitmap.recycle(); } return returnBm; } private static int setSubstractSize (int imageMB) { if (imageMB > 1000 ) { return 60 ; } else if (imageMB > 750 ) { return 40 ; } else if (imageMB > 500 ) { return 20 ; } else { return 10 ; } } public static void deleteFiles (File file) { if (file.isDirectory()) { File[] files = file.listFiles(); for (int i = 0 ; i < files.length; i++) { File f = files[i]; deleteFiles(f); } file.delete(); } else if (file.exists()) { file.delete(); }} }
备注
参考资料 :Android中Bitmap内存优化 Android drawable微技巧
传送门 :GitHub