Android Study 之聊聊关于图片压缩那点事儿

LZ-Says:

小沈阳版程序员~~~
程序员其实可痛苦的了......需求一做一改,一个月就过去了;嚎~
需求再一改一调,一季度就过去了;嚎~
程序员最痛苦的事儿是啥,知道不?就是,程序没做完,需求又改了;
程序员最最痛苦的事儿是啥,知道不? 就是,系统好不容易做完了,方案全改了;
程序员最最最痛苦的事儿是啥,知道不? 就是,系统做完了,狗日的客户跑了;
程序员最最最最最痛苦的事儿是啥,知道不? 就是,狗日的客户又回来了,程序给删没了!

独乐乐不如众乐乐~

前言

个人认为技术的累积在于不断的一点一滴积累,所谓厚积薄发,便是这个道理。以前写博客总是心里会发怵,为什么呢?说白了还是自己对自己的能力不认可,怕别人说,更怕别人直接怒喷。慢慢的也看开了,原因嘛,有如下几点:

  • 写的东西也没几个人看,还担心毛线?

  • 有人喷还不好,有人喷说明人家能发现你的问题,改正的过程不也是一种提升吗?

  • 写博文,真不是那么简单,你以为随便拷贝拷贝就ok了?那不是闹呢。不想当将军的不是好士兵,同样既然决定写博客,那就要对自己负责,对别人负责,即使没人看,又何妨?

  • 既然身为Android开发大军中的一员,总要尽自己的一份力,想当初毕业后参加工作一直是跌跌撞撞到现在,往事不堪回首,so,现在内心更是希望博文能对在这Android路上的伙伴有所帮助~

好了,闲话也不扯了,一起来看今天的干货吧~

本文目标

  • 通过列举常用的图片压缩方式以及目前比较好的图片压缩方式,希望可以帮助有需要的同志面对图片压缩不再那么棘手~

图片压缩原因

我们为什么要进行图片压缩,大家有没有想过?

关于这个原因,LZ从下面三个方面进行简单说明(如有不对,欢迎指正~):

  • 服务器

从服务器的角度上来说,不可能让app传太大的图片,服务器本身就对上传资源大小有限制,太多太大的图片反而会增加服务器的压力,得不偿失

  • 用户

关于用户,我们就不得不说,目前手机拍照像素越来越高,相对应拍摄照片的体积也逐渐增大。假设一个场景,用户使用你APP进行换头像,假设拍摄头像大小为10MB,直接上传所需流量为1MB,而进过处理后,图片大小小于100kb,上传所需流量仅为0.1kb,当然这里说法有些夸大,但是我们真正去考虑下,如果是你去选择,你会如何抉择;

  • Android Coding

大家都知道,Android中,图片的处理可谓恶心的没谁了,假设图片太大,项目又需要加载很多图片,最直接的结果就是导致APP卡顿,OOM也不为过,So,图片处理 很有必要,顺便在这里简单说明下OOM。

OOM:全称“Out Of Memory”,简单可以理解为没有内存了。下面引入官方说法:

Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector.

意思就是说,<font color=FF0000>当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非Exception,因为这个问题APP已无法处理-崩溃)

目前图片压缩方式

总结来说,在Android设备中,图片有如下三种存在形式:

  1. 在硬盘上时,图片展现的方式是File;

  2. 在网络传输时,图片展现的方式是Stream;

  3. 在内存中,图片展现的方式是Stream或Bitmap。

以上三点作为基础,由此衍生以下内容,从而方便我们更好的理解掌握本文技能。

  • 质量压缩

所谓的质量压缩,它其实只能实现对file的影响,你可以把一个file转成bitmap再转成file,或者直接将一个bitmap转成file时,这个最终的file是被压缩过的,但是中间的bitmap并没有被压缩(或者说几乎没有被压缩,我不确定),因为bigmap在内存中的大小是按像素计算的,也就是width * height,对于质量压缩,并不会改变图片的像素,所以就算质量被压缩了,但是bitmap在内存的占有率还是没变小,但你做成file时,它确实变小了;

  • 尺寸压缩

尺寸压缩是减小了图片的像素,所以直接对bitmap产生了影响,当然最终的file也是相对的变小了

  • 采样率压缩

采样率压缩,的的确确的改变了图片占用内存问题,但是由于像素改变,压缩容易造成失真问题。使用采样率压缩,首先读取图片的边,然后设置图片的尺寸,然后再根据尺寸,选择的读取像素。这种方法避免了一开始就吧图片读入内存而造成的oom异常。

  • jpeg引擎库压缩(微信压缩)

绕过Android Bitmao层,通过开启"哈夫曼编码"实现。

  • Luban压缩(目前最接近微信压缩效果)

Luban(鲁班)就是通过在微信朋友圈发送近100张不同分辨率图片,对比原图与微信压缩后的图片逆向推算出来的压缩算法,因为是逆向推算,效果还没法跟微信一模一样,但是已经很接近微信朋友圈压缩后的效果,具体看以下对比
<center>

这里写图片描述
这里写图片描述

算法步骤如下:

注:下面所说“比例”统一表示:图片短边除以长边为该图片比例

1.判断图片比例值,是否处于以下区间内;

[1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内 ;
[0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内;
[0.5, 0) 即图片处于 [1:2 ~ 1:∞) 比例范围内

2.判断图片最长边是否过边界值;

[1, 0.5625) 边界值为:1664 * n(n=1), 4990 * n(n=2), 1280 * pow(2, n-1)(n≥3);
[0.5625, 0.5) 边界值为:1280 * pow(2, n-1)(n≥1);
[0.5, 0) 边界值为:1280 * pow(2, n-1)(n≥1)

3.计算压缩图片实际边长值,以第2步计算结果为准,超过某个边界值则:width / pow(2, n-1),height/pow(2, n-1);

4.计算压缩图片的实际文件大小,以第2、3步结果为准,图片比例越大则文件越大 size = (newW * newH) / (width * height) * m;

[1, 0.5625) 则 width & height 对应 1664,4990,1280 * n(n≥3),m 对应 150,300,300;
[0.5625, 0.5) 则 width = 1440,height = 2560, m = 200;
[0.5, 0) 则 width = 1280,height = 1280 / scale,m = 500;注:scale为比例值

5.判断第4步的size是否过小;

[1, 0.5625) 则最小 size 对应 60,60,100;
[0.5625, 0.5) 则最小 size 都为 100;
[0.5, 0) 则最小 size 都为 100

6.将前面求到的值压缩图片 width, height, size 传入压缩流程,压缩图片直到满足以上数值;

  • 第三方压缩

这里指的是别人封装比较好的压缩方式

以上几种方式是LZ目前总结的图片压缩方式,下面将针对以上几种方式分别举例说明,同时自己也重新回顾下,顺道希望可以帮助到有需要的人~

撸码讲解图片压缩

质量压缩 尺寸压缩 采样率压缩运行结果:

<center>
这里写图片描述
这里写图片描述

<font color=#FF0000>1.质量压缩

运行效果如下:

<center>
这里写图片描述
这里写图片描述

代码如下:

    /**
     * 1. 质量压缩
     * 设置bitmap options属性,降低图片的质量,像素不会减少
     * 设置options 属性0-100,来实现压缩
     *
     * @param bmp  需要压缩的bitmap图片对象
     * @param file 压缩后图片保存的位置
     */
    public static void compressImageToFile(Bitmap bmp, File file) {
        // 0-100 100为不压缩
        int options = 20;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        bmp.compress(Bitmap.CompressFormat.JPEG, options, baos);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 压缩后返回bitmap
     *
     * @param bmp     需要压缩的bitmap图片对象
     * @return
     */
    public static Bitmap compressImageToBitmap(Bitmap bmp) {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        bmp.compress(Bitmap.CompressFormat.JPEG, 100, os);
        if (os.toByteArray().length / 1024 > 1024) {//判断如果图片大于1M,进行压缩避免在生成图片(BitmapFactory.decodeStream)时溢出
            os.reset();//重置baos即清空baos
            bmp.compress(Bitmap.CompressFormat.JPEG, 50, os);//这里压缩50%,把压缩后的数据存放到baos中
        }
        ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
        BitmapFactory.Options newOpts = new BitmapFactory.Options();
        //开始读入图片,此时把options.inJustDecodeBounds 设回true了
        newOpts.inJustDecodeBounds = true;
        newOpts.inPreferredConfig = Bitmap.Config.RGB_565;
        Bitmap bitmap = BitmapFactory.decodeStream(is, null, newOpts);
        newOpts.inJustDecodeBounds = false;
        int w = newOpts.outWidth;
        int h = newOpts.outHeight;
        float hh = 240f;// 设置高度为240f时,可以明显看到图片缩小了
        float ww = 120f;// 设置宽度为120f,可以明显看到图片缩小了
        //缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
        int be = 1;//be=1表示不缩放
        if (w > h && w > ww) {//如果宽度大的话根据宽度固定大小缩放
            be = (int) (newOpts.outWidth / ww);
        } else if (w < h && h > hh) {//如果高度高的话根据宽度固定大小缩放
            be = (int) (newOpts.outHeight / hh);
        }
        if (be <= 0) be = 1;
        newOpts.inSampleSize = be;//设置缩放比例
        //重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了
        is = new ByteArrayInputStream(os.toByteArray());
        bitmap = BitmapFactory.decodeStream(is, null, newOpts);
        //压缩好比例大小后再进行质量压缩
//      return compress(bitmap, maxSize); // 这里再进行质量压缩的意义不大,反而耗资源,删除
        return bitmap;
    }

关于尺寸压缩,LZ这里有话要说,以前通常质量压缩我们都是这么玩的,如下:

    /**
     * 图片质量压缩
     * 
     * @param image
     * @return
     * @size 图片大小(kb)
     */
    public static Bitmap compressImage(Bitmap image, int size, String imageType) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            if (imageType.equalsIgnoreCase("png")) {
                image.compress(Bitmap.CompressFormat.PNG, 100, baos);
            } else {
                image.compress(Bitmap.CompressFormat.JPEG, 100, baos);// 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
            }
            int options = 100;
            while (baos.toByteArray().length / 1024 > size) { // 循环判断如果压缩后图片是否大于100kb,大于继续压缩
                baos.reset();// 重置baos即清空baos
                if (imageType.equalsIgnoreCase("png")) {
                    image.compress(Bitmap.CompressFormat.PNG, options, baos);
                } else {
        image.compress(Bitmap.CompressFormat.JPEG, options, baos);// 这里压缩options%,把压缩后的数据存放到baos中
                }
                options -= 10;// 每次都减少10
            }
            Log.e("压缩后的大小", baos.toByteArray().length / 1024 + "");
            ByteArrayInputStream isBm = new ByteArrayInputStream(
                    baos.toByteArray());// 把压缩后的数据baos存放到ByteArrayInputStream中
            Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);// 把ByteArrayInputStream数据生成图片
            return bitmap;
        } catch (Exception e) {
            return null;
        }
    }

for循环中不断对图片进行压缩,直到达到我们要求图片大小限制。在之前的设备这么玩没啥问题,但是在现在的一些像素比较高的手机,极易出现问题。循环的条件是如果不符合我们限定大小,options-10,依次相减,知道options等于0,图片依旧不满足,继续相减之后再次进行压缩,呵呵,你中奖了,具体可以自己尝试下~

<font color=#FF0000>So,如果你执意要使用这种方式,请做好options为0时,图片依旧不满足我们限定大小的操作

<font color=#FF0000>2.尺寸压缩

    /**
     * 2. 尺寸压缩
     * 通过缩放图片像素来减少图片占用内存大小
     *
     * @param bmp
     * @param file
     */
    public static void compressBitmapToFile(Bitmap bmp, File file) {
        // 尺寸压缩倍数,值越大,图片尺寸越小
        int ratio = 8;
        // 压缩Bitmap到对应尺寸
        Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
        canvas.drawBitmap(bmp, null, rect, null);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 压缩后转为bitmap
     *
     * @param bmp
     * @return
     */
    public static Bitmap compressBitmapToBitmap(Bitmap bmp) {
        // 尺寸压缩倍数,值越大,图片尺寸越小
        int ratio = 8;
        // 压缩Bitmap到对应尺寸
        Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
        canvas.drawBitmap(bmp, null, rect, null);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());// 把压缩后的数据baos存放到ByteArrayInputStream中
        return BitmapFactory.decodeStream(isBm, null, null);// 把ByteArrayInputStream数据生成图片;
    }

当然如果你觉得上面方式太麻烦,你也可以这么干:

    /**
     * 计算缩放比
     */
    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 halfHeight = height / 2;
            final int halfWidth = width / 2;
            while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }

    /**
     * 根据路径获得图片信息并按比例压缩,返回bitmap
     */
    public static Bitmap getSmallBitmap(String filePath) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;// 只解析图片边沿,获取宽高
        BitmapFactory.decodeFile(filePath, options);
        // 计算缩放比
        options.inSampleSize = calculateInSampleSize(options, 160, 240); // 这里160 240 随便输 不过要靠谱哦~
        // 完整解析图片返回bitmap
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(filePath, options);
    }

<font color=#FF0000>3.采样率压缩

    /**
     * 设置图片的采样率,降低图片像素
     *
     * @param filePath
     * @param file
     */
    public static void compressBitmap(String filePath, File file) {
        // 数值越高,图片像素越低
        int inSampleSize = 8;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = false;
//          options.inJustDecodeBounds = true;//为true的时候不会真正加载图片,而是得到图片的宽高信息。
        //采样率
        options.inSampleSize = inSampleSize;
        Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        try {
            if (file.exists()) {
                file.delete();
            } else {
                file.createNewFile();
            }
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 设置图片的采样率,降低图片像素
     *
     * @param filePath
     */
    public static Bitmap compressBitmap(String filePath) {
        // 数值越高,图片像素越低
        int inSampleSize = 8;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = false;
//          options.inJustDecodeBounds = true;//为true的时候不会真正加载图片,而是得到图片的宽高信息。
        //采样率
        options.inSampleSize = inSampleSize;
        Bitmap bitmap = BitmapFactory.decodeFile(filePath, options);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());// 把压缩后的数据baos存放到ByteArrayInputStream中
        return BitmapFactory.decodeStream(isBm, null, null);// 把ByteArrayInputStream数据生成图片;
    }

<font color=#FF0000>4.Luban压缩(目前最接近微信压缩效果)

以上已对Luban进行简单介绍,下面一起先来看一下运行效果,稍等片刻开始撸码~

Luban压缩运行结果:

<center>
这里写图片描述
这里写图片描述

第一步:加入依赖

compile 'top.zibin:Luban:1.1.2'

第二步:新建布局文件 包含对应Activity

一个选择图片按钮,俩个ImageView用于显示图片,俩个TextView用于显示图片大小。

第三步:开启Luban压缩(省略代码可直接在GitHub上查看)

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_PHOTO_CODE && resultCode == RESULT_OK) {
            if (data == null) {
                Toast.makeText(this, "打开图片失败~", Toast.LENGTH_SHORT).show();
                return;
            }
            try {
                // 获取图片路径
                oldFile = FileUtil.getTempFile(this, data.getData());
                // 设置获取图片显示ImageView
                ((ImageView) (findViewById(R.id.id_luban_choose_pic_show))).setImageBitmap(BitmapFactory.decodeFile(oldFile.getAbsolutePath()));
                // 设置获取图片大小
                ((TextView) (findViewById(R.id.id_luban_choose_pic_size))).setText(String.format("Size : %s", StringUtils.getReadableFileSize(oldFile.length())));
                // 启动Luban进行图片压缩
                Luban.with(this) // 初始化
                        .load(oldFile) // 要压缩的图片
                        .setCompressListener(new OnCompressListener() {
                            @Override
                            public void onStart() {
                                // 压缩开始前调用 可以在方法内启动loading UI
                                Toast.makeText(LubanActivity.this, "我要开着Luban浪荡了~", Toast.LENGTH_SHORT).show();
                            }

                            @Override
                            public void onSuccess(File newFile) {
                                // 压缩成功后调用,返回压缩后的图片文件
                                Toast.makeText(LubanActivity.this, "开车到达目的地~", Toast.LENGTH_SHORT).show();
                                // 设置获取图片显示ImageView
                                ((ImageView) (findViewById(R.id.id_luban_compress_end_show))).setImageBitmap(BitmapFactory.decodeFile(newFile.getAbsolutePath()));
                                // 设置获取图片大小
                                ((TextView) (findViewById(R.id.id_luban_compress_end_size))).setText(String.format("Size : %s", StringUtils.getReadableFileSize(newFile.length())));
                            }

                            @Override
                            public void onError(Throwable e) {
                                // 压缩过程中出现异常
                                Toast.makeText(LubanActivity.this, "丫的,翻车了" + e.getMessage(), Toast.LENGTH_SHORT).show();
                            }
                        }).launch(); // 启动压缩
            } catch (IOException e) {
                Toast.makeText(this, "读取图片失败~", Toast.LENGTH_SHORT).show();
                e.printStackTrace();
            }
        }
    }

<font color=#FF0000>5.jpeg引擎库压缩(微信压缩)本文重点

嘿嘿嘿~

<center>
这里写图片描述
这里写图片描述

来个效果图瞅瞅:

<center>
这里写图片描述
这里写图片描述

在这里,我们首先要思考以下问题:

  • 为什么IOS拍1M的图片要比安卓拍5M的图片还要清晰?<font color=#FF0000>(假设都是在同一个环境下,保存格式都是JPEG)

在这里,我们不得不提一个叫做图像引擎的东东。话说Android和IOS底层都采用了JPEG这个图像处理引擎,官方地址如下:

http://libjpeg-turbo.virtualgl.org/

简单介绍下历史:

  • 95年 JPEG推出 主要服务于PC端图片保存以及解码后图片展示;

  • 05年 基于JPEG推出skia引擎 此时主要服务于Web端(浏览器)对于图片的处理。而我们的Android端,目前也是采用skia引擎来对图片进行处理。但是唯一不同的是,谷歌Baba并没有完全使用skia图像处理引擎。skia官方地址如下;

https://skia.org/

  • 07年,谷歌Baba在这个基础上进行"优化",而"优化"的结果,就是<font color=#FF0000>取消使用《哈夫曼算法》,而采用《定长编码》,从而导致经过处理后的"图片会比之前图片大"。

这时候大家就会说了,Why?Tell me why?
表急,谷歌Baba这么做,确实有不得已的理由,如下:

在当时Android端设备上的CPU以及内存非常吃紧,由于哈夫曼算法非常吃CPU以及内存,So,无奈之下,只能选择取消哈夫曼算法(编码),但是读取图片依然保留哈夫曼解码方式。

而今天,由于Android设备有着充足的CPU以及内存,鉴于对于图片的要求,我们开始绕过Android的Bitmap API层,开启哈夫曼算法,从而达到我们今天的目的~

而所谓的哈夫曼算法,又是什么鬼呢?

其实可以理解为是一种数据结构,主要涉及到最优二叉树这方面。下面为大家引入简单介绍:

哈夫曼编码是广泛地用于数据文件压缩的十分有效的编码方法。压缩率通常在20%~90%之间。哈夫曼编码算法用字符在文件中出现的频率表来建立一个用0,1串表示各字符的最优表示方式

由于一个像素点包涵四个信息:alpha,red,green,blue。下面基于argb,我们假设定义a b c d e五个元素,通过不断不同组合去表示不同的信息,类似如下:

abcde acdbe bacde ……多种不同形式组合方式表示数据信息

而在计算机中,实际却是以0和1去表示,那么我们如何将我们保存的数据信息转为机器可读信息呢?

假设上述定义a b c d e表示方式如下:

a : 0001
b : 0010
c : 0011
d : 0100
e : 0101

从上述假设中,我们能发现首位是0,这个空间是不是被浪费了?那么也就是说我们可以对此进行优化,优化后如下:

a : 001
b : 010
c : 011
d : 100
e : 101

这种优化方式也就被称为定长编码,那么进过优化后,abcde表示方式如下:

001 010 011 100 101(用3位来表示一个字符信息,属于定长编码的最优。)

那么这个时候,还能不能继续优化呢?

必须的。我们可以通过加权信息编码进行深度优化,而什么是加权呢?简单可以理解为单一字符占用整体百分比例

那么,假设在如上数据中,abcde的占用比例如下:

a : 80%
b : 10%
c : 10%
d : 0%
e : 0%

假设一张图片由如上abced不同组合组成,而每个所占用比例如上,由它们构成了一张完整图片。
那么我们的优化如下:

a : 01
b : 10
c : 11

大家可能对这个结果有所怀疑,LZ在此进一步分解,之前我们的数据如下:

a : 001
b : 010
c : 011
d : 100
e : 101

d 和 e在这张图片中所占权重百分比为0,我们就可以直接进行优化,优化后结果如下:

a : 001
b : 010
c : 100

而通过最优定长编码优化后结果如下:

优化后abc表示为:01 10 11
在此对于之前abc的表示:001 010 011

到现在我们会有疑问:

如何得到每一个字符出现的权重?

而哈夫曼算法(编码)重点便是<font color=#FF0000>通过读取字符出现的权重从而实现动态编码。</font>而实际上,哈夫曼通过不断扫描图片信息,也就是a r g b所占比例,通过大量计算才能实现,so,所以说它很吃CPU!

到现在,大家应该明白,使用哈夫曼算法(编码),代价就是牺牲部分的性能,But,由于现在Android设备今非昔比,所谓的CPU以及内存紧张已经可以忽略,当然你拿个小破手机,LZ也是无奈至极,So,玩玩又何妨?

简单了解如上内容后,我们首先要做的就是下载JPEG图像处理引擎jar包,下面为大家附上地址:

http://www.ijg.org/

下载解压后会发现很多源文件,我们无需统统编译,只需要找到我们需要的即可。

接下来,一块开启Eclipse配置环境:

LZ当年配置的时候也没选择开启NDK方面,这会让我好找啊。

1.找到之前配置ADT路径,选择NDK方面install即可;

<center>
这里写图片描述
这里写图片描述

<center>
这里写图片描述
这里写图片描述

<center>
这里写图片描述
这里写图片描述

2.配置NDK路径

<center>
这里写图片描述
这里写图片描述

在这里会遇到一个奇葩的小问题,你会发现这个选择路径总会提示"Not a valid NDK directory",经过搜索之后,发现在NDK位置下创建一个"ndk-build"空文件即可,具体原因暂时不清楚。

<center>
这里写图片描述
这里写图片描述

之后再去选择变显示正常了,怪异。

<center>
这里写图片描述
这里写图片描述

3.开始配置Builder

<center>
这里写图片描述
这里写图片描述

<center>
这里写图片描述
这里写图片描述

<center>
这里写图片描述
这里写图片描述

注:<font color=#FF0000>勾选“After a Clean”,(勾选这个操作后,如果你想编译ndk的时候,只需要clean一下项目 就开始交叉编译) ,是不是很棒?

<center>
这里写图片描述
这里写图片描述

点击“Specify Resources…”勾选工程中新建的“jni“目录,点击”finish“, 点击“OK“,完成配置。

现在开始创建一个项目,创建jni目录,右键项目选择"Android Tools" ---> "Add Native Support",添加依赖。

<center>
这里写图片描述
这里写图片描述

<font color=#FF0000>开启正式撸码之路~!

一、创建NativeUtil工具类,编写native方法。

    /**
     * @param bit      bitmap对象
     * @param fileName 指定保存目录名
     * @param optimize 是否采用哈弗曼表数据计算 品质相差5-10倍
     * @Description: JNI基本压缩
     */
    public static void compressBitmap(Bitmap bit, String fileName, boolean optimize) {
        saveBitmap(bit, DEFAULT_QUALITY, fileName, optimize);
    }

    /**
     * @param image    bitmap对象
     * @param filePath 要保存的指定目录
     * @Description: 通过JNI图片压缩把Bitmap保存到指定目录
     */
    public static void compressBitmap(Bitmap image, String filePath) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        int options = 20;
        // JNI调用保存图片到SD卡 这个关键
        NativeUtil.saveBitmap(image, options, filePath, true);
    }

    /**
     * 调用native方法
     *
     * @param bit
     * @param quality
     * @param fileName
     * @param optimize
     * @Description:函数描述
     */
    public static void saveBitmap(Bitmap bit, int quality, String fileName, boolean optimize) {
        compressBitmap(bit, bit.getWidth(), bit.getHeight(), quality, fileName.getBytes(), optimize);
    }

    /**
     * 调用底层 libhlqImgCompress.c中的方法
     *
     * @param bit
     * @param w
     * @param h
     * @param quality
     * @param fileNameBytes
     * @param optimize
     * @return
     * @Description:函数描述
     */
    public static native String compressBitmap(Bitmap bit, int w, int h, int quality, byte[] fileNameBytes,
                                               boolean optimize);

二、生成头文件 漫长的路啊

首先进入工程src目录下,输入“javah native方法路径”

这时候会出现以下这个问题:

<center>
这里写图片描述
这里写图片描述

出现这种错误的原因是由于JDK是国际版的,在编译的时候,如果我们没有用-encoding参数指定我们的JAVA源程序的编码格式,则javac.exe首先获得我们操作系统默认采用的编码格式

修正后,输入以下:

javah -jni -encoding UTF-8 cn.hlqstruggle.image.NativeCompress

这个时候又出现以下错误:

<center>
这里写图片描述
这里写图片描述

原因: javah 无法识别Android的bitmap,So,我们需要指定本地sdk

修正后如下:

javah -classpath F:\HLQJobSoftware-Android\AndroidStudio\sdk\platforms\android-23\android.jar;. cn.hlq.compress.NativeUtil

PS:

  • 大家可能会发现上述几张图包名不一致,是因为测试过程中手残把之前的项目删除了,然后又重新创建的新项目;

  • 可能有些人在第二步可以生成头文件,but,LZ学习之路总是充满了波折,23333。。。。。。

刷新之后,为我们生成如下文件:

cn_hlq_compress_NativeUtil.h

接下来创建我们的cpp文件,so easy~ 怎么创建就不需要说明了吧?

三、编写Android.mk以及Application.mk

Android.mk:

LOCAL_PATH := $(call my-dir) 
include $(CLEAR_VARS) # 数据清理 例如变量等
LOCAL_MODULE    :=jpegbither # 模块名称 自定义即可
LOCAL_SRC_FILES :=libjpegbither.so # 库文件
include $(PREBUILT_SHARED_LIBRARY) # 预编译以上内容
include $(CLEAR_VARS) # 数据清理 例如变量等
LOCAL_MODULE    :=hlqImgCompress # 模块名称 自定义即可
LOCAL_SRC_FILES :=hlqImgCompress.cpp # 指定的cpp源地址
LOCAL_SHARED_LIBRARIES :=jpegbither # 对外使用库
LOCAL_LDLIBS := -ljnigraphics -llog  # Log日志
include $(BUILD_SHARED_LIBRARY)

Application.mk:

APP_ABI := armeabi armeabi-v7a # 指定生成ABI架构
APP_PLATFORM := android-21

四、选取jpeg图片处理引擎中需要文件

在之前我们说过,下载好的JPEG图片处理引擎我们没有必要完全编译,只需要选取部分,也就是我们真正需要的即可。

大家可下载demo,直接把demo中的jpeg相关拷贝过来即可。

还记得上面生成的头文件吗?搞过来,放到jni目录下即可。

到现在目录结构如下:

<center>
这里写图片描述
这里写图片描述
  • 有人说,截个报错的图干嘛?

  • 没事,有问题再解决呗~

五、确定编写思路

  1. 将android的bitmap解码,并转换成RGB数据,其中将alpha去掉;
  2. JPEG对象分配空间以及初始化;
  3. 指定压缩数据源;
  4. 获取文件信息;
  5. 为压缩设置参数,比如图像大小、类型、颜色空间;
    6.开始压缩 jpeg_start_compress();
    7.压缩结束 jpeg_finish_compress();
    8.释放资源

基于以上内容,我们开始编写cpp内容。

六、重点,编写cpp真正图片压缩代码

#include "cn_hlq_compress_NativeUtil.h"
#include <string.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <stdio.h>
#include <setjmp.h>
#include <math.h>
#include <stdint.h>
#include <time.h>

//统一编译方式
extern "C" {
#include "jpeg/jpeglib.h"
#include "jpeg/cdjpeg.h"        /* Common decls for cjpeg/djpeg applications */
#include "jpeg/jversion.h"      /* for version message */
#include "jpeg/android/config.h"
}

#define LOG_TAG "jni"
#define LOGW(...)  __android_log_write(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)

#define true 1
#define false 0

typedef uint8_t BYTE;

char *error;
struct my_error_mgr {
  struct jpeg_error_mgr pub;
  jmp_buf setjmp_buffer;
};

typedef struct my_error_mgr * my_error_ptr;

METHODDEF(void)
my_error_exit (j_common_ptr cinfo)
{
  my_error_ptr myerr = (my_error_ptr) cinfo->err;
  (*cinfo->err->output_message) (cinfo);
  error=(char*)myerr->pub.jpeg_message_table[myerr->pub.msg_code];
  longjmp(myerr->setjmp_buffer, 1);
}

int generateJPEG(BYTE* data, int w, int h, int quality,
        const char* outfilename, jboolean optimize) {
    //jpeg的结构体,保存的比如宽、高、位深、图片格式等信息,相当于java的类
    struct jpeg_compress_struct jcs;
    //当读完整个文件的时候就会回调my_error_exit这个退出方法。setjmp是一个系统级函数,是一个回调。
    struct my_error_mgr jem;
    jcs.err = jpeg_std_error(&jem.pub);
    jem.pub.error_exit = my_error_exit;
    if (setjmp(jem.setjmp_buffer)) {
        return 0;
    }
    //初始化jsc结构体
    jpeg_create_compress(&jcs);
    //打开输出文件 wb:可写byte
    FILE* f = fopen(outfilename, "wb");
    if (f == NULL) {
        return 0;
    }
    //设置结构体的文件路径
    jpeg_stdio_dest(&jcs, f);
    jcs.image_width = w;//设置宽高
    jcs.image_height = h;
    //看源码注释,设置哈夫曼编码:/* TRUE=arithmetic coding, FALSE=Huffman */
    jcs.arith_code = false;
    int nComponent = 3;
    /* 颜色的组成 rgb,三个 # of color components in input image */
    jcs.input_components = nComponent;
    //设置结构体的颜色空间为rgb
    jcs.in_color_space = JCS_RGB;
    //全部设置默认参数/* Default parameter setup for compression */
    jpeg_set_defaults(&jcs);
    //是否采用哈弗曼表数据计算 品质相差5-10倍
    jcs.optimize_coding = optimize;
    //设置质量
    jpeg_set_quality(&jcs, quality, true);
    //开始压缩,(是否写入全部像素)
    jpeg_start_compress(&jcs, TRUE);
    JSAMPROW row_pointer[1];
    int row_stride;
    //一行的rgb数量
    row_stride = jcs.image_width * nComponent;
    //一行一行遍历
    while (jcs.next_scanline < jcs.image_height) {
        //得到一行的首地址
        row_pointer[0] = &data[jcs.next_scanline * row_stride];
        //此方法会将jcs.next_scanline加1
        jpeg_write_scanlines(&jcs, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数
    }
    jpeg_finish_compress(&jcs);//结束
    jpeg_destroy_compress(&jcs);//销毁 回收内存
    fclose(f);//关闭文件
    return 1;
}

/**
 * byte数组转C的字符串
 */
char* jstrinTostring(JNIEnv* env, jbyteArray barr) {
    char* rtn = NULL;
    jsize alen = env->GetArrayLength( barr);
    jbyte* ba = env->GetByteArrayElements( barr, 0);
    if (alen > 0) {
        rtn = (char*) malloc(alen + 1);
        memcpy(rtn, ba, alen);
        rtn[alen] = 0;
    }
    env->ReleaseByteArrayElements( barr, ba, 0);
    return rtn;
}

jstring Java_cn_hlq_compress_NativeUtil_compressBitmap(JNIEnv* env,
        jclass thiz, jobject bitmapcolor, int w, int h, int quality,
        jbyteArray fileNameStr, jboolean optimize) {
    BYTE *pixelscolor;
    //1.将bitmap里面的所有像素信息读取出来,并转换成RGB数据,保存到二维byte数组里面
    //处理bitmap图形信息方法1 锁定画布
    AndroidBitmap_lockPixels(env,bitmapcolor,(void**)&pixelscolor);
    //2.解析每一个像素点里面的rgb值(去掉alpha值),保存到一维数组data里面
    BYTE *data;
    BYTE r,g,b;
    data = (BYTE*)malloc(w*h*3);//每一个像素都有三个信息RGB
    BYTE *tmpdata;
    tmpdata = data;//临时保存data的首地址
    int i=0,j=0;
    int color;
    for (i = 0; i < h; ++i) {
        for (j = 0; j < w; ++j) {
            //解决掉alpha
            //获取二维数组的每一个像素信息(四个部分a/r/g/b)的首地址
            color = *((int *)pixelscolor);//通过地址取值
            //0~255:
//          a = ((color & 0xFF000000) >> 24);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));
            //改值!!!----保存到data数据里面
            *data = b;
            *(data+1) = g;
            *(data+2) = r;
            data = data + 3;
            //一个像素包括argb四个值,每+4就是取下一个像素点
            pixelscolor += 4;
        }
    }
    //处理bitmap图形信息方法2 解锁
    AndroidBitmap_unlockPixels(env,bitmapcolor);
    char* fileName = jstrinTostring(env,fileNameStr);
    //调用libjpeg核心方法实现压缩
    int resultCode = generateJPEG(tmpdata,w,h,quality,fileName,optimize);
    if(resultCode ==0){
        jstring result = env->NewStringUTF("-1");
        return result;
    }
    return env->NewStringUTF("1");
}

七、关于生成so库

不知道大家还记得在上面LZ介绍过的:

注:<font color=#FF0000>勾选“After a Clean”,(勾选这个操作后,如果你想编译ndk的时候,只需要clean一下项目 就开始交叉编译) ,是不是很棒?

So,只要配置好了,撸码编译也是很爽的,下面附上编译结果图:

<center>
这里写图片描述
这里写图片描述

八、调用编译生成so库

使用so库要注意以下几点:

  • 包名路径要对应,也就是如下:

cn.hlq.compress.NativeUtil

  • 记得去加载so库
    /**
     * 加载lib下两个so文件
     */
    static {
        System.loadLibrary("hlqImgCompress");
        System.loadLibrary("jpegbither");
    }

<center>
这里写图片描述
这里写图片描述

<font color=#FF0000>关于这部分,文章末尾给大家附上 红橙 讲解地址 大家有兴趣可以查看。

<font color=#FF0000>6.南尘CompressHelper

首先查看运行效果:

<center>
这里写图片描述
这里写图片描述

撸码 不过几行而已~

南尘提供默认压缩以及自定义压缩,今天LZ从此入手,开启撸码之路,顺道测试下多张图压缩如何?一起来看~

一、使用默认配置压缩图片(附上关键代码)

通过CompressHelper.getDefault(getApplicationContext())构建一个CompressHelper,之后提供将压缩图片转成File以及Bitmap俩种方式,进一步方便,感谢~

<center>
这里写图片描述
这里写图片描述

关键代码如下:

        // 开始压缩图片
        findViewById(R.id.id_nanchen_default_compress).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 默认压缩方法,多张图片直接加入循环即可
                // 提供俩种方式,大家可自行选择:compressToFile compressToBitmap
                defaultStarTime = System.currentTimeMillis();
                newFile = CompressHelper.getDefault(getApplicationContext()).compressToFile(oldFile);
                defaultEndTime = System.currentTimeMillis();
                mOldCompressMs.setText(StringUtils.getMS(defaultStarTime, defaultEndTime));
                Log.e("HLQ_Struggle", "压缩后图片地址:" + newFile.getAbsolutePath());
                // 设置ImageView显示图片
                mImageNew.setImageBitmap(BitmapFactory.decodeFile(newFile.getAbsolutePath()));
                // 设置压缩后图片大小
                mTextNew.setText(String.format("Size : %s", StringUtils.getReadableFileSize(newFile.length())));
            }
        });

二、使用自定义配置压缩图片

        // 开始压缩图片
        findViewById(R.id.id_nanchen_weight_compress).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String yourFileName = "HLQ_test.jpg";
                // 你也可以自定义压缩
                weightStarTime = System.currentTimeMillis();
                newFile = new CompressHelper.Builder(getApplicationContext())
                        .setMaxWidth(720)  // 默认最大宽度为720
                        .setMaxHeight(960) // 默认最大高度为960
                        .setQuality(60)    // 默认压缩质量为80
                        .setCompressFormat(Bitmap.CompressFormat.JPEG) // 设置默认压缩为jpg格式
                        .setFileName(yourFileName) // 设置你的文件名 具体使用可以仿造系统拍照时存储照片的规则
                        .setDestinationDirectoryPath(Environment.getExternalStoragePublicDirectory(
                                Environment.DIRECTORY_PICTURES).getAbsolutePath())
                        .build()
                        .compressToFile(oldFile);
                weightEndTime = System.currentTimeMillis();
                mNewCompressMs.setText(StringUtils.getMS(weightStarTime, weightEndTime));
                mWeightImageNew.setImageBitmap(BitmapFactory.decodeFile(newFile.getAbsolutePath()));
                mWeightTextNew.setText(String.format("Size : %s", StringUtils.getReadableFileSize(newFile.length())));
            }
        });

三、压缩多张图片 照样无所畏惧

LZ为了方便,从本地找了5张图片进行测试,如下:

    private ArrayList<String> initImgsPath() {
        ArrayList<String> imgPath = new ArrayList<>();
        // HLQ测试图片地址
        imgPath.add("/storage/emulated/0/DCIM/Camera/IMG_20170703_144715.jpg");
        imgPath.add("/storage/emulated/0/DCIM/Camera/IMG_20170414_125420.jpg");
        imgPath.add("/storage/emulated/0/HLQ_test_pic/IMG_20170427_125301_1_1.jpg");
        imgPath.add("/storage/emulated/0/HLQ_test_pic/Screenshot_20170525-165109.jpg");
        imgPath.add("/storage/emulated/0/HLQ_test_pic/IMG_20170404_160055.jpg");
        return imgPath;
    }

偷个小懒~

for循环依次遍历压缩,输出结果~

for (int i = 0; i < initImgsPath().size(); i++) {
                File testEnd = CompressHelper.getDefault(getApplicationContext()).compressToFile(new File(initImgsPath().get(i)));
                String end = "图片地址:" + initImgsPath().get(i) + "\n" + "图片大小:" + String.format("Size : %s", StringUtils.getReadableFileSize(new File(initImgsPath().get(i)).length())) + "\n" + "压缩后大小为:" + String.format("Size : %s", StringUtils.getReadableFileSize(testEnd.length())) + "\n";
                if (i == 0) {
                    tvTestEnd1.setText(end);
                }
                if (i == 1) {
                    tvTestEnd2.setText(end);
                }
                if (i == 2) {
                    tvTestEnd3.setText(end);
                }
                if (i == 3) {
                    tvTestEnd4.setText(end);
                }
                if (i == 4) {
                    tvTestEnd5.setText(end);
                }
            }

GitHub查看地址

https://github.com/HLQ-Struggle/ImageCompress

Eclipse编译so源码下载地址

http://download.csdn.net/detail/u012400885/9907696

参考文献

感谢如下几位奉献,特在此附上链接,再次感谢~

  1. 一叶飘舟CSDN地址: http://blog.csdn.net/jdsjlzx/article/details/44228935
  2. Luban GitHub地址:https://github.com/Curzibn/Luban
  3. Luban算法 GitHub地址:https://github.com/Curzibn/Luban/blob/master/DESCRIPTION.md;
  4. 编译so异常解决:https://stackoverflow.com/questions/41478323/cmakemissing-and-no-known-rule-to-make-it-when-i-import-a-prebuilt-library
  5. 南尘GitHub地址:https://github.com/nanchen2251/CompressHelper;
  6. 哈夫曼算法简介:http://blog.csdn.net/liufeng_king/article/details/8720896#comments
  7. 红橙Darren Android图片压缩加密上传 - JPEG压缩算法解析:http://www.jianshu.com/p/097f4486f3e6
  8. 红橙Darren Android图片压缩加密上传 - NDK终极压缩和加密上传:http://www.jianshu.com/p/eebe2107da6d
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,566评论 25 707
  • 摘要:对android 上图片压缩,其实总结起来基本可以分为两类压缩:尺寸压缩和质量压缩, 尺寸压缩其实也可以理解...
    男爵是只猫丶阅读 8,663评论 2 14
  • 因为男票近来的状态,想画个画告诉他,这样玩法,我们会走向什么结果。。。。 开始画时倒没什么,画完之后,内心却有些伤...
    小伍的今天阅读 267评论 2 4
  • 记住这10句话,心静了,人就不会累。 1. 人,其实不需要太多的东西,只要健康的活着,真诚的爱着,也不失为一种富有...
    圆梦共同话题阅读 389评论 1 1
  • 多年以来一直不自信,这深深影响了在工作和生活中的表现,原本可以表现得更好,但事实上却没有。新的一年,需要不断完善自...
    duanxiaomao阅读 1,578评论 0 0