美文网首页
Android 开发艺术探索笔记之十二 -- Bitmap 的加

Android 开发艺术探索笔记之十二 -- Bitmap 的加

作者: whd_Alive | 来源:发表于2018-08-04 11:43 被阅读0次

    学习内容:

    • 如何有效加载 Bitmap
    • Android 常用的缓存策略
      • LurChche - 内存缓存
      • DiskLurCache - 存储缓存
    • 优化列表的卡顿现象

    1. Bitmap 的高效加载

    如何加载图片?

    四类方法:

    1. BitmapFactory.decodeFile / decodeResource / decodeStream / decodeByteArray
    2. 分别对应从 文件系统 / 资源 / 输入流 / 以及字节数组 中加载 Bitmap 对象
    3. 关系:decodeFile 和 decodeResource 间接调用了 decodeStream

    如何高效加载图片?

    1. 核心思想:采用 BitmapFactory.Options 加载所需尺寸的图片

    2. 说明:主要是 inSampleSize 参数,即采样率。inSampleSize 为 1 时,表示原始大小;当 inSampleSize 为 2 时,采样后的图片 宽/高 均变为原来的 1/2,即整个图片缩小为原来的 1/4。inSampleSize 必须大于 1 才能起作用,效果以此类推。

    3. 具体方法(获取采样率):

      1. 将 BitmapFactory.Options 的 inJustDecodeBounds 参数设为 true 并加载图片
      2. 从 BitmapFactory.Options 中取出图片的原始宽/高信息,对应于 outWidth 和 outHeight 参数
      3. 计算所需的采样率 inSampleSize
      4. 将 BitmapFactory.Options 的 inJustDecodeBounds 参数设为 false,然后重新加载图片

      inJustDecodeBounds 参数设为 true 时,BitmapFactory 只会解析图片的原始 宽/高 信息,并不会真正的加载图片


    2. Android 中的缓存策略

    为什么需要缓存?

    两方面原因:提高程序效率 + 节约流量开销

    缓存策略

    一般来说,缓存策略主要包括 缓存的添加获取删除 这三个操作。
    目前常用的一种缓存算法是 LRU,即最近最少使用算法,核心思想是缓存满了时,优先淘汰最近最少使用的缓存对象。

    2.1 LruCache

    兼容性

    LruCache 是 Android 3.1 提供的一个缓存类,如果需要兼容 3.1 以下的版本,则需要使用 support-v4 兼容包中提供的 LruCache

    实现思想

    LruCache 是一个泛型类,内部采用一个 LinkedHashMap 以强引用的方式存储外界的缓存对象,通过 get 和 set 方法完成缓存的获取和添加,当缓存满时,LruCache 会移除较早使用的缓存对象,然后添加新的缓存对象。

    LruCache 是线程安全的。

    关于 强引用、软引用和弱引用:

    • 强引用:直接的对象引用
    • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被 GC 回收
    • 弱引用:当一个对象只有弱引用存在时,此对象随时被 GC 回收

    原理

    留待后续学习。

    具体使用

    1. 创建:提供缓存的总容量大小并重写 sizeOf 方法
    2. 获取:get 方法
    3. 添加:put 方法
    4. 删除:remove 方法删除指定的缓存对象

    2.2 DiskLruCache

    简述

    DiskLruCache 用于实现存储设备缓存,通过将缓存对象写入文件系统从而实现缓存的效果。
    源码地址:https://github.com/JakeWharton/DiskLruCache

    使用方式

    1. DiskLruCache 的创建

      • 通过 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) 方法创建自身。
      • 参数:
        • directory:磁盘缓存在文件系统中的存储路径
        • appVersion:应用的版本号,一般设为 1 即可
        • valueCount:单个节点所对应的数据的个数,一般设为 1 即可
        • maxSize:缓存总大小,超过这个设定值后,会清除一些缓存
      • 典型代码如下:
        private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50; //50MB
        
        File diskCacheDir = getDiskCacheDir(mContext,"bitmap");
        if (!diskCacheDir.exists()) {
            distCacheDir.mkdirs();
        }
        mDiskLurCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
        
    2. DiskLruCache 的缓存添加

      • 核心:通过 Editor 完成,Editor 表示一个缓存对象的编辑对象

      • 步骤:

        1. 获取图片 url 对应的 key

          public static String hashKeyFromUrl(String key) {
               String cacheKey;
               try {
                   final MessageDigest mDigest = MessageDigest.getInstance("MD5");
                   mDigest.update(key.getBytes());
                   cacheKey = bytesToHexString(mDigest.digest());
               } catch (NoSuchAlgorithmException e) {
                   cacheKey = String.valueOf(key.hashCode());
               }
               return cacheKey;
           }
          
           private static String bytesToHexString(byte[] bytes) {
               StringBuilder sb = new StringBuilder();
               for (int i = 0; i < bytes.length; i++) {
                   String hex = Integer.toHexString(0xFF & bytes[i]);
                   if (hex.length() == 1) {
                       sb.append('0');
                   }
                   sb.append(hex);
               }
               return sb.toString();
           }
          
          
        2. 根据 key 通过 edit() 获取 Editor 对象,得到输出流,并再下载图片时通过该输出流写入到文件系统,最后通过 commit() 提交。
          注意:此部分应当通过 子线程 执行,避免下载图片造成 ANR;

          String key = Util.hashKeyFromUrl(url);
          //得到DiskLruCache.Editor
          DiskLruCache.Editor editor = diskLruCache.edit(key);
          if (editor != null) {
              OutputStream outputStream = editor.newOutputStream(0);
              if (downloadUrlToStream(Util.IMG_URL, outputStream)) {
                  publishProgress("");
                  //写入缓存
                  editor.commit();
              } else {
                  //写入失败
                  editor.abort();
              }
          }
          
        3. 关于下载图片:

              private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
                  HttpURLConnection urlConnection = null;
                  BufferedOutputStream out = null;
                  BufferedInputStream in = null;
          
                  try {
                      final URL url = new URL(urlString);
                      urlConnection = (HttpURLConnection) url.openConnection();
                      in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
                      out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
                      int b;
                      while ((b = in.read()) != -1) {
                          out.write(b);
                      }
                      return true;
                  } catch (Exception e) {
                      Log.e(TAG, "Error in downloadBitmap - " + e);
                  } finally {
                      if (urlConnection != null) {
                          urlConnection.disconnect();
                      }
                      try {
                          if (out != null) {
                              out.close();
                          }
                          if (in != null) {
                              in.close();
                          }
                      } catch (final IOException e) {
                      }
                  }
                  return false;
              }
          
    1. DiskLruCache 的缓存查找

      • 将 url 转换为 key,然后通过 DiskLruCache 的 get 方法得到一个 Snapshot 对象,接着通过 Snapshot.getInputStream() 即可得到缓存的文件输入流,进而得到缓存图片:

        private Bitmap getCache() {
             try {
                 String key = Util.hashKeyFromUrl(url);
                 DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
                 if (snapshot != null) {
                     InputStream in = snapshot.getInputStream(0);
                     return BitmapFactory.decodeStream(in);
                 }
             } catch (IOException e) {
                 e.printStackTrace();
             }
             return null;
        }
        
    2. 关于 FileInputStream 下的缩放

      • 不适用 BitmapFactory.Options 缩放的方法:原因在于 两次 decodeStream 调用影响了文件流的位置属性,导致第二次 decodeStream 得到 null。

      • 解决方法:文件描述符

        Bitmap bitmap = null;
        String key = Util.hashKeyFromUrl(url);
        DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
        if (snapshot != null) {
           FileInputStream in = (FileInputStream)snapshot.getInputStream(0);
           FileDescriptor fd = fildInputStream.getFD();
            bitmap = mImageResizer.decodeSampleBitmapFromFileDecriptor(fd,reqWidth,reqHeight);
            if(bitmap != null){
                addBitmapToMemoryCache(key, bitmap);
            }
        }
        

    2.3 ImageLoader 的实现

    优秀的 ImageLoader 应当具备的功能:

    • 图片的异步加载
    • 图片的同步加载
    • 图片压缩:降低 OOM
    • 内存缓存:核心,提高效率并降低流量消耗
    • 磁盘缓存:核心,提高效率并降低流量消耗
    • 网络拉取:当两种缓存都不可用时,通过网络拉取图片

    代码实现

    图片压缩部分

    public class ImageResizer {
        /**
         * 从资源文件中加载图片。
         * @param res
         * @param resId
         * @param reqWidth
         * @param reqHeight
         * @return
         */
        public static Bitmap decodeSampleBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight){
     
            BitmapFactory.Options options = new BitmapFactory.Options();
            //第一个options设置为true,去加载图片
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeResource(res,resId,options);
            //计算采样率
            options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
            options.inJustDecodeBounds = false;
            return BitmapFactory.decodeResource(res,resId,options);
        }
     
        /**
         * 从内存卡中加载图片
         * @param fd
         * @param reqWidth
         * @param reqHeight
         * @return
         */
        public static Bitmap decodeSampleBitmapFromFile(FileDescriptor fd,int reqWidth,int reqHeight){
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeFileDescriptor(fd,null,options);
            options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
            options.inJustDecodeBounds = false;
     
            return BitmapFactory.decodeFileDescriptor(fd,null,options);
        }
        /**
         *计算图片的采样率
         * 原理,如果设置的图片宽、高小于原图的宽、高。则inSampleSize呈2的指数缩小
         * @param options
         * @param reqWidth
         * @param reqHeight
         * @return
         */
     
        private static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
            if(reqHeight==0 || reqWidth ==0){
                return 1;
            }
            //默认的采样率
            int inSampleSize = 1;
            //获取原图的宽高
            int width = options.outWidth;
            int height = options.outHeight;
     
            if(width > reqWidth || height > reqHeight){
                int halfWidth = width / 2;
                int halfHeight = height / 2;
                while((halfHeight / inSampleSize) >= reqHeight
                        && (halfWidth / inSampleSize) >= reqWidth){
                    inSampleSize *= 2;
                }
            }
            return inSampleSize;
        }
    }
    

    ImageLoader 部分

    public class ImageLoader {
        private static final String TAG = "ImageLoader";
        public static final int MESSAGE_POST_RESULT = 1;
    
        //CPU 核心数
        private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
        //CPU 核心线程数
        private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
        private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
        //线程超时时长
        private static final long KEEP_ALIVE = 10L;
    
        private static final int TAG_KEY_URI = R.id.imageloader_uri;
        private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
        private static final int IO_BUFFER_SIZE = 8 * 1024;
        private static final int DISK_CACHE_INDEX = 0;
        private boolean mIsDiskLruCacheCreated = false;
    
        private static final ThreadFactory sThreadFactory = new ThreadFactory() {
            private final AtomicInteger mCount = new AtomicInteger(1);
    
            public Thread newThread(Runnable r) {
                return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
            }
        }
    
        public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(Runnable), sThreadFactory);
    
        private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
            public void handleMessage(Message msg) {
                LoaderResult result = (LoaderResult) msg.obj;
                ImageView imageView = result.imageView;
                String uri = (String) imageView.getTag(TAG_KEY_URI);
                if (uri.equals(result.uri)) {
                    imageView.setImageBitmap(result.bitmap);
                } else {
                    Log.w(TAG, "set image bitmap,but url has changed,ignored!");
                }
            }
        };
    
        private Context mContext;
        private ImageResizer mImageResizer = new ImageResizer();
        private LruCache<String, Bitmap> mMemoryCache;
        private DiskLruCache mDiskLruCache;
    
        //无参构造方法,初始化 LruCache内存缓存 和 DiskLruCache磁盘缓存
        private ImageLoader(Context context) {
            mContext = context.getApplicationContext();
            int maxMemory = (int) (Runtime.getRuntime().maxMemory / 1024);
            int cacheSize = maxMemory / 8;
            mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
                protected int sizeOf(String key, Bitmap bitmap) {
                    return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
                }
            };
            File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
            if (!diskCacheDir.exists()) {
                diskCacheDir.mkdirs();
            }
            if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
                try {
                    mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
                    mIsDiskLruCacheCreated = true;
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //提供给外界的创建接口
        public static ImageLoader build(Context context) {
            return new ImageLoader(context);
        }
    
        //内存缓存 的 添加
        private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
            if (getBitmapFromMemCache(key) == null) {
                mMemoryCache.put(key, bitmap);
            }
        }
    
        //内存缓存 的 获取
        private Bitmap getBitmapFromMemCache(String key) {
            return mMemoryCache.get(key);
        }
    
        public void bindBitmap(final String uri, final ImageView imageView) {
            bindBitmap(uri, imageView, 0, 0);
        }
    
        /**
         * load bitmap from memory cache or disk or network async,then bind imageview and bitmap
         * NOTE THAT: should run in UI thread
         * 异步加载接口
         */
        public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight) {
            imageView.setTag(TAG_KEY_URI, uri);
            Bitmap bitmap = loadBitmapFromMemCache(uri);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }
    
            Runnable loadBitmapTask = new Runnable() {
                public void run() {
                    Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
                    if (bitmap != null) {
                        LoaderResult result = new LoaderResult(imageView, uri, bitmap);
                        mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
                    }
                }
            }
            THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
        }
    
        //同步加载接口
        public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
            Bitmap bitmap = loadBitmapFromMemCache(uri);
            if (bitmap != null) {
                Log.d(TAG, "loadBitmapFromMemCache,url:" + uri);
                return bitmap;
            }
    
            try {
                bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
                if (bitmap != null) {
                    Log.d(TAG, "loadBitmapFromDiskCache,url:" + uri);
                    return bitmap;
                }
                bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
                Log.d(TAG, "loadBitmapFromHttp,url:" + uri);
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            if (bitmap == null && !mIsDiskLruCacheCreated) {
                Log.w(TAG, "encounter error,DiskLruCache is not created.");
                bitmap = downloadBitmapFromUrl(uri);
            }
            return bitmap;
        }
    
        private Bitmap loadBitmapFromMemCache(String url) {
            final String key = hashKeyFromUrl(url);
            Bitmap bitmap = getBitmapFromMemCache(key);
            return bitmap;
        }
    
        //磁盘缓存 的 添加
        private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
            if (Looper.myLooper() == Looper.getMainLooper()) {
                throw new RuntimeException("can not visit network from UI Thread.");
            }
            if (mDiskLruCache == null) {
                return null;
            }
            String key = hashKeyFromUrl(url);
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);
            if (editor != null) {
                OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
                if (downloadUrlToStream(url, outputStream)) {
                    editor.commit();
                } else {
                    editor.abort();
                }
                mDiskLruCache.flush();
            }
            return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
        }
        
        //磁盘缓存 的 读取,并 添加 到 内存缓存
        private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) {
            if (Looper.myLooper() == Looper.getMainLooper()) {
                Log.w(TAG, "load bitmap from UI Thread,it is not recommended!");
            }
            if (mDiskLruCache == null) {
                return null;
            }
    
            Bitmap bitmap = null;
            String key = hashKeyFromUrl(url);
            DiskLruCache.SnapShot snapShot = mDiskLruCache.get(key);
            if (snapShot != null) {
                FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
                FileDescriptor fileDescriptor = fileInputStream.getFD();
                bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
                if (bitmap != null) {
                    addBitmapToMemoryCache(key, bitmap);
                }
            }
            return bitmap;
        }
    
        //将图片写入到本地文件中
        public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
            HttpURLConnection urlConnection = null;
            BufferedOutputStream out = null;
            BufferedInputStream in = null;
            try {
                final URL url = new URL(urlString);
                urlConnection = (HttpURLConnection) url.openConnection();
                in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
                out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
                int b;
                while ((b = in.read()) != -1) {
                    out.write(b);
                }
                return true;
            } catch (IOException e) {
                Log.e(TAG, "downloadBitmap failed." + e);
            } finally {
                if (urlConnection != null) {
                    urlConnection.disconnect();
                }
                MyUtils.close(out);
                MyUtils.close(in);
            }
            return false;
        }
    
        //下载图片
        private Bitmap downloadBitmapFromUrl(String urlString) {
            Bitmap bitmap = null;
            HttpURLConnection urlConnection = null;
            BufferedInputStream in = null;
            try {
                final URL url = new URL(urlString);
                urlConnection = (HttpURLConnection) url.openConnection();
                in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
                bitmap = BitmapFactory.decodeStream(in);
            } catch (IOException e) {
                Log.e(TAG, "Error in downloadBitmap:" + e);
            } finally {
                if (urlConnection != null) {
                    urlConnection.disconnect();
                }
                MyUtils.close(in);
            }
            return bitmap;
        }
    
        //将 url 转换为 key
        private String hashKeyFromUrl(String url) {
            String cacheKey;
            try {
                final MessageDigest mDigest = MessageDigest.getInstance("MD5");
                mDigest.update(url.getBytes());
                cacheKey = bytesToHexString(mDigest.digest());
            } catch (NoSuchAlgorithmException e) {
                cacheKey = String.valueOf(url.hasCode());
            }
            return cacheKey;
        }
    
        private String bytesToHexString(byte[] bytes) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < bytes.length; i++) {
                String hex = Integer.toHexString(0xFF & byte[i]);
                if (hex.length() == 1) {
                    sb.append('0');
                }
                sb.append(hex);
            }
            return sb.toString();
        }
    
        public File getDiskCacheDir(Context context, String uniqueName) {
            boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
            final String cachePath;
            if (externalStorageAvailable) {
                cachePath = context.getExternalCacheDir().getPath();
            } else {
                cachePath = context.getCacheDir().getPath();
            }
            return new File(cachePath + File.separator + uniqueName);
        }
    
        private long getUsableSpace(File path) {
            if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
                return path.getUsableSpace();
            }
            final StatFs stats = new StatFs(path.getPath());
            return (long) stats.getBloackSize() * (long) stats.getAvailableBlocks();
        }
    
        private static class LoaderResult {
            public ImageView imageView;
            public String uri;
            public Bitmap bitmap;
    
            public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
                this.imageView = imageView;
                this.uri = uri;
                this.bitmap = bitmap;
            }
        }
    }
    

    3. ImageLoader 的使用

    3.1 照片墙效果

    • 想要实现宽高相等的 ImageView 时,自定义一个 ImageView 子类,并在 onMeasure 方法中将 heightMeasureSpec 替换为 widthMeasureSpec 即可。这样做省时省力

      @Override
      protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
        super.onMeasure(widthMeasureSpec,widthMeasureSpec);
      }
      
    • 具体实现:

      • ImageAdapter 的 getView 方法中,通过 ImageLoader.bindBitmap() 方法将图片加载过程交给 ImageLoader

    3.2 优化列表的卡顿现象

    一些建议

    1. 不要再 getView 中执行耗时操作,比如直接加载图片
    2. 控制异步任务的执行频率;如果用户可以频繁上下滑动,会在一瞬间产生大量异步任务,会造成线程池的拥堵并带来大量的 UI 更新操作。应当考虑在滑动时停止加载,列表停下来之后再加载图片。
    3. 开启硬件加速,通过设置 android:hardwareAccelerated="true"

    大概就这么多吧。

    本章结束。

    相关文章

      网友评论

          本文标题:Android 开发艺术探索笔记之十二 -- Bitmap 的加

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