Bitmap精炼详解第(三)节:Bitmap的压缩

一,前期基础知识储备

笔者之前有两篇文章:《Bitmap精炼详解第(一)节:Bitmap解析和加载》《Bitmap精炼详解第(二)节:Bitmap常见处理方式》解释了一些Bitmap的基础知识,有兴趣的读者可自行查看。

1)如何计算Bitmap占用的内存

  • 如果图片要显示下Android设备上,ImageView最终是要加载Bitmap对象的,就要考虑单个Bitmap对象的内存占用了,如何计算一张图片的加载到内存的占用呢?其实就是所有像素的内存占用总和:
  • bitmap内存大小 = 图片长度 x 图片宽度 x 单位像素占用的字节数
  • 起决定因素就是最后那个参数了,Bitmap'常见有2种编码方式:ARGB_8888和RGB_565,ARGB_8888每个像素点4个byte,RGB_565是2个byte,一般都采用ARGB_8888这种。那么常见的1080*1920的图片内存占用就是:1920 x 1080 x 4 = 7.9M。

使用Bitmap的API可直接获取占用内存大小:

  • getByteCount()
    • getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。API19开始getAllocationByteCount()方法代替了getByteCount()。
  • getAllocationByteCount()
    • API19之后,Bitmap加了一个Api:getAllocationByteCount();代表在内存中为Bitmap分配的内存大小。

2)Bitmap常见的四种颜色格式

  • 位图文件(Bitmap),扩展名可以是.bmp或者.dib。位图是Windows标准格式图形文件,它将图像定义为由点(像素)组成,每个点可以由多种色彩表示,包括2、4、8、16、24和32位色彩。位图文件是非压缩格式的,需要占用较大存储空间。

3)Bitmap的压缩方法

Bitmap的压缩技术共有三种:

缩放法压缩

原理:Android中使用Matrix对图像进行缩放、旋转、平移、斜切等变换的。

  • Matrix提供了一些方法来控制图片变换:Matrix调用一系列set,pre,post方法时,可视为将这些方法插入到一个队列。当然,按照队列中从头至尾的顺序调用执行。其中pre表示在队头插入一个方法,post表示在队尾插入一个方法。而set表示把当前队列清空,并且总是位于队列的最中间位置。当执行了一次set后:pre方法总是插入到set前部的队列的最前面,post方法总是插入到set后部的队列的最后面。

缩放法压缩工具类代码:

/**
 * 按缩放压缩
 *
 * @param src                   源图片
 * @param newWidth              新宽度
 * @param newHeight             新高度
 * @param recycle               是否回收
 * @return                      缩放压缩后的图片
 */
public static Bitmap compressByScale(final Bitmap src, final int newWidth, final int newHeight, final boolean recycle) {
    return scale(src, newWidth, newHeight, recycle);
}

public static Bitmap compressByScale(final Bitmap src, final float scaleWidth, final float scaleHeight, final boolean recycle) {
    return scale(src, scaleWidth, scaleHeight, recycle);
}

/**
 * 缩放图片
 *
 * @param src                   源图片
 * @param scaleWidth            缩放宽度倍数
 * @param scaleHeight           缩放高度倍数
 * @param recycle               是否回收
 * @return                      缩放后的图片
 */
private static Bitmap scale(final Bitmap src, final float scaleWidth, final float scaleHeight, final boolean recycle) {
    if (src == null || src.getWidth() == 0 || src.getHeight() == 0) {
        return null;
    }
    Matrix matrix = new Matrix();
    matrix.setScale(scaleWidth, scaleHeight);
    Bitmap ret = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), matrix, true);
    if (recycle && !src.isRecycled()) {
        src.recycle();
    }
    return ret;
}

② 质量压缩

原理:质量压缩方法:在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,这样适合去传递二进制的图片数据,比如分享图片,要传入二进制数据过去,限制500kb之内。

  • 1、bitmap图片的大小不会改变(注意包括图片占用的内存和图片本身的尺寸)
  • 2、bytes.length是随着quality变小而变小的。

质量法压缩工具类代码:

/**
 * 质量压缩法
 * @param image     目标原图
 * @param maxSize   最大的图片大小
 * @return          bitmap,注意可以测试以下压缩前后bitmap的大小值
 */
public static Bitmap compressImage(Bitmap image , long maxSize) {
    int byteCount = image.getByteCount();
    Log.i("yc压缩图片","压缩前大小"+byteCount);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    // 把ByteArrayInputStream数据生成图片
    Bitmap bitmap = null;
    // 质量压缩方法,options的值是0-100,这里100表示原来图片的质量,不压缩,把压缩后的数据存放到baos中
    image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    int options = 90;
    // 循环判断如果压缩后图片是否大于maxSize,大于继续压缩
    while (baos.toByteArray().length  > maxSize) {
        // 重置baos即清空baos
        baos.reset();
        // 这里压缩options%,把压缩后的数据存放到baos中
        image.compress(Bitmap.CompressFormat.JPEG, options, baos);
        // 每次都减少10,当为1的时候停止,options<10的时候,递减1
        if(options == 1){
            break;
        }else if (options <= 10) {
            options -= 1;
        } else {
            options -= 10;
        }
    }
    byte[] bytes = baos.toByteArray();
    if (bytes.length != 0) {
        // 把压缩后的数据baos存放到bytes中
        bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
        int byteCount1 = bitmap.getByteCount();
        Log.i("yc压缩图片","压缩后大小"+byteCount1);
    }
    return bitmap;
}


/**
 * 质量压缩法
 *
 * @param src           源图片
 * @param maxByteSize   允许最大值字节数
 * @param recycle       是否回收
 * @return              质量压缩压缩过的图片
 */
public static Bitmap compressByQuality(final Bitmap src, final long maxByteSize, final boolean recycle) {
    if (src == null || src.getWidth() == 0 || src.getHeight() == 0 || maxByteSize <= 0) {
        return null;
    }
    Log.i("yc压缩图片","压缩前大小"+src.getByteCount());
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    src.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    byte[] bytes;
    if (baos.size() <= maxByteSize) {// 最好质量的不大于最大字节,则返回最佳质量
        bytes = baos.toByteArray();
    } else {
        baos.reset();
        src.compress(Bitmap.CompressFormat.JPEG, 0, baos);
        if (baos.size() >= maxByteSize) { // 最差质量不小于最大字节,则返回最差质量
            bytes = baos.toByteArray();
        } else {
            // 二分法寻找最佳质量
            int st = 0;
            int end = 100;
            int mid = 0;
            while (st < end) {
                mid = (st + end) / 2;
                baos.reset();
                src.compress(Bitmap.CompressFormat.JPEG, mid, baos);
                int len = baos.size();
                if (len == maxByteSize) {
                    break;
                } else if (len > maxByteSize) {
                    end = mid - 1;
                } else {
                    st = mid + 1;
                }
            }
            if (end == mid - 1) {
                baos.reset();
                src.compress(Bitmap.CompressFormat.JPEG, st, baos);
            }
            bytes = baos.toByteArray();
        }
    }
    if (recycle && !src.isRecycled()){
        src.recycle();
    }
    Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    Log.i("yc压缩图片","压缩后大小"+bitmap.getByteCount());
    return bitmap;
}


/**
 * 质量压缩法
 *
 * @param src     源图片
 * @param quality 质量
 * @param recycle 是否回收
 * @return 质量压缩后的图片
 */
public static Bitmap compressByQuality(final Bitmap src, @IntRange(from = 0, to = 100) final int quality, final boolean recycle) {
    if (src == null || src.getWidth() == 0 || src.getHeight() == 0) {
        return null;
    }
    Log.i("yc压缩图片","压缩前大小"+src.getByteCount());
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    src.compress(Bitmap.CompressFormat.JPEG, quality, baos);
    byte[] bytes = baos.toByteArray();
    if (recycle && !src.isRecycled()) {
        src.recycle();
    }
    Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    Log.i("yc压缩图片","压缩后大小"+bitmap.getByteCount());
    return bitmap;
}

③ 采样率压缩

原理:—《Android开发艺术探索》“通过BitmapFactory.Options来缩放图片,主要利用了他的inSampleSize参数,即采样率。当inSampleSize为1时,采样后的图片大小为原始图片的原始大小;当imSampleSize大于1时,比如为2,那么采样后的图片其宽/高均为原图大小的1/2,而像素为原图的1/4,其占有的内存大小也为原图的1/4。拿一张1024*1024像素的图片来说,假定采用ARGB8888格式存储,那么他占有的内存为1024*1024*4,即4MB,如果imSampleSIze为2,那么采样后的图片其内存占用只有512*512*4,即1MB。可以发现采样率imSampleSize必须是大于1的整数才会有缩小的效果,并且采样率同时作用于宽、高,这导致缩放后的图片大小以采用率的2次方递减,即缩放比例为1/(imSamolSize的2次方),比如inSamoleSize为4,那么缩放比例就是1/16。例外最新的官方文档指出,imSampleSize的取值应该总是为2的指数,比如1,2,4,8,16等等。如果外界传递给系统的inSamoleSize不为2的指数,那么系统会向下取整并选择最接近的2的指数来代替,比如2,系统会选择2来代替,但是经过验证发现这个结论并非在所有的Android版本上都成立,因此这只是一个建议。”

注:与质量压缩法正相反:图片占据的内存和图片本身的尺寸都会发生变化

采样法压缩工具类代码:

/**
 * 按采样大小压缩
 *
 * @param src        源图片
 * @param sampleSize 采样率大小
 * @param recycle    是否回收
 * @return 按采样率压缩后的图片
 */
public static Bitmap compressBySampleSize(final Bitmap src, final int sampleSize, final boolean recycle) {
    if (src == null || src.getWidth() == 0 || src.getHeight() == 0) {
        return null;
    }
    Log.i("yc压缩图片","压缩前大小"+src.getByteCount());
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inSampleSize = sampleSize;
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    src.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    byte[] bytes = baos.toByteArray();
    if (recycle && !src.isRecycled()) {
        src.recycle();
    }
    Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
    Log.i("yc压缩图片","压缩后大小"+bitmap.getByteCount());
    return bitmap;
}


/**
 * 第二种:按采样大小压缩
 *
 * @param src       源图片
 * @param maxWidth  最大宽度
 * @param maxHeight 最大高度
 * @param recycle   是否回收
 * @return 按采样率压缩后的图片
 */
public static Bitmap compressBySampleSize(final Bitmap src, final int maxWidth, final int maxHeight, final boolean recycle) {
    if (src == null || src.getWidth() == 0 || src.getHeight() == 0) {
        return null;
    }
    Log.i("yc压缩图片","压缩前大小"+src.getByteCount());
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    src.compress(Bitmap.CompressFormat.JPEG, 100, baos);
    byte[] bytes = baos.toByteArray();
    BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
    options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
    options.inJustDecodeBounds = false;
    if (recycle && !src.isRecycled()) {
        src.recycle();
    }
    Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
    Log.i("yc压缩图片","压缩后大小"+bitmap.getByteCount());
    return bitmap;
}

/**
 * 计算获取缩放比例inSampleSize
 */
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
        final int heightRatio = Math.round((float) height / (float) reqHeight);
        final int widthRatio = Math.round((float) width / (float) reqWidth);
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
    }
    final float totalPixels = width * height;
    final float totalReqPixelsCap = reqWidth * reqHeight * 2;
    while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
        inSampleSize++;
    }
    return inSampleSize;
}

注意:使用采样率压缩有两种方法

确定采样率inSampleSize:直接传入Bitmap和samplsize即可;

不确定采样率inSampleSize:所以此时需要传入支持的最大宽高,即ImageView可以显示的最大宽高。比如最大可传入屏幕的宽高。后续的算法中,会根据此最大宽高,自行进行采样率samplsize的计算。

二,上代码,具体实现

在此安利一个工具类:ImageUtils,来源于开源项目

implementation 'com.blankj:utilcode:1.23.5'

以下就是ImageUtils中的所有方法,其中就有快速获取Bitmap实例和压缩图片的方法。压缩方法刚好涵盖上面三种方法。

实践上述代码,进行图片压缩:

protected void init() {
        super.init();
        screenWidth = ScreenUtils.getScreenWidth();
        screenHeight = ScreenUtils.getScreenHeight();
        Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.wall_5);
        Log.d(TAG, "init: bitmap1,," + bitmap1.getWidth() + ",," + bitmap1.getHeight());

        Bitmap compressBitmap1 = CompressUtil.compressBySampleSize(bitmap1, screenWidth, screenHeight, false);
        Log.d(TAG, "init: compressBitmap1,," + compressBitmap1.getWidth() + ",," + compressBitmap1.getHeight());

        /*按照缩放压缩*/
        Bitmap compressBitmap2 = ImageUtils.compressByScale(bitmap1, 0.5f, 0.5f);
        Log.d(TAG, "init: compressBitmap2,," + compressBitmap2.getWidth() + ",," + compressBitmap2.getHeight());

        /*按照质量压缩*/
        Bitmap compressBitmap3 = ImageUtils.compressByQuality(bitmap1, 50);
        Log.d(TAG, "init: compressBitmap3,," + compressBitmap3.getWidth() + ",," + compressBitmap3.getHeight());
        Bitmap compressBitmap4 = ImageUtils.compressByQuality(bitmap1, 10L * 1024);
        Log.d(TAG, "init: compressBitmap4,," + compressBitmap4.getWidth() + ",," + compressBitmap4.getHeight());

        /*按照采样率压缩*/
        Bitmap compressBitmap5 = ImageUtils.compressBySampleSize(bitmap1, 2);
        Log.d(TAG, "init: compressBitmap5,," + compressBitmap5.getWidth() + ",," + compressBitmap5.getHeight());

        img1.setImageBitmap(bitmap1);
        img2.setImageBitmap(compressBitmap1);
        img3.setImageBitmap(compressBitmap2);
        img4.setImageBitmap(compressBitmap3);
        img5.setImageBitmap(compressBitmap4);
        img6.setImageBitmap(compressBitmap5);
        /*计算各个Bitmap占据的内存大小*/
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            int size1 = bitmap1.getAllocationByteCount();
            int size2 = compressBitmap1.getAllocationByteCount();
            int size3 = compressBitmap2.getAllocationByteCount();
            int size4 = compressBitmap3.getAllocationByteCount();
            int size5 = compressBitmap4.getAllocationByteCount();
            int size6 = compressBitmap5.getAllocationByteCount();
            Log.d(TAG, "init: size1," + size1);
            Log.d(TAG, "init: size2," + size2);
            Log.d(TAG, "init: size3," + size3);
            Log.d(TAG, "init: size4," + size4);
            Log.d(TAG, "init: size5," + size5);
            Log.d(TAG, "init: size6," + size6);
        }

        /*计算各个Bitmap本身实际的大小*/
        File actualImage1 = CompressUtil.getFile(bitmap1);
        File actualImage2 = CompressUtil.getFile(compressBitmap1);
        File actualImage3 = CompressUtil.getFile(compressBitmap2);
        File actualImage4 = CompressUtil.getFile(compressBitmap3);
        File actualImage5 = CompressUtil.getFile(compressBitmap4);
        File actualImage6 = CompressUtil.getFile(compressBitmap5);
        Log.d(TAG, "init: actualImage1," + String.format("Size : %s", CompressUtil.getReadableFileSize(actualImage1.length())));
        Log.d(TAG, "init: actualImage2," + String.format("Size : %s", CompressUtil.getReadableFileSize(actualImage2.length())));
        Log.d(TAG, "init: actualImage3," + String.format("Size : %s", CompressUtil.getReadableFileSize(actualImage3.length())));
        Log.d(TAG, "init: actualImage4," + String.format("Size : %s", CompressUtil.getReadableFileSize(actualImage4.length())));
        Log.d(TAG, "init: actualImage5," + String.format("Size : %s", CompressUtil.getReadableFileSize(actualImage5.length())));
        Log.d(TAG, "init: actualImage6," + String.format("Size : %s", CompressUtil.getReadableFileSize(actualImage6.length())));

    }

得到的结果如下:

以下是对应的Log表现:

可以看到,缩放法和采样法最终得到的结果都是图片尺寸和内存占用都减小了。

但是,都没有减小图片本身的实际尺寸。在一些场合中,我们需要减小的不仅是图片的内存占用,而是整个图片本身的大小,比如,图片的上传,如微信使用时,朋友之间发送图片,此时的图片本身的大小应该要被压缩。

 

三,上代码,压缩图片本身的大小

讲到微信,讲到压缩,就必须提到Luban——最接近微信压缩算法的男人。

Github地址:https://github.com/Curzibn/Luban

1)添加依赖;

implementation 'top.zibin:Luban::1.1.8'

2)获取Bitmap的File;

注意:Luban内不直接使用Bitmap,而是使用File,所以需要Bitmap专为File后使用。

    private List<File> assetsToFiles() {
        final List<File> files = new ArrayList<>();
        /*读本地图片 转成Bitmap 再转成Uri 最后转成File*/
        Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.wall_5);
        /*Uri uri1 = Uri.parse(MediaStore.Images.Media.insertImage(getActivity().getContentResolver(), bitmap1, null, null));
        try {
            actualImage2 = FileUtil.from(getActivity(), uri1);
            Log.d("Luban", "assetsToFiles: 222," + actualImage2);
        } catch (IOException e) {
            e.printStackTrace();
        }*/

        actualImage1 = getFile(bitmap1);
        Log.d("Luban", "assetsToFiles: 111," + actualImage1);
        files.add(actualImage1);
        originPhotos.add(actualImage1);

        return files;
    }

    private File actualImage1, actualImage2;

    public File getFile(Bitmap bitmap) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos);
        File tempFile = new File(Environment.getExternalStorageDirectory() + "/temp.jpg");
        try {
            tempFile.createNewFile();
            FileOutputStream fos = new FileOutputStream(tempFile);
            InputStream is = new ByteArrayInputStream(baos.toByteArray());
            int x = 0;
            byte[] b = new byte[1024 * 100];
            while ((x = is.read(b)) != -1) {
                fos.write(b, 0, x);
            }
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return tempFile;
    }

注意:可使用Bitmap的Compress方法直接转为File,不需要转为Uri,再进行转换。

3)传入File,使用Luban;

                Luban.with(getActivity())
                        .load(assetsToFiles())
                        .setTargetDir(getPath())
                        .ignoreBy(100)
                        .setFocusAlpha(false)
                        .filter(new CompressionPredicate() {
                            @Override
                            public boolean apply(String path) {
                                return !(TextUtils.isEmpty(path) || path.toLowerCase().endsWith(".gif"));
                            }
                        })
                        .setRenameListener(new OnRenameListener() {
                            @Override
                            public String rename(String filePath) {
                                try {
                                    MessageDigest md = MessageDigest.getInstance("MD5");
                                    md.update(filePath.getBytes());
                                    return new BigInteger(1, md.digest()).toString(32);
                                } catch (NoSuchAlgorithmException e) {
                                    e.printStackTrace();
                                }
                                return "";
                            }
                        })
                        .setCompressListener(new OnCompressListener() {
                            @Override
                            public void onStart() {
                            }

                            @Override
                            public void onSuccess(File file) {
                                Log.i("Luban", file.getAbsolutePath());
                                showResult(originPhotos, file);
                            }

                            @Override
                            public void onError(Throwable e) {
                            }
                        }).launch();

    private String getPath() {
        /*/storage/emulated/0/Luban/image/1571023894623138.jpeg*/
        String path = Environment.getExternalStorageDirectory() + "/Luban/image/";
        /*/storage/emulated/0/Android/data/com.intelligent.octobertest/files/Luban/image/1571024498251255.jpeg*/
        String path1 = getActivity().getExternalFilesDir("") + "/Luban/image/";
        LubanFile = new File(path1);
        if (LubanFile.mkdirs()) {
            return path1;
        }
        return path1;
    }

这里使用Luban的常规用法,实际上Luban还提供了RxJava的方式,可根据自己的需求进行选择。

得到的结果如下:

可以看到,出来图片尺寸的缩小,最重要图片本身的大小也进行了压缩,达到了我们的需求。

三,FileAndroid文件夹的正确用法

最后再延伸一下关于File文件夹的用法,可以看到为压缩图片设置目标文件夹的方法中:

    private String getPath() {
        /*/storage/emulated/0/Luban/image/1571023894623138.jpeg*/
        String path = Environment.getExternalStorageDirectory() + "/Luban/image/";
        /*/storage/emulated/0/Android/data/com.intelligent.octobertest/files/Luban/image/1571024498251255.jpeg*/
        String path1 = getActivity().getExternalFilesDir("") + "/Luban/image/";
        LubanFile = new File(path1);
        if (LubanFile.mkdirs()) {
            return path1;
        }
        return path1;
    }

我们在里面使用了两个方法去设置文件地址,得到的结果如上,这实际上是两个不同的目录。

/storage/emulated/0/Luban/image/1571023894623138.jpeg
/storage/emulated/0/Android/data/com.intelligent.octobertest/files/Luban/image/1571024498251255.jpeg

1)提纲挈领:文件存储的三个部分

应用操作的文件存储位置分为三个部分

  1. 应用内部存储私有文件目录

  2. 应用外部存储私有文件目录

  3. 公有目录

我们有两种api去获取这三个部分的存储位置,它们分别归属于Context和Environment。

Context

Context是应用的上下文,它用来获取与应用相关的文件目录,可以获取应用私有和应用公有目录,常用的api有(后面是所对应的路径):

1. Context#getCacheDir() /data/user/0/cn.appname.xxx/cache
2. Context#getDir("spanner",MODE_PRIVATE) /data/user/0/cn.appname.xxx/app_spanner
3. Context#getFileDir() /data/user/0/cn.appname.xxx/files
3. Context#getExternalCacheDir() /storage/emulated/0/Android/data/cn.appname.xxx/cache
4. Context#getExternalFilesDir(Environment.DIRECTORY_PICTURES) /storage/emulated/0/Android/data/cn.appname.xxx/files/Pictures
Context#getExternalFilesDir(null) /storage/emulated/0/Android/data/cn.appname.xxx/files
5. Context#getExternalMediaDirs() /storage/emulated/0/Android/media/cn.appname.xxx

前两个是应用内部存储私有目录,后面4个都是应用外部存储私有文件目录。注意:/data/user/0/ 等同于 /data/data/。

Environment

Environment和应用无关,它用于获取公有存储位置的文件目录,常用的api有:

1. Environment#getExternalStorageDirectory() /storage/emulated/0
2. Environment#getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) /storage/emulated/0/DCIM
Environment#getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) /storage/emulated/0/Pictures
3. Environment#getDataDirectory() /data
4. Environment#getDownloadCacheDirectory() /data/cache
5. Environment#getRootDirectory() /system

2)到底什么时候要用什么api呢?

应用私有文件目录

应用私有目录由Context获取控制,分为内部存储外部存储,内部存储不需要申请文件读写权限也能够使用,外部存储需要权限(getetExternalCacheDir() 和 getExternalFilesDir() 这两个方法从4.4之后不再需要读写权限)。用户对app进行数据清理或卸载可以清理外部存储和内部存储下的所有文件目录。

内部存储

内部存储的文件夹其他应用和用户无法直接访问,可以用于存放敏感数据。

  • getCacheDir()

    • 专门用于存放缓存数据。

    • 用户对app进行缓存清理的时候会清理缓存目录cache的数据,手机空间不足的时候系统也会对缓存目录内的数据进行清理。但尽管如此,开发者仍要管理好缓存数据特别是内部存储的缓存,避免缓存数据过大。

  • getFileDir()

    • 可用于用于存放私有持久文件。

    • 非常适合用于存放app各种伴随app运行周期所需要的文件数据,它既不会因为手机存储空间不足而被清理,也不会因卸载app而遗留数据垃圾,并且它是私有的。

  • getDir(String name,int mode)

    • 归类存放私有文件。

    • 在内部私有目录下会创建一个名为app_name的文件夹,mode以前是可以设置文件夹私有(MODE_PRIVATE)和公有的(MODE_WORLD_READABLE、MODE_WORLD_WRITEABLE),但目前公有的mode都已经废弃,意味着这个api创建的文件夹已经完全私有,不能再共享出去了。

外部存储

在Android Q之前其他应用是可以访问修改外部存储的应用私有目录的,这个要注意。

使用外部存储之前一定要检查外部存储是否可用,因为旧设备不一定会有外部存储,新手机也不一定会给你读写权限,就算用户不给你权限,你的app也要运行啊,不然就不用你的了。

public static boolean isSDCardEnable() {
  return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
  • getExternalCacheDir()

    • 专门用于存放缓存数据。和内部存储的getCacheDir()相似。

  • getExternalFilesDir(String type)

    • 归类存放公有文件。

    • 如果type不为null的话在外部私有目录下创建返回一个名为type的文件夹,为null直接返回外部私有根目录。如无特别需要,个人的做法是传入Environment的DIRECTORY常量进行文件夹创建。

    • 如果看完这篇你还不会选用api,那就把你应用杂七杂八的东西都放进去吧。文件至少不用东一件西一件的,卸载之后也能够被正确清理掉,不过也要控制好占用的存储空间。

  • getExternalMediaDirs()

    • 可存放共享媒体文件。

    • 这个是在Android 5.0加入的api,创建和获取位于/sdcard/Android/media目录下的应用目录,该目录下的文件能够被其他应用访问和被MediaStore查询和获取。但目前较少开发者在使用这个api。

公有目录

获取公有目录要使用Environment的Api,它返回的目录全都是共享的公有目录。造成Android手机文件存储混乱的罪魁祸首!为数众多的无责任开发者在这里胡乱创建文件夹,乱起名、乱放文件,普通用户根本无法判断哪些文件夹、文件是有用的,卸载app之后留下庞大的无法清理的垃圾文件,导致手机空间不足。于是它们在Android Q被废弃了,但是Q之前还是能好好使用的,我认为要开始减少使用它们,更多地使用Context下的私有目录API。

  • getExternalStorageDirectory()

    • 获取外部存储(SD卡)的根目录。使用getExternalStoragePublicDirectory(String)进行替代即可。

  • getExternalStoragePublicDirectory(String type)

    • 使用频率极高的api,返回在根目录下的名为type的文件夹,我把它分为两种用法:一种是传入Environment的DIRECTORY常量再创建子目录使用;一种是传入appPackageName或者易被识别归属的名称创建子目录使用。前者会比较通用,内容可以被各种工具app搜索发现(包括微信);后者算是私用,可以存放不跟随app生命的文件,即卸载后也可以保留。

    • Environment.DIRECTORY_DCIM是手机的相册,这个文件夹都是系统相关的app在用,存放相机拍摄的图片,手机截图之类的,不推荐开发者使用这个文件夹,避免混乱。值得一提的是淘宝有在使用这个文件夹,用于保存它的商品分享截图,这个位置的确可以避免被微信封杀~哈哈

    • Environment.DIRECTORY_PICTURES用于存放各种“正式的”图片,强烈建议在这里创建文件夹存放你想要被用户发现的图片,并且微信会扫描这个文件夹,让你的图片更容易分享。不过还。

    • Environment.DIRECTORY_DOWNLOADS可以用于存放app更新的apk等下载资源。

    • 其他几个比较少用就不介绍了。

获取文件路径这件事永远不能写死某个路径,不存在SD卡怎么办呢?某个路径无法使用了怎么办呢?所以管理文件的时候必须要有存储策略。比如一个文件的保存地址获取方法里不能只有一个api,要保有兜底措施,如果我不能存在外部储存,那我就存在内部,保证app的功能正常运行。

 

 

 

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值