Android 图片相关知识

前言

开发中图片加载、选择、压缩,一般都使用第三方库如Glide、PictureSelector、Luban,使用起来简单便捷又安全,不会出现莫名Bug。虽说不大可能去解读源码,解读了也不可能完全记住,但至少要知道图片加载、相册选择、图片压缩这些最基本的功能是怎么实现,也就是说自己写要怎么实现。
图片相关的问题很多,如下:
1.图片是如何加载的?
2.三级缓存是怎么实现的?
3.图片压缩是怎么实现的?
4.图片保存、通知图库更新是怎么实现?
5.打开相册选择图片、拍照怎么实现?
6.圆形图片、圆角图片怎么实现?
...
Android中图片先从了解Bitmap 和 BitmapFactory开始。

相关知识

1.Bitmap

Bitmap 在 Android 中指的是一张图片,可以是 png,也可以是 jpg等其他图片格式,它的作用是可以获取图像文件信息,对图像进行剪切、旋转、缩放、压缩等操作,并可以指定格式保存图像文件。
Bitmap类是final类,bitmap可以通过Bitmap.createBitmap(...)、BitmapDrawable.getBitmap()、和BitmapFactory.decodeXXX(...)得到。 Bitmap.createBitmap用于创建Bitmap,比如可以创建一定宽高的空Bitmap;BitmapDrawable一般用在得到画布上的Bitmap;BitmapFactory是解析Bitmap,常见在图片加载、压缩上。
常见如下:

//1.创建一定宽高的空Bimtap
Bitmap result = Bitmap.createBitmap(width, heigth, Bitmap.Config);

//2.Drawable得到Bitmap
Bitmap b = ((BitmapDrawable) drawable).getBitmap();

//3.BitmapFactory解析,decodeResource,decodeFile最终都会调用decodeStream。
BitmapFactory.decodeResource(Resources res, int id);
BitmapFactory.decodeFile(String pathName);
BitmapFactory.decodeStream(InputStream is);

2.BitmapFactory.Options

BitmapFactory.Optinos是用于解码Bitmap时的各种参数控制,参数很多,此处对最常见的做个解释。
inPreferredConfig:色彩模式,默认值为Bitmap.Config.ARGB_8888(每像素占4byte,有透明度),压缩一般使用RGB_565(每像素占2byte,没有透明度);
inJustDecodeBounds:为true时仅返回 Bitmap 宽高属性,不加载Bitmap到内存,返回的Bitmap=null,为false时才返回占内存的 Bitmap;
outputWidth:返回的 Bitmap的宽;
outputHeight:返回的 Bitmap的高;
inSampleSize:表示 Bitmap 的压缩比例,值必须 > 1 & 是2的幂次方。为2是指目标宽高是原宽高的1/2;
...

3.Android的文件目录和缓存目录

android保存文件的路径有5种,分别如下:
getExternalFilesDir(): SDCard/Android/data//files/目录
getFilesDir(): data/data//files/目录
getExternalCacheDir():SDCard/Android/data//cache/目录
getCacheDir(): data/data//cache/目录
Environment.getExternalStorageDirectory(): SDCard/目录
FilesDir一般放一些长时间保存的数据,CacheDir放临时缓存数据,有External的是指外部SD卡。前4个路径下的数据都会随着app被用户卸载而删除,FilesDir 和 CacheDir 分别对应的是 设置->应用->应用详情里面的“清除数据”和”清除缓存“选项。
使用如下:

private static File getCacheDir(){
        if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){
            return App.ctx.getExternalCacheDir();
        }
        return App.ctx.getCacheDir();
    }

private static File getFilesDir(){
        if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){
            return App.ctx.getExternalFilesDir(null); //传null,访问的是files文件夹
        }
        return App.ctx.getFilesDir();
    }

Environment.getExternalStorageDirectory()和前面4个路径的区别是,它不依赖于app。也就是说app卸载,它也不会删除。所以具体使用看情况处理,比如app里有邀请推广二维码的,就不要保存在xxxFilesDir()了,在Environment.getExternalStorageDirectory()创建一个文件夹来保存,如果保存到前面4个路径下,是不会在系统相册显示的。

问题

1.图片是如何加载的?

在Android中, 网络、本地文件、资源id的图片,最终都是解析成Bitmap,系统提供了解析Bitmap的静态工厂方法——BitmapFactory。最常见的三种解析方法如下:

//加载资源id
BitmapFactory.decodeResource(Resources res, int id);
//加载本地图片
BitmapFactory.decodeFile(String pathName);
//加载网络
BitmapFactory.decodeStream(InputStream is);
//其实decodeResource,decodeFile最终都会调用decodeStream。

2.三级缓存是怎么实现的?

原理:内存 -> 文件(本地)->网络

流程:
1)内存,创建LruCache> 作为内存缓存容器,每次从文件或网络加载图片时,要加入缓存中。
2)文件,在缓存目录 getExternalCacheDir() 或 getCacheDir()下找到该文件,用BitmapFactory.decodeFile(xx)得到bitmap, 并将bitmap放入LruCache(内存)中。
3)网络,请求网络流数据,放入内存中且保存File到本地。
选用LruCache的原因如下:

/**
* LruCache其实是一个Hash表,内部使用的是LinkedHashMap存储数据。
* 使用LruCache类可以规定缓存内存的大小,并且这个类内部使用到了最近最少使用算法来管理缓存内存。
* 这里定义 8M的大小作为缓存
*/
private static LruCache> mImageCache = new LruCache<>(1024 * 1024 * 8);
流程处理
public static void load(ImageView iv, String url){
        //1.从内存读取
        SoftReference reference = mImageCache.get(url);
        Bitmap cacheBitmp;
        if(reference != null){
            cacheBitmp = reference.get();
            iv.setImageBitmap(cacheBitmp);
            KLog.d(TAG,"内存中图片显示");
            return;
        }
        //2.从文件读取
        cacheBitmp = getBitmapFromFile(url);
        if(cacheBitmp!=null){
            //bitmap保存到内存
            mImageCache.put(url,new SoftReference(cacheBitmp));
            iv.setImageBitmap(cacheBitmp);
            KLog.d(TAG,"文件中图片显示");
            return;
        }
        //3.连网处理
        getBitmapFromUrl(iv,url);
    }

//网络加载图片,在onResponse()里解析文件流
okHttpClient.newCall(request).enqueue(new Callback() {
            public void onFailure(Call call, IOException e) {

            }
            public void onResponse(Call call, Response response) throws IOException {
                KLog.d(TAG,"文件中图片显示");
                InputStream inputStream = response.body().byteStream();//得到图片的流
                final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                saveBitmap(url,bitmap);  //加入内存缓存、放入cache目录
                if(weakReference.get()!=null){
                    weakReference.get().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            iv.setImageBitmap(bitmap);
                        }
                    });
                }
            }
        });

3.图片压缩

Bitmap常用压缩方法

1)质量压缩
质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度,来达到压缩图片的目的,图片的长,宽,像素都不会改变,那么bitmap所占内存大小是不会变的,但是保存成文件,它的大小会变化的。
注意:质量压缩对png格式图片没效,因为png是无损压缩。
2)宽高压缩
有3种方式改变宽高,采样率压缩(inSampleSize);缩放法压缩(Matrix),通过矩阵对图片进行缩放;Bitmap.createScaledBitmap。常用的是改变inSampleSize。
3)RGB_565压缩
改用内存占用更小的编码格式来达到压缩的效果。Android默认的颜色模式为ARGB_8888,如果对透明度没有要求,可以把颜色模式改为RGB_565,相比ARGB_8888将节省一半的内存开销。

注意点

1)图片的所占的内存大小和很多因素相关,常规方法bitmap.getByteCount()得到的内存大小不一定准确,但用来判断内存大小是否改变时可以用它。
2)Bitmap所占内存大小和文件大小不是一样的,所占内存比文件大得多。
3)质量压缩不改变所占内存大小。

实例

图片要求,宽1080,高1920,文件大小不超过1M。
先进行宽高压缩,再进行质量压缩,最终通过BitmapFactory.decodeByteArray 得到目标Bitmap,在解析的时候改成RGB_565颜色模式,可以少占一半的内存,如果不改成RGB_565,可以看到质量压缩前后,所占内存是没有变化的。

    /**
     * 压缩图片
     * 压缩要求,宽1080,高1920,文件大小不超过1M。
     * @param path  图片路径
     * */
    public static Bitmap getCompressBitmap(String path){
        Bitmap bitmap = getResizeBitmap(path,1080,1920) ;
        return getQualityBitmap(bitmap,1024);
    }

    /**
     * 宽高压缩
     * @param filePath  文件路径
     * @param width     目标宽度
     * @param height    目标高度
     * @return
     */
    public static Bitmap getResizeBitmap(String filePath, int width, int height) {
        Bitmap bitmap = null;
        File f = new File(filePath);
        if (f.exists() && f.length() > 0) {
            try {
                BitmapFactory.Options options = new BitmapFactory.Options();
                //只取宽高
                options.inJustDecodeBounds = true;
                BitmapFactory.decodeFile(filePath, options);
                int picWidth = options.outWidth;
                int picHeight = options.outHeight;
                KLog.e(TAG, "宽高压缩前图片宽度="+ picWidth + ",高度=" + picHeight);

                //如果原图,宽比高大,则 宽/height,高/width比。否则,宽/width,高/height比。
                if(picWidth>picHeight && (picWidth > height || picHeight > width)){
                    options.inSampleSize = Math.max(options.outWidth / height, options.outHeight / width);
                }else if(picWidth > width || picHeight > height){
                    options.inSampleSize = Math.max(options.outWidth / width, options.outHeight / height);
                }else{
                    options.inSampleSize = 1;
                }
                options.inJustDecodeBounds = false;
                bitmap = BitmapFactory.decodeFile(filePath, options);
                KLog.e(TAG, "宽高压缩后图片宽度="+ bitmap.getWidth() + ",高度=" + bitmap.getHeight()
                        + ",所占内存大小=" + bitmap.getByteCount()/ 1024 +"KB");
            } catch (OutOfMemoryError e) {
                e.printStackTrace();
            }
        }
        return bitmap;
    }

    /**
     * 质量压缩
     * 这个方法只会改变图片的存储大小,不会改变bitmap的大小
     * @param bitmap  bitmap
     * @param maxFileSize 最大大小
     * @return Bitmap 压缩后bitmap
     */
    public static Bitmap getQualityBitmap(Bitmap bitmap, int maxFileSize) {
        if(bitmap == null){
            return null;
        }
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int quality = 100;
        bitmap.compress(Bitmap.CompressFormat.JPEG,quality,baos);
        int baosLength = baos.toByteArray().length;
        KLog.e(TAG, "质量压缩前所占内存大小=" + (bitmap.getByteCount() / 1024 +"KB")
                + ",文件大小(bytes.length)=" + (baosLength/ 1024) + "KB"
                + ",quality=" + quality);
        while (baosLength/1024 > maxFileSize){
            //清空baos
            baos.reset();
            quality = quality <= 10 ? quality - 1 : quality - 10;
            if (quality == 0) {
                break;
            }
            bitmap.compress(Bitmap.CompressFormat.JPEG,quality,baos);
            //将压缩后的图片保存到baos中
            baosLength = baos.toByteArray().length;
        }
        KLog.e(TAG, "质量压缩后所占内存大小=" + (bitmap.getByteCount() / 1024 +"KB")
                + ",文件大小(bytes.length)=" + (baosLength/ 1024) + "KB"
                + ",quality=" + quality);
        bitmap.recycle();
        bitmap = null;

        //最终目标Bitmap是经过压缩后,再decodeByteArray出来的,而decodeByteArray默认的是ARGB_8888,为了减少内存占用,
        //要用RGB_565编码解析。
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        Bitmap targetBitmap = BitmapFactory.decodeByteArray(baos.toByteArray(),0,baosLength,options);
        KLog.e("BitmapUtils", "最终解析后所占内存大小" + (targetBitmap.getByteCount() / 1024 + "KB"));
        return targetBitmap;
    }

最终打印数据:


打印结果.png

可以看到宽高压缩了,但是宽高都比我们希望的1080和1920大,是因为inSampleSize是整数,而用2592/1080 或者 4608/1920得到2.4,取整就是2了。如果是要严格不大于希望的值,可以用个while循环去继续调整inSampleSize,我项目中不处理是因为,再/2得到的图片太小了。质量压缩前后了10%,文件大小也比1M小了,内存大小没有变化,之所以最终解析出内存少了一半,是因为用了RGB_565。图片压缩大概流程就是这样,具体使用根据需求进行修改。

4.图片保存、通知图库更新的代码实现。

图片保存就是保存文件,用文件流或输出流都行。
考虑使用Context.getExternalFilesDir() 还是 Environment.getExternalStorageDirectory(),两者区别是前者会随着app删除而删除,且不会更新到图库。后者不会删除,可以更新到图库。通知图库更新发送一个广播即可。

/**
     * 保存图片到 /storage/emulated/0//DASImage/ 下
     * 且更新到图库
     * @param bitmap
     * @param fileName
     * @return 是否保存成功
     */
    public static boolean saveImageInSdCard(Bitmap bitmap, String fileName){
        boolean isSuccess = false;
        if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){
            String path = Environment.getExternalStorageDirectory().getAbsolutePath() +
                    File.separator + App.ctx.getPackageName() +  File.separator + "DASImage";
            File dirFile = new File(path);
            if (!dirFile.exists()) {
                dirFile.mkdirs();
            }
            File file = new File(path, fileName + ".jpg");
            try {
                FileOutputStream out = new FileOutputStream(file);
                //format:JPEG, PNG 和 WEBP,保存JPEG比PNG格式的文件小。
                isSuccess = bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
                out.flush();
                out.close();

                //通知图库更新
                Uri uri = Uri.fromFile(file);
                App.ctx.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
            }catch (IOException e) {
                e.printStackTrace();
            }
            KLog.e(TAG,"Bitmap已保存至" + file.getAbsolutePath());
        }
        return isSuccess;
    }

5.打开相册选择图片、拍照的代码实现。

相册选择和拍照要优化的地方很多,比如选择图片如何选多张图片、拍照的Uri问题、拍照保存的图片路径、图片剪切、加载图片太大等问题。项目中还是用第三方库好些,例如这个https://github.com/LuckSiege/PictureSelector,连权限都写上了。。。

调取系统相册和拍照的关键代码如下:

    public static final int REQUEST_TAKEPHOTO = 1;       // 拍照
    public static final int REQUEST_GALLERY = 2;         // 从相册中选择
    /**
     * 相册选取
     */
    private void onGallery() {
        Intent intent = new Intent(Intent.ACTION_PICK, null);
        intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
        startActivityForResult(intent, REQUEST_GALLERY);
    }

    /**
     * 拍照
     */
    private void onCamera() {
        if (AppUtils.hasSdcard()) {
            //1.创建图片文件夹
            String path = Environment.getExternalStorageDirectory().getAbsolutePath() +
                    File.separator + App.ctx.getPackageName() +  File.separator + "DASImage";
            imagePath = path +  File.separator + BitmapUtils.getFileName() + ".jpg";
            //创建目录
            File dirFile = new File(path);
            if (!dirFile.exists()) {
                dirFile.mkdirs();
            }
            File file = new File(imagePath);
            //2.获取Uri
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
                imageUri = FileProvider.getUriForFile(getActivity(), getActivity().getPackageName() + ".fileProvider", file);
            }else{
                imageUri = Uri.fromFile(file);
            }
            //3.拍照
            Intent it = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            it.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
            startActivityForResult(it, REQUEST_TAKEPHOTO);
        } else {
            ToastUtils.showToast("SdCard不存在,不允许拍照");
        }
    }

 @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case REQUEST_TAKEPHOTO:  //拍照
                    //data为null,因为是保存在指定路径下,所以获取图片,直接拿那个路径即可
                    //imageUri为content://com.sz.dzh.dandroidsummary.fileProvider/my_images/DAS_1562837817999.jpg
                    
                    break;
                case REQUEST_GALLERY:  //画库选择图片
                    //data不为null,content://media/external/file/1710928 flg=0x1
                    if (data != null) {
                        
                    }
                    break;
            }
        }
    }

6.圆形图片、圆角图片等如何实现?

圆形图片、圆角图片的方式有很多。
如果是用Glide,可以写个转换器完成,转换器继承BitmapTransformation,对Bitmap做操作,最后画圆画or画圆角,Glide的转换器网上有很好的库——glide-transformations(链接:https://github.com/wasabeef/glide-transformations)。或者自定义ImageView,在onDraw方法里canvas.drawCircle(...)画圆、用canvas.clipPath(...)裁剪画布等。不管什么方式,最终都是在onDraw方法,对Bitmap进行处理,再画出来。涉及的内容就是Canvas、Bitmap、BitmapShader、Paint、Xfermode等。(链接:https://blog.csdn.net/shenggaofei/article/details/83793536)

参考:

Android性能优化:Bitmap详解&你的Bitmap占多大内存?
深入理解Android Bitmap的各种操作
Android 第三方RoundedImageView设置各种圆形、方形头像

你可能感兴趣的:(Android 图片相关知识)