美文网首页
Android 图片相关知识

Android 图片相关知识

作者: Dengszzzzz | 来源:发表于2019-07-15 16:47 被阅读0次

    前言

    开发中图片加载、选择、压缩,一般都使用第三方库如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/<application package>/files/目录
    getFilesDir(): data/data/<application package>/files/目录
    getExternalCacheDir():SDCard/Android/data/<application package>/cache/目录
    getCacheDir(): data/data/<application package>/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<String,SoftReference<Bitmap>> 作为内存缓存容器,每次从文件或网络加载图片时,要加入缓存中。
    2)文件,在缓存目录 getExternalCacheDir() 或 getCacheDir()下找到该文件,用BitmapFactory.decodeFile(xx)得到bitmap, 并将bitmap放入LruCache(内存)中。
    3)网络,请求网络流数据,放入内存中且保存File到本地。
    选用LruCache的原因如下:

    /**
    * LruCache其实是一个Hash表,内部使用的是LinkedHashMap存储数据。
    * 使用LruCache类可以规定缓存内存的大小,并且这个类内部使用到了最近最少使用算法来管理缓存内存。
    * 这里定义 8M的大小作为缓存
    */
    private static LruCache<String, SoftReference<Bitmap>> mImageCache = new LruCache<>(1024 * 1024 * 8);
    
    流程处理
    public static void load(ImageView iv, String url){
            //1.从内存读取
            SoftReference<Bitmap> 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<Bitmap>(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/<application package>/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 图片相关知识

          本文链接:https://www.haomeiwen.com/subject/lwwnkctx.html